From d62e1dbb232e1b5ddc22069649ba2468418164d4 Mon Sep 17 00:00:00 2001 From: Bernhard Haumacher Date: Sun, 14 May 2023 10:14:23 +0200 Subject: [PATCH] Imported version 1.6 of mjsip from http://mjsip.org/. --- Makefile | 109 ++ README.txt | 31 + changes.txt | 497 +++++++ lib/COPYRIGHT.txt | 22 + lib/license.txt | 340 +++++ lib/server.jar | Bin 0 -> 40955 bytes lib/sip.jar | Bin 0 -> 201058 bytes lib/ua.jar | Bin 0 -> 276481 bytes make-HOW-TO.txt | 26 + makefile-config | 68 + media/local/media/icon.gif | Bin 0 -> 144 bytes media/local/media/logo.gif | Bin 0 -> 3397 bytes media/local/ua/call.gif | Bin 0 -> 84 bytes media/local/ua/hangup.gif | Bin 0 -> 89 bytes media/local/ua/off.wav | Bin 0 -> 97016 bytes media/local/ua/on.wav | Bin 0 -> 80856 bytes media/local/ua/ring.wav | Bin 0 -> 33120 bytes mjsip.cfg.txt | 494 +++++++ src/COPYRIGHT.txt | 22 + src/license.txt | 340 +++++ src/local/media/AudioClipPlayer.java | 183 +++ src/local/media/AudioClipPlayerListener.java | 14 + src/local/media/AudioInput.java | 166 +++ src/local/media/AudioOutput.java | 174 +++ src/local/media/AudioOutput.java.saved | 251 ++++ src/local/media/AudioOutputStream.java | 129 ++ src/local/media/AudioReceiver.java | 129 ++ src/local/media/AudioSender.java | 176 +++ src/local/media/ExtendedPipedInputStream.java | 79 ++ .../media/ExtendedPipedOutputStream.java | 60 + src/local/media/G711.java | 259 ++++ src/local/media/JMediaReceiver.java | 106 ++ src/local/media/JMediaReceiverListener.java | 12 + src/local/media/JMediaSender.java | 264 ++++ src/local/media/JVisualReceiver.java | 131 ++ src/local/media/Mixer.java | 152 ++ src/local/media/MixerLine.java | 155 ++ src/local/media/RtpStreamReceiver.java | 174 +++ src/local/media/RtpStreamSender.java | 228 +++ src/local/media/RtpStreamTranslator.java | 252 ++++ src/local/media/SplitterLine.java | 118 ++ src/local/media/ToneInputStream.java | 169 +++ src/local/media/UdpStreamReceiver.java | 147 ++ src/local/media/UdpStreamSender.java | 189 +++ src/local/net/KeepAliveSip.java | 102 ++ src/local/net/KeepAliveUdp.java | 177 +++ src/local/net/RtpFlow.java | 167 +++ src/local/net/RtpInputFlow.java | 65 + src/local/net/RtpOutputFlow.java | 54 + src/local/net/RtpPacket.java | 287 ++++ src/local/net/RtpSocket.java | 86 ++ src/local/net/UdpMultiRelay.java | 135 ++ src/local/net/UdpRelay.java | 214 +++ src/local/net/UdpRelayListener.java | 13 + src/local/server/AuthenticationServer.java | 48 + .../server/AuthenticationServerImpl.java | 249 ++++ src/local/server/AuthenticationService.java | 80 ++ .../server/AuthenticationServiceImpl.java | 254 ++++ src/local/server/CallLogger.java | 13 + src/local/server/CallLoggerImpl.java | 153 ++ src/local/server/DomainRoutingRule.java | 65 + src/local/server/LocationService.java | 92 ++ src/local/server/LocationServiceImpl.java | 414 ++++++ src/local/server/PrefixRoutingRule.java | 68 + src/local/server/Proxy.java | 356 +++++ src/local/server/Proxy.java.saved | 390 +++++ src/local/server/Redirect.java | 124 ++ src/local/server/Registrar.java | 474 +++++++ src/local/server/Repository.java | 68 + src/local/server/RoutingRule.java | 20 + src/local/server/ServerEngine.java | 334 +++++ src/local/server/ServerProfile.java | 218 +++ src/local/server/StatefulProxy.java | 374 +++++ src/local/server/StatefulProxyState.java | 153 ++ src/local/ua/CommandLineMA.java | 232 +++ src/local/ua/CommandLinePA.java | 279 ++++ src/local/ua/CommandLineUA.java | 568 ++++++++ src/local/ua/GraphicalUA.java | 740 ++++++++++ src/local/ua/ImagePanel.java | 61 + src/local/ua/JAudioLauncher.java | 231 +++ src/local/ua/JMFAudioLauncher.java | 87 ++ src/local/ua/JMFVideoLauncher.java | 89 ++ src/local/ua/Jukebox.java | 234 +++ src/local/ua/MediaLauncher.java | 15 + src/local/ua/MessageAgent.java | 190 +++ src/local/ua/MessageAgentListener.java | 19 + src/local/ua/MiniJukebox.java | 178 +++ src/local/ua/PopupFrame.java | 96 ++ src/local/ua/PresenceAgent.java | 191 +++ src/local/ua/PresenceAgentListener.java | 26 + src/local/ua/RATLauncher.java | 88 ++ src/local/ua/RegisterAgent.java | 328 +++++ src/local/ua/RegisterAgentListener.java | 17 + src/local/ua/StringList.java | 116 ++ src/local/ua/UserAgent.java | 777 ++++++++++ src/local/ua/UserAgentListener.java | 31 + src/local/ua/UserAgentProfile.java | 245 ++++ src/local/ua/VICLauncher.java | 89 ++ src/org/zoolu/net/IpAddress.java | 123 ++ src/org/zoolu/net/SocketAddress.java | 105 ++ src/org/zoolu/net/TcpConnection.java | 199 +++ src/org/zoolu/net/TcpConnectionListener.java | 37 + src/org/zoolu/net/TcpServer.java | 151 ++ src/org/zoolu/net/TcpServerListener.java | 38 + src/org/zoolu/net/TcpSocket.java | 121 ++ src/org/zoolu/net/UdpPacket.java | 124 ++ src/org/zoolu/net/UdpProvider.java | 195 +++ src/org/zoolu/net/UdpProviderListener.java | 38 + src/org/zoolu/net/UdpSocket.java | 103 ++ src/org/zoolu/sdp/AttributeField.java | 65 + src/org/zoolu/sdp/ConnectionField.java | 96 ++ src/org/zoolu/sdp/MediaDescriptor.java | 194 +++ src/org/zoolu/sdp/MediaField.java | 101 ++ src/org/zoolu/sdp/OriginField.java | 84 ++ src/org/zoolu/sdp/SdpField.java | 98 ++ src/org/zoolu/sdp/SdpParser.java | 166 +++ src/org/zoolu/sdp/SessionDescriptor.java | 378 +++++ src/org/zoolu/sdp/SessionNameField.java | 58 + src/org/zoolu/sdp/TimeField.java | 68 + src/org/zoolu/sip/address/NameAddress.java | 125 ++ src/org/zoolu/sip/address/SipURL.java | 265 ++++ .../authentication/DigestAuthentication.java | 288 ++++ src/org/zoolu/sip/call/Call.java | 361 +++++ src/org/zoolu/sip/call/CallListener.java | 83 ++ .../zoolu/sip/call/CallListenerAdapter.java | 222 +++ src/org/zoolu/sip/call/ExtendedCall.java | 225 +++ .../zoolu/sip/call/ExtendedCallListener.java | 55 + src/org/zoolu/sip/call/SdpTools.java | 106 ++ src/org/zoolu/sip/dialog/Dialog.java | 277 ++++ src/org/zoolu/sip/dialog/DialogInfo.java | 170 +++ .../sip/dialog/ExtendedInviteDialog.java | 313 ++++ .../dialog/ExtendedInviteDialogListener.java | 59 + src/org/zoolu/sip/dialog/InviteDialog.java | 721 ++++++++++ .../sip/dialog/InviteDialogListener.java | 118 ++ src/org/zoolu/sip/dialog/NotifierDialog.java | 359 +++++ .../sip/dialog/NotifierDialogListener.java | 52 + .../zoolu/sip/dialog/SubscriberDialog.java | 304 ++++ .../sip/dialog/SubscriberDialogListener.java | 51 + src/org/zoolu/sip/header/AcceptHeader.java | 52 + src/org/zoolu/sip/header/AlertInfoHeader.java | 56 + .../zoolu/sip/header/AllowEventsHeader.java | 55 + src/org/zoolu/sip/header/AllowHeader.java | 55 + .../sip/header/AuthenticationHeader.java | 418 ++++++ .../sip/header/AuthenticationInfoHeader.java | 110 ++ .../zoolu/sip/header/AuthorizationHeader.java | 49 + src/org/zoolu/sip/header/BaseSipHeaders.java | 170 +++ src/org/zoolu/sip/header/CSeqHeader.java | 82 ++ src/org/zoolu/sip/header/CallIdHeader.java | 70 + src/org/zoolu/sip/header/ContactHeader.java | 138 ++ .../zoolu/sip/header/ContentLengthHeader.java | 55 + .../zoolu/sip/header/ContentTypeHeader.java | 58 + src/org/zoolu/sip/header/DateHeader.java | 49 + src/org/zoolu/sip/header/EndPointHeader.java | 105 ++ src/org/zoolu/sip/header/EventHeader.java | 71 + src/org/zoolu/sip/header/ExpiresHeader.java | 109 ++ src/org/zoolu/sip/header/FromHeader.java | 62 + src/org/zoolu/sip/header/Header.java | 89 ++ src/org/zoolu/sip/header/ListHeader.java | 70 + .../zoolu/sip/header/MaxForwardsHeader.java | 75 + src/org/zoolu/sip/header/MultipleHeader.java | 247 ++++ .../zoolu/sip/header/NameAddressHeader.java | 83 ++ src/org/zoolu/sip/header/OptionHeader.java | 49 + .../zoolu/sip/header/ParametricHeader.java | 143 ++ .../sip/header/ProxyAuthenticateHeader.java | 51 + .../sip/header/ProxyAuthorizationHeader.java | 51 + .../zoolu/sip/header/ProxyRequireHeader.java | 39 + .../zoolu/sip/header/RecordRouteHeader.java | 44 + src/org/zoolu/sip/header/ReferToHeader.java | 48 + .../zoolu/sip/header/ReferredByHeader.java | 51 + src/org/zoolu/sip/header/RequestLine.java | 77 + src/org/zoolu/sip/header/RequireHeader.java | 39 + src/org/zoolu/sip/header/RouteHeader.java | 44 + src/org/zoolu/sip/header/ServerHeader.java | 49 + src/org/zoolu/sip/header/SipDateHeader.java | 74 + src/org/zoolu/sip/header/SipHeaders.java | 59 + src/org/zoolu/sip/header/StatusLine.java | 68 + src/org/zoolu/sip/header/SubjectHeader.java | 53 + .../sip/header/SubscriptionStateHeader.java | 112 ++ src/org/zoolu/sip/header/SupportedHeader.java | 39 + src/org/zoolu/sip/header/ToHeader.java | 77 + .../zoolu/sip/header/UnsupportedHeader.java | 38 + src/org/zoolu/sip/header/UserAgentHeader.java | 49 + src/org/zoolu/sip/header/ViaHeader.java | 203 +++ .../sip/header/WwwAuthenticateHeader.java | 49 + src/org/zoolu/sip/message/BaseMessage.java | 1255 +++++++++++++++++ .../zoolu/sip/message/BaseMessageFactory.java | 373 +++++ src/org/zoolu/sip/message/BaseMessageOtp.java | 416 ++++++ src/org/zoolu/sip/message/BaseSipMethods.java | 77 + .../zoolu/sip/message/BaseSipResponses.java | 127 ++ src/org/zoolu/sip/message/Message.java | 209 +++ src/org/zoolu/sip/message/MessageFactory.java | 136 ++ src/org/zoolu/sip/message/SipMethods.java | 68 + src/org/zoolu/sip/message/SipResponses.java | 62 + .../sip/provider/ConnectedTransport.java | 48 + .../sip/provider/ConnectionIdentifier.java | 60 + .../zoolu/sip/provider/DialogIdentifier.java | 43 + src/org/zoolu/sip/provider/Identifier.java | 69 + .../zoolu/sip/provider/MethodIdentifier.java | 42 + src/org/zoolu/sip/provider/SipInterface.java | 169 +++ .../sip/provider/SipInterfaceListener.java | 36 + src/org/zoolu/sip/provider/SipParser.java | 411 ++++++ .../sip/provider/SipPromisqueInterface.java | 43 + src/org/zoolu/sip/provider/SipProvider.java | 1229 ++++++++++++++++ .../SipProviderExceptionListener.java | 35 + .../sip/provider/SipProviderListener.java | 36 + src/org/zoolu/sip/provider/SipStack.java | 258 ++++ src/org/zoolu/sip/provider/TcpTransport.java | 167 +++ .../sip/provider/TransactionIdentifier.java | 54 + src/org/zoolu/sip/provider/Transport.java | 47 + .../zoolu/sip/provider/TransportListener.java | 39 + src/org/zoolu/sip/provider/UdpTransport.java | 124 ++ .../sip/transaction/AckTransactionClient.java | 81 ++ .../sip/transaction/AckTransactionServer.java | 147 ++ .../AckTransactionServerListener.java | 36 + .../transaction/InviteTransactionClient.java | 187 +++ .../transaction/InviteTransactionServer.java | 246 ++++ .../InviteTransactionServerListener.java | 38 + .../zoolu/sip/transaction/Transaction.java | 191 +++ .../sip/transaction/TransactionClient.java | 177 +++ .../TransactionClientListener.java | 46 + .../sip/transaction/TransactionServer.java | 177 +++ .../TransactionServerListener.java | 38 + src/org/zoolu/tools/Archive.java | 164 +++ src/org/zoolu/tools/Assert.java | 59 + src/org/zoolu/tools/AssertException.java | 43 + src/org/zoolu/tools/Base64.java | 182 +++ src/org/zoolu/tools/Configurable.java | 34 + src/org/zoolu/tools/Configure.java | 111 ++ src/org/zoolu/tools/DateFormat.java | 169 +++ src/org/zoolu/tools/ExceptionPrinter.java | 46 + src/org/zoolu/tools/HashSet.java | 65 + src/org/zoolu/tools/InnerTimer.java | 49 + src/org/zoolu/tools/InnerTimerListener.java | 33 + src/org/zoolu/tools/InnerTimerST.java | 49 + src/org/zoolu/tools/Iterator.java | 56 + src/org/zoolu/tools/Log.java | 209 +++ src/org/zoolu/tools/LogLevel.java | 39 + src/org/zoolu/tools/MD5.java | 670 +++++++++ src/org/zoolu/tools/MD5.java.saved | 670 +++++++++ src/org/zoolu/tools/MD5OTP.java | 190 +++ src/org/zoolu/tools/Mangle.java | 267 ++++ src/org/zoolu/tools/MessageDigest.java | 82 ++ src/org/zoolu/tools/Parser.java | 468 ++++++ src/org/zoolu/tools/Random.java | 109 ++ src/org/zoolu/tools/RotatingLog.java | 137 ++ src/org/zoolu/tools/SimpleDigest.java | 143 ++ src/org/zoolu/tools/Timer.java | 133 ++ src/org/zoolu/tools/TimerListener.java | 32 + src/overview.html | 20 + 249 files changed, 37596 insertions(+) create mode 100644 Makefile create mode 100644 README.txt create mode 100644 changes.txt create mode 100644 lib/COPYRIGHT.txt create mode 100644 lib/license.txt create mode 100644 lib/server.jar create mode 100644 lib/sip.jar create mode 100644 lib/ua.jar create mode 100644 make-HOW-TO.txt create mode 100644 makefile-config create mode 100644 media/local/media/icon.gif create mode 100644 media/local/media/logo.gif create mode 100644 media/local/ua/call.gif create mode 100644 media/local/ua/hangup.gif create mode 100644 media/local/ua/off.wav create mode 100644 media/local/ua/on.wav create mode 100644 media/local/ua/ring.wav create mode 100644 mjsip.cfg.txt create mode 100644 src/COPYRIGHT.txt create mode 100644 src/license.txt create mode 100644 src/local/media/AudioClipPlayer.java create mode 100644 src/local/media/AudioClipPlayerListener.java create mode 100644 src/local/media/AudioInput.java create mode 100644 src/local/media/AudioOutput.java create mode 100644 src/local/media/AudioOutput.java.saved create mode 100644 src/local/media/AudioOutputStream.java create mode 100644 src/local/media/AudioReceiver.java create mode 100644 src/local/media/AudioSender.java create mode 100644 src/local/media/ExtendedPipedInputStream.java create mode 100644 src/local/media/ExtendedPipedOutputStream.java create mode 100644 src/local/media/G711.java create mode 100644 src/local/media/JMediaReceiver.java create mode 100644 src/local/media/JMediaReceiverListener.java create mode 100644 src/local/media/JMediaSender.java create mode 100644 src/local/media/JVisualReceiver.java create mode 100644 src/local/media/Mixer.java create mode 100644 src/local/media/MixerLine.java create mode 100644 src/local/media/RtpStreamReceiver.java create mode 100644 src/local/media/RtpStreamSender.java create mode 100644 src/local/media/RtpStreamTranslator.java create mode 100644 src/local/media/SplitterLine.java create mode 100644 src/local/media/ToneInputStream.java create mode 100644 src/local/media/UdpStreamReceiver.java create mode 100644 src/local/media/UdpStreamSender.java create mode 100644 src/local/net/KeepAliveSip.java create mode 100644 src/local/net/KeepAliveUdp.java create mode 100644 src/local/net/RtpFlow.java create mode 100644 src/local/net/RtpInputFlow.java create mode 100644 src/local/net/RtpOutputFlow.java create mode 100644 src/local/net/RtpPacket.java create mode 100644 src/local/net/RtpSocket.java create mode 100644 src/local/net/UdpMultiRelay.java create mode 100644 src/local/net/UdpRelay.java create mode 100644 src/local/net/UdpRelayListener.java create mode 100644 src/local/server/AuthenticationServer.java create mode 100644 src/local/server/AuthenticationServerImpl.java create mode 100644 src/local/server/AuthenticationService.java create mode 100644 src/local/server/AuthenticationServiceImpl.java create mode 100644 src/local/server/CallLogger.java create mode 100644 src/local/server/CallLoggerImpl.java create mode 100644 src/local/server/DomainRoutingRule.java create mode 100644 src/local/server/LocationService.java create mode 100644 src/local/server/LocationServiceImpl.java create mode 100644 src/local/server/PrefixRoutingRule.java create mode 100644 src/local/server/Proxy.java create mode 100644 src/local/server/Proxy.java.saved create mode 100644 src/local/server/Redirect.java create mode 100644 src/local/server/Registrar.java create mode 100644 src/local/server/Repository.java create mode 100644 src/local/server/RoutingRule.java create mode 100644 src/local/server/ServerEngine.java create mode 100644 src/local/server/ServerProfile.java create mode 100644 src/local/server/StatefulProxy.java create mode 100644 src/local/server/StatefulProxyState.java create mode 100644 src/local/ua/CommandLineMA.java create mode 100644 src/local/ua/CommandLinePA.java create mode 100644 src/local/ua/CommandLineUA.java create mode 100644 src/local/ua/GraphicalUA.java create mode 100644 src/local/ua/ImagePanel.java create mode 100644 src/local/ua/JAudioLauncher.java create mode 100644 src/local/ua/JMFAudioLauncher.java create mode 100644 src/local/ua/JMFVideoLauncher.java create mode 100644 src/local/ua/Jukebox.java create mode 100644 src/local/ua/MediaLauncher.java create mode 100644 src/local/ua/MessageAgent.java create mode 100644 src/local/ua/MessageAgentListener.java create mode 100644 src/local/ua/MiniJukebox.java create mode 100644 src/local/ua/PopupFrame.java create mode 100644 src/local/ua/PresenceAgent.java create mode 100644 src/local/ua/PresenceAgentListener.java create mode 100644 src/local/ua/RATLauncher.java create mode 100644 src/local/ua/RegisterAgent.java create mode 100644 src/local/ua/RegisterAgentListener.java create mode 100644 src/local/ua/StringList.java create mode 100644 src/local/ua/UserAgent.java create mode 100644 src/local/ua/UserAgentListener.java create mode 100644 src/local/ua/UserAgentProfile.java create mode 100644 src/local/ua/VICLauncher.java create mode 100644 src/org/zoolu/net/IpAddress.java create mode 100644 src/org/zoolu/net/SocketAddress.java create mode 100644 src/org/zoolu/net/TcpConnection.java create mode 100644 src/org/zoolu/net/TcpConnectionListener.java create mode 100644 src/org/zoolu/net/TcpServer.java create mode 100644 src/org/zoolu/net/TcpServerListener.java create mode 100644 src/org/zoolu/net/TcpSocket.java create mode 100644 src/org/zoolu/net/UdpPacket.java create mode 100644 src/org/zoolu/net/UdpProvider.java create mode 100644 src/org/zoolu/net/UdpProviderListener.java create mode 100644 src/org/zoolu/net/UdpSocket.java create mode 100644 src/org/zoolu/sdp/AttributeField.java create mode 100644 src/org/zoolu/sdp/ConnectionField.java create mode 100644 src/org/zoolu/sdp/MediaDescriptor.java create mode 100644 src/org/zoolu/sdp/MediaField.java create mode 100644 src/org/zoolu/sdp/OriginField.java create mode 100644 src/org/zoolu/sdp/SdpField.java create mode 100644 src/org/zoolu/sdp/SdpParser.java create mode 100644 src/org/zoolu/sdp/SessionDescriptor.java create mode 100644 src/org/zoolu/sdp/SessionNameField.java create mode 100644 src/org/zoolu/sdp/TimeField.java create mode 100644 src/org/zoolu/sip/address/NameAddress.java create mode 100644 src/org/zoolu/sip/address/SipURL.java create mode 100644 src/org/zoolu/sip/authentication/DigestAuthentication.java create mode 100644 src/org/zoolu/sip/call/Call.java create mode 100644 src/org/zoolu/sip/call/CallListener.java create mode 100644 src/org/zoolu/sip/call/CallListenerAdapter.java create mode 100644 src/org/zoolu/sip/call/ExtendedCall.java create mode 100644 src/org/zoolu/sip/call/ExtendedCallListener.java create mode 100644 src/org/zoolu/sip/call/SdpTools.java create mode 100644 src/org/zoolu/sip/dialog/Dialog.java create mode 100644 src/org/zoolu/sip/dialog/DialogInfo.java create mode 100644 src/org/zoolu/sip/dialog/ExtendedInviteDialog.java create mode 100644 src/org/zoolu/sip/dialog/ExtendedInviteDialogListener.java create mode 100644 src/org/zoolu/sip/dialog/InviteDialog.java create mode 100644 src/org/zoolu/sip/dialog/InviteDialogListener.java create mode 100644 src/org/zoolu/sip/dialog/NotifierDialog.java create mode 100644 src/org/zoolu/sip/dialog/NotifierDialogListener.java create mode 100644 src/org/zoolu/sip/dialog/SubscriberDialog.java create mode 100644 src/org/zoolu/sip/dialog/SubscriberDialogListener.java create mode 100644 src/org/zoolu/sip/header/AcceptHeader.java create mode 100644 src/org/zoolu/sip/header/AlertInfoHeader.java create mode 100644 src/org/zoolu/sip/header/AllowEventsHeader.java create mode 100644 src/org/zoolu/sip/header/AllowHeader.java create mode 100644 src/org/zoolu/sip/header/AuthenticationHeader.java create mode 100644 src/org/zoolu/sip/header/AuthenticationInfoHeader.java create mode 100644 src/org/zoolu/sip/header/AuthorizationHeader.java create mode 100644 src/org/zoolu/sip/header/BaseSipHeaders.java create mode 100644 src/org/zoolu/sip/header/CSeqHeader.java create mode 100644 src/org/zoolu/sip/header/CallIdHeader.java create mode 100644 src/org/zoolu/sip/header/ContactHeader.java create mode 100644 src/org/zoolu/sip/header/ContentLengthHeader.java create mode 100644 src/org/zoolu/sip/header/ContentTypeHeader.java create mode 100644 src/org/zoolu/sip/header/DateHeader.java create mode 100644 src/org/zoolu/sip/header/EndPointHeader.java create mode 100644 src/org/zoolu/sip/header/EventHeader.java create mode 100644 src/org/zoolu/sip/header/ExpiresHeader.java create mode 100644 src/org/zoolu/sip/header/FromHeader.java create mode 100644 src/org/zoolu/sip/header/Header.java create mode 100644 src/org/zoolu/sip/header/ListHeader.java create mode 100644 src/org/zoolu/sip/header/MaxForwardsHeader.java create mode 100644 src/org/zoolu/sip/header/MultipleHeader.java create mode 100644 src/org/zoolu/sip/header/NameAddressHeader.java create mode 100644 src/org/zoolu/sip/header/OptionHeader.java create mode 100644 src/org/zoolu/sip/header/ParametricHeader.java create mode 100644 src/org/zoolu/sip/header/ProxyAuthenticateHeader.java create mode 100644 src/org/zoolu/sip/header/ProxyAuthorizationHeader.java create mode 100644 src/org/zoolu/sip/header/ProxyRequireHeader.java create mode 100644 src/org/zoolu/sip/header/RecordRouteHeader.java create mode 100644 src/org/zoolu/sip/header/ReferToHeader.java create mode 100644 src/org/zoolu/sip/header/ReferredByHeader.java create mode 100644 src/org/zoolu/sip/header/RequestLine.java create mode 100644 src/org/zoolu/sip/header/RequireHeader.java create mode 100644 src/org/zoolu/sip/header/RouteHeader.java create mode 100644 src/org/zoolu/sip/header/ServerHeader.java create mode 100644 src/org/zoolu/sip/header/SipDateHeader.java create mode 100644 src/org/zoolu/sip/header/SipHeaders.java create mode 100644 src/org/zoolu/sip/header/StatusLine.java create mode 100644 src/org/zoolu/sip/header/SubjectHeader.java create mode 100644 src/org/zoolu/sip/header/SubscriptionStateHeader.java create mode 100644 src/org/zoolu/sip/header/SupportedHeader.java create mode 100644 src/org/zoolu/sip/header/ToHeader.java create mode 100644 src/org/zoolu/sip/header/UnsupportedHeader.java create mode 100644 src/org/zoolu/sip/header/UserAgentHeader.java create mode 100644 src/org/zoolu/sip/header/ViaHeader.java create mode 100644 src/org/zoolu/sip/header/WwwAuthenticateHeader.java create mode 100644 src/org/zoolu/sip/message/BaseMessage.java create mode 100644 src/org/zoolu/sip/message/BaseMessageFactory.java create mode 100644 src/org/zoolu/sip/message/BaseMessageOtp.java create mode 100644 src/org/zoolu/sip/message/BaseSipMethods.java create mode 100644 src/org/zoolu/sip/message/BaseSipResponses.java create mode 100644 src/org/zoolu/sip/message/Message.java create mode 100644 src/org/zoolu/sip/message/MessageFactory.java create mode 100644 src/org/zoolu/sip/message/SipMethods.java create mode 100644 src/org/zoolu/sip/message/SipResponses.java create mode 100644 src/org/zoolu/sip/provider/ConnectedTransport.java create mode 100644 src/org/zoolu/sip/provider/ConnectionIdentifier.java create mode 100644 src/org/zoolu/sip/provider/DialogIdentifier.java create mode 100644 src/org/zoolu/sip/provider/Identifier.java create mode 100644 src/org/zoolu/sip/provider/MethodIdentifier.java create mode 100644 src/org/zoolu/sip/provider/SipInterface.java create mode 100644 src/org/zoolu/sip/provider/SipInterfaceListener.java create mode 100644 src/org/zoolu/sip/provider/SipParser.java create mode 100644 src/org/zoolu/sip/provider/SipPromisqueInterface.java create mode 100644 src/org/zoolu/sip/provider/SipProvider.java create mode 100644 src/org/zoolu/sip/provider/SipProviderExceptionListener.java create mode 100644 src/org/zoolu/sip/provider/SipProviderListener.java create mode 100644 src/org/zoolu/sip/provider/SipStack.java create mode 100644 src/org/zoolu/sip/provider/TcpTransport.java create mode 100644 src/org/zoolu/sip/provider/TransactionIdentifier.java create mode 100644 src/org/zoolu/sip/provider/Transport.java create mode 100644 src/org/zoolu/sip/provider/TransportListener.java create mode 100644 src/org/zoolu/sip/provider/UdpTransport.java create mode 100644 src/org/zoolu/sip/transaction/AckTransactionClient.java create mode 100644 src/org/zoolu/sip/transaction/AckTransactionServer.java create mode 100644 src/org/zoolu/sip/transaction/AckTransactionServerListener.java create mode 100644 src/org/zoolu/sip/transaction/InviteTransactionClient.java create mode 100644 src/org/zoolu/sip/transaction/InviteTransactionServer.java create mode 100644 src/org/zoolu/sip/transaction/InviteTransactionServerListener.java create mode 100644 src/org/zoolu/sip/transaction/Transaction.java create mode 100644 src/org/zoolu/sip/transaction/TransactionClient.java create mode 100644 src/org/zoolu/sip/transaction/TransactionClientListener.java create mode 100644 src/org/zoolu/sip/transaction/TransactionServer.java create mode 100644 src/org/zoolu/sip/transaction/TransactionServerListener.java create mode 100644 src/org/zoolu/tools/Archive.java create mode 100644 src/org/zoolu/tools/Assert.java create mode 100644 src/org/zoolu/tools/AssertException.java create mode 100644 src/org/zoolu/tools/Base64.java create mode 100644 src/org/zoolu/tools/Configurable.java create mode 100644 src/org/zoolu/tools/Configure.java create mode 100644 src/org/zoolu/tools/DateFormat.java create mode 100644 src/org/zoolu/tools/ExceptionPrinter.java create mode 100644 src/org/zoolu/tools/HashSet.java create mode 100644 src/org/zoolu/tools/InnerTimer.java create mode 100644 src/org/zoolu/tools/InnerTimerListener.java create mode 100644 src/org/zoolu/tools/InnerTimerST.java create mode 100644 src/org/zoolu/tools/Iterator.java create mode 100644 src/org/zoolu/tools/Log.java create mode 100644 src/org/zoolu/tools/LogLevel.java create mode 100644 src/org/zoolu/tools/MD5.java create mode 100644 src/org/zoolu/tools/MD5.java.saved create mode 100644 src/org/zoolu/tools/MD5OTP.java create mode 100644 src/org/zoolu/tools/Mangle.java create mode 100644 src/org/zoolu/tools/MessageDigest.java create mode 100644 src/org/zoolu/tools/Parser.java create mode 100644 src/org/zoolu/tools/Random.java create mode 100644 src/org/zoolu/tools/RotatingLog.java create mode 100644 src/org/zoolu/tools/SimpleDigest.java create mode 100644 src/org/zoolu/tools/Timer.java create mode 100644 src/org/zoolu/tools/TimerListener.java create mode 100644 src/overview.html diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d7ba210 --- /dev/null +++ b/Makefile @@ -0,0 +1,109 @@ +# ********************************************************************** +# * MJSIP MAKEFILE * +# ********************************************************************** +# +# This is a make file that builds everything. +# this works with the gnu make tool. +# If you are working with MS Windows, you can install +# cygwin (http://www.cygwin.org) or +# djgpp (http://www.delorie.com/djgpp) +# +# Major make targets: +# +# all cleans, builds everything +# sip builds sip.jar +# server builds server.jar +# ua builds ua.jar +# qsip builds qsip.jar +# gw builds gw.jar +# +# ********************************************************************** + +ROOT= . +include $(ROOT)/makefile-config + + +DOCDIR= doc +SRCDIR= src +CLASSDIR= classes +LIBDIR= lib +#LOGDIR= log + + +ifeq (${OS},Windows) + COLON= ; +else + COLON= : +endif + + +MJSIP_LIBS= $(LIBDIR)/sip.jar + +NAMESPACE= org.zoolu +NAMESPACE_PATH= org/zoolu + +#SIP_PACKAGES= address header message provider transaction dialog +SIP_PACKAGES= $(notdir $(wildcard $(SRCDIR)/$(NAMESPACE_PATH)/sip/*)) + + +#%.class: %.java +# $(JAVAC) $< + + +# **************************** Default action ************************** +default: +# $(MAKE) all + @echo MjSIP: select the package you want to build + + +# ******************************** Cleans ****************************** +clean: + @echo make clean: to be implemented.. + + +cleanlogs: + cd $(LOGDIR);$(RM) *.log; cd.. + + +# ****************************** Builds all **************************** +all: + $(MAKE) sip + $(MAKE) server + $(MAKE) ua + + + +# *************************** Creates sip.jar ************************** +sip: + @echo ------------------ MAKING SIP ------------------ + cd $(SRCDIR); \ + $(JAVAC) -d ../$(CLASSDIR) $(NAMESPACE_PATH)/tools/*.java $(NAMESPACE_PATH)/net/*.java $(NAMESPACE_PATH)/sdp/*.java; \ + $(JAVAC) -classpath ../$(CLASSDIR) -d ../$(CLASSDIR) $(addsuffix /*.java,$(addprefix $(NAMESPACE_PATH)/sip/,$(SIP_PACKAGES))); \ + cd .. + + cd $(CLASSDIR); \ + $(JAR) -cf ../$(MJSIP_LIBS) $(addprefix $(NAMESPACE_PATH)/,tools net sdp sip) -C ../$(LIBDIR) COPYRIGHT.txt -C ../$(LIBDIR) license.txt; \ + cd .. + + + +# ************************** Creates server.jar ************************ +server: + @echo ----------------- MAKING SERVER ---------------- + $(JAVAC) -classpath "$(MJSIP_LIBS)" -d $(CLASSDIR) $(addsuffix /*.java,$(addprefix $(SRCDIR)/local/,server)) + + cd $(CLASSDIR); \ + $(JAR) -cf ../$(LIBDIR)/server.jar $(addprefix local/,server); \ + cd .. + + + +# **************************** Creates ua.jar ************************** +ua: + @echo ------------------- MAKING UA ------------------ + $(JAVAC) -classpath "$(MJSIP_LIBS)" -d $(CLASSDIR) $(addsuffix /*.java,$(addprefix $(SRCDIR)/local/,net media ua)) + + cd $(CLASSDIR); \ + $(JAR) -cf ../$(LIBDIR)/ua.jar $(addprefix local/,net media ua) -C .. /media/local/ua; \ + cd .. + diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..88b8843 --- /dev/null +++ b/README.txt @@ -0,0 +1,31 @@ + + My Java SIP Stack (MjSip) +__________________________________________________ +author: Luca Veltri (luca.veltri@unipr.it) +date: 31/2/2004 + + + +This package includes: +-------------------------------------------------------------------------- +- README.txt : this file +- lib : folder containing the jar file(s) +- src : folder containing the java source codes +- classes : folder containing the classes byte codes +- log : default folder where the log files are saved +- bin : folder with some scripts ('bin_win' for windows OS, and 'bin_linux' for linux OS) +- config : default folder with configuration files +- server.cfg : a simple server configuration file +- xxx.cfg : other configuration files +- users.db : the db file for the embedded location server +- proxy.bat : script for starting a stateless proxy server +- sproxy.bat : script for starting a stateful proxy server +- registrar.bat : script for starting a registrar server +- redirect.bat : script for starting a redirect server +- uac.bat : script for starting a command-line UA +- uaw.bat : script for starting a graphical UA +- Makefile : make file for building everyting (it uses configuration parameters from file 'build-config') +- build-config : some configurations used in the Makefile +- mjsip.cfg.txt : the complete list of all configuration parameters + + diff --git a/changes.txt b/changes.txt new file mode 100644 index 0000000..81520f5 --- /dev/null +++ b/changes.txt @@ -0,0 +1,497 @@ +Changes: +---------------------------------------------------- +28/2/2006 +-add: ExtendedCallListener,ExtendedCall,CallListenerAdapter,UserAgent: new ExtendedCallListener methods onCallTransferAccepted() and onCallTransferRefused() +-change: ExtendedCallListener: changed method onCallTransferFailure(call,code,reason,msg) into onCallTransferFailure(call,reason,notify) + +23/2/2006 +-fix: GraphicalUA: command-line option 'call_to' was disabled: re-activated +-change: GraphicalUA,CommandLineUA: renamed option '-o' to '-n' +-add: GraphicalUA,CommandLineUA: added option '-o' for setting the outbound_proxy + +14/2/2006 +-change: InviteDialog: renamed method isCallActive() to isSessionActive() + +12/2/2006 +-change: ParametersHeader: renamed method getParameters() to getParameterNames() +-change: header: renamed class ParametersHeader to ParametricHeader +-change: BaseMessageFactory: removed parameter body from method createResponse(req,code,reason,contact) +-change: BaseMessage: removed method setNullBody() + +11/2/2006 +-add: dialog: new class DialogInformation that collects basic Dialog attributes (URLs, tags, route, etc.) +-change: Dialog: it now extends DialogInformation +-change: Dialog,InviteDialog: listener is automatically added and removed within metods changeStatus() and update() +-change: SubscriberDialog: minor changes +-change: NotifierDialog: minor changes + +4/2/2006 +-change: SipURL: minor changes to costructors; port is added only if >0 +-change: dialog: changed name and visibility of methods that manage the dialog state + +29/1/2006 +-add: UserAgentProfile: new method updateUserAddress() +-change: ua: auto answer mode became -y +-change: AckTransactionServer: renamed method onAckSrvTimeout() to onSrvAckTimeout() +-change: CallListener: in method onCallIncoming() changed order of caller and callee parameters +-change: InviteDialogListener: in method onDlgInvite() changed order of caller and callee parameters +-change: UserAgentListener: in method onUaCallIncoming() changed order of caller and callee parameters +-change: org.zoolu.sip.transaction: minor changes to transaction initialization and logs +-change: Transaction: renamed attribute 'method' to 'request' +-change: InviteDialog: added costructor for an already received INVITE request (for server-side InviteDialog) +-change: NotifierDialog: added costructor for an already received SUBSCRIBE request + +28/1/2006 +-change: Dialog: method update() automatically updates the dialog_id used as listener key with the SipProvider; Dialog now implements SipProivderListener +-change: SubscriberDialog, NotifierDialog: changed the implementation + +23/1/2006 +-add: header: added headers ListHeader, AllowEventsHeader +-change: AllowHeader: now it extends ListHeader +-change: SipHeaders: added "Allow-Events" + +21/1/2006 +-fix: Random: fixed an error in method nextHexString() + +20/1/2006 +-change: ServerEngine,Proxy: changed the rule for performing loop detection: added temporary Loop-Tag header filed +-change: SipProvider: minor changes within method pickBranch(msg) + +19/1/2006 +-change: ServerEngine: changed the rule for performing loop detection: no detection for messagres the server is responsible for + +18/1/2006 +-change: ServerEngine: new method isResponsibleFor(msg) replaces method isTargetOf(msg) + +17/1/2006 +-change: ServerEngine: renamed method isForLocalServer() to isTargetOf() +-change: ServerEngine,Proxy: renamed method matchesDomainName() to isResponsibleFor() + +1/1/2006 +-change: ServerEngine,Proxy: renamed method isLocalDomain() to matchesDomainName() +-change: ServerEngine,Registrar: renamed method localDomains() to getLocalDomains() + +29/12/2005 +-add: SipURL: added methods hasLr() addLr() for processing param "lr" + +25/12/2005 +-change: Dialog: renamed T_Client and T_Server to UAC and UAS respectively +-change: InviteDialog: close the dialog when no ACK is received +-change: InviteDialog: close the dialog when no response is received also for a re-INVITE + +21/12/2005 +-change: RtpStreamSender: changed the synchronization method: the frame time is now simply calculated from frame length and sample rate +-change: RtpStreamSender: added attribute sync_adj (milliseconds) that accellerates the frame rate respect to the nominal value in order to compensate program latencies +-change: JAudioLauncher: now it forces synchronization when capturing system audio +-change: UserAgentProfile: changed default attribute value of audio_frame_size from 500 to 160 + +17/12/2005 +-fix: ServerEngine: when validating an ACK request do not generate error response +-fix: ServerEngine: do not detect a loop for requests with Route header field +-fix: AckTransactionServer: 2xx retransmission did not use exponential backoff; now it starts after T1 seconds and doubles for each retransmission until it reaches T2 seconds (as defined in RFC3261) + +11/12/2005 +-change: ServerProfile: renamed parameter pstn_gateways to phone_routing_rules +-fix: Proxy,StatefulProxy: now in case of is_open_proxy=false does not filter messages with to_url corresponding to local users +-add: AuthenticationServer,AuthenticationServerImpl: added Proxy-Authentication +-add: Proxy,StatefulProxy: added Proxy-Authentication +-add: ExtendedInviteDialog,ExtendedCall,UserAgent: added proxy and server authentication +-fix: UserAgent: fixed JMFAudioLauncher and JMFVideoLauncher costructor declaration with java reflection ('int' parameters were not declared java.lang.Integer.TYPE) + +10/12/2005 +-change: KeepAliveSip,KeepAliveUdp: moved to package local.net +-add: SipProvider: added option 'log_all_packets'; defalut value is false (no) +-change: SipProvider: improved message logs +-fix: Transaction: the t_number value ,that is the transaction's sqn, was not incremented; fixed +-change: Proxy: sends a 484 if no username is set in non-REGISTER requests addressed to local server +-add: Random: new methods nextBytes(len), nextString(len), nextNumString(), nextHexString() +-change: SipProvider: used method Random.nextNumString(len) for random token generation +-add: server: added new interface RoutingRule; added new class PrefixRoutingRule +-change: ServerProfile,Proxy: use new class PrefixRoutingRule + +9/12/2005 +-add: ServerEngine,Proxy: implementted loop detection, distinguishing spirals, as suggested in RFC3261 +-change: AddressResolverKeepAlive,Makefile: updated AddressResolverKeepAlive and Makefile with new KeepAliveSip + +4/12/2005 +-add: KeepAliveUdp,KeepAliveSip: new classes media.KeepAliveUdp and media.KeepAliveSip +-add: RegisterAgent: added support for SIP keep-alive (by means of KeepAliveSip) +-add: CommandLineUA,GraphicalUA: added support for SIP keep-alive when registering +-fix: UserAgentProfile: 'expires' value eas not read from configuration file; fixed. +-change: SipStack: attribute expires renamed to default_expires +-change: ServerProfile: new attribute expires +-add: SocketAddress: added costructor SocketAddress(String) +-change: SipProvider: attributes outbound_addr and outbound_port replaced by outbound_proxy +-change: ServerProfile,Registrar: pstn_gw_addr,pstn_gw_port,pstn_gw_prefix have been generalize by a list of pairs {pstn_gateway,pstn_prefix} that are the Vectors pstn_gateways and pstn_prefixes + +3/12/2005 +-change: RegisterAgent: renew_time for succesive re-registrations is updated with the expire value(s) in the 2xx response + +1/12/2005 +-fix: Registrar: do not generate a 501 response for ACK methods + +28/11/2005 +-change: AuthenticationService,AuthenticationServiceImpl,LdapAuthenticationServiceImpl: removed user attributes 'rand' and 'seqn' +-add: AuthenticationServiceImpl: file aaa.db now can contain passwd in plain text (passwd=) + +27/11/2005 +-change: AuthenticationServer,Registrar: AuthenticationServer becomes an interface +-add: AuthenticationServerImpl: new class + +24/11/2005 +-change: UserAgentProfile,: renamed attribute audio_rate to audio_sample_rate +-add: UserAgentProfile,JAudioLauncher,UserAgent: in UserAgentProfile new attributes audio_sample_size and audio_frame_size + +21/11/2005 +-change: Dialog: status becomes protected (it was private) + +19/11/2005 +-fix: JAdudioLauncher: fixed a problem with DatagramSocket: the socket was not explicitly closed when halting audio; fixed +-add: UserAgentProfile,CommandLineUA,GraphicalUA: added command-line parameters -d (debug level) and --log-path + +18/11/2005 +-change: UserAgent,UserAgentProfile,CommandLineUA,GraphicalUA: added auto-accept time; UserAgentProfile.auto_accept replaced by UserAgentProfile.accept_time + +13/11/2005 +-change: RtpStreamSender,RtpStreamReceiver: by default, the debug mode is disabled (DEBUG=false) +-change: RtpStreamSender,RtpStreamReceiver: attribute DEBUG is now public and not final +-change: RtpStreamReceiver: attributes BUFFER_SIZE and SO_TIMEOUT are now public +-change: RtpSocket: now method close() does not close the DatagramSocket +-add: media: new classes MixerLine, SplitterLine, Mixer, ExtendedPipedInputStream, ExtendedPipedOutputStream + +12/11/2005 +-add: added new class AudioOutputStream for audio conversion and output (javax.sound contains only AudioInputStream, and no conversion seems to be possible in output) +-change: AudioOutput: new AudioOutput class, without the internal Thread for audio playback +-change: UserAgentProfile: removed SipStack initialization (and dependaces) +-change: UserAgentProfile: added costructor UserAgentProfile() + +11/11/2005 +-add: GraphicalUA, CommandLineUA: use audio as default media in case of.. +-fix: GraphicalUA, CommandLineUA: some command-line option was not used (such as transfer_to); fixed +-add: GraphicalUA, CommandLineUA: new command-line parameters: -p, --via-addr, -m, --username, --realm, --passwd, --from-url, --contact-url +-change: GraphicalUA: the window title now reports the registered url (AOR), or (in case of registration failure) the contact url + +3/11/2005 +-change: a.cfg,b.cfg: bin_vic set to mbone/vic (inseted of mbone/rat!) +-change: UserAgent,CommandLineUA,GraphicalUA: AudioInput.initAudioLine() moved from CommandLineUA,GraphicalUA to UserAgent +-fix: Redirect: fixed a casting problem caused by a non-updated use of getTargets() method +-fix: UserAgent: patch to make rat working: in case of rat, do not load and play audio clips +-fix: Makefile: 'make server' did not include the local.auth package; fixed + +16/10/2005 +- fix: Registrar.java: myclass=Class.forName(authentication_service_class) in place of myclass=Class.forName(profile.authentication_service); +- add: UserAgentProfile, UserAgent: added realm parameter; if not defined it is taken from contact_url +- change: UserAgentProfile, UserAgent: contact_user becomes username; if not defined it is taken from contact_url +- change: UserAgent: used reflection to create a JMFAudioLauncher or JMFVideoLauncher +- change: AuthenticationServer(): removed methods getRealm(), getQopOptions(), getRand() +- change: AuthenticationServer(): removed parameter 'authentication_scheme' from the costructor; only 'Digest' is currently supported + +9/10/2005 +- change: Registrar: use reflection to init location_service and authentication_service +- add: CSeqHeader: added method incSequenceNumber() +- fix: RegisterAgent: when it tried to retransmit a deregistration (after had received 401) the Expires value was not set to 0; now fixed +- add: RegisterAgent: maximum number of authentication attempts is added +- change: RegisterAgent: after 401 responses, register methods are resent with the same Call-Id (and CSeq+1) + +8/10/2005 +- fix: AuthenticationServer: in case of authentication failure the Registrar sends a 403 instead of 400 +- add: local.server: new interface Repository +- change: LocationService: it now extends Repository +- change: AuthenticationService: AuthenticationService renamed to AuthenticationServiceImpl +- add: local.server: new interface AuthenticationService, extending Repositoy +- change: LocationServiceImpl: UserBinding becomes private; removed method getUserBinding() +- change: UserBinding: it becomes part of LocationServiceImpl +- change: LocationServiceImpl, AuthenticationServiceImpl: removed attribute 'default_fext' +- add: ServerProfile: added new parapeters: is_open_proxy, location_service, authentication_service +- add: Proxy, StatefulProxy: relay only message for or to local user if 'is_open_proxy' is not set +- add: AuthenticationServer: added method getAuthenticationInfoHeader() +- change: Registrar: used method AuthenticationServer.getAuthenticationInfoHeader() +- change: Makefile, local: package auth renamed to authentication + +3/10/2005 +- change: SipProvider: after call onReceivedMessage() for promisque mode, check the message validity (it could be changed for dropping..) + +21/9/2005 +- fix: SipProvider: when halted, it resets the listener tables, without setting them to 'null'; this overcome the Exception the could occured when trying to uses attribute 'listeners' or 'exception_listeners' after method halt() is called + +19/9/2005 +- fix: InviteDialog: the reception of a re-invite was not handled correctly (only 100 trying was sent); fixed +- fix: CommandLineUA, GraphicalUA: re-invitation option was not handled; fixed + +11/9/2005 +- add: InnerTimerST: new class InnerTimerST for executing all InnerTimerSTs in a single thread (it uses class java.util.Timer) +- add: SipStack: new parameter single_timer (default value is 'false') +- change: Timer: it uses InnerTimer or InnerTimerST based on new static attribute Timer.SINGLE_THREAD +- change: SipStack: it set Timer.SINGLE_THREAD to SipStack.single_timer attribute value +- change: Transaction: trasaction states are now int (instead of String) +- change: Dialog: dialog states are now int (instead of String) + +10/9/2005 +- add: BaseMessageOtp: new class BaseMessageOtp (one-time-parsing version of BaseMessage) +- add: ExceptionPrinter: new class org.zoolu.tools.ExceptionPrinter and j2me.ExceptionPrinter +- change: Log: it now uses class ExceptionPrinter +- change: SipProvier: replaced method Log.toHexString(long) with Log.toString(long,16) (for compatibility with j2me) +- add: Makefile: added 'net' target +- change: SipResponses, BaseSipResponses: method init() becomes protected; attribute is_init becomes private +- change: AuthenticationHeader: method isQuotedParameter(String) was final (no reason), now is static +- add: MessageDigest: new abstract class MessageDigest +- add: SimpleDigest: new class SimpleDigest +- change: MD5: now it extends MessageDigest +- change: SipProvider: it uses SimpleDigest in place of MD5 + +8/9/2005 +- add: BaseMessage: added methods getRecordRouteHeader() and removeRecordRouteHeader() +- fix: BaseMessage: getViaHeader(), getRecordRouteHeader(), getRouteHeader(), getContactHeader() have retuned multiple header fields in case of comma-separated fields: fixed + +6/9/2005 +- change: ua: renamed class VisualUA to GraphicalUA +- change: mjsip-cfg.txt: updated the example of configuration file server authentication attributes +- change: SipProvider: checks whether ttl is present before getting it from request-uri +- change: SipProvider: if maddr is used, add maddr and ttl to the top most Via field + +5/9/2005 +- change: RegisterAgentListener: added attribute RegisterAgent in callback methos +- change: UserAgentListener: added attribute UserAgent in callback methos +- change: MessageAgentListener: added attribute MessageAgent in callback methos + +3/9/2005 +- change: tools: renamed class OTP to MD5OTP +- add: MessageAgent: added method send(recipient,subject,content_type,content) +- change: MessageAgentListener: renamed onReceivedMessage() to onMaReceivedMessage() +- add: MessageAgentListener: added method onMaDeliveryFailure() +- change: package domotica: completly updated +- change: Makefile: domotica +- add: Archive: new public methods getFileURL(file) and getJarURL(jar,file) +- change: Archive: methods getImage(jar,file), getImageIcon(jar,file), getInputStream(jar,file), getAudioInputStream(jar,file) replaced by getImage(url), getImageIcon(url), etc. + +31/8/2005 +- change: BaseMessage: getContacts(), getVias(), getRecordRoutes(), and getRoutes() now return null if no such header fields are found +- fix: Register: in case of no Contact header field, it returns 200 Ok with the list contacts (fetching bindings) + +28/8/2005 +- add: server: added class AuthenticationServer +- add: ServerProfile: new parameter 'authentication_db' +- change: server: removed class AuthenticationRegistrar +- add: Registrar: added support for message authentication + +28/8/2005 +- change: SipProvider: removed method setViaAddress() +- add: UserAgentProfile: new parameter 'passwd' +- add: RegisterAgent: added support for digest-authentication +- add: ServerProfile: added parameters 'do_authentication' (and 'do_proxy') +- add: Makefile: ua: added DigestAuthentication +- change: AuthenticationService: added user check +- change: AuthenticationHeader: LWS_SEPARATOR=" " +- add: added methods add/set/hasQopOptionsParam() + +26/8/2005 +- change: ServerEngine: method validateRequest(msg) returns an error message (or null) in place of an error code (or 0) + +25/8/2005 +- change: AuthHeader: AuthHeader renamed to AuthenticationHeader + +24/8/2005 +- add: Makefile: server: added DigestAuthentication +- change: SipProvider: pickBranch(), pickCallId(), pickTag() now use org.zoolu.Random.nextLong() instead of MD5.digest() +- add: BaseMessageFactory: added method create2xxAckRequest(dialog,body) + +23/8/2005 +- add: AuthHeader: added parameter "nextnonce" +- change: server: removed class AkaRegistrar +- change: server: renamed class AuthenticationServer to AuthenticationService + +22/8/2005 +- fix: ServerProfile: domain-names 'auto-configuration' value didn't have any effect, fixed +- add: InviteDialog, InviteDialogListener: new methods onDlgByeSuccessResponse(dialog,code,reason,msg) and onDlgByeFailureResponse(dialog,code,reason,msg) in place of method onDlgByeResponse(dialog,msg) +- change: InviteDialog, InviteDialogListener: renamed methods onDlgInviteProvisionalResponse(), onDlgInviteSuccessResponse(), onDlgInviteRedirectResponse(), onDlgInviteFailureResponse() +- add: BaseMessage: getProxyAuthorizationHeader(), getProxyAuthenticateHeader(), etc. +- add: BaseMessage: getAcceptHeader(), getAlertInfoHeader(), getAllowHeader(), etc. +- fix: AttributeField: getAttributeValue() returned ':'; fixed +- add: new class local.auth.DigestAuthentication + +21/8/2005 +- add: UserAgent, UserAgentProfile, JAudioLauncher: audio can be read or written from/to a selected file + +20/8/2005 +- change: CommandLineUA: new method run(), simplified method main(), some other improvements +- change: VisualUA: new method run(), simplified method main() + +18/8/2005 +- change: ua: renamed classes RegisterUA and RegisterUAListener into RegisterAgent and RegisterAgentListener +- add: ua: new class CommandLineUA with static main method and call-back methods of class UserAgent +- change: bin: new scripts uac.bat and uac.sh +- add: ua: new class CommandLineMA with static main method and call-back methods of previous class MessageAgent +- add: ua: new class MessageAgentListener +- change: bin: new scripts ma.bat and ma.sh + +15/8/2005 +- add: SipProvider: added method addSipProviderListener(listener) equal to addSipProviderListener(ANY,listener) +- change: Call: ExtendedCall: removed costructors Call(sip_provider,from_url,contact_url,sdp,call_listener) +- change: Call: removed method getInviteDialog() +- add: Call: added method isOnCall() +- add: Transaction, Dialog: added method getSipProvider() +- add: Transaction, Dialog: added costructors Transaction(SipProvider) and Dialog(SipProvider) +- change: MessageFactory, BaseMessageFactory: removed parameter sip_provider in methods that have a dialog +- change: InviteDialog: attribute transaction substituted by invite_ts, ack_ts, and bye_ts + +24/7/2005 +- change: UdpSocket: costructor UdpSocket() becomes public +- add: media: added new classes UdpStreamReceiver and UdpStreamSender + +21/7/2005 +- fix: EndPointHeader: added "expires" to the ENDPOINT_PARAMS list, in order to overcome the problem of SIP (SIP bug?) when dealing with both URL parameters and header parameters + +19/7/2005 +- fix: ParametersHeader: fixed method removeParameter(String) + +18/7/2005 +- add: ContactHeader: added method removeExpires() + +10/7/2005 +- add: Assert: new class Assert +- change: Log: removed costructor Log(label,debuglevel) +- change: added reference to GPL license into SipProvider.java and SipStack.java files +- change: modified reference to LGPL license into MessageAgent.java file +- fix: AuthenticationInfoHeader: fixed hasParameter() getParameter() methods +- add: ProxyAuthenticateHeader, ProxyAuthorizationHeader, BaseSipHeaders: added new header +- add: AuthHeader: added methods getAuthScheme(), getParameters() +- add: AuthHeader: added LWS_SEPARATOR static attribute +- add: AuthHeader: added array of QUOTED_PARAMETERS; added methods addQuotedParameter(), addUnQuotedParameter(); changed method addParameter() +- change: ParametersHeader: method getParameters() now return a void Vector in case of no parameter (instead of null) +- change: Parser: renamed getWordUnquoted() into getWordSkippingQuoted(); renamed goToUnquoted() into goToSkippingQuoted() + +9/7/2005 +- add: RegisterUA, RegisterUAlistener: new classes +- change: UserAgent: now it uses RegisterUA for handling registrations +- fix: SipProvider: when sending through udp transport it erroneously returned conn_id!=null: now fixed +- change: MessageAgent: new version of command line MessageAgent +- change: UserAgent: now used inline reInvite() and transferTo() threads instead of nested classes +- change: Log: removed static method timestamp() +- change: Log: removed methods verifyThat(boolean), assertThat(boolean), and printWarning() + +7/7/2005 +- fix: SessionDescriptor: fixed the handling of session-type attributes + +5/7/2005 +- change: removed package org.zoolu.sipx; modified Makefile + +3/7/2005 +- add: new class j2me.BaseMessage + +29/6/2005 +- change: Parser: removed methods isSpace(); changed method subParser(len) +- change: SipParser: renamed and changed methods getHeader(), getHeader(hname), getDate(), getSipURL(), getNameAddress(); now they go to the end of the objects read or to the end of parser + +29/6/2005 +- change: Parser, SipParser: new costructor Parser(StringBuffer) +- change: BaseMessage, Message: String replaced with StringBuffer + +19/6/2005 +- fix: UserBinding.getData() fixed (it didn't return data) +- change: Makefile: now make mjsip does not obfuscate sipx.jar +- change: org.zoolu.sip.transaction: Transaction.startTransaction() method renamed to TransactionClient.request() or TransactionServer.request() +- change: Transaction: terminateTransaction() method renamed to terminate() +- change: TransactionServer, InviteTransactionServer: automatically starting the transaction with costructor TransactionServer(SipProvider,Message,Listener) +- change: InviteTransactionServer: new parameter auto_tryng in costructor InviteTransactionServer(SipProvider,Message,boolean,Listener) +- change: InviteTransactionServer: method auto100Tryng(bool) renamed to setAuto100Tryng(bool) +- change: AckTransactionServer: startTransaction() method renamed to respond() + +18/6/2005 +- change: LocationServiceImpl: added user check, before getting user's data +- change: LocationService: removed public method userToString(user) + +10/6/2005 +- fix: TransactionServer: fixed the initializiation of transaction_id in costructor TransactionServer(SipProvider,String,TransactionServerListener) + +2/6/2005 +- add: ExpiresHeader: added method getDate() +- change: getDeltaSeconds() now returns int; changed implementation of getDeltaSeconds() +- add: ContactHeader: added method getExpiresDate() +- change: ContactHeader: getExpires() now returns int; changed implementation of getExpires() and isExpired() +- change: UserBinding: getExpires() renamed getExpirationDate(), and it now use getExpiresDate() +- change: LocationService, LocationServiceImpl: getUserContactExpire() renamed to getUserContactExpirationDate() +- change: Registrar: updateRegistration() now returns directly the response message +- add: Registrar: updateRegistration() limits the expire value (lesser than SipStack.expires) +- add: Registrar: set the contact urls and expire values in REGISTER responses +- add: G711, RtpStreamTranslator: in local.media added classes G711 and RtpStreamTranslator + +29/5/2005 +- change: AudioSender, AudioReceiver: added command-line parameter '-U' for PCMU codec +- change: UserAgent, VisualUA: audio file is now 'audio.wav' (expected in 8000 mono PCMU format) +- add: AudioFormat: added method formatYYYYMMDD() for "YYYY-MM-DD hh:mm:ss.mmm" format +- change: CallLoggerImpl: now it uses AudioFormat.formatYYYYMMDD() +//- add: RtpStreamSender: added costructor with parameter 'src_port' +- fix: JAudioLauncher: when sending audio tone or file, now it uses the DatagramSocket opened on specific local_port +- fix: UserAgent, VisualUA: when sending audio tone or file, do not init (and get use of) the javax sound +- change: SipStack: NO_UA_INFO and NO_SERVER renamed to NONE +- change: SipProvider: NO_OUTBOUND renamed to NONE + +28/5/2005 +- change: Timer: removed the list of listener, removed methods addTemerListener(), removeTimerListener(), changed Timer costructors +- change: ExtendedSipProvider: now it uses ExtendedAddressResolver supporting keep-alive functionality +- change: AddressResolver: setBinding(): do not remove a binding if it is going to be set again +- change: AddressResolver: periodically refreshes the address cache and removes the expired bindings +- change: ServerProfile: renamed do_log_calls to call_log + +24/5/2005 +- change: SipStack: parameter expires becomes an int (it was a long) +- add: presence: new classes SubscriberDialog and NotifierDialog +- add: Event: new added parameters 'id' +- add: sipx: new SubscriptionStateHeader +- add: Messageadd: added method isSubscribe() +- change: MessageFactory: added parameter 'id' when creating SUBSCRIBE and NOTIFY methods + +23/5/2005 +- add: Parser: added static method equalIgnoreCase(char,char) +- add: Parser: added methods startsWith() and startsWithIgnoreCase() +- add: Parser: added methods indexOfIgnoreCase() and goToIgnoreCase() +- change: SipParser: changed indexOfHeader(String): now it is case-unsensitive +- change: SipProvider: Exceptions throwed by callback method onMessageException() are now captured + +19/5/2005 +- add: SessionDescriptor: added methods addAttribute(AttributeField attribute), addAttributes(Vector attribute_fields), removeAttributes(), hasAttribute(String attribute_name), getAttribute(String attribute_name), getAttributes(String attribute_name), getAttributes() + +15/5/2005 +- change: LocationService: removed method getName() +- change: LocationServiceImpl: renamed LocationServiceImplementation to LocationServiceImpl +- change: CallLoggerImpl: new CallLoggerImpl with setup-time and call-duration records (the previous version has been saved in CallLoggerImpl.java.saved) + +14/5/2005 +- add: SessionDescriptor: added method removeMediaDescriptor(String) +- add: UserProfile, UserAgent: added various new configuration parameters +- change: UserAgent: addAudioDescriptor becomes addMediaDescriptor +- add: Configure: added static attribute 'NONE' +- add: InviteDialog: dialog states become public (they were private) +- change: UserAgent, VisualUA: some changes on UA configuration and startup +- add: VisualUA: added redirect, automatic accept, transfer, and re-invite features +- add: server: added new class CallLogger +- add: Proxy, ServerProfile: added CallLogger support +- add: Log: new costructor Log(filename,logtag,loglevel,logsize,append) + +13/5/2005 +- change: Dialog, InviteDialog: removed method init(side,msg); used method update(side,msg) instead +- change: Dialog: attributes CLIENT and SERVER renamed to T_CLIENT and T_SERVER +- change: removed all DEBUG_PERFORMANCE statements +- change: renamed class ProxyState to StatefulProxyState + +12/5/2005 +- change: Dialog: dialog_sequence becomes dialog_counter +- change: Transaction: transaction_sequence becomes transaction_counter +- change: Dialog, InviteDialog: moved several attributes and methods from InviteDialog to Dialog + +11/5/2005 +- change: NameAddressHeader, EndPointHeader: now it is EndPointHeader: that remove eventual 'tag' or other EndPointHeader parameters (ENDPOINT_PARAMS) from returned NameAddress +- change: Dialog, InviteDialog: renamed class DialogState to Dialog; InviteDialog noe extends Dialog (instead of use it as attribute) + +10/5/2005 +- add: SipStack, SipProvider: current date and stack version +- fix: NameAddressHeader: indexOfFirstParam(): in case of no '>'-tag, do par.setPos(0).goToUnquoted(';').skipChar().skipWSP(); +- change: ParametersHeader: removeParameter(String): moved up (within the 'if' clause) and removed the line: par=new SipParser(header,par.getPos()); +- add: SipURL: added removeParameters() and removeParameter(String) methods +- fix: NameAddressHeader: remove eventual tag parameter from name-address +- add: SipProvider: added SipStack version and current time in the event log + +28/4/2005 +- add: SipStack, SipProvider: new parameter 'force_rport' that force the rport on every incoming requests +- change: SipProvider: renamed hasRport() into isRportSet() +- change: mjsip: new version: 1.5.1 \ No newline at end of file diff --git a/lib/COPYRIGHT.txt b/lib/COPYRIGHT.txt new file mode 100644 index 0000000..9aa8e35 --- /dev/null +++ b/lib/COPYRIGHT.txt @@ -0,0 +1,22 @@ +MjSip - http://www.mjsip.org + +Copyright (C) 2005 by Luca Veltri - University of Parma - Italy. +__________________________________________________ + + +MjSip is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +MjSip is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with MjSip; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +PLEASE READ CAREFULLY THE LICENSE DOCUMENT RECEIVED ALONG WITH THIS LIBRARY. \ No newline at end of file diff --git a/lib/license.txt b/lib/license.txt new file mode 100644 index 0000000..5b6e7c6 --- /dev/null +++ b/lib/license.txt @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/lib/server.jar b/lib/server.jar new file mode 100644 index 0000000000000000000000000000000000000000..6411e9fef7fa259e119e12e89b2827c27fcb9a11 GIT binary patch literal 40955 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%0DRdK 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̩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 VpR0jB_EQMiy#XfZ36NSr{tuVPi~9fo literal 0 HcmV?d00001 diff --git a/lib/ua.jar b/lib/ua.jar new file mode 100644 index 0000000000000000000000000000000000000000..d76fd16339fba41a92da15d807d41e85440278c6 GIT binary patch literal 276481 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 a7wsmOz*9;!Q|PuS6_F`7j==zctA7KHN3X{K literal 0 HcmV?d00001 diff --git a/media/local/ua/call.gif b/media/local/ua/call.gif new file mode 100644 index 0000000000000000000000000000000000000000..f6aa468758fda326917f4c7bde611981d30b09cc GIT binary patch literal 84 zcmZ?wbhEHbG+^LiXkcJqIKc4#|9{1wEQ|~cj0`$J0S1r^1Cw@7f98R=>lcNrzs3FR n$R6{(6^kT#B_(E@=upc!v2{{;$)~jYzs`OC$ItVNi@_QIX+t1Q literal 0 HcmV?d00001 diff --git a/media/local/ua/hangup.gif b/media/local/ua/hangup.gif new file mode 100644 index 0000000000000000000000000000000000000000..e502399f25819a6df23f4e98d517b69808b6268f GIT binary patch literal 89 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�IR~K#4OdQ)07|PrqCUF5pNX*+4R`;K?f@o+WJ*) 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 literal 0 HcmV?d00001 diff --git a/media/local/ua/on.wav b/media/local/ua/on.wav new file mode 100644 index 0000000000000000000000000000000000000000..91a65a3f5672b2bec5e05a2b5cecb8e7eb8907ea GIT binary patch literal 80856 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 QI3aLC;Do^c6M^^s4?b6#@&Et; literal 0 HcmV?d00001 diff --git a/mjsip.cfg.txt b/mjsip.cfg.txt new file mode 100644 index 0000000..60d0c21 --- /dev/null +++ b/mjsip.cfg.txt @@ -0,0 +1,494 @@ +# +# MjSip-1.6 Configuration File +# ________________________________________________________________ +# +# This file inclues all MjSip configuration parameters. +# MjSip parameters are organized into 6 sections: +# o Section 1: SipStack base configuration +# o Section 2: Logs +# o Section 3: SipProvider configuration +# o Section 4: Server configuration +# o Section 5: UA configuration +# o Section 6: SBC configuration +# + + + +# ________________________________________________________________ +# +# Section 1: SipStack base configuration +# ________________________________________________________________ +# +# Normally, you do not have to change the base configuration, +# and you can go directly to Section 2. +# SIP and transport layer configurations are handled in Section 3. +# + +# Default SIP port +# Note that this is not the port used by the running stack, but simply the standard default SIP port. +# Normally it sould be set to 5060 as defined by RFC 3261. Using a different value may cause +# some problems when interacting with other unaware SIP UAs. +# Default value: default_port=5060 +#default_port=5060 + +# Default supported transport protocols. +# Default value: default_transport_protocols=udp,tcp +#default_transport_protocols=udp + +# Default max number of contemporary open transport connections. +# Default value: default_nmax_connections=32 +#default_nmax_connections=0 + +# Whether adding 'rport' parameter on via header fields of outgoing requests. +# Default value: use_rport=yes +#use_rport=no + +# Whether adding (forcing) 'rport' parameter on via header field of incoming responses. +# Default value: force_rport=no +#force_rport=yes + +# Default max-forwards value (RFC3261 recommends value 70). +# Default value: max_forwards=70 +#max_forwards=10 + +# Starting retransmission timeout (milliseconds); called T1 in RFC2361; they suggest T1=500ms +# Default value: retransmission_timeout=500 +#retransmission_timeout=2000 + +# Maximum retransmission timeout (milliseconds); called T2 in RFC2361; they suggest T2=4sec +# Default value: max_retransmission_timeout=4000 +#max_retransmission_timeout=4000 + +# Transaction timeout (milliseconds); RFC2361 suggests 64*T1=32000ms +# Default value: transaction_timeout=32000 +#transaction_timeout=10000 + +# Clearing timeout (milliseconds); T4 in RFC2361; they suggest T4=5sec +# Default value: clearing_timeout=5000 +#clearing_timeout=5000 + +# Whether using only one thread for all timer instances. +# Default value: single_timer=no +#single_timer=yes + +# Whether 1xx responses create an "early dialog" for methods that create dialog. +# Default value: early_dialog=no +#early_dialog=yes + +# Default 'expires' time in seconds. RFC2361 gives as default value expires=3600 +# Default value: default_expires=3600 +#default_expires=1800 + +# UA info included in request messages (in the User-Agent header field). +# Use 'NONE' string or let it blank if the User-Agent header filed must be added. +# Default: ua_info= +# ua_info=NONE + +# Server info included in request messages (in the Server header field). +# Use 'NONE' string or let it blank if the Server header filed must be added. +# Default: server_info= +# server_info=NONE + + + +# ________________________________________________________________ +# +# Section 2: Logs +# ________________________________________________________________ +# +# Change these parameters in order to customize how log-files are handled. +# By default log files are placed into the ./log folder, they are not rotated, +# and the maximum size is 2M. +# + +# Log level. Only logs with a level less or equal to this are written. +# Default value: debug_level=3 +#debug_level=0 + +# Path for the log folder where log files are written. +# By default, it is used the "./log" folder +# Use ".", to store logs in the current root folder +# Default value: log_path=./log +#log_path= . + +#The size limit of the log file [kB]. +# Default value: max_logsize=2048 +#max_logsize=4096 + +# The number of rotations of log files. Use '0' for NO rotation, '1' for rotating a single file. +# Default value: log_rotations=0 +#log_rotations=4 + +# The rotation period in MONTHs, DAYs, HOURs, or MINUTEs. +# example: "log_rotation_time=3 MONTHS", that is equivalent to "log_rotations=90 DAYS" +# Default value: log_rotation_time=2 MONTHS +#log_rotation_time=7 DAYS + + + +# ________________________________________________________________ +# +# Section 3: SipProvider configuration +# ________________________________________________________________ +# +# Change these parameters in order to customize the SIP transport layer. +# Usually you have to deal with some of these configuration parameters. +# + +# Via address/name. +# Use 'AUTO-CONFIGURATION' for auto detection, or let it undefined. +# Default value: host_addr=AUTO-CONFIGURATION +#host_addr=192.168.0.33 + +# Local SIP port +# Default value: host_port=5060 +#host_port=5060 + +# Network interface (IP address) used by SIP. +# Use 'all-interfaces' for binding SIP to all interfaces (or let it undefined). +# Default value: host_ifaddr=ALL-INTERFACES +#host_ifaddr=192.168.0.33 + +# List of enabled transport protocols (the first protocol is used as default) +# Default value: transport_protocols=udp +#transport_protocols=udp,tcp + +# Max number of contemporary open transport connections. +# Default value: nmax_connections=32 +#nmax_connections=0 + +# Outbound proxy (host_addr[:host_port]). +# Use 'NONE' for not using an outbound proxy (or let it undefined). +# Default value: outbound_proxy=NONE +#outbound_proxy=proxy.wonderland.net:5060 + +# Whether logging all packets (including non-SIP keepalive tokens). +# Default value: log_all_packets=no +#log_all_packets=yes + + + +# ________________________________________________________________ +# +# Section 4: Server configuration +# ________________________________________________________________ +# +# Change these parameters in order to customize the Server behaviour. +# You need to edit this section only if you are using a MjSip Server. +# + +# The domain name(s) that the server administers. +# It lists the domain names for which the Location Service wmaintains user bindings. +# Use 'auto-configuration' for automatic configuration of the domain name. +#domain_names=wonderland.net biloxi.example.com +#domain_names=AUTO-CONFIGURATION + +# Whether consider any port as valid local domain port (regardless which sip port is used). +# Default value: domain_port_any=no +#domain_port_any=yes + +# Whether the Server should act as Registrar (i.e. respond to REGISTER requests). +# Default value: is_registrar=yes +#is_registrar=no + +# Maximum expires time (in seconds). +# Default value: expires=3600 +#expires=1800 + +# Whether the Registrar can register new users (i.e. REGISTER requests from unregistered users). +# Default value: register_new_users=yes +#register_new_users=no + +# Whether the Server relays requests for (or to) non-local users. +# Default value: is_open_proxy=yes +#is_open_proxy=no + +# The type of location service. +# You can specify the location service type (e.g. local, ldap, radius, mysql) +# or the class name (e.g. local.server.LocationServiceImpl). +# Default value: location_service=local +#location_service=ldap + +# The name of the location DB. +# Default value: location_db=users.db +#location_db=config/users.db + +# Whether location DB has to be cleaned at startup. +# Default value: location_db_clean=no +#location_db_clean=yes + +# Whether the Server authenticates local users. +# Default value: do_authentication=no +#do_authentication=yes + +# The authentication scheme. +# You can specify the authentication scheme name (e.g. Digest, AKA, etc.) +# or the class name (e.g. local.server.AuthenticationServerImpl). +# Default value: authentication_scheme=Digest +#authentication_scheme=AKA + +# The authentication realm. +# If not defined or equal to 'NONE' (default), the used via address is used instead. +# Default value: authentication_realm=NONE +#authentication_realm=wonderland.net + +# The type of authentication service. +# You can specify the authentication service type (e.g. local, ldap, radius, mysql) +# or the class name (e.g. local.server.AuthenticationServiceImpl). +# Default value: authentication_service=local +#authentication_service=ldap + +# The name of the authentication DB. +# Default value: authentication_db=aaa.db +#authentication_db=config/aaa.db + +# Whether maintaining a complete call log. +# Default value: call_log=no +#call_log=yes + +# Whether the server should stay in the signaling path (uses Record-Route/Route). +# Default value: on_route=no +#on_route=yes + +# Whether implementing the RFC3261 Loose Route (or RFC2543 Strict Route) rule. +# Default value: loose_route=yes +#loose_route=no + +# Whether checking for loops before forwarding a request (Loop Detection). In RFC3261 it is optional. +# Default value: loop_detection=yes +#loop_detection=no + +# Sequence of pairs of username or phone prefix and corresponding nexthop address. +# It provides static rules for routing number-based SIP-URL the server is responsible for. +# Use "default" (or "*") as default prefix. +# Example, request URL sip:01234567@zoopera.com received by a server responsible for domain name 'zoopera.com'. +#phone_routing_rules={prefix=0123,nexthop=127.0.0.2:7002} {prefix=*,nexthop=127.0.0.3:7003} + +# Sequence of pairs of destination domain and corresponding nexthop address. +# It provides static rules for routing domain-based SIP-URL the server is NOT responsible for. +# It make the server acting (also) as 'Interrogating' Proxy, i.e. I-CSCF in the 3G networks. +# domain_routing_rules={domain=wonderland.net,nexthop=neverland.net:5060} */ + + +# ________________________________________________________________ +# +# Section 5: UA configuration +# ________________________________________________________________ +# +# Change these parameters in order to customize the UA profile. +# You need to edit this section only if you are using a MjSip UA or +# you are managing 3PCC services. +# + +# User's AOR (Address Of Record), used also as From URL. +# The AOR is the SIP address used to register with the user's registrar server (if requested). +# The address of the registrar is taken from the hostport field of the AOR, i.e. the value(s) host[:port] after the '@' character. +# If not defined (default), it equals the 'contact_url' parameter +#from_url=sip:alice@wonderland.net + +# Contact URL. +# If not defined (default), it is formed by sip:username@host_address:host_port +#contact_url=sip:alice@192.168.0.55:5070 + +# User's name (used to build the contact_url if not explitely defined). +#username=alice + +# User's realm. +#realm=wonderland.net + +# User's passwd. +#passwd=mypassword + +# Path for the 'ua.jar' lib, used to retrive various UA media (gif, wav, etc.). +# Default value: ua_jar=lib/ua.jar +#ua_jar=./ua.jar + +# Path for the 'contacts.lst' file where save and load the list of VisualUA contacts. +# Default value: contacts_file=config/contacts.lst +#contacts_file=contacts.lst + +# Whether registering with the registrar server. +# Default value: do_register=no +#do_register=yes + +# Whether unregistering the contact address. +# Default value: do_unregister=no +#do_unregister=yes + +# Whether unregistering all contacts beafore registering the contact address. +# Default value: do_unregister_all=no +#do_unregister_all=yes + +# Expires time. +# Default value: expires=3600 +#expires=1800 + +# Rate of keep-alive packets sent toward the registrar server (in milliseconds). +# Set keepalive_time=0 to disable the sending of keep-alive datagrams. +# Default value: keepalive_time=0 +#keepalive_time=8000 + +# Automatic call a remote user secified by the 'call_to' value. +# Use value 'NONE' for manual calls (or let it undefined). +# Default value: call_to=NONE +#call_to=sip:alice@wonderland.net + +# Automatic answer time in seconds; time<0 corresponds to manual answer mode. +# Default value: accept_time=-1 +#accept_time=0 + +# Automatic hangup time (call duartion) in seconds; time<=0 corresponds to manual hangup mode. +# Default value: hangup_time=-1 +#hangup_time=10 + +# Automatic call transfer time in seconds; time<0 corresponds to no auto transfer mode. +# Default value: transfer_time=-1 +#transfer_time=10 + +# Automatic re-inviting time in seconds; time<0 corresponds to no auto re-invite mode. +# Default value: re_invite_time=-1 +#re_invite_time=10 + +# Redirect incoming call to the secified url. +# Use value 'NONE' for not redirecting incoming calls (or let it undefined). +# Default value: redirect_to=NONE +#redirect_to=sip:alice@wonderland.net + +# Transfer calls to the secified url. +# Use value 'NONE' for not transferring calls (or let it undefined). +# Default value: transfer_to=NONE +#transfer_to=sip:alice@wonderland.net + +# No offer in the invite. +# Default value: no_offer=no +#no_offer=yes + +# Whether using audio. +# Default value: audio=no +#audio=yes + +# Whether using video. +# Default value: video=no +#video=yes + +# Whether playing in receive only mode. +# Default value: recv_only=no +#recv_only=yes + +# Whether playing in send only mode. +# Default value: send_only=no +#send_only=yes + +# Whether playing a test tone in send only mode. +# Default value: send_tone=no +#send_tone=yes + +# Audio file to be played. +# Default value: send_file=NONE +#send_file=audio1.in + +# Audio file to be recored. +# Default value: recv_file=NONE +#recv_file=audio1.out + +# Audio port. +# Default value: audio_port=21068 +#audio_port=4000 + +# Audio avp. +# Default value: audio_avp=0 +#audio_avp=101 + +# Audio codec. +# Default value: audio_codec=PCMU +#audio_codec=GSM + +# Audio sample rate. +# Default value: audio_sample_rate=8000 +#audio_sample_rate=16000 + +# Audio sample size. +# Default value: audio_sample_size=1 +#audio_sample_size=2 + +# Audio frame size. +# Default value: audio_frame_size=160 +#audio_frame_size=500 + +# Video port. +# Default value: video_port=21070 +#video_port=4002 + +# Video avp. +# Default value: video_avp=17 +#video_avp=101 + +# Whether using JMF for audio/video streaming. +# Default value: use_jmf=no +#use_jmf=yes + +# Whether using RAT (Robust Audio Tool) as audio sender/receiver. +# Default value: use_rat=no +#use_rat=yes + +# RAT command-line executable. +# Default value: bin_rat=rat +#bin_rat=c:\programmi\mbone\rat + +# Whether using VIC (Video Conferencing Tool) as video sender/receiver. +# Default value: use_vic=no +#use_vic=yes + +# VIC command-line executable. +# Default value: bin_vic=vic +#bin_vic=c:\programmi\mbone\rat + + +# ________________________________________________________________ +# +# Section 6: SBC +# ________________________________________________________________ +# +# Change these parameters in order to customize the SessionBorderController (SBC) behaviour. +# You need to edit this section only if you are using a SBC. +# + +# Maximum time that the UDP relay remains active without receiving UDP datagrams (in milliseconds). +# Default value: relay_timeout=60000 + +# Refresh time of address-binding cache (in milliseconds) +# Default value: binding_timeout=3600000 + +# Minimum time between two changes of peer address (in milliseconds) +# Default value: handover_time=0 + +# Rate of keep-alive datagrams sent toward all registered UAs (in milliseconds). +# Set keepalive_time=0 to disable the sending of keep-alive datagrams +# Default value: keepalive_time=0 + +# Whether sending keepalive datagram to all contacted UAs (also toward non-registered UAs) +# Default value: keepalive_aggressive=no + +# Whether intercepting media traffics. +# Default value: do_interception=no + +# Whether injecting new media flows. +# Default value: do_active_interception=no + +# Sink address for media traffic interception. +# Default value: sink_addr=127.0.0.1 + +# Sink port for media traffic interception. +# Default value: sink_port=0 + +# Media address. +# Default value: media_addr=0.0.0.0 + +# Available media ports. +# Default value: media_ports=41000-41499 + +# Backend proxy where all requests not coming from it are passed to. +# It can be specified as FQDN or host_addr[:host_port]. +# Use 'NONE' for not using a backend proxy (or let it undefined). +# Default value: backend_proxy=NONE +#backend_proxy=127.0.0.2:5069 diff --git a/src/COPYRIGHT.txt b/src/COPYRIGHT.txt new file mode 100644 index 0000000..9aa8e35 --- /dev/null +++ b/src/COPYRIGHT.txt @@ -0,0 +1,22 @@ +MjSip - http://www.mjsip.org + +Copyright (C) 2005 by Luca Veltri - University of Parma - Italy. +__________________________________________________ + + +MjSip is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +MjSip is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with MjSip; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +PLEASE READ CAREFULLY THE LICENSE DOCUMENT RECEIVED ALONG WITH THIS LIBRARY. \ No newline at end of file diff --git a/src/license.txt b/src/license.txt new file mode 100644 index 0000000..5b6e7c6 --- /dev/null +++ b/src/license.txt @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/src/local/media/AudioClipPlayer.java b/src/local/media/AudioClipPlayer.java new file mode 100644 index 0000000..ec77612 --- /dev/null +++ b/src/local/media/AudioClipPlayer.java @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This source code is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package local.media; + + +import java.io.*; + +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioInputStream; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.Clip; +import javax.sound.sampled.DataLine; +import javax.sound.sampled.LineEvent; +import javax.sound.sampled.LineListener; + + +/** Plays an audio file. + */ +public class AudioClipPlayer implements LineListener +{ + /** The sound clip */ + Clip clip=null; + + /** The player listener */ + AudioClipPlayerListener p_listener=null; + + + /** Creates the SoundPlayer */ + public AudioClipPlayer(String filename, AudioClipPlayerListener listener) + { try + { FileInputStream inputstream=new FileInputStream(new File(filename)); + AudioInputStream audio_inputstream=AudioSystem.getAudioInputStream(inputstream); + init(audio_inputstream,listener); + } + catch (Exception e) { e.printStackTrace(); } + } + + + /** Creates the SoundPlayer */ + public AudioClipPlayer(File file, AudioClipPlayerListener listener) + { try + { AudioInputStream audio_inputstream=AudioSystem.getAudioInputStream(new FileInputStream(file)); + init(audio_inputstream,listener); + } + catch (Exception e) { e.printStackTrace(); } + } + + + /** Creates the SoundPlayer */ + public AudioClipPlayer(InputStream inputstream, AudioClipPlayerListener listener) + { try + { AudioInputStream audio_inputstream=AudioSystem.getAudioInputStream(inputstream); + init(audio_inputstream,listener); + } + catch (Exception e) { e.printStackTrace(); } + } + + + /** Creates the SoundPlayer */ + public AudioClipPlayer(AudioInputStream audio_inputstream, AudioClipPlayerListener listener) + { + init(audio_inputstream,listener); + } + + + /** Inits the SoundPlayer */ + void init(AudioInputStream audio_inputstream, AudioClipPlayerListener listener) + { p_listener=listener; + if (audio_inputstream!=null) + try + { AudioFormat format=audio_inputstream.getFormat(); + DataLine.Info info=new DataLine.Info(Clip.class,format); + clip=(Clip)AudioSystem.getLine(info); + clip.addLineListener(this); + clip.open(audio_inputstream); + } + catch (Exception e) { e.printStackTrace(); } + } + + + /** Loops the sound until stopped */ + public void loop() + { + loop(0); + } + + + /** Loops the sound n times. + * if n=0, it loops until stopped */ + public void loop(int n) + { + rewind(); + if (clip!=null) + { if (n<=0) clip.loop(Clip.LOOP_CONTINUOUSLY); + else clip.loop(n-1); + } + } + + + /** Plays the sound */ + public void play() + { + if (clip!=null) clip.start(); + } + + + /** Stops and rewinds the sound */ + public void stop() + { + if (clip!=null) clip.stop(); + } + + + /** Rewinds the sound */ + public void rewind() + { + if (clip!=null) clip.setMicrosecondPosition(0); + } + + + /** Goes to a time position */ + public void goTo(long millisec) + { + if (clip!=null) clip.setMicrosecondPosition(millisec); + } + + + /** Plays the sound from begining (restart) */ + public void replay() + { + if (clip!=null) { rewind(); clip.start(); } + } + + + /** Called by the sound line */ + public void update(LineEvent event) + { + if (event.getType().equals(LineEvent.Type.STOP)) + { //System.out.println("DEBUG: clip stop"); + //clip.close(); + if (p_listener!=null) p_listener.onAudioClipStop(this); + } + } + + + // ***************************** MAIN ***************************** + + /** The main method. */ + public static void main(String[] args) + { + if (args.length<1) + { + System.out.println("AudioClipPlayer: usage:\n java AudioClipPlayer "); + System.exit(0); + } + + AudioClipPlayer p=new AudioClipPlayer(args[0],null); + p.play(); + //try { Thread.sleep(3000); } catch (Exception e) { e.printStackTrace(); } + //p.stop(); + } +} + + diff --git a/src/local/media/AudioClipPlayerListener.java b/src/local/media/AudioClipPlayerListener.java new file mode 100644 index 0000000..549d240 --- /dev/null +++ b/src/local/media/AudioClipPlayerListener.java @@ -0,0 +1,14 @@ +package local.media; + + +/** Listener for AudioClipPlayer. + * It captures onAudioClipStop() events, fired when the sound stops. + */ +public interface AudioClipPlayerListener +{ + /** When the sound stops. */ + public void onAudioClipStop(AudioClipPlayer player); + +} + + diff --git a/src/local/media/AudioInput.java b/src/local/media/AudioInput.java new file mode 100644 index 0000000..5ba3c13 --- /dev/null +++ b/src/local/media/AudioInput.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This source code is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package local.media; + + +import javax.sound.sampled.*; +import java.io.InputStream; + + +/** AudioInput allows the access of system audio input in pure-java manner. + * It uses the javax.sound library (package). + */ +public class AudioInput +{ + + + // ####################### BEGIN STATIC ####################### + + static boolean DEBUG=true; + + static final int INTERNAL_BUFFER_SIZE=40960; + + static TargetDataLine target_line=null; + + + /** Inits the static system audio input line */ + public static void initAudioLine() + { + /*println("Available Mixers:"); + Mixer.Info[] aInfos=AudioSystem.getMixerInfo(); + for (int i=0; i < aInfos.length; i++) print(" "+i+") "+aInfos[i].getName()+"\n"); + if (aInfos.length == 0) + { println("WARNING: NO mixers available."); + //System.exit(0); + return; + }*/ + + // 44100 Hz, Linear, 16bit, Stereo : + //float fFrameRate = 44100.0F; + //AudioFormat format = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, fFrameRate, 16, 2, 4, fFrameRate, false); + + // 44100 Hz, Linear, 16bit, Mono : + //float fFrameRate = 44100.0F; + //AudioFormat format = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, fFrameRate, 16, 1, 2, fFrameRate, false); + + // 8000 Hz, Linear, 16bit, Mono : + float fFrameRate=8000.0F; + AudioFormat format=new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, fFrameRate, 16, 1, 2, fFrameRate, false); + + DataLine.Info lineInfo=new DataLine.Info(TargetDataLine.class,format,INTERNAL_BUFFER_SIZE); + + if (!AudioSystem.isLineSupported(lineInfo)) + { System.err.println("ERROR: AudioLine not supported by this System."); + } + try + { target_line=(TargetDataLine)AudioSystem.getLine(lineInfo); + if (DEBUG) println("TargetDataLine: "+target_line); + target_line.open(format,INTERNAL_BUFFER_SIZE); + } + catch (LineUnavailableException e) + { System.err.println("ERROR: LineUnavailableException at AudioSender()"); + e.printStackTrace(); + } + } + + /** Closes the static system audio input line */ + static public void closeAudioLine() + { target_line.close(); + } + + + // ######################## END STATIC ######################## + + + + AudioInputStream audio_input_stream=null; + + + /** Constructs an AudioInput with audio_format=[8000 Hz, ULAW, 8bit, Mono] */ + public AudioInput() + { init(null); + } + + /** Constructs an AudioInput */ + public AudioInput(AudioFormat audio_format) + { init(audio_format); + } + + /** Inits the AudioInput */ + private void init(AudioFormat format) + { + if (target_line==null) initAudioLine(); + + if (format==null) + { // by default use 8000 Hz, ULAW, 8bit, Mono + float fFrameRate=8000.0F; + format=new AudioFormat(AudioFormat.Encoding.ULAW, fFrameRate, 8, 1, 1, fFrameRate, false); + } + + if (target_line.isOpen()) + { audio_input_stream=new AudioInputStream(target_line); + // convert the audio stream to the selected format + audio_input_stream=AudioSystem.getAudioInputStream(format,audio_input_stream); + } + else + { System.err.print("WARNING: Audio init error: target line is not open."); + } + } + + + /** Gets the audio InputStream */ + public InputStream getInputStream() + { return audio_input_stream; + } + + + + /** Starts playing */ + public void play() + { if (target_line.isOpen()) target_line.start(); + else + { System.err.print("WARNING: Audio play error: target line is not open."); + } + } + + + /** Stops playing */ + public void stop() + { if (target_line.isOpen()) target_line.stop(); + else + { System.err.print("WARNING: Audio stop error: target line is not open."); + } + //target_line.close(); + } + + + /** Debug output */ + private static void println(String str) + { System.out.println("AudioInput: "+str); + } + + /** Debug output */ + private static void print(String str) + { System.out.print("AudioInput: "+str); + } + +} \ No newline at end of file diff --git a/src/local/media/AudioOutput.java b/src/local/media/AudioOutput.java new file mode 100644 index 0000000..6b32657 --- /dev/null +++ b/src/local/media/AudioOutput.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This source code is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package local.media; + + +import javax.sound.sampled.*; +import java.io.*; + + +/** AudioOutput allows the access of system audio output in pure-java manner. + * It uses the javax.sound library (package). + */ +public class AudioOutput +{ + + // ####################### BEGIN STATIC ####################### + + static boolean DEBUG=true; + + static final int INTERNAL_BUFFER_SIZE=40960; + + private static SourceDataLine source_line; + + + /** Init the static system audio output line */ + public static void initAudioLine() + { + /*println("Available Mixers:"); + Mixer.Info[] aInfos=AudioSystem.getMixerInfo(); + for (int i=0; i < aInfos.length; i++) print(" "+i+") "+aInfos[i].getName()+"\n"); + if (aInfos.length == 0) + { println("WARNING: NO mixers available."); + //System.exit(0); + return; + }*/ + + // 44100 Hz, Linear, 16bit, Stereo : + //float fFrameRate = 44100.0F; + //AudioFormat format = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, fFrameRate, 16, 2, 4, fFrameRate, false); + + // 44100 Hz, Linear, 16bit, Mono : + //float fFrameRate = 44100.0F; + //AudioFormat format = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, fFrameRate, 16, 1, 2, fFrameRate, false); + + // 8000 Hz, Linear, 16bit, Mono : + float fFrameRate=8000.0F; + AudioFormat format=new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, fFrameRate, 16, 1, 2, fFrameRate, false); + + DataLine.Info lineInfo=new DataLine.Info(SourceDataLine.class, format, INTERNAL_BUFFER_SIZE); + + if (!AudioSystem.isLineSupported(lineInfo)) + { System.err.println("ERROR: AudioLine not supported by this System."); + } + try + { source_line=(SourceDataLine)AudioSystem.getLine(lineInfo); + if (DEBUG) println("SourceDataLine: "+source_line); + source_line.open(format,INTERNAL_BUFFER_SIZE); + } + catch (LineUnavailableException e) + { System.err.println("ERROR: LineUnavailableException at AudioReceiver()"); + e.printStackTrace(); + } + } + + /** Closes the static system audio output line */ + static public void closeAudioLine() + { source_line.close(); + } + + + // ######################## END STATIC ######################## + + + AudioOutputStream audio_output_stream=null; + + + /** Constructs an AudioOutput with audio_format=[8000 Hz, ULAW, 8bit, Mono] */ + public AudioOutput() + { init(null); + } + + /** Constructs an AudioOutput */ + public AudioOutput(AudioFormat audio_format) + { init(audio_format); + } + + /** Inits an AudioOutput */ + private void init(AudioFormat format) + { + if (source_line==null) initAudioLine(); + + if (format==null) + { // by default use 8000 Hz, ULAW, 8bit, Mono + float fFrameRate=8000.0F; + format=new AudioFormat(AudioFormat.Encoding.ULAW, fFrameRate, 8, 1, 1, fFrameRate, false); + } + + if (source_line.isOpen()) + { // convert the audio stream to the selected format + try + { audio_output_stream=new AudioOutputStream(source_line,format); + } + catch (Exception e) + { e.printStackTrace(); + } + } + else + { System.err.print("WARNING: Audio init error: source line is not open."); + } + + } + + + /** Gets the audio OuputStream */ + public OutputStream getOuputStream() + { //return output_stream; + return audio_output_stream; + } + + + /** Starts playing */ + public void play() + { if (source_line.isOpen()) source_line.start(); + else + { System.err.print("WARNING: Audio play error: source line is not open."); + } + } + + + /** Stops playing */ + public void stop() + { if (source_line.isOpen()) + { source_line.drain(); + source_line.stop(); + } + else + { System.err.print("WARNING: Audio stop error: source line is not open."); + } + //source_line.close(); + } + + + /** Debug output */ + private static void println(String str) + { System.out.println("AudioOutput: "+str); + } + + /** Debug output */ + private static void print(String str) + { System.out.print("AudioOutput: "+str); + } + +} + + diff --git a/src/local/media/AudioOutput.java.saved b/src/local/media/AudioOutput.java.saved new file mode 100644 index 0000000..bb24968 --- /dev/null +++ b/src/local/media/AudioOutput.java.saved @@ -0,0 +1,251 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This source code is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package local.media; + + +import local.net.RtpPacket; +import local.net.RtpSocket; + +import javax.sound.sampled.*; +import java.io.*; +import java.net.InetAddress; +import java.net.DatagramSocket; + + +/** AudioOutput allows the access of system audio output in pure-java manner. + * It uses the javax.sound library (package). + */ +public class AudioOutput +{ + + // ####################### BEGIN STATIC ####################### + + static boolean DEBUG=true; + + static final int INTERNAL_BUFFER_SIZE=40960; + + private static SourceDataLine source_line; + + private static AudioFormat line_format; + + static final int RESILIENCE_TIME=50; // milliseconds + + /** Init the static system audio output line */ + public static void initAudioLine() + { + /*println("Available Mixers:"); + Mixer.Info[] aInfos=AudioSystem.getMixerInfo(); + for (int i=0; i < aInfos.length; i++) print(" "+i+") "+aInfos[i].getName()+"\n"); + if (aInfos.length == 0) + { println("WARNING: NO mixers available."); + //System.exit(0); + return; + }*/ + + // 44100 Hz, Linear, 16bit, Stereo : + //float fFrameRate = 44100.0F; + //AudioFormat format = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, fFrameRate, 16, 2, 4, fFrameRate, false); + + // 44100 Hz, Linear, 16bit, Mono : + //float fFrameRate = 44100.0F; + //AudioFormat format = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, fFrameRate, 16, 1, 2, fFrameRate, false); + + // 8000 Hz, Linear, 16bit, Mono : + float fFrameRate=8000.0F; + line_format=new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, fFrameRate, 16, 1, 2, fFrameRate, false); + + DataLine.Info lineInfo=new DataLine.Info(SourceDataLine.class, line_format, INTERNAL_BUFFER_SIZE); + + if (!AudioSystem.isLineSupported(lineInfo)) + { System.err.println("ERROR: AudioLine not supported by this System."); + } + try + { source_line=(SourceDataLine)AudioSystem.getLine(lineInfo); + if (DEBUG) println("SourceDataLine: "+source_line); + source_line.open(line_format,INTERNAL_BUFFER_SIZE); + } + catch (LineUnavailableException e) + { System.err.println("ERROR: LineUnavailableException at AudioReceiver()"); + e.printStackTrace(); + } + } + + /** Closes the static system audio output line */ + static public void closeAudioLine() + { source_line.close(); + } + + + // ######################## END STATIC ######################## + + + AudioInputStream audio_input_stream=null; + + PipedOutputStream output_stream=null; + + /** The internal AudioPlayer */ + AudioPlayer audio_player=null; + + + + /** Constructs an AudioOutput with audio_format=[8000 Hz, ULAW, 8bit, Mono] */ + public AudioOutput() + { init(null); + } + + /** Constructs an AudioOutput */ + public AudioOutput(AudioFormat audio_format) + { init(audio_format); + } + + /** Inits an AudioOutput */ + private void init(AudioFormat format) + { + if (source_line==null) initAudioLine(); + + if (format==null) + { // by default use 8000 Hz, ULAW, 8bit, Mono + float fFrameRate=8000.0F; + format=new AudioFormat(AudioFormat.Encoding.ULAW, fFrameRate, 8, 1, 1, fFrameRate, false); + } + + try + { + PipedInputStream input_stream=new PipedInputStream(); + output_stream=new PipedOutputStream(input_stream); + + audio_input_stream=new AudioInputStream(input_stream,format,-1); + if (audio_input_stream==null) println("ERROR: audio stream null"); + audio_input_stream=AudioSystem.getAudioInputStream(line_format,audio_input_stream); + if (audio_input_stream==null) println("ERROR: audio stream null (after codec conversion)"); + } + catch (Exception e) { e.printStackTrace(); } + + } + + + /** Gets the audio OuputStream */ + public OutputStream getOuputStream() + { return output_stream; + } + + + /** Starts playing */ + public void play() + { audio_player=new AudioPlayer(audio_input_stream); + } + + /** Stops playing */ + public void stop() + { if (audio_player!=null) + { // stop the player + audio_player.halt(); + // wait in order to not break the pipe (i think it is a java bug..) + while (audio_player.isRunning()); + } + } + + + // ************ Begin of class AudioPlayer ************ + + /** Size of the read buffer */ + //private static final int BUFFER_SIZE=200; + private static final int BUFFER_SIZE=40960; + + /** AudioPlayer */ + class AudioPlayer extends Thread + { + /** The AudioInputStream to be played */ + AudioInputStream audio_input_stream; + + /** Whether the player is running */ + boolean running=false; + + /** Whether the player has to stop */ + boolean stop; + + /** Costructs a new AudioPlayer */ + AudioPlayer (AudioInputStream audio_input_stream) + { this.audio_input_stream=audio_input_stream; + start(); + } + + /** Whether is running */ + public boolean isRunning() + { return running; + } + + + /** Stops playing */ + public void halt() + { stop=true; + } + + /** Play it in a new Thread. */ + public void run() + { + AudioOutput.source_line.start(); + + byte[] buffer=new byte[BUFFER_SIZE]; + + running=true; + stop=false; + + try + { while (!stop) + { if (audio_input_stream.available()>0) + { System.out.println("DEBUG AudioOutput: "+audio_input_stream.available()); + int num=audio_input_stream.read(buffer, 0, buffer.length); + AudioOutput.source_line.write(buffer,0,num); + } + else + { // wait a while beafore check for new samples, otherwise it starves the CPU + try { Thread.sleep(AudioOutput.RESILIENCE_TIME); } catch (Exception e) {} + } + } + } + catch (IOException e) { e.printStackTrace(); } + running=false; + + AudioOutput.source_line.stop(); + //AudioOutput.source_line.close(); + + if (DEBUG) AudioOutput.println("audio terminated"); + } + } + + // ************* End of class AudioPlayer ************* + + + /** Debug output */ + private static void println(String str) + { System.out.println("AudioOutput: "+str); + } + + /** Debug output */ + private static void print(String str) + { System.out.print("AudioOutput: "+str); + } + +} + + diff --git a/src/local/media/AudioOutputStream.java b/src/local/media/AudioOutputStream.java new file mode 100644 index 0000000..3e01076 --- /dev/null +++ b/src/local/media/AudioOutputStream.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This source code is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package local.media; + + +import javax.sound.sampled.*; +import java.io.*; + + +/** AudioOutputStream is equivalent to javax.sound.sampled.AudioInputStream + * for audio playout. + *

+ * AudioOutputStream also provides audio codec conversion, as + * javax.sound.sampled.AudioSystem does for the corresponding + * javax.sound.sampled.AudioInputStream class. + */ +class AudioOutputStream extends OutputStream +{ + static final int INTERNAL_BUFFER_SIZE=40960; + + /** The SourceDataLine */ + protected SourceDataLine source_line; + + /** Converted InputStream. */ + protected InputStream input_stream; + + /** Piped OutputStream. */ + protected OutputStream output_stream; + + /** Internal buffer. */ + private byte[] buff=new byte[INTERNAL_BUFFER_SIZE]; + + + /** Creates a new AudioOutputStream from a SourceDataLine. */ + public AudioOutputStream(SourceDataLine source_line) + { this.source_line=source_line; + input_stream=null; + output_stream=null; + } + + /** Creates a new AudioOutputStream from a SourceDataLine converting the audio format. */ + public AudioOutputStream(SourceDataLine source_line, AudioFormat format) throws IOException + { this.source_line=source_line; + + PipedInputStream piped_input_stream=new PipedInputStream(); + output_stream=new PipedOutputStream(piped_input_stream); + + AudioInputStream audio_input_stream=new AudioInputStream(piped_input_stream,format,-1); + if (audio_input_stream==null) + { String err="Failed while creating a new AudioInputStream."; + throw new IOException(err); + } + + input_stream=AudioSystem.getAudioInputStream(source_line.getFormat(),audio_input_stream); + if (input_stream==null) + { String err="Failed while getting a transcoded AudioInputStream from AudioSystem."; + err+="\n input codec: "+format.toString(); + err+="\n output codec:"+source_line.getFormat().toString(); + throw new IOException(err); + } + } + + /** Closes this output stream and releases any system resources associated with this stream. */ + public void close() + { //source_line.close(); + } + + /** Flushes this output stream and forces any buffered output bytes to be written out. */ + public void flush() + { source_line.flush(); + } + + /** Writes b.length bytes from the specified byte array to this output stream. */ + public void write(byte[] b) throws IOException + { if (output_stream!=null) + { output_stream.write(b); + int len=input_stream.read(buff,0,buff.length); + source_line.write(buff,0,len); + } + else + { source_line.write(b,0,b.length); + } + } + + /** Writes len bytes from the specified byte array starting at offset off to this output stream. */ + public void write(byte[] b, int off, int len) throws IOException + { if (output_stream!=null) + { output_stream.write(b,off,len); + len=input_stream.read(buff,0,buff.length); + source_line.write(buff,0,len); + } + else + { source_line.write(b,off,len); + } + } + + /** Writes the specified byte to this output stream. */ + public void write(int b) throws IOException + { if (output_stream!=null) + { output_stream.write(b); + int len=input_stream.read(buff,0,buff.length); + source_line.write(buff,0,len); + } + else + { buff[0]=(byte)b; + source_line.write(buff,0,1); + } + } + +} diff --git a/src/local/media/AudioReceiver.java b/src/local/media/AudioReceiver.java new file mode 100644 index 0000000..9447784 --- /dev/null +++ b/src/local/media/AudioReceiver.java @@ -0,0 +1,129 @@ +package local.media; + + +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.AudioFileFormat; +import java.io.File; +import java.io.FileOutputStream; + + +/** AudioReceiver is a pure-java audio stream receiver. + * It uses the javax.sound library (package). + */ +public class AudioReceiver +{ + + // ******************************* MAIN ******************************* + + /** The main method. */ + public static void main(String[] args) + { + int port=0; + + int sample_rate=8000; + int sample_size=1; + boolean linear_signed=false; + boolean pcmu=false; + boolean big_endian=false; + String filename=null; + boolean sound=true; + + boolean help=true; + + for (int i=0; i(i+1)) + { sound=false; + filename=args[++i]; + continue; + } + if (args[i].equals("-S") && args.length>(i+2)) + { sample_rate=Integer.parseInt(args[++i]); + sample_size=Integer.parseInt(args[++i]); + continue; + } + if (args[i].equals("-Z")) + { linear_signed=true; + continue; + } + if (args[i].equals("-U")) + { pcmu=true; + continue; + } + if (args[i].equals("-E")) + { big_endian=true; + continue; + } + + // else, do: + System.out.println("unrecognized param '"+args[i]+"'\n"); + help=true; + } + + if (help) + { System.out.println("usage:\n java AudioReceiver [options]"); + System.out.println(" options:"); + System.out.println(" -h this help"); + System.out.println(" -F records to audio file"); + System.out.println(" -S sample rate [B/s], and size [B]"); + System.out.println(" -Z uses PCM linear signed format (linear unsigned is used as default)"); + System.out.println(" -U uses PCMU format"); + System.out.println(" -E uses big endian format"); + System.exit(0); + } + + AudioFormat.Encoding codec; + + if (pcmu) codec=AudioFormat.Encoding.ULAW; + else + if (linear_signed) codec=AudioFormat.Encoding.PCM_SIGNED; + else + codec=AudioFormat.Encoding.PCM_UNSIGNED; + + try + { RtpStreamReceiver receiver; + AudioOutput audio_output=null; + if (sound) AudioOutput.initAudioLine(); + + if (sound) + { AudioFormat format=new AudioFormat(codec,sample_rate,8*sample_size,1,sample_size,sample_rate,big_endian); + audio_output=new AudioOutput(format); + receiver=new RtpStreamReceiver(audio_output.getOuputStream(),port); + } + else + //if (filename!=null) + { File file=new File(filename); + /* + AudioFileFormat format=AudioSystem.getAudioFileFormat(file); + System.out.println("File audio format: "+format); + OutputStream output_stream=new OutputStream() { public void write(int b) {} }; + receiver=new RtpStreamReceiver(output_stream,port); + */ + FileOutputStream output_stream=new FileOutputStream(file); + receiver=new RtpStreamReceiver(output_stream,port); + } + + receiver.start(); + if (sound) audio_output.play(); + + System.out.println("Press 'Return' to stop"); + System.in.read(); + + receiver.halt(); + if (sound) audio_output.stop(); + if (sound) AudioOutput.closeAudioLine(); + } + catch (Exception e) { e.printStackTrace(); } + } + +} + diff --git a/src/local/media/AudioSender.java b/src/local/media/AudioSender.java new file mode 100644 index 0000000..5584497 --- /dev/null +++ b/src/local/media/AudioSender.java @@ -0,0 +1,176 @@ +package local.media; + + +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.AudioInputStream; +import javax.sound.sampled.AudioFileFormat; +import java.io.File; +import java.io.FileInputStream; + + +/** AudioSender is a pure-java audio stream sender. + * It uses the javax.sound library (package). + */ +public class AudioSender +{ + + // ******************************* MAIN ******************************* + + /** The main method. */ + public static void main(String[] args) + { + String daddr=null; + int dport=0; + int payload_type=0; + + int tone_freq=500; + double tone_amp=1.0; + int sample_rate=8000; + int sample_size=1; + int frame_size=500; + int frame_rate; //=sample_rate/(frame_size/sample_size); + // byte_rate=frame_rate/frame_size=8000 + boolean linear_signed=false; + boolean pcmu=false; + boolean big_endian=false; + String filename=null; + boolean sound=true; + + boolean help=true; + + for (int i=0; i1) + { daddr=args[i]; + dport=Integer.parseInt(args[++i]); + help=false; + continue; + } + if (args[i].equals("-p") && args.length>(i+1)) + { payload_type=Integer.parseInt(args[++i]); + continue; + } + if (args[i].equals("-F") && args.length>(i+1)) + { sound=false; + filename=args[++i]; + continue; + } + if (args[i].equals("-T") && args.length>(i+1)) + { sound=false; + tone_freq=Integer.parseInt(args[++i]); + continue; + } + if (args[i].equals("-A") && args.length>(i+1)) + { tone_amp=Double.parseDouble(args[++i]); + continue; + } + if (args[i].equals("-S") && args.length>(i+2)) + { sample_rate=Integer.parseInt(args[++i]); + sample_size=Integer.parseInt(args[++i]); + continue; + } + if (args[i].equals("-L") && args.length>(i+1)) + { frame_size=Integer.parseInt(args[++i]); + continue; + } + if (args[i].equals("-Z")) + { linear_signed=true; + continue; + } + if (args[i].equals("-U")) + { pcmu=true; + continue; + } + if (args[i].equals("-E")) + { big_endian=true; + continue; + } + + // else, do: + System.out.println("unrecognized param '"+args[i]+"'\n"); + help=true; + } + + if (help) + { System.out.println("usage:\n java AudioSender [options]"); + System.out.println(" options:"); + System.out.println(" -h this help"); + System.out.println(" -p payload type"); + System.out.println(" -F sends an audio file"); + System.out.println(" -T sends a tone of given frequency [Hz]"); + System.out.println(" -A sets an amplitude factor [0:1]"); + System.out.println(" -S sample rate [B/s], and size [B]"); + System.out.println(" -L frame size"); + System.out.println(" -Z uses PCM linear signed format (linear unsigned is used as default)"); + System.out.println(" -U uses PCMU format"); + System.out.println(" -E uses big endian format"); + System.exit(0); + } + + frame_rate=sample_rate/(frame_size/sample_size); + + AudioFormat.Encoding codec; + + if (pcmu) codec=AudioFormat.Encoding.ULAW; + else + if (linear_signed) codec=AudioFormat.Encoding.PCM_SIGNED; + else + codec=AudioFormat.Encoding.PCM_UNSIGNED; // default + + int tone_codec=ToneInputStream.PCM_LINEAR_UNSIGNED; + if (linear_signed) tone_codec=ToneInputStream.PCM_LINEAR_SIGNED; + + try + { RtpStreamSender sender; + AudioInput audio_input=null; + + if (sound) AudioInput.initAudioLine(); + + if (sound) + { AudioFormat format=new AudioFormat(codec,sample_rate,8*sample_size,1,sample_size,sample_rate,big_endian); + System.out.println("System audio format: "+format); + audio_input=new AudioInput(format); + sender=new RtpStreamSender(audio_input.getInputStream(),false,payload_type,frame_rate,frame_size,daddr,dport); + } + else + if (filename!=null) + { File file=new File(filename); + if (filename.indexOf(".wav")>0) + { AudioFileFormat format=AudioSystem.getAudioFileFormat(file); + System.out.println("File audio format: "+format); + AudioInputStream audio_input_stream=AudioSystem.getAudioInputStream(file); + sender=new RtpStreamSender(audio_input_stream,true,payload_type,frame_rate,frame_size,daddr,dport); + } + else + { FileInputStream input_stream=new FileInputStream(file); + sender=new RtpStreamSender(input_stream,true,payload_type,frame_rate,frame_size,daddr,dport); + } + } + else + { ToneInputStream tone=new ToneInputStream(tone_freq,tone_amp,sample_rate,sample_size,tone_codec,big_endian); + sender=new RtpStreamSender(tone,true,payload_type,frame_rate,frame_size,daddr,dport); + } + if (sender!=null) + { + sender.start(); + if (sound) audio_input.play(); + + System.out.println("Press 'Return' to stop"); + System.in.read(); + + sender.halt(); + if (sound) audio_input.stop(); + if (sound) AudioInput.closeAudioLine(); + } + else + { System.out.println("Error creating the rtp stream."); + } + } + catch (Exception e) { e.printStackTrace(); } + } + +} \ No newline at end of file diff --git a/src/local/media/ExtendedPipedInputStream.java b/src/local/media/ExtendedPipedInputStream.java new file mode 100644 index 0000000..a1bf696 --- /dev/null +++ b/src/local/media/ExtendedPipedInputStream.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This source code is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package local.media; + + +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.io.IOException; + + +/** ExtendedPipedInputStream. + */ +public class ExtendedPipedInputStream extends PipedInputStream +{ + + /** The circular buffer size. */ + public static int EXT_PIPE_SIZE=32768; + + + /** Creates a new ExtendedPipedInputStream. */ + public ExtendedPipedInputStream() + { super(); + buffer=new byte[EXT_PIPE_SIZE]; + } + + /** Creates a new ExtendedPipedInputStream. */ + public ExtendedPipedInputStream(PipedOutputStream src) throws IOException + { super(src); + buffer=new byte[EXT_PIPE_SIZE]; + } + + /** Returns the number of bytes that can be read from this input stream without blocking. */ + //public int available() + + /** Closes this piped input stream and releases any system resources associated with the stream. */ + //public void close() throws IOException + + /** Causes this piped input stream to be connected to the piped output stream src. */ + //public void connect(PipedOutputStream src) throws IOException + + /** Reads the next byte of data from this piped input stream. */ + public int read() throws IOException + { try + { return super.read(); + } + catch (IOException e) { return -1; } + } + + /** Reads up to len bytes of data from this piped input stream into an array of bytes. */ + //public int read(byte[] b, int off, int len) + + /** Receives a byte of data. */ + protected void receive(int b) throws IOException + { try + { if (in!=out) super.receive(b); + } + catch (IOException e) {} + } + +} \ No newline at end of file diff --git a/src/local/media/ExtendedPipedOutputStream.java b/src/local/media/ExtendedPipedOutputStream.java new file mode 100644 index 0000000..45376f2 --- /dev/null +++ b/src/local/media/ExtendedPipedOutputStream.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This source code is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package local.media; + + +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.io.IOException; + + +/** ExtendedPipedOutputStream. + */ +public class ExtendedPipedOutputStream extends PipedOutputStream +{ + + /** Creates a new ExtendedPipedOutputStream. */ + public ExtendedPipedOutputStream() + { super(); + } + + /** Creates a new ExtendedPipedOutputStream. */ + public ExtendedPipedOutputStream(PipedInputStream snk) throws IOException + { super(snk); + } + + /** Closes this piped output stream and releases any system resources associated with this stream. */ + //public void close() + + /** Connects this piped output stream to a receiver. */ + //public void connect(PipedInputStream snk) throws IOException + + /** Flushes this output stream and forces any buffered output bytes to be written out. */ + //public void flush() + + /** Writes len bytes from the specified byte array starting at offset off to this piped output stream. */ + //public void write(byte[] b, int off, int len) + + /** Writes the specified byte to the piped output stream. */ + //public void write(int b) throws IOException + +} \ No newline at end of file diff --git a/src/local/media/G711.java b/src/local/media/G711.java new file mode 100644 index 0000000..b61fc1c --- /dev/null +++ b/src/local/media/G711.java @@ -0,0 +1,259 @@ +package local.media; + + + +/** G.711 codec. + * This class provides methods for u-law, A-law and linear PCM conversions. + */ +public class G711 +{ + static final int SIGN_BIT=0x80; // Sign bit for a A-law byte. + static final int QUANT_MASK=0xf; // Quantization field mask. + static final int NSEGS=8; // Number of A-law segments. + static final int SEG_SHIFT=4; // Left shift for segment number. + static final int SEG_MASK=0x70; // Segment field mask. + + static final int[] seg_end={ 0xFF, 0x1FF, 0x3FF, 0x7FF, 0xFFF, 0x1FFF, 0x3FFF, 0x7FFF }; + + // copy from CCITT G.711 specifications + + /** u- to A-law conversions */ + static final int[] _u2a={ + 1, 1, 2, 2, 3, 3, 4, 4, + 5, 5, 6, 6, 7, 7, 8, 8, + 9, 10, 11, 12, 13, 14, 15, 16, + 17, 18, 19, 20, 21, 22, 23, 24, + 25, 27, 29, 31, 33, 34, 35, 36, + 37, 38, 39, 40, 41, 42, 43, 44, + 46, 48, 49, 50, 51, 52, 53, 54, + 55, 56, 57, 58, 59, 60, 61, 62, + 64, 65, 66, 67, 68, 69, 70, 71, + 72, 73, 74, 75, 76, 77, 78, 79, + 81, 82, 83, 84, 85, 86, 87, 88, + 89, 90, 91, 92, 93, 94, 95, 96, + 97, 98, 99, 100, 101, 102, 103, 104, + 105, 106, 107, 108, 109, 110, 111, 112, + 113, 114, 115, 116, 117, 118, 119, 120, + 121, 122, 123, 124, 125, 126, 127, 128 }; + + /** A- to u-law conversions */ + static final int[] _a2u={ + 1, 3, 5, 7, 9, 11, 13, 15, + 16, 17, 18, 19, 20, 21, 22, 23, + 24, 25, 26, 27, 28, 29, 30, 31, + 32, 32, 33, 33, 34, 34, 35, 35, + 36, 37, 38, 39, 40, 41, 42, 43, + 44, 45, 46, 47, 48, 48, 49, 49, + 50, 51, 52, 53, 54, 55, 56, 57, + 58, 59, 60, 61, 62, 63, 64, 64, + 65, 66, 67, 68, 69, 70, 71, 72, + 73, 74, 75, 76, 77, 78, 79, 79, + 80, 81, 82, 83, 84, 85, 86, 87, + 88, 89, 90, 91, 92, 93, 94, 95, + 96, 97, 98, 99, 100, 101, 102, 103, + 104, 105, 106, 107, 108, 109, 110, 111, + 112, 113, 114, 115, 116, 117, 118, 119, + 120, 121, 122, 123, 124, 125, 126, 127 }; + + + static int search(int val, int[] table) + { for (int i=0; i=0) + { mask=0xD5; // sign (7th) bit = 1 + } + else + { mask=0x55; // sign bit = 0 + pcm_val=-pcm_val-8; + } + + // Convert the scaled magnitude to segment number. + seg=search(pcm_val,seg_end); + + // Combine the sign, segment, and quantization bits. + if (seg>=8) // out of range, return maximum value. + return (0x7F^mask); + else + { aval=seg<>4)&QUANT_MASK; + else aval|=(pcm_val>>(seg+3))&QUANT_MASK; + return (aval^mask); + } + } + + /** Converts an A-law value to 16-bit linear PCM + */ + //public static int alaw2linear(unsigned char a_val) + public static int alaw2linear(int a_val) + { int t; + int seg; + a_val^=0x55; + t=(a_val&QUANT_MASK)<<4; + //seg=((unsigned)a_val&SEG_MASK)>>SEG_SHIFT; + seg=(a_val&SEG_MASK)>>SEG_SHIFT; + switch (seg) + { case 0: + t+=8; + break; + case 1: + t+=0x108; + break; + default: + t+=0x108; + t<<=seg-1; + } + return ((a_val&SIGN_BIT)!=0)? t : -t; + } + + + /** Bias for linear code. */ + public static final int BIAS=0x84; + + + /** Converts a linear PCM value to u-law + * + * In order to simplify the encoding process, the original linear magnitude + * is biased by adding 33 which shifts the encoding range from (0 - 8158) to + * (33 - 8191). The result can be seen in the following encoding table: + * + * Biased Linear Input Code Compressed Code + * ------------------------ --------------- + * 00000001wxyza 000wxyz + * 0000001wxyzab 001wxyz + * 000001wxyzabc 010wxyz + * 00001wxyzabcd 011wxyz + * 0001wxyzabcde 100wxyz + * 001wxyzabcdef 101wxyz + * 01wxyzabcdefg 110wxyz + * 1wxyzabcdefgh 111wxyz + * + * Each biased linear code has a leading 1 which identifies the segment + * number. The value of the segment number is equal to 7 minus the number + * of leading 0's. The quantization interval is directly available as the + * four bits wxyz. The trailing bits (a - h) are ignored. + * + * Ordinarily the complement of the resulting code word is used for + * transmission, and so the code word is complemented before it is returned. + * + * For further information see John C. Bellamy's Digital Telephony, 1982, + * John Wiley & Sons, pps 98-111 and 472-476. + */ + public static int linear2ulaw(int pcm_val) // 2's complement (16-bit range) + { int mask; + int seg; + //unsigned char uval; + int uval; + + // Get the sign and the magnitude of the value. + if (pcm_val<0) + { pcm_val=BIAS-pcm_val; + mask=0x7F; + } + else + { pcm_val+=BIAS; + mask=0xFF; + } + // Convert the scaled magnitude to segment number. + seg=search(pcm_val,seg_end); + + // Combine the sign, segment, quantization bits; and complement the code word. + + if (seg>=8) return (0x7F^mask); // out of range, return maximum value. + else + { uval=(seg<<4) | ((pcm_val>>(seg+3)) & 0xF); + return (uval^mask); + } + } + + /** ConvertS a u-law value to 16-bit linear PCM. + * + * First, a biased linear code is derived from the code word. An unbiased + * output can then be obtained by subtracting 33 from the biased code. + * + * Note that this function expects to be passed the complement of the + * original code word. This is in keeping with ISDN conventions. + */ + //public static int ulaw2linear(unsigned char u_val) + public static int ulaw2linear(int u_val) + { int t; + // Complement to obtain normal u-law value. + u_val=~u_val; + // Extract and bias the quantization bits. Then shift up by the segment number and subtract out the bias. + t=((u_val&QUANT_MASK)<<3) + BIAS; + //t<<=((unsigned)u_val&SEG_MASK)>>SEG_SHIFT; + t<<=(u_val&SEG_MASK)>>SEG_SHIFT; + + return ((u_val&SIGN_BIT)!=0)? (BIAS-t) : (t-BIAS); + } + + + /** A-law to u-law conversion. + */ + //public static int alaw2ulaw(unsigned char aval) + public static int alaw2ulaw(int aval) + { aval&=0xff; + return ((aval & 0x80)!=0)? (0xFF^_a2u[aval^0xD5]) : (0x7F^_a2u[aval^0x55]); + } + + + /** u-law to A-law conversion. + */ + //public static int ulaw2alaw(unsigned char uval) + public static int ulaw2alaw(int uval) + { uval&=0xff; + return ((uval&0x80)!=0)? (0xD5^(_u2a[0xFF^uval]-1)) : (0x55^(_u2a[0x7F^uval]-1)); + } + + + /** PCM ecoder/decoder tests. */ + /*public static void main(String[] args) + { + for (int i=0; i<0xFF; i++) + { int pcmu,pcma,linear; + pcmu=i; + System.out.print(" pcmu:"+pcmu); + pcma=ulaw2alaw(pcmu); + System.out.print(" pcma:"+pcma); + pcmu=alaw2ulaw(pcma); + System.out.print(" pcmu:"+pcmu); + linear=alaw2linear(pcma); + System.out.print(" linear:"+linear); + pcma=linear2alaw(linear); + System.out.print(" pcma:"+pcma); + linear=ulaw2linear(pcmu); + System.out.print(" linear:"+linear); + pcmu=linear2ulaw(linear); + System.out.print(" pcmu:"+pcmu); + System.out.println(" ."); + if (i%20==19) + try { System.in.read(); } catch (Exception e) {} + } + }*/ +} diff --git a/src/local/media/JMediaReceiver.java b/src/local/media/JMediaReceiver.java new file mode 100644 index 0000000..8b3ace0 --- /dev/null +++ b/src/local/media/JMediaReceiver.java @@ -0,0 +1,106 @@ +package local.media; + + +import javax.media.*; +import javax.media.format.*; +import java.util.Vector; + + +/** JMediaReceiver is a JMF-based media receiver. + */ +public class JMediaReceiver implements ControllerListener +{ + Player player=null; + + JMediaReceiverListener ctr_listener=null; + + + /** Constructs a JMediaReceiver */ + public JMediaReceiver(String media_type, int port, JMediaReceiverListener listener) + { + ctr_listener=listener; + try + { String media_url="rtp://:"+port+"/"+media_type; + System.out.println("Receiver URL= "+media_url); + MediaLocator media_locator=new MediaLocator(media_url); + player=Manager.createPlayer(media_locator); + if (player==null) { System.out.println("Player cannot be created"); return; } + //else + + player.addControllerListener(this); + } + catch(Exception e) { e.printStackTrace(); } + } + + + /** Starts receiving the stream */ + public String start() + { + String err=null; + try + { System.out.println("Trying to realize the player"); + player.realize(); + while(player.getState()!=player.Realized); + System.out.println("Player realized"); + player.start(); + } + catch (Exception e) + { e.printStackTrace(); + err="Failed trying to start the player"; + } + return err; + } + + + /** Stops the receiver */ + public String stop() + { + if (player!=null) + { player.stop(); + player.deallocate(); + player.close(); + System.out.println("Player stopped"); + player=null; + } + return null; + } + + + public synchronized void controllerUpdate(ControllerEvent event) + { if (ctr_listener!=null) ctr_listener.controllerUpdate(event); + } + + public java.awt.Component getVisualComponent() + { return player.getVisualComponent(); + } + + public java.awt.Component getControlPanelComponent() + { return player.getControlPanelComponent(); + } + + /*public Player getPlayer() + { return player; + }*/ + + + + // ******************************* MAIN ******************************* + + /** The main method. */ + public static void main(String[] args) + { + if (args.length>=2) + try + { int port=Integer.parseInt(args[1]); + JMediaReceiver media_receiver = new JMediaReceiver(args[0],port,null); + media_receiver.start(); + return; + } + catch (Exception e) { System.out.println("Error creating the receiver"); } + + System.out.println("usage:\n java JMediaReceiver audio|video "); + } + +} + + diff --git a/src/local/media/JMediaReceiverListener.java b/src/local/media/JMediaReceiverListener.java new file mode 100644 index 0000000..8c0900d --- /dev/null +++ b/src/local/media/JMediaReceiverListener.java @@ -0,0 +1,12 @@ +package local.media; + + + +/** JMediaReceiverListener. + */ +public interface JMediaReceiverListener +{ + public void controllerUpdate(javax.media.ControllerEvent event); +} + + diff --git a/src/local/media/JMediaSender.java b/src/local/media/JMediaSender.java new file mode 100644 index 0000000..35f9fb0 --- /dev/null +++ b/src/local/media/JMediaSender.java @@ -0,0 +1,264 @@ +package local.media; + + +import java.io.*; + +import javax.media.*; +import javax.media.format.*; +import javax.media.protocol.*; +import javax.media.control.TrackControl; + +import java.util.Vector; + + +/** JMediaSender is a JMF-based media sender. + */ +public class JMediaSender +{ + Processor processor=null; + MediaLocator dest; + DataSink sink; + + + /** Constructs a JMediaSender */ + public JMediaSender(String type, String media, String dest_addr, int dest_port) + { + String result; + + // #### 1) set the media-locator + MediaLocator media_locator; + //try + { if (media==null) + { if (type.equals("audio")) media_locator=getAudio(); + else media_locator=getVideo(); + } + else media_locator=new MediaLocator(media); + } + //catch (Exception e) { e.printStackTrace(); } + System.out.println("MediaLocator: "+media_locator.toString()); + + // #### 2) create the processor + result=createProcessor(media_locator); + if (result!=null) + { System.out.println(result); + //System.exit(0); + return; + } + //else + System.out.println("Processor created"); + + // #### 3) configure the processor + processor.configure(); + while(processor.getState()0 || format.toString().indexOf("Mono")>0) + { if (type.equals("audio")) enabled=track_control.setFormat(new AudioFormat(AudioFormat.GSM_RTP))!=null; + else enabled=false; + } + else + { if (type.equals("video")) enabled=track_control.setFormat(new VideoFormat(VideoFormat.H263_RTP))!=null; + else enabled=false; + } + track_control.setEnabled(enabled); + System.out.println("track#"+i+" enabled="+enabled); + } + else + { track_control.setEnabled(false); + System.out.println("track#"+i+" disabled"); + } + } + } + + // #### 4) realize the processor + processor.realize(); + while(processor.getState()] "); + System.out.println("\n with: = \"file://filename\""); + } + else + { + String type=args[0]; + String media=null; + String addr; + int port; + if (args.length>=4) + { media=args[1]; + addr=args[2]; + port=Integer.parseInt(args[3]); + } + else + { addr=args[1]; + port=Integer.parseInt(args[2]); + } + + JMediaSender sender = new JMediaSender(type,media,addr,port); + + String result; + result=sender.start(); + if (result!=null) + { System.out.println("ERROR: "+result); + System.exit(0); + } + + System.out.println("Press 'Return' to stop"); + try { System.in.read(); } catch (IOException e) { e.printStackTrace(); } + + result=sender.stop(); + if (result!=null) + { System.out.println("ERROR: "+result); + System.exit(0); + } + } + } + +} \ No newline at end of file diff --git a/src/local/media/JVisualReceiver.java b/src/local/media/JVisualReceiver.java new file mode 100644 index 0000000..8bc31af --- /dev/null +++ b/src/local/media/JVisualReceiver.java @@ -0,0 +1,131 @@ +package local.media; + + +import javax.media.*; +import javax.media.format.*; + +import java.awt.*; +import javax.swing.*; +import java.awt.event.*; +import java.util.Vector; +import javax.swing.border.Border; + + + +/** JVisualReceiver is a JMF-based graphical media receiver. + */ +public class JVisualReceiver extends Frame implements JMediaReceiverListener +{ + + JMediaReceiver receiver; + + // GUI attributes + Panel panel = new Panel(); + Image icon=Toolkit.getDefaultToolkit().getImage("media/media/icon.gif"); + Image image=Toolkit.getDefaultToolkit().getImage("media/media/logo.gif"); + + Component visualComp=null, controlComp=null; + + /** Constructs a VisualReceiver */ + public JVisualReceiver(String media_type, int port) + { + receiver=new JMediaReceiver(media_type,port,this); + try + { jbInit(); + } + catch(Exception e) { e.printStackTrace(); } + } + + + public String start() + { + return receiver.start(); + } + + + public String stop() + { + return receiver.stop(); + } + + + private void jbInit() throws Exception + { + this.setTitle("Zoolu Player"); + this.setIconImage(icon); + this.addWindowListener(new java.awt.event.WindowAdapter() + { public void windowClosing(WindowEvent e) { this_windowClosing(); } + }); + visualComp=new ImagePanel(image); + panel.setLayout(new BorderLayout()); + panel.add(visualComp,BorderLayout.CENTER); + this.add(panel); + + this.setSize(new Dimension(150, 150)); + Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); + Dimension frameSize = this.getSize(); + if (frameSize.height > screenSize.height) + frameSize.height = screenSize.height; + if (frameSize.width > screenSize.width) + frameSize.width = screenSize.width; + this.setLocation((screenSize.width - frameSize.width) / 2, (screenSize.height - frameSize.height) / 2); + this.setVisible(true); + } + + public synchronized void controllerUpdate(ControllerEvent event) + { //System.out.println("DEBUG: controllerUpdate()"); + if (event instanceof RealizeCompleteEvent || event instanceof FormatChangeEvent) + { if (event instanceof RealizeCompleteEvent) System.out.println("RealizeComplete event"); + if (event instanceof FormatChangeEvent) System.out.println("FormatChange event"); + + if ((visualComp = receiver.getVisualComponent()) != null) panel.add("Center", visualComp); + if ((controlComp = receiver.getControlPanelComponent()) != null) panel.add("South", controlComp); + this.setVisible(true); + return; + } + else + { //System.out.println("Event: "+event.toString()+": Do nothing"); + } + } + + // ******************************* MAIN ******************************* + + public static void main(String[] args) + { + if (args.length>=2) + try + { int port=Integer.parseInt(args[1]); + JVisualReceiver tv = new JVisualReceiver(args[0],port); + tv.start(); + return; + } + catch (Exception e) { System.out.println("Error creating the receiver"); } + + System.out.println("usage:\n java JVisualReceiver audio|video "); + } + + void this_windowClosing() + { if (receiver!=null) receiver.stop(); + System.exit(0); + } + +} + + + // ******************************************************************** + // ************************* class ImagePanel ************************* + // ******************************************************************** + +class ImagePanel extends JPanel +{ Image image; + + public ImagePanel(Image image) { this.image = image; } + + public void paintComponent(Graphics g) + { super.paintComponent(g); //paint background + int x=this.getWidth(); + int y=this.getHeight(); + g.drawImage(image,0,0,x,y,this); //draw the image scaled + } +} + diff --git a/src/local/media/Mixer.java b/src/local/media/Mixer.java new file mode 100644 index 0000000..99389cd --- /dev/null +++ b/src/local/media/Mixer.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This source code is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package local.media; + + +import java.io.*; +import java.util.Hashtable; +import java.util.Enumeration; + + +/** MixerLine is a simple G711 mixer with M input lines (OutputStreams) + * and N output lines (InputStreams). + *

+ * Each line has an identifier (Object) used as key when adding or + * removing the line. + */ +public class Mixer +{ + /** The splitter lines (as Hashtable of Object->SplitterLine). */ + Hashtable splitter_lines; + + /** The mixer lines (as Hashtable of Object->MixerLine). */ + Hashtable mixer_lines; + + + /** Creates a new Mixer. */ + public Mixer() + { splitter_lines=new Hashtable(); + mixer_lines=new Hashtable(); + } + + + /** Close the Mixer. */ + public void close() throws IOException + { for (Enumeration e=splitter_lines.elements(); e.hasMoreElements(); ) + { ((SplitterLine)e.nextElement()).close(); + } + for (Enumeration e=mixer_lines.elements(); e.hasMoreElements(); ) + { ((MixerLine)e.nextElement()).close(); + } + mixer_lines=null; + splitter_lines=null; + } + + + /** Adds a new input line. */ + public OutputStream newInputLine(Object id) throws IOException + { //System.err.println("Mixer: add input line: "+id); + SplitterLine sl=new SplitterLine(id); + for (Enumeration e=mixer_lines.keys(); e.hasMoreElements(); ) + { Object mid=e.nextElement(); + if (!mid.equals(id)) + { //PipedInputStream is=new PipedInputStream(); + //PipedOutputStream os=new PipedOutputStream(is); + ExtendedPipedInputStream is=new ExtendedPipedInputStream(); + ExtendedPipedOutputStream os=new ExtendedPipedOutputStream(is); + //System.err.println("Mixer: SL("+id+"): add line: "+mid); + sl.addLine(mid,os); + //System.err.println("Mixer: ML("+mid+"): add line: "+id); + ((MixerLine)mixer_lines.get(mid)).addLine(id,is); + } + } + splitter_lines.put(id,sl); + return sl; + } + + + /** Removes a input line. */ + public void removeInputLine(Object id) + { //System.err.println("Mixer: remove input line: "+id); + SplitterLine sl=(SplitterLine)splitter_lines.get(id); + splitter_lines.remove(id); + /*for (Enumeration e=mixer_lines.elements(); e.hasMoreElements(); ) + { System.err.println("Mixer: ML(?): remove line: "+id); + ((MixerLine)e.nextElement()).removeLine(id); + }*/ + for (Enumeration e=mixer_lines.keys(); e.hasMoreElements(); ) + { Object mid=e.nextElement(); + if (!mid.equals(id)) + { //System.err.println("Mixer: SL("+id+"): remove line: "+mid); + sl.removeLine(mid); + //System.err.println("Mixer: ML("+mid+"): remove line: "+id); + ((MixerLine)mixer_lines.get(mid)).removeLine(id); + } + } + try { sl.close(); } catch (Exception e) {} + } + + + /** Adds a new output line. */ + public InputStream newOutputLine(Object id) throws IOException + { //System.err.println("Mixer: add output line: "+id); + MixerLine ml=new MixerLine(id); + for (Enumeration e=splitter_lines.keys(); e.hasMoreElements(); ) + { Object sid=e.nextElement(); + if (!sid.equals(id)) + { //PipedInputStream is=new PipedInputStream(); + //PipedOutputStream os=new PipedOutputStream(is); + ExtendedPipedInputStream is=new ExtendedPipedInputStream(); + ExtendedPipedOutputStream os=new ExtendedPipedOutputStream(is); + //System.err.println("Mixer: ML("+id+"): add line: "+sid); + ml.addLine(sid,is); + //System.err.println("Mixer: SL("+sid+"): add line: "+id); + ((SplitterLine)splitter_lines.get(sid)).addLine(id,os); + } + } + mixer_lines.put(id,ml); + return ml; + } + + + /** Removes a output line. */ + public void removeOutputLine(Object id) + { //System.err.println("Mixer: remove output line: "+id); + MixerLine ml=(MixerLine)mixer_lines.get(id); + mixer_lines.remove(id); + /*for (Enumeration e=splitter_lines.elements(); e.hasMoreElements(); ) + { System.err.println("Mixer: SL(?): remove line: "+id); + ((SplitterLine)e.nextElement()).removeLine(id); + }*/ + for (Enumeration e=splitter_lines.keys(); e.hasMoreElements(); ) + { Object sid=e.nextElement(); + if (!sid.equals(id)) + { //System.err.println("Mixer: ML("+id+"): remove line: "+sid); + ml.removeLine(sid); + //System.err.println("Mixer: SL("+sid+"): remove line: "+id); + ((SplitterLine)splitter_lines.get(sid)).removeLine(id); + } + } + try { ml.close(); } catch (Exception e) {} + } + +} \ No newline at end of file diff --git a/src/local/media/MixerLine.java b/src/local/media/MixerLine.java new file mode 100644 index 0000000..4bf9cfe --- /dev/null +++ b/src/local/media/MixerLine.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This source code is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package local.media; + + +import java.io.*; +import java.util.Hashtable; +import java.util.Enumeration; + + +/** MixerLine is a simple G711 mixer with N input lines (InputStream) + * and one output line (the MixerLine itself). + *

+ * Each input line has an identifier (Object) used as key when adding or + * removing the line. + */ +public class MixerLine extends InputStream +{ + /** SplitterLine identifier. */ + Object mixer_id; + + /** The input lines (as Hashtable of Object->InputStream). */ + Hashtable input_lines; + + + /** Creates a new MixerLine. */ + public MixerLine(Object mixer_id) + { this.mixer_id=mixer_id; + input_lines=new Hashtable(); + } + + /** Creates a new MixerLine. */ + public MixerLine(Object mixer_id, Hashtable input_lines) + { this.mixer_id=mixer_id; + this.input_lines=input_lines; + } + + + /** Adds a new line. */ + public void addLine(Object id, InputStream is) + { //System.err.println("ML: add: "+id+" "+is); + input_lines.put(id,is); + } + + /** Removes a line. */ + public void removeLine(Object id) + { //System.err.println("ML: remove: "+id); + input_lines.remove(id); + } + + + /** Returns the number of bytes that can be read (or skipped over) from this input stream without blocking by the next caller of a method for this input stream. */ + public int available() throws IOException + { int max=0; + for (Enumeration e=input_lines.elements(); e.hasMoreElements(); ) + { int n=((InputStream)e.nextElement()).available(); + if (n>max) max=n; + } + return max; + } + + /** Closes this input stream and releases any system resources associated with the stream. */ + public void close() throws IOException + { for (Enumeration e=input_lines.elements(); e.hasMoreElements(); ) + { ((InputStream)e.nextElement()).close(); + } + input_lines=null; + } + + /** Marks the current position in this input stream. */ + public void mark(int readlimit) + { for (Enumeration e=input_lines.elements(); e.hasMoreElements(); ) + { ((InputStream)e.nextElement()).mark(readlimit); + } + } + + /** Tests if this input stream supports the mark and reset methods. */ + public boolean markSupported() + { boolean supported=true; + for (Enumeration e=input_lines.elements(); e.hasMoreElements(); ) + { if (!((InputStream)e.nextElement()).markSupported()) supported=false; + } + return supported; + } + + /** Reads the next byte of data from the input stream. */ + public int read() throws IOException + { int sum=0; + int count=0; + int err_code=0; + for (Enumeration e=input_lines.elements(); e.hasMoreElements(); ) + { InputStream is=(InputStream)e.nextElement(); + if (is.available()>0); + { int value=is.read(); + if (value>0) + { count++; + sum+=G711.ulaw2linear(value); + } + else err_code=value; + } + } + if (count>0 || err_code==0) return G711.linear2ulaw(sum); + else return err_code; + } + + /** Reads some number of bytes from the input stream and stores them into the buffer array b. */ + public int read(byte[] b) throws IOException + { //System.err.print("o"); + int ret=super.read(b); + //System.err.print("."); + return ret; + } + + /** Reads up to len bytes of data from the input stream into an array of bytes. */ + public int read(byte[] b, int off, int len) throws IOException + { //System.err.print("o"); + int ret=super.read(b,off,len); + //System.err.print("."); + return ret; + } + + /** Repositions this stream to the position at the time the mark method was last called on this input stream. */ + public void reset() throws IOException + { for (Enumeration e=input_lines.elements(); e.hasMoreElements(); ) + { ((InputStream)e.nextElement()).reset(); + } + } + + /** Skips over and discards n bytes of data from this input stream. */ + public long skip(long n) throws IOException + { for (Enumeration e=input_lines.elements(); e.hasMoreElements(); ) + { ((InputStream)e.nextElement()).skip(n); + } + return n; + } +} \ No newline at end of file diff --git a/src/local/media/RtpStreamReceiver.java b/src/local/media/RtpStreamReceiver.java new file mode 100644 index 0000000..4379c87 --- /dev/null +++ b/src/local/media/RtpStreamReceiver.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This source code is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package local.media; + + +import local.net.RtpPacket; +import local.net.RtpSocket; + +import java.io.*; +import java.net.InetAddress; +import java.net.DatagramSocket; + + +/** RtpStreamReceiver is a generic stream receiver. + * It receives packets from RTP and writes them into an OutputStream. + */ +public class RtpStreamReceiver extends Thread +{ + + /** Whether working in debug mode. */ + //private static final boolean DEBUG=true; + public static boolean DEBUG=false; + + /** Size of the read buffer */ + public static final int BUFFER_SIZE=32768; + + /** Maximum blocking time, spent waiting for reading new bytes [milliseconds] */ + public static final int SO_TIMEOUT=200; + + /** The OutputStream */ + OutputStream output_stream=null; + + /** The RtpSocket */ + RtpSocket rtp_socket=null; + + /** Whether the socket has been created here */ + boolean socket_is_local=false; + + /** Whether it is running */ + boolean running=false; + + + /** Constructs a RtpStreamReceiver. + * @param output_stream the stream sink + * @param local_port the local receiver port */ + public RtpStreamReceiver(OutputStream output_stream, int local_port) + { try + { DatagramSocket socket=new DatagramSocket(local_port); + socket_is_local=true; + init(output_stream,socket); + } + catch (Exception e) { e.printStackTrace(); } + } + + /** Constructs a RtpStreamReceiver. + * @param output_stream the stream sink + * @param socket the local receiver DatagramSocket */ + public RtpStreamReceiver(OutputStream output_stream, DatagramSocket socket) + { init(output_stream,socket); + } + + /** Inits the RtpStreamReceiver */ + private void init(OutputStream output_stream, DatagramSocket socket) + { this.output_stream=output_stream; + if (socket!=null) rtp_socket=new RtpSocket(socket); + } + + + /** Whether is running */ + public boolean isRunning() + { return running; + } + + /** Stops running */ + public void halt() + { running=false; + } + + /** Runs it in a new Thread. */ + public void run() + { + if (rtp_socket==null) + { if (DEBUG) println("ERROR: RTP socket is null"); + return; + } + //else + + byte[] buffer=new byte[BUFFER_SIZE]; + RtpPacket rtp_packet=new RtpPacket(buffer,0); + + if (DEBUG) println("Reading blocks of max "+buffer.length+" bytes"); + + //byte[] aux=new byte[BUFFER_SIZE]; + + running=true; + try + { rtp_socket.getDatagramSocket().setSoTimeout(SO_TIMEOUT); + while (running) + { try + { // read a block of data from the rtp socket + rtp_socket.receive(rtp_packet); + //if (DEBUG) System.out.print("."); + + // write this block to the output_stream (only if still running..) + if (running) output_stream.write(rtp_packet.getPacket(), rtp_packet.getHeaderLength(), rtp_packet.getPayloadLength()); + /*if (running) + { byte[] pkt=rtp_packet.getPacket(); + int offset=rtp_packet.getHeaderLength(); + int len=rtp_packet.getPayloadLength(); + int pos=0; + for (int i=0; i>8); + aux[pos++]=(byte)G711.linear2ulaw(linear); + } + output_stream.write(aux,0,pos); + }*/ + } + catch (java.io.InterruptedIOException e) { } + } + } + catch (Exception e) { running=false; e.printStackTrace(); } + + // close RtpSocket and local DatagramSocket + DatagramSocket socket=rtp_socket.getDatagramSocket(); + rtp_socket.close(); + if (socket_is_local && socket!=null) socket.close(); + + // free all + output_stream=null; + rtp_socket=null; + + if (DEBUG) println("rtp receiver terminated"); + } + + + /** Debug output */ + private static void println(String str) + { System.out.println("RtpStreamReceiver: "+str); + } + + + public static int byte2int(byte b) + { //return (b>=0)? b : -((b^0xFF)+1); + //return (b>=0)? b : b+0x100; + return (b+0x100)%0x100; + } + + public static int byte2int(byte b1, byte b2) + { return (((b1+0x100)%0x100)<<8)+(b2+0x100)%0x100; + } +} + + diff --git a/src/local/media/RtpStreamSender.java b/src/local/media/RtpStreamSender.java new file mode 100644 index 0000000..1b4dced --- /dev/null +++ b/src/local/media/RtpStreamSender.java @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This source code is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package local.media; + + +import local.net.RtpPacket; +import local.net.RtpSocket; + +import java.io.InputStream; +import java.net.InetAddress; +import java.net.DatagramSocket; + + +/** RtpStreamSender is a generic stream sender. + * It takes an InputStream and sends it through RTP. + */ +public class RtpStreamSender extends Thread +{ + /** Whether working in debug mode. */ + //private static final boolean DEBUG=true; + public static boolean DEBUG=false; + + /** The InputStream */ + InputStream input_stream=null; + + /** The RtpSocket */ + RtpSocket rtp_socket=null; + + /** Payload type */ + int p_type; + + /** Number of frame per second */ + long frame_rate; + + /** Number of bytes per frame */ + int frame_size; + + /** Whether the socket has been created here */ + boolean socket_is_local=false; + + /** Whether it works synchronously with a local clock, or it it acts as slave of the InputStream */ + boolean do_sync=true; + + /** Synchronization correction value, in milliseconds. + * It accellarates the sending rate respect to the nominal value, + * in order to compensate program latencies. */ + int sync_adj=0; + + /** Whether it is running */ + boolean running=false; + + + /** Constructs a RtpStreamSender. + * @param input_stream the stream source + * @param do_sync whether time synchronization must be performed by the RtpStreamSender, + * or it is performed by the InputStream (e.g. the system audio input) + * @param payload_type the payload type + * @param frame_rate the frame rate, i.e. the number of frames that should be sent per second; + * it is used to calculate the nominal packet time and,in case of do_sync==true, + the next departure time + * @param frame_size the size of the payload + * @param dest_addr the destination address + * @param dest_port the destination port */ + public RtpStreamSender(InputStream input_stream, boolean do_sync, int payload_type, long frame_rate, int frame_size, String dest_addr, int dest_port) + { init(input_stream,do_sync,payload_type,frame_rate,frame_size,null,dest_addr,dest_port); + } + + + /** Constructs a RtpStreamSender. + * @param input_stream the stream source + * @param do_sync whether time synchronization must be performed by the RtpStreamSender, + * or it is performed by the InputStream (e.g. the system audio input) + * @param payload_type the payload type + * @param frame_rate the frame rate, i.e. the number of frames that should be sent per second; + * it is used to calculate the nominal packet time and,in case of do_sync==true, + the next departure time + * @param frame_size the size of the payload + * @param src_port the source port + * @param dest_addr the destination address + * @param dest_port the destination port */ + //public RtpStreamSender(InputStream input_stream, boolean do_sync, int payload_type, long frame_rate, int frame_size, int src_port, String dest_addr, int dest_port) + //{ init(input_stream,do_sync,payload_type,frame_rate,frame_size,null,src_port,dest_addr,dest_port); + //} + + + /** Constructs a RtpStreamSender. + * @param input_stream the stream to be sent + * @param do_sync whether time synchronization must be performed by the RtpStreamSender, + * or it is performed by the InputStream (e.g. the system audio input) + * @param payload_type the payload type + * @param frame_rate the frame rate, i.e. the number of frames that should be sent per second; + * it is used to calculate the nominal packet time and,in case of do_sync==true, + the next departure time + * @param frame_size the size of the payload + * @param src_socket the socket used to send the RTP packet + * @param dest_addr the destination address + * @param dest_port the thestination port */ + public RtpStreamSender(InputStream input_stream, boolean do_sync, int payload_type, long frame_rate, int frame_size, DatagramSocket src_socket, String dest_addr, int dest_port) + { init(input_stream,do_sync,payload_type,frame_rate,frame_size,src_socket,dest_addr,dest_port); + } + + + /** Inits the RtpStreamSender */ + private void init(InputStream input_stream, boolean do_sync, int payload_type, long frame_rate, int frame_size, DatagramSocket src_socket, /*int src_port,*/ String dest_addr, int dest_port) + { + this.input_stream=input_stream; + this.p_type=payload_type; + this.frame_rate=frame_rate; + this.frame_size=frame_size; + this.do_sync=do_sync; + try + { if (src_socket==null) + { //if (src_port>0) src_socket=new DatagramSocket(src_port); else + src_socket=new DatagramSocket(); + socket_is_local=true; + } + rtp_socket=new RtpSocket(src_socket,InetAddress.getByName(dest_addr),dest_port); + } + catch (Exception e) { e.printStackTrace(); } + } + + + /** Sets the synchronization adjustment time (in milliseconds). */ + public void setSyncAdj(int millisecs) + { sync_adj=millisecs; + } + + /** Whether is running */ + public boolean isRunning() + { return running; + } + + /** Stops running */ + public void halt() + { running=false; + } + + /** Runs it in a new Thread. */ + public void run() + { + if (rtp_socket==null || input_stream==null) return; + //else + + byte[] buffer=new byte[frame_size+12]; + RtpPacket rtp_packet=new RtpPacket(buffer,0); + rtp_packet.setPayloadType(p_type); + int seqn=0; + long time=0; + //long start_time=System.currentTimeMillis(); + long byte_rate=frame_rate*frame_size; + + running=true; + + if (DEBUG) println("Reading blocks of "+(buffer.length-12)+" bytes"); + + try + { while (running) + { + //if (DEBUG) System.out.print("o"); + int num=input_stream.read(buffer,12,buffer.length-12); + //if (DEBUG) System.out.print("*"); + if (num>0) + { rtp_packet.setSequenceNumber(seqn++); + rtp_packet.setTimestamp(time); + rtp_packet.setPayloadLength(num); + rtp_socket.send(rtp_packet); + // update rtp timestamp (in milliseconds) + long frame_time=(num*1000)/byte_rate; + time+=frame_time; + // wait fo next departure + if (do_sync) + { // wait before next departure.. + //long frame_time=start_time+time-System.currentTimeMillis(); + // accellerate in order to compensate possible program latency.. ;) + frame_time-=sync_adj; + try { Thread.sleep(frame_time); } catch (Exception e) {} + } + } + else + if (num<0) + { running=false; + if (DEBUG) println("Error reading from InputStream"); + } + } + } + catch (Exception e) { running=false; e.printStackTrace(); } + + //if (DEBUG) println("rtp time: "+time); + //if (DEBUG) println("real time: "+(System.currentTimeMillis()-start_time)); + + // close RtpSocket and local DatagramSocket + DatagramSocket socket=rtp_socket.getDatagramSocket(); + rtp_socket.close(); + if (socket_is_local && socket!=null) socket.close(); + + // free all + input_stream=null; + rtp_socket=null; + + if (DEBUG) println("rtp sender terminated"); + } + + + /** Debug output */ + private static void println(String str) + { System.out.println("RtpStreamSender: "+str); + } + +} \ No newline at end of file diff --git a/src/local/media/RtpStreamTranslator.java b/src/local/media/RtpStreamTranslator.java new file mode 100644 index 0000000..f5c264d --- /dev/null +++ b/src/local/media/RtpStreamTranslator.java @@ -0,0 +1,252 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This source code is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package local.media; + + +import local.net.RtpPacket; +import local.net.RtpSocket; + +import java.io.*; +import java.net.InetAddress; +import java.net.DatagramSocket; + + +/** RtpStreamReceiver is a generic stream receiver. + * It receives packets from RTP and writes them into an OutputStream. + */ +public class RtpStreamTranslator extends Thread +{ + + /** Whether debug mode */ + private static final boolean DEBUG=true; + + /** Size of the read buffer */ + private static final int BUFFER_SIZE=32768; + + /** Maximum blocking time, spent waiting for reading new bytes [milliseconds] */ + private static final int SO_TIMEOUT=200; + + /** The OutputStream */ + OutputStream output_stream=null; + + /** The input RtpSocket */ + RtpSocket rtp_socket_in=null; + + /** The input RtpSocket */ + RtpSocket rtp_socket_out=null; + + /** Whether the receive socket has been created here */ + boolean socket_in_is_local=false; + + /** Whether the send socket has been created here */ + boolean socket_out_is_local=false; + + /** Whether it is running */ + boolean running=false; + + + /** Constructs a RtpStreamTranslator. + * @param recv_port the local receiver port + * @param dest_addr the destination address + * @param dest_port the thestination port */ + public RtpStreamTranslator(int recv_port, String dest_addr, int dest_port) + { try + { DatagramSocket recv_socket=new DatagramSocket(recv_port); + socket_in_is_local=true; + init(recv_socket,null,dest_addr,dest_port); + } + catch (Exception e) { e.printStackTrace(); } + } + + /** Constructs a RtpStreamTranslator. + * @param socket_in the local receiver socket + * @param socket_out the socket used to send the RTP packet + * @param dest_addr the destination address + * @param dest_port the thestination port */ + public RtpStreamTranslator(DatagramSocket socket_in, DatagramSocket socket_out, String dest_addr, int dest_port) + { init(socket_in,socket_out,dest_addr,dest_port); + } + + /** Inits the RtpStreamTranslator */ + private void init(DatagramSocket socket_in, DatagramSocket socket_out, String dest_addr, int dest_port) + { + try + { if (socket_out==null) + { socket_out=new DatagramSocket(); + socket_out_is_local=true; + } + rtp_socket_in=new RtpSocket(socket_in); + rtp_socket_out=new RtpSocket(socket_out,InetAddress.getByName(dest_addr),dest_port); + } + catch (Exception e) { e.printStackTrace(); } + } + + + /** Whether is running */ + public boolean isRunning() + { return running; + } + + /** Stops running */ + public void halt() + { running=false; + } + + /** Runs it in a new Thread. */ + public void run() + { + if (rtp_socket_in==null || rtp_socket_out==null) + { if (DEBUG) println("ERROR: RTP socket_in or socket_out is null"); + return; + } + //else + + byte[] buffer_in=new byte[BUFFER_SIZE]; + RtpPacket rtp_packet_in=new RtpPacket(buffer_in,0); + + byte[] buffer_out=new byte[BUFFER_SIZE]; + RtpPacket rtp_packet_out=new RtpPacket(buffer_out,0); + //rtp_packet_out.setPayloadType(p_type); + + if (DEBUG) println("Reading blocks of max "+BUFFER_SIZE+" bytes"); + + //File file=new File("audio.wav"); + //javax.sound.sampled.AudioInputStream audio_input_stream=null; + //try { audio_input_stream=javax.sound.sampled.AudioSystem.getAudioInputStream(file); } catch (Exception e) { e.printStackTrace(); } + + running=true; + try + { rtp_socket_in.getDatagramSocket().setSoTimeout(SO_TIMEOUT); + while (running) + { try + { // read a block of data from the rtp_socket_in + rtp_socket_in.receive(rtp_packet_in); + //if (DEBUG) System.out.print("."); + + // send the block to the rtp_socket_out (if still running..) + if (running) + { byte[] pkt1=rtp_packet_in.getPacket(); + int offset1=rtp_packet_in.getHeaderLength(); + int len1=rtp_packet_in.getPayloadLength(); + + byte[] pkt2=rtp_packet_out.getPacket(); + int offset2=rtp_packet_out.getHeaderLength(); + int pos2=offset2; + + for (int i=0; i>8); + //int linear2=G711.ulaw2linear(audio_input_stream.read()); + //linear+=linear2; + pkt2[pos2++]=(byte)G711.linear2ulaw(linear); + } + rtp_packet_out.setPayloadType(rtp_packet_in.getPayloadType()); + rtp_packet_out.setSequenceNumber(rtp_packet_in.getSequenceNumber()); + rtp_packet_out.setTimestamp(rtp_packet_in.getTimestamp()); + rtp_packet_out.setPayloadLength(pos2-offset2); + rtp_socket_out.send(rtp_packet_out); + } + + } + catch (java.io.InterruptedIOException e) { } + } + } + catch (Exception e) { running=false; e.printStackTrace(); } + + // close RtpSocket and local DatagramSocket + DatagramSocket socket_in=rtp_socket_in.getDatagramSocket(); + rtp_socket_in.close(); + if (socket_in_is_local && socket_in!=null) socket_in.close(); + DatagramSocket socket_out=rtp_socket_out.getDatagramSocket(); + rtp_socket_out.close(); + if (socket_out_is_local && socket_out!=null) socket_out.close(); + + // free all + rtp_socket_in=null; + rtp_socket_out=null; + + if (DEBUG) println("rtp translator terminated"); + } + + + /** Debug output */ + private static void println(String str) + { System.out.println("RtpStreamTranslator: "+str); + } + + + public static int byte2int(byte b) + { //return (b>=0)? b : -((b^0xFF)+1); + //return (b>=0)? b : b+0x100; + return (b+0x100)%0x100; + } + + + public static int byte2int(byte b1, byte b2) + { return (((b1+0x100)%0x100)<<8)+(b2+0x100)%0x100; + } + + + // ******************************* MAIN ******************************* + + /** The main method. */ + public static void main(String[] args) + { + String daddr=null; + int dport=0; + int rport=0; + + boolean help=true; + + for (int i=0; i1) + { rport=Integer.parseInt(args[i++]); + daddr=args[i++]; + dport=Integer.parseInt(args[i++]); + help=false; + continue; + } + + // else, do: + System.out.println("unrecognized param '"+args[i]+"'\n"); + help=true; + } + + if (help) + { System.out.println("usage:\n java RtpStreamTranslator [options]"); + System.out.println(" options:"); + System.out.println(" -h this help"); + System.exit(0); + } + + RtpStreamTranslator translator=new RtpStreamTranslator(rport,daddr,dport); + translator.start(); + } + +} + + diff --git a/src/local/media/SplitterLine.java b/src/local/media/SplitterLine.java new file mode 100644 index 0000000..396bfcd --- /dev/null +++ b/src/local/media/SplitterLine.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This source code is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package local.media; + + +import java.io.*; +import java.util.Hashtable; +import java.util.Enumeration; + + +/** SplitterLine is a simple splitter with one input line (the SplitterLine itself) + * and N output lines (OutputStreams). + *

+ * Each output line has an identifier (Object) used as key when adding or + * removing the line. + */ +public class SplitterLine extends OutputStream +{ + /** SplitterLine identifier. */ + Object splitter_id; + + /** The output lines (as Hashtable of Object->OutputStream). */ + Hashtable output_lines; + + + /** Creates a new SplitterLine. */ + public SplitterLine(Object splitter_id) + { this.splitter_id=splitter_id; + output_lines=new Hashtable(); + } + + /** Creates a new SplitterLine. */ + public SplitterLine(Object splitter_id, Hashtable output_lines) + { this.splitter_id=splitter_id; + this.output_lines=output_lines; + } + + + /** Adds a new line. */ + public void addLine(Object id, OutputStream os) + { //System.err.println("SL: add: "+id+" "+os); + output_lines.put(id,os); + } + + /** Removes a line. */ + public void removeLine(Object id) + { //System.err.println("SL: remove: "+id); + output_lines.remove(id); + } + + + /** Closes this output stream and releases any system resources associated with this stream. */ + public void close() throws IOException + { for (Enumeration e=output_lines.elements(); e.hasMoreElements(); ) + { ((OutputStream)e.nextElement()).close(); + } + output_lines=null; + } + + /** Flushes this output stream and forces any buffered output bytes to be written out. */ + public void flush() throws IOException + { for (Enumeration e=output_lines.elements(); e.hasMoreElements(); ) + { ((OutputStream)e.nextElement()).flush(); + } + } + + /** Writes b.length bytes from the specified byte array to this output stream. */ + public void write(byte[] b) throws IOException + { //System.err.print("*"); + super.write(b); + //System.err.print("@"); + } + + /** Writes len bytes from the specified byte array starting at offset off to this output stream. */ + public void write(byte[] b, int off, int len) throws IOException + { //System.err.print("*"); + super.write(b,off,len); + //System.err.print("@"); + } + + /** Writes the specified byte to this output stream. */ + public void write(int b) throws IOException + { /*for (Enumeration e=output_lines.elements(); e.hasMoreElements(); ) + { ((OutputStream)e.nextElement()).write(b); + }*/ + for (Enumeration e=output_lines.keys(); e.hasMoreElements(); ) + { Object line_id=e.nextElement(); + try + { ((OutputStream)output_lines.get(line_id)).write(b); + } + catch (IOException ex) + { System.err.println("SL("+splitter_id+"): ERROR while writing on line "+line_id); + ex.printStackTrace(); + throw new IOException("SplitterLine error"); + } + } + } + +} diff --git a/src/local/media/ToneInputStream.java b/src/local/media/ToneInputStream.java new file mode 100644 index 0000000..69bca71 --- /dev/null +++ b/src/local/media/ToneInputStream.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This source code is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package local.media; + + +//import java.io.*; +import java.lang.Math; + + +/** Generates a single tone. + */ +public class ToneInputStream extends java.io.InputStream +{ + /** The number of bytes that are notified as available */ + static int MAX_AVAILABLE_BYTES=65536; + + /** Identifier of linear unsigned PCM */ + public static final int PCM_LINEAR_UNSIGNED=0; + + /** Identifier of linear signed PCM */ + public static final int PCM_LINEAR_SIGNED=1; + + + /** Tone frequence */ + int f0; + /** Tone ampliture in the interval [0:2^(n-1)] where n is the sample sinze in bits. */ + double A; + /** Offset to be added in case uo unsigned PCM */ + double zero; + /** Sample rate [samples per seconds] */ + int fs; + /** Sample size [bytes] */ + int size; + /** Whether use big endian foramt */ + boolean big_endian; + + /** B=2*Pi*f0/fs */ + double B; + /** Sample sequence number */ + double k; + /** Buffer containing the current sample */ + byte[] s_buff; + /** Index within s_buff */ + int s_index; + + /** Creates a new 8-bit per sample ToneInputStream */ + /*public ToneInputStream(int frequence, double ampliture, int sample_rate, int codec) + { init(frequence,ampliture,sample_rate,1,codec); + }*/ + + /** Creates a new ToneInputStream */ + public ToneInputStream(int frequence, double ampliture, int sample_rate, int sample_size, int codec, boolean big_endian) + { init(frequence,ampliture,sample_rate,sample_size,codec,big_endian); + } + + /** Inits the ToneInputStream */ + private void init(int frequence, double ampliture, int sample_rate, int sample_size, int codec, boolean big_endian) + { this.f0=frequence; + this.fs=sample_rate; + this.size=sample_size; + this.big_endian=big_endian; + B=(2*Math.PI*f0)/fs; + long range=((long)1)<<((sample_size*8)-1); + A=ampliture*range; + if (codec==PCM_LINEAR_SIGNED) zero=0.0F; + else zero=range/2; + k=0; + s_index=0; + s_buff=new byte[size]; + + //System.out.println("Tone: PI: "+Math.PI); + //System.out.println("Tone: sin(PI/6): "+Math.sin(Math.PI/6)); + //System.out.println("Tone: s_rate: "+fs); + //System.out.println("Tone: s_size: "+size); + //System.out.println("Tone: A: "+A); + //System.out.println("Tone: 0: "+zero); + } + + + /** Returns the number of bytes that can be read (or skipped over) from this input stream without blocking by the next caller of a method for this input stream. */ + public int available() + { return MAX_AVAILABLE_BYTES; + } + + + /** Reads the next sample. */ + private double nextSample() + { return A*Math.sin(B*(k++))+zero; + } + + /** Reads the next byte of data from the input stream. */ + public int read() + { if (s_index==0) + { // get next sample + long next_sample=(long)(nextSample()); + // set the s_buff + for (int i=0; i0) + { udp_packet.setLength(num); + udp_socket.send(udp_packet); + // update rtp timestamp (in milliseconds) + time+=(num*1000)/byte_rate; + // wait fo next departure + if (do_sync) + { long frame_time=start_time+time-System.currentTimeMillis(); + // wait before next departure.. + try { Thread.sleep(frame_time); } catch (Exception e) {} + } + } + else + if (num<0) + { println("Error reading from InputStream"); + running=false; + } + } + } + catch (Exception e) { running=false; e.printStackTrace(); } + + //if (DEBUG) println("rtp time: "+time); + //if (DEBUG) println("real time: "+(System.currentTimeMillis()-start_time)); + + // close UdpSocket + if (socket_is_local && udp_socket!=null) udp_socket.close(); + + // free all + input_stream=null; + udp_socket=null; + + if (DEBUG) println("udp sender terminated"); + } + + + /** Debug output */ + private static void println(String str) + { System.out.println("UdpStreamSender: "+str); + } + +} \ No newline at end of file diff --git a/src/local/net/KeepAliveSip.java b/src/local/net/KeepAliveSip.java new file mode 100644 index 0000000..c033363 --- /dev/null +++ b/src/local/net/KeepAliveSip.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This source code is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package local.net; + + +import org.zoolu.net.*; +import org.zoolu.sip.provider.SipProvider; +import org.zoolu.sip.message.Message; + +import org.zoolu.tools.Timer; +import org.zoolu.tools.TimerListener; +import java.util.Date; + + +/** KeepAliveSip thread, for keeping the connection up toward a target SIP node + * (e.g. toward the seriving proxy/gw or a remote UA). + * It periodically sends keep-alive tokens in order to refresh TCP connection timeouts + * and/or NAT TCP/UDP session timeouts. + */ +public class KeepAliveSip extends KeepAliveUdp +{ + /** SipProvider */ + SipProvider sip_provider; + + /** Sip message */ + Message message=null; + + + /** Creates a new SIP KeepAliveSip daemon */ + public KeepAliveSip(SipProvider sip_provider, SocketAddress target, long delta_time) + { super(target,delta_time); + init(sip_provider,null); + start(); + } + + /** Creates a new SIP KeepAliveSip daemon */ + public KeepAliveSip(SipProvider sip_provider, SocketAddress target, Message message, long delta_time) + { super(target,delta_time); + init(sip_provider,message); + start(); + } + + + /** Inits the KeepAliveSip in SIP mode */ + private void init(SipProvider sip_provider, Message message) + { this.sip_provider=sip_provider; + if (message==null) + { message=new Message("\r\n"); + } + //if (target!=null) + //{ message.setRemoteAddress(target.getAddress().toString()); + // message.setRemotePort(target.getPort()); + //} + this.message=message; + } + + + /** Sends the kepp-alive packet now. */ + public void sendToken() throws java.io.IOException + { // do send? + if (!stop && target!=null && sip_provider!=null) + { sip_provider.sendMessage(message,sip_provider.getDefaultTransport(),target.getAddress().toString(),target.getPort(),127); + } + } + + + /** Main thread. */ + public void run() + { super.run(); + sip_provider=null; + } + + + /** Gets a String representation of the Object */ + public String toString() + { String str=null; + if (sip_provider!=null) + { str="sip:"+sip_provider.getViaAddress()+":"+sip_provider.getPort()+"-->"+target.toString(); + } + return str+" ("+delta_time+"ms)"; + } + +} \ No newline at end of file diff --git a/src/local/net/KeepAliveUdp.java b/src/local/net/KeepAliveUdp.java new file mode 100644 index 0000000..76a3c76 --- /dev/null +++ b/src/local/net/KeepAliveUdp.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This source code is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package local.net; + + +import org.zoolu.net.*; + +import org.zoolu.tools.Timer; +import org.zoolu.tools.TimerListener; +import java.util.Date; + + +/** KeepAliveUdp thread, for keeping the connection up toward a target node + * (e.g. toward the seriving proxy/gw or a remote UA). + * It periodically sends keep-alive tokens in order to refresh NAT UDP session timeouts. + *

It can be used for both signaling (SIP) or data plane (RTP/UDP). + */ +public class KeepAliveUdp extends Thread +{ + /** Destination socket address (e.g. the registrar server) */ + protected SocketAddress target; + + /** Time between two keep-alive tokens [millisecs] */ + protected long delta_time; + + /** UdpSocket */ + UdpSocket udp_socket; + + /** Udp packet */ + UdpPacket udp_packet=null; + + /** Expiration date [millisecs] */ + long expire=0; + + /** Whether it is running */ + boolean stop=false; + + + /** Creates a new KeepAliveUdp daemon */ + protected KeepAliveUdp(SocketAddress target, long delta_time) + { this.target=target; + this.delta_time=delta_time; + } + + + /** Creates a new KeepAliveUdp daemon */ + public KeepAliveUdp(UdpSocket udp_socket, SocketAddress target, long delta_time) + { this.target=target; + this.delta_time=delta_time; + init(udp_socket,null); + start(); + } + + + /** Creates a new KeepAliveUdp daemon */ + public KeepAliveUdp(UdpSocket udp_socket, SocketAddress target, UdpPacket udp_packet, long delta_time) + { this.target=target; + this.delta_time=delta_time; + init(udp_socket,udp_packet); + start(); + } + + + /** Inits the KeepAliveUdp */ + private void init(UdpSocket udp_socket, UdpPacket udp_packet) + { this.udp_socket=udp_socket; + if (udp_packet==null) + { byte[] buff={(byte)'\r',(byte)'\n'}; + udp_packet=new UdpPacket(buff,0,buff.length); + } + if (target!=null) + { udp_packet.setIpAddress(target.getAddress()); + udp_packet.setPort(target.getPort()); + } + this.udp_packet=udp_packet; + } + + + /** Whether the UDP relay is running */ + public boolean isRunning() + { return !stop; + } + + /** Sets the time (in milliseconds) between two keep-alive tokens */ + public void setDeltaTime(long delta_time) + { this.delta_time=delta_time; + } + + /** Gets the time (in milliseconds) between two keep-alive tokens */ + public long getDeltaTime() + { return delta_time; + } + + + /** Sets the destination SocketAddress */ + public void setDestSoAddress(SocketAddress soaddr) + { target=soaddr; + if (udp_packet!=null && target!=null) + { udp_packet.setIpAddress(target.getAddress()); + udp_packet.setPort(target.getPort()); + } + + } + + /** Gets the destination SocketAddress */ + public SocketAddress getDestSoAddress() + { return target; + } + + + /** Sets the expiration time (in milliseconds) */ + public void setExpirationTime(long time) + { if (time==0) expire=0; + else expire=System.currentTimeMillis()+time; + } + + + /** Stops sending keep-alive tokens */ + public void halt() + { stop=true; + } + + + /** Sends the kepp-alive packet now. */ + public void sendToken() throws java.io.IOException + { // do send? + if (!stop && target!=null && udp_socket!=null) + { udp_socket.send(udp_packet); + } + } + + + /** Main thread. */ + public void run() + { try + { while(!stop) + { sendToken(); + //System.out.print("."); + Thread.sleep(delta_time); + if (expire>0 && System.currentTimeMillis()>expire) halt(); + } + } + catch (Exception e) { e.printStackTrace(); } + //System.out.println("o"); + udp_socket=null; + } + + + /** Gets a String representation of the Object */ + public String toString() + { String str=null; + if (udp_socket!=null) + { str="udp:"+udp_socket.getLocalAddress()+":"+udp_socket.getLocalPort()+"-->"+target.toString(); + } + return str+" ("+delta_time+"ms)"; + } + +} \ No newline at end of file diff --git a/src/local/net/RtpFlow.java b/src/local/net/RtpFlow.java new file mode 100644 index 0000000..efde871 --- /dev/null +++ b/src/local/net/RtpFlow.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This source code is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package local.net; + + +import java.net.DatagramSocket; +//import java.net.InetAddress; +//import java.net.UnknownHostException; +//import org.zoolu.tools.Random; + + +/** This abstract class represents a RTP flow for sending or receiving rtp packets. + *

A RtpFlow is always associated to a SSRC number that represents the source end point (the S-SAP). + */ +public abstract class RtpFlow +{ + + /* version (V): 2 bits + This field identifies the version of RTP. The version defined by + this specification is two (2). */ + static final int version=2; + + /* padding (P): 1 bit + If the padding bit is set, the packet contains one or more + additional padding octets at the end which are not part of the + payload. The last octet of the padding contains a count of how + many padding octets should be ignored, including itself. */ + boolean padding; + + /* extension (X): 1 bit + If the extension bit is set, the fixed header MUST be followed by + exactly one header extension. */ + boolean extension; + + /* CSRC count (CC): 4 bits + The CSRC count contains the number of CSRC identifiers that follow + the fixed header. */ + int csrc_count; + + /* marker (M): 1 bit + The interpretation of the marker is defined by a profile. It is + intended to allow significant events such as frame boundaries to + be marked in the packet stream. */ + boolean marker; + + /* payload type (PT): 7 bits + This field identifies the format of the RTP payload and determines + its interpretation by the application. A profile MAY specify a + default static mapping of payload type codes to payload formats. + Additional payload type codes MAY be defined dynamically through + non-RTP means. */ + int payload_type; + + /* sequence number: 16 bits + The sequence number increments by one for each RTP data packet + sent, and may be used by the receiver to detect packet loss and to + restore packet sequence. The initial value of the sequence number + SHOULD be random (unpredictable). */ + int sequence_number; + + /* timestamp: 32 bits + The timestamp reflects the sampling instant of the first octet in + the RTP data packet. The sampling instant MUST be derived from a + clock that increments monotonically and linearly in time to allow + synchronization and jitter calculations. If + RTP packets are generated periodically, the nominal sampling + instant as determined from the sampling clock is to be used, not a + reading of the system clock. + The initial value of the timestamp SHOULD be random, as for the + sequence number. Several consecutive RTP packets will have equal + timestamps if they are (logically) generated at once, e.g., belong + to the same video frame. */ + long timestamp; + + /* SSRC: 32 bits + The SSRC field identifies the synchronization source. This + identifier SHOULD be chosen randomly, with the intent that no two + synchronization sources within the same RTP session will have the + same SSRC identifier. */ + long ssrc; + + /* CSRC list: 0 to 15 items, 32 bits each + The CSRC list identifies the contributing sources for the payload + contained in this packet. CSRC identifiers are inserted by + mixers, using the SSRC identifiers of + contributing sources. */ + long[] csrc_list; + + + + + /** The RTP socket used for send or receive RTP packet */ + RtpSocket socket; + + /** Whether the RTP flow has been initialized, + i.e. it has been associated with a PT, SSRC, sequence number, and timestamp. */ + boolean initialized; + + + + + /** Whether the RTP flow has been already initialized */ + public boolean isInitialized() + { return initialized; + } + + /** Gets the payload type (PT) */ + public int getPayloadType() + { return payload_type; + } + + /** Gets the last sequence number */ + public int getSequenceNumber() + { return sequence_number; + } + + /** Gets the last timestamp */ + public long getTimestamp() + { return timestamp; + } + + /** Gets the SSCR */ + public long getSscr() + { return ssrc; + } + + /** Gets the CSCR list */ + public long[] getCscrList() + { return csrc_list; + } + + + /** Creates a new RTP flow */ + public RtpFlow() + { padding=false; + extension=false; + csrc_count=0; + marker=false; + payload_type=0; + sequence_number=0; + timestamp=0; + ssrc=0; + csrc_list=null; + + initialized=false; + } + +} diff --git a/src/local/net/RtpInputFlow.java b/src/local/net/RtpInputFlow.java new file mode 100644 index 0000000..f58ed56 --- /dev/null +++ b/src/local/net/RtpInputFlow.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This source code is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package local.net; + + +import java.net.DatagramSocket; +import java.io.IOException; + + +/** This class represents a RTP flow for receiving rtp packets. + */ +public class RtpInputFlow extends RtpFlow +{ + static final int MAX_PACKET_SIZE=1500; + + /** Buffered RTP packet */ + RtpPacket packet; + + /** Creates a new RTP input flow */ + public RtpInputFlow(DatagramSocket datagram_socket) + { super(); + socket=new RtpSocket(datagram_socket); + byte[] buff=new byte[MAX_PACKET_SIZE]; + packet=new RtpPacket(buff,0); + } + + /** Receives a block of RTP data */ + public byte[] receive() throws IOException + { socket.receive(packet); + + padding=packet.hasPadding(); + extension=packet.hasExtension(); + csrc_count=packet.getCscrCount(); + marker=packet.hasMarker(); + payload_type=packet.getPayloadType(); + sequence_number=packet.getSequenceNumber(); + timestamp=packet.getTimestamp(); + ssrc=packet.getSscr(); + csrc_list=packet.getCscrList(); + + initialized=true; + + return packet.getPayload(); + } + +} diff --git a/src/local/net/RtpOutputFlow.java b/src/local/net/RtpOutputFlow.java new file mode 100644 index 0000000..a0b70f6 --- /dev/null +++ b/src/local/net/RtpOutputFlow.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This source code is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package local.net; + + +import java.net.InetAddress; +import java.net.DatagramSocket; +import java.io.IOException; +//import org.zoolu.tools.Random; + + +/** This class represents a RTP flow for sending rtp packets. + */ +public class RtpOutputFlow extends RtpFlow +{ + static final int MAX_PACKET_SIZE=1500; + + /** Buffered RTP packet */ + RtpPacket packet; + + /** Creates a new RTP input flow */ + public RtpOutputFlow(DatagramSocket datagram_socket, InetAddress remote_address, int remote_port) + { super(); + socket=new RtpSocket(datagram_socket,remote_address,remote_port); + byte[] buff=new byte[MAX_PACKET_SIZE]; + packet=new RtpPacket(buff,0); + } + + /** Receives a block of RTP data */ + public void send(byte[] data) throws IOException + { packet.setSequenceNumber(packet.getSequenceNumber()+1); + packet.setPayload(data,data.length); + } + +} diff --git a/src/local/net/RtpPacket.java b/src/local/net/RtpPacket.java new file mode 100644 index 0000000..6b8bbe6 --- /dev/null +++ b/src/local/net/RtpPacket.java @@ -0,0 +1,287 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This source code is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package local.net; + + +import org.zoolu.tools.Random; + + +/** RtpPacket implements a RTP packet. + */ +public class RtpPacket +{ + /* RTP packet buffer containing both the RTP header and payload */ + byte[] packet; + + /* RTP packet length */ + int packet_len; + + /* RTP header length */ + //int header_len; + + /** Gets the RTP packet */ + public byte[] getPacket() + { return packet; + } + + /** Gets the RTP packet length */ + public int getLength() + { return packet_len; + } + + /** Gets the RTP header length */ + public int getHeaderLength() + { if (packet_len>=12) return 12+4*getCscrCount(); + else return packet_len; // broken packet + } + + /** Gets the RTP header length */ + public int getPayloadLength() + { if (packet_len>=12) return packet_len-getHeaderLength(); + else return 0; // broken packet + } + + /** Sets the RTP payload length */ + public void setPayloadLength(int len) + { packet_len=getHeaderLength()+len; + } + + // version (V): 2 bits + // padding (P): 1 bit + // extension (X): 1 bit + // CSRC count (CC): 4 bits + // marker (M): 1 bit + // payload type (PT): 7 bits + // sequence number: 16 bits + // timestamp: 32 bits + // SSRC: 32 bits + // CSRC list: 0 to 15 items, 32 bits each + + + /** Gets the version (V) */ + public int getVersion() + { if (packet_len>=12) return (packet[0]>>6 & 0x03); + else return 0; // broken packet + } + + /** Sets the version (V) */ + public void setVersion(int v) + { if (packet_len>=12) packet[0]=(byte)((packet[0] & 0x3F) | ((v & 0x03)<<6)); + } + + /** Whether has padding (P) */ + public boolean hasPadding() + { if (packet_len>=12) return getBit(packet[0],5); + else return false; // broken packet + } + + /** Set padding (P) */ + public void setPadding(boolean p) + { if (packet_len>=12) setBit(p,packet[0],5); + } + + /** Whether has extension (X) */ + public boolean hasExtension() + { if (packet_len>=12) return getBit(packet[0],4); + else return false; // broken packet + } + + /** Set extension (X) */ + public void setExtension(boolean x) + { if (packet_len>=12) setBit(x,packet[0],4); + } + + /** Gets the CSCR count (CC) */ + public int getCscrCount() + { if (packet_len>=12) return (packet[0] & 0x0F); + else return 0; // broken packet + } + + /** Whether has marker (M) */ + public boolean hasMarker() + { if (packet_len>=12) return getBit(packet[1],7); + else return false; // broken packet + } + + /** Set marker (M) */ + public void setMarker(boolean m) + { if (packet_len>=12) setBit(m,packet[1],7); + } + + /** Gets the payload type (PT) */ + public int getPayloadType() + { if (packet_len>=12) return (packet[1] & 0x7F); + else return -1; // broken packet + } + + /** Sets the payload type (PT) */ + public void setPayloadType(int pt) + { if (packet_len>=12) packet[1]=(byte)((packet[1] & 0x80) | (pt & 0x7F)); + } + + /** Gets the sequence number */ + public int getSequenceNumber() + { if (packet_len>=12) return getInt(packet,2,4); + else return 0; // broken packet + } + + /** Sets the sequence number */ + public void setSequenceNumber(int sn) + { if (packet_len>=12) setInt(sn,packet,2,4); + } + + /** Gets the timestamp */ + public long getTimestamp() + { if (packet_len>=12) return getLong(packet,4,8); + else return 0; // broken packet + } + + /** Sets the timestamp */ + public void setTimestamp(long timestamp) + { if (packet_len>=12) setLong(timestamp,packet,4,8); + } + + /** Gets the SSCR */ + public long getSscr() + { if (packet_len>=12) return getLong(packet,8,12); + else return 0; // broken packet + } + + /** Sets the SSCR */ + public void setSscr(long ssrc) + { if (packet_len>=12) setLong(ssrc,packet,8,12); + } + + /** Gets the CSCR list */ + public long[] getCscrList() + { int cc=getCscrCount(); + long[] cscr=new long[cc]; + for (int i=0; i=12) + { int cc=cscr.length; + if (cc>15) cc=15; + packet[0]=(byte)(((packet[0]>>4)<<4)+cc); + cscr=new long[cc]; + for (int i=0; i=12) + { int header_len=getHeaderLength(); + for (int i=0; i=begin; end--) + { data[end]=(byte)(n%256); + n>>=8; + } + } + + /** Gets Int value */ + private static int getInt(byte[] data, int begin, int end) + { return (int)getLong(data,begin,end); + } + + /** Sets Int value */ + private static void setInt(int n, byte[] data, int begin, int end) + { setLong(n,data,begin,end); + } + + /** Gets bit value */ + private static boolean getBit(byte b, int bit) + { return (b>>bit)==1; + } + + /** Sets bit value */ + private static void setBit(boolean value, byte b, int bit) + { if (value) b=(byte)(b|(1< RtpSocket is associated to a DatagramSocket that is used + * to send and/or receive RtpPackets. + */ +public class RtpSocket +{ + /** UDP socket */ + DatagramSocket socket; + + /** Remote address */ + InetAddress r_addr; + + /** Remote port */ + int r_port; + + /** Creates a new RTP socket (only receiver) */ + public RtpSocket(DatagramSocket datagram_socket) + { socket=datagram_socket; + r_addr=null; + r_port=0; + } + + /** Creates a new RTP socket (sender and receiver) */ + public RtpSocket(DatagramSocket datagram_socket, InetAddress remote_address, int remote_port) + { socket=datagram_socket; + r_addr=remote_address; + r_port=remote_port; + } + + /** Returns the RTP DatagramSocket */ + public DatagramSocket getDatagramSocket() + { return socket; + } + + /** Receives a RTP packet from this socket */ + public void receive(RtpPacket rtpp) throws IOException + { DatagramPacket datagram=new DatagramPacket(rtpp.packet,rtpp.packet.length); + socket.receive(datagram); + rtpp.packet_len=datagram.getLength(); + } + + /** Sends a RTP packet from this socket */ + public void send(RtpPacket rtpp) throws IOException + { DatagramPacket datagram=new DatagramPacket(rtpp.packet,rtpp.packet_len); + datagram.setAddress(r_addr); + datagram.setPort(r_port); + socket.send(datagram); + } + + /** Closes this socket */ + public void close() + { //socket.close(); + } + +} diff --git a/src/local/net/UdpMultiRelay.java b/src/local/net/UdpMultiRelay.java new file mode 100644 index 0000000..5f92839 --- /dev/null +++ b/src/local/net/UdpMultiRelay.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This source code is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package local.net; + + +import org.zoolu.net.*; +import java.io.InterruptedIOException; +import java.util.Vector; + + +/** UdpMultiRelay implements an UDP multiple relay agent. + * It receives UDP packets at a local port and relays them + * toward a list of remote UDP sockets (a list of address/port pairs). + */ +public class UdpMultiRelay extends Thread +{ + /** Source port */ + int local_port; + /** Destination sockets */ + Vector dest_sockets; + /** Destination socket of remote host to which the packets must not be relayed */ + SocketAddress no_relay_dest_socket; + + /** Whether it is running */ + boolean stop; + /** Maximum time that the UDP relay can remain active after been halted */ + int socket_to=3000; // 3sec + + + /** Creates a new UDP relay and starts it */ + public UdpMultiRelay(int local_port, Vector dest_sockets, SocketAddress no_dest_socket) + { init(local_port,dest_sockets,no_dest_socket); + start(); + } + + /** Creates a new UDP relay and starts it */ + public UdpMultiRelay(int local_port, Vector dest_sockets) + { init(local_port,dest_sockets,null); + start(); + } + + /** Inits a new UDP relay and starts it */ + private void init(int local_port, Vector dest_sockets, SocketAddress no_dest_socket) + { this.local_port=local_port; + this.dest_sockets=dest_sockets; + this.no_relay_dest_socket=no_dest_socket; + stop=false; + } + + /** Gets the local port */ + public int getLocalPort() + { return local_port; + } + + /** Gets the destination sockets */ + public Vector getDestSockets() + { return dest_sockets; + } + + /** Gets the destination socket to which the packets must not be relayed */ + public SocketAddress getNoRelayDestSocket() + { return no_relay_dest_socket; + } + + /** Stops the UDP relay */ + public void halt() + { stop=true; + } + + /** Sets the maximum time that the UDP relay can remain active after been halted */ + public void setSoTimeout(int so_to) + { socket_to=so_to; + } + + /** Gets the maximum time that the UDP relay can remain active after been halted */ + public int getSoTimeout() + { return socket_to; + } + + /** Redirect packets from source addr/port to destination addr/port */ + public void run() + { try + { UdpSocket socket=new UdpSocket(local_port); + byte []buf = new byte[2000]; + + socket.setSoTimeout(socket_to); + while(!stop) + { UdpPacket packet = new UdpPacket(buf, buf.length); + + // non-blocking receiver + try + { socket.receive(packet); + } + catch (InterruptedIOException ie) { continue; } + + for (int i=0; i The UdpRelay remains active until method halt() is called. */ + public UdpRelay(int local_port, String dest_addr, int dest_port, UdpRelayListener listener) + { init(local_port,dest_addr,dest_port,0,listener); + start(); + } + + /** Creates a new UDP relay and starts it. + *

The UdpRelay will automatically stop after alive_time seconds + * of idle time (i.e. without receiving UDP datagrams) */ + public UdpRelay(int local_port, String dest_addr, int dest_port, int alive_time, UdpRelayListener listener) + { init(local_port,dest_addr,dest_port,alive_time,listener); + start(); + } + + /** Inits a new UDP relay */ + private void init(int local_port, String dest_addr, int dest_port, int alive_time, UdpRelayListener listener) + { this.local_port=local_port; + this.dest_addr=dest_addr; + this.dest_port=dest_port; + this.alive_to=alive_time; + this.listener=listener; + src_addr="0.0.0.0"; + src_port=0; + stop=false; + } + + /** Gets the local receiver/sender port */ + public int getLocalPort() + { return local_port; + } + + /** Gets the destination address */ + /*public String getDestAddress() + { return dest_addr; + }*/ + + /** Gets the destination port */ + /*public int getDestPort() + { return dest_port; + }*/ + + /** Sets a new destination address */ + public UdpRelay setDestAddress(String dest_addr) + { this.dest_addr=dest_addr; + return this; + } + + /** Sets a new destination port */ + public UdpRelay setDestPort(int dest_port) + { this.dest_port=dest_port; + return this; + } + + /** Whether the UDP relay is running */ + public boolean isRunning() + { return !stop; + } + + /** Stops the UDP relay */ + public void halt() + { stop=true; + } + + /** Sets the maximum time that the UDP relay can remain active after been halted */ + public void setSoTimeout(int so_to) + { socket_to=so_to; + } + + /** Gets the maximum time that the UDP relay can remain active after been halted */ + public int getSoTimeout() + { return socket_to; + } + + /** Redirect packets received from remote source addr/port to destination addr/port */ + public void run() + { //System.out.println("DEBUG: starting UdpRelay "+toString()+" (it expires after "+alive_to+" sec)"); + try + { DatagramSocket socket=new DatagramSocket(local_port); + byte []buf=new byte[MAX_PKT_SIZE]; + + socket.setSoTimeout(socket_to); + // datagram packet + DatagramPacket packet=new DatagramPacket(buf, buf.length); + + // convert alive_to in milliseconds + long keepalive_to=((1000)*(long)alive_to)-socket_to; + + // end time + long expire=System.currentTimeMillis()+keepalive_to; + // whether reset the receiver + //boolean reset=true; + + while(!stop) + { // non-blocking receiver + try + { socket.receive(packet); + } + catch (InterruptedIOException ie) + { // if expired, stop relaying + if (alive_to>0 && System.currentTimeMillis()>expire) halt(); + continue; + } + // check whether the source address and port are changed + if (src_port!=packet.getPort() || !src_addr.equals(packet.getAddress().getHostAddress().toString())) + { src_port=packet.getPort(); + src_addr=packet.getAddress().getHostAddress(); + if (listener!=null) listener.onUdpRelaySourceChanged(this,src_addr,src_port); + } + // relay + packet.setAddress(InetAddress.getByName(dest_addr)); + packet.setPort(dest_port); + socket.send(packet); + // reset + packet=new DatagramPacket(buf, buf.length); + expire=System.currentTimeMillis()+keepalive_to; + } + socket.close(); + if (listener!=null) listener.onUdpRelayTerminated(this); + } + catch (Exception e) { e.printStackTrace(); } + + //System.out.println("DEBUG: closing UdpRelay "+toString())"); + } + + + + /** Gets a String representation of the Object */ + public String toString() + { return Integer.toString(local_port)+"-->"+dest_addr+":"+dest_port; + } + + + // ********************************** MAIN ********************************* + + /** The main method. */ + public static void main(String[] args) + { + if (args.length<3) + { + System.out.println("usage:\n java UdpRelay

[ ]"); + System.exit(0); + } + + int local_port=Integer.parseInt(args[0]); + int remote_port=Integer.parseInt(args[2]); + String remote_address=args[1]; + + int alive_time=0; + if (args.length>3) alive_time=Integer.parseInt(args[3]); + + new UdpRelay(local_port,remote_address,remote_port,alive_time,null); + } +} + \ No newline at end of file diff --git a/src/local/net/UdpRelayListener.java b/src/local/net/UdpRelayListener.java new file mode 100644 index 0000000..48e3262 --- /dev/null +++ b/src/local/net/UdpRelayListener.java @@ -0,0 +1,13 @@ +package local.net; + + +/** Listener for UdpRelay. + */ +public interface UdpRelayListener +{ + /** When the remote source address changes. */ + public void onUdpRelaySourceChanged(UdpRelay udp_relay, String remote_src_addr, int remote_src_port); + + /** When UdpRelay stops relaying UDP datagrams. */ + public void onUdpRelayTerminated(UdpRelay udp_relay); +} diff --git a/src/local/server/AuthenticationServer.java b/src/local/server/AuthenticationServer.java new file mode 100644 index 0000000..ae48630 --- /dev/null +++ b/src/local/server/AuthenticationServer.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This source code is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package local.server; + + +import org.zoolu.sip.header.AuthenticationInfoHeader; +import org.zoolu.sip.message.*; + + +/** AuthenticationServer is the interface used by a SIP server to authenticate SIP requests. + */ +public interface AuthenticationServer +{ + /** Authenticates a SIP request. + * @param msg is the SIP request to be authenticated + * @return it returns the error Message in case of authentication failure, + * or null in case of authentication success. */ + public Message authenticateRequest(Message msg); + + /** Authenticates a proxing SIP request. + * @param msg is the SIP request to be authenticated + * @return it returns the error Message in case of authentication failure, + * or null in case of authentication success. */ + public Message authenticateProxyRequest(Message msg); + + /** Gets AuthenticationInfoHeader. */ + public AuthenticationInfoHeader getAuthenticationInfoHeader(); + +} \ No newline at end of file diff --git a/src/local/server/AuthenticationServerImpl.java b/src/local/server/AuthenticationServerImpl.java new file mode 100644 index 0000000..64d3a46 --- /dev/null +++ b/src/local/server/AuthenticationServerImpl.java @@ -0,0 +1,249 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This source code is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package local.server; + + +import org.zoolu.sip.address.*; +import org.zoolu.sip.header.WwwAuthenticateHeader; +import org.zoolu.sip.header.AuthorizationHeader; +import org.zoolu.sip.header.AuthenticationInfoHeader; +import org.zoolu.sip.header.ProxyAuthenticateHeader; +import org.zoolu.sip.header.ProxyAuthorizationHeader; +import org.zoolu.sip.message.*; +import org.zoolu.sip.provider.SipStack; +import org.zoolu.sip.authentication.DigestAuthentication; +import org.zoolu.tools.Log; +import org.zoolu.tools.LogLevel; +import org.zoolu.tools.Parser; +import org.zoolu.tools.MD5; + + +/** Class AuthenticationServerImpl implements an AuthenticationServer + * for HTTP Digest authentication. + */ +public class AuthenticationServerImpl implements AuthenticationServer +{ + + /** Server authentication. */ + protected static final int SERVER_AUTHENTICATION=0; + + /** Proxy authentication. */ + protected static final int PROXY_AUTHENTICATION=1; + + + /** Event logger. */ + protected Log log=null; + + /** The repository of users's authentication data. */ + protected AuthenticationService authentication_service; + + /** The authentication realm. */ + protected String realm; + + /** The authentication scheme. */ + protected String authentication_scheme="Digest"; + + /** The authentication qop-options. */ + protected String qop_options="auth,auth-int"; + + /** The current random value. */ + protected byte[] rand; + + /** DIGEST */ + //public static final String DIGEST="Digest"; + /** AKA */ + //public static final String AKA="AKA"; + /** CHAP */ + //public static final String CHAP="CHAP"; + + + /** Costructs a new AuthenticationServerImpl. */ + public AuthenticationServerImpl(String realm, AuthenticationService authentication_service, Log log) + { init(realm,authentication_service,log); + } + + + /** Inits the AuthenticationServerImpl. */ + private void init(String realm, AuthenticationService authentication_service, Log log) + { this.log=log; + this.realm=realm; + this.authentication_service=authentication_service; + this.rand=pickRandBytes(); + } + + /** Gets the realm. */ + /*public String getRealm() + { return realm; + }*/ + + + /** Gets the qop-options. */ + /*public String getQopOptions() + { return qop_options; + }*/ + + + /** Gets the current rand value. */ + /*public String getRand() + { return HEX(rand); + }*/ + + + /** Authenticates a SIP request. + * @param msg is the SIP request to be authenticated + * @return it returns the error Message in case of authentication failure, + * or null in case of authentication success. */ + public Message authenticateRequest(Message msg) + { return authenticateRequest(msg,SERVER_AUTHENTICATION); + } + + + /** Authenticates a SIP request. + * @param msg is the SIP request to be authenticated + * @return it returns the error Message in case of authentication failure, + * or null in case of authentication success. */ + public Message authenticateProxyRequest(Message msg) + { return authenticateRequest(msg,PROXY_AUTHENTICATION); + } + + + /** Authenticates a SIP request. + * @param msg is the SIP request to be authenticated + * @param proxy_authentication whether performing Proxy-Authentication or simple Authentication + * @return it returns the error Message in case of authentication failure, + * or null in case of authentication success. */ + protected Message authenticateRequest(Message msg, int type) + { Message err_resp=null; + + //String username=msg.getFromHeader().getNameAddress().getAddress().getUserName(); + //String user=username+"@"+realm; + + AuthorizationHeader ah; + if (type==SERVER_AUTHENTICATION) ah=msg.getAuthorizationHeader(); + else ah=msg.getProxyAuthorizationHeader(); + + if (ah!=null && ah.getNonceParam().equals(HEX(rand))) + { + //String username=ah.getUsernameParam(); + String realm=ah.getRealmParam(); + String nonce=ah.getNonceParam(); + String username=ah.getUsernameParam(); + String scheme=ah.getAuthScheme(); + + String user=username+"@"+realm; + + if (authentication_service.hasUser(user)) + { + if (authentication_scheme.equalsIgnoreCase(scheme)) + { + DigestAuthentication auth=new DigestAuthentication(msg.getRequestLine().getMethod(),ah,null,keyToPasswd(authentication_service.getUserKey(user))); + + // check user's authentication response + boolean is_authorized=auth.checkResponse(); + + rand=pickRandBytes(); + + if (!is_authorized) + { // authentication/authorization failed + int result=403; // response code 403 ("Forbidden") + err_resp=MessageFactory.createResponse(msg,result,SipResponses.reasonOf(result),null); + printLog("LOGIN ERROR: Authentication of '"+user+"' failed",LogLevel.HIGH); + } + else + { // authentication/authorization successed + printLog("Authentication of '"+user+"' successed",LogLevel.HIGH); + } + } + else + { // authentication/authorization failed + int result=400; // response code 400 ("Bad request") + err_resp=MessageFactory.createResponse(msg,result,SipResponses.reasonOf(result),null); + printLog("Authentication method '"+scheme+"' not supported.",LogLevel.HIGH); + } + } + else + { // no authentication credential found for this user + int result=404; // response code 404 ("Not Found") + err_resp=MessageFactory.createResponse(msg,result,SipResponses.reasonOf(result),null); + } + } + else + { // no Authorization header found + int result; + if (type==SERVER_AUTHENTICATION) result=401; // response code 401 ("Unauthorized") + else result=407; // response code 407 ("Proxy Authentication Required") + err_resp=MessageFactory.createResponse(msg,result,SipResponses.reasonOf(result),null); + WwwAuthenticateHeader wah; + if (type==SERVER_AUTHENTICATION) wah=new WwwAuthenticateHeader("Digest"); + else wah=new ProxyAuthenticateHeader("Digest"); + wah.addRealmParam(realm); + wah.addQopOptionsParam(qop_options); + wah.addNonceParam(HEX(rand)); + err_resp.setWwwAuthenticateHeader(wah); + } + return err_resp; + } + + + /** Gets AuthenticationInfoHeader. */ + public AuthenticationInfoHeader getAuthenticationInfoHeader() + { AuthenticationInfoHeader aih=new AuthenticationInfoHeader(); + aih.addRealmParam(realm); + aih.addQopOptionsParam(qop_options); + aih.addNextnonceParam(HEX(rand)); + return aih; + } + + + /** Picks a random array of 16 bytes. */ + private static byte[] pickRandBytes() + { return MD5(Long.toHexString(org.zoolu.tools.Random.nextLong())); + } + + /** Converts the byte[] key in a String passwd. */ + private static String keyToPasswd(byte[] key) + { return new String(key); + } + + /** Calculates the MD5 of a String. */ + private static byte[] MD5(String str) + { return MD5.digest(str); + } + + /** Calculates the MD5 of an array of bytes. */ + private static byte[] MD5(byte[] bb) + { return MD5.digest(bb); + } + + /** Calculates the HEX of an array of bytes. */ + private static String HEX(byte[] bb) + { return MD5.asHex(bb); + } + + // ****************************** Logs ***************************** + + /** Adds a new string to the default Log */ + protected void printLog(String str, int level) + { if (log!=null) log.println("AuthenticationServer: "+str,level+SipStack.LOG_LEVEL_UA); + } + +} \ No newline at end of file diff --git a/src/local/server/AuthenticationService.java b/src/local/server/AuthenticationService.java new file mode 100644 index 0000000..e601c8f --- /dev/null +++ b/src/local/server/AuthenticationService.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This source code is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package local.server; + + +import java.util.Enumeration; +//import org.zoolu.sip.provider.SipStack; +//import org.zoolu.tools.Parser; +//import org.zoolu.tools.Base64; +//import java.util.Hashtable; +//import java.io.*; + + +/** AuthenticationService is the interface used by a SIP server to access to + * an AAA repository. + */ +public interface AuthenticationService extends Repository +{ + /** Adds a new user at the database. + * @param user the user name + * @param key the user key + * @return this object */ + public AuthenticationService addUser(String user, byte[] key); + + /** Adds a new user at the database. + * @param user the user name + * @param key the user key + * @param seqn the current user SQN value + * @return this object */ + //public AuthenticationService addUser(String user, byte[] key, int seqn); + + + /** Sets the user key. + * @param user the user name + * @param key the user key + * @return this object */ + public AuthenticationService setUserKey(String user, byte[] key); + + /** Gets the user key. + * @param user the user name + * @return the user key */ + public byte[] getUserKey(String user); + + + /** Sets the user sequence number. + * @param user the user name + * @param rand the user sequence number + * @return this object */ + //public AuthenticationService setUserSeqn(String user, int seqn); + + /** Gets the user sequence number. + * @param user the user name + * @return the user sequence number */ + //public int getUserSeqn(String user); + + /** Increments the user sequence number. + * @param user the user name + * @return the new user sequence number */ + //public int incUserSeqn(String user); + +} diff --git a/src/local/server/AuthenticationServiceImpl.java b/src/local/server/AuthenticationServiceImpl.java new file mode 100644 index 0000000..a80bae5 --- /dev/null +++ b/src/local/server/AuthenticationServiceImpl.java @@ -0,0 +1,254 @@ +package local.server; + + +import org.zoolu.sip.provider.SipStack; +import org.zoolu.tools.Parser; +import org.zoolu.tools.Base64; + +import java.util.Hashtable; +import java.util.Enumeration; +import java.io.*; + + +/** AuthenticationServiceImpl is a simple implementation of a AuthenticationService. + * AuthenticationServiceImpl allows creation and maintainance of a + * AAA service for registered users. + */ +public class AuthenticationServiceImpl implements AuthenticationService +{ + /** AuthenticationService name */ + String filename=null; + + /** Whether the AuthenticationService DB has been changed without saving */ + boolean changed=false; + + /** Users AAA DB. Set of pairs of { (String)user , (UserAuthInfo)binding }. */ + Hashtable users; + + /** Void byte array. */ + private static final byte[] NULL_ARRAY=new byte[0]; + + + /** Creates a new AuthenticationService. */ + public AuthenticationServiceImpl(String file_name) + { filename=file_name; + users=new Hashtable(); + load(); + } + + + // **************** Methods of interface Registry **************** + + /** Syncronizes the database. + *

Can be used, for example, to save the current memory image of the DB. */ + public void sync() + { if (changed) save(); + } + + /** Returns the numbers of users in the database. + * @return the numbers of user entries */ + public int size() + { return users.size(); + } + + /** Returns an enumeration of the users in this database. + * @return the list of user names as an Enumeration of String */ + public Enumeration getUsers() + { return users.keys(); + } + + /** Whether a user is present in the database and can be used as key. + * @param user the user name + * @return true if the user name is present as key */ + public boolean hasUser(String user) + { return (users.containsKey(user)); + } + + /** Adds a new user at the database. + * @param user the user name + * @return this object */ + public Repository addUser(String user) + { addUser(user,NULL_ARRAY); + return this; + } + + /** Removes the user from the database. + * @param user the user name + * @return this object */ + public Repository removeUser(String user) + { users.remove(user); + changed=true; + return this; + } + + /** Removes all users from the database. + * @return this object */ + public Repository removeAllUsers() + { users.clear(); + changed=true; + return this; + } + + + // **************** Methods of interface AuthenticationService **************** + + /** Adds a new user at the database. + * @param user the user name + * @param key the user key + * @return this object */ + public AuthenticationService addUser(String user, byte[] key) + { if (hasUser(user)) return this; + UserAuthInfo ur=new UserAuthInfo(user,key); + users.put(user,ur); + changed=true; + return this; + } + + /** Sets the user key */ + public AuthenticationService setUserKey(String user, byte[] key) + { UserAuthInfo ur=getUserAuthInfo(user); + if (ur!=null) + { ur.setKey(key); + changed=true; + } + return this; + } + /** Gets the user key */ + public byte[] getUserKey(String user) + { if (hasUser(user)) return getUserAuthInfo(user).getKey(); + else return null; + } + + + // ******************************* New methods ******************************* + + /** Returns the name of the database. */ + public String getName() { return filename; } + + /** Whether the database is changed. */ + public boolean isChanged() { return changed; } + + /** Adds a user record in the database */ + private void addUserAuthInfo(UserAuthInfo ur) + { if (hasUser(ur.getName())) removeUser(ur.getName()); + users.put(ur.getName(),ur); + } + + /** Gets the record of the user */ + private UserAuthInfo getUserAuthInfo(String user) + { return (UserAuthInfo)users.get(user); + } + + /** Returns an enumeration of the values in this database */ + private Enumeration getUserAuthInfos() + { return users.elements(); + } + + /** Loads the database */ + public void load() + { BufferedReader in=null; + changed=false; + try { in = new BufferedReader(new FileReader(filename)); } + catch (FileNotFoundException e) + { System.err.println("WARNING: file \""+filename+"\" not found: created new empty DB"); + return; + } + String user=null; + byte[] key=NULL_ARRAY; + while (true) + { String line=null; + try { line=in.readLine(); } catch (Exception e) { e.printStackTrace(); System.exit(0); } + + if (line==null) + break; + + Parser par=new Parser(line); + + if (line.startsWith("#")) + continue; + if (line.startsWith("user")) + { if (user!=null) addUser(user,key); + user=par.goTo('=').skipChar().getString(); + key=NULL_ARRAY; + continue; + } + if (line.startsWith("key")) + { key=Base64.decode(par.goTo('=').skipChar().getString()); + continue; + } + if (line.startsWith("passwd")) + { key=par.goTo('=').skipChar().getString().getBytes(); + continue; + } + } + if (user!=null) addUser(user,key); + + try + { in.close(); + } + catch (Exception e) { e.printStackTrace(); } + } + + /** Saves the database */ + public void save() + { BufferedWriter out=null; + changed=false; + try + { out=new BufferedWriter(new FileWriter(filename)); + out.write(this.toString()); + out.close(); + } + catch (IOException e) + { System.err.println("WARNING: error trying to write on file \""+filename+"\""); + return; + } + } + + /** Gets the String value of this Object. + * @return the String value */ + public String toString() + { String str=""; + for (Enumeration e=getUserAuthInfos(); e.hasMoreElements(); ) + { UserAuthInfo ur=(UserAuthInfo)e.nextElement(); + str+=ur.toString(); + //str+="\r\n"; + } + return str; + } + +} + + +/** User's authentication info. + * This class represents a user record of the AAA DB. + */ +class UserAuthInfo +{ + /** User name */ + String name; + String getName() { return name; } + void setName(String name) { this.name=name; } + + /** User key */ + byte[] key; + byte[] getKey() { return key; } + void setKey(byte[] key) { this.key=key; } + + + /** Gets the String value of this Object. + * @return the String value */ + public String toString() + { String str=""; + str+="user= "+name+"\r\n"; + str+="key= "+Base64.encode(key)+"\r\n"; + return str; + } + + /** Costructs a new UserAuthInfo for user name + * @param name the user name */ + UserAuthInfo(String name, byte[] key) + { this.name=name; + this.key=key; + } +} + diff --git a/src/local/server/CallLogger.java b/src/local/server/CallLogger.java new file mode 100644 index 0000000..613ab19 --- /dev/null +++ b/src/local/server/CallLogger.java @@ -0,0 +1,13 @@ +package local.server; + + +import org.zoolu.sip.message.Message; + + +/** CallLogger keeps a complete trace of processed calls. + */ +public interface CallLogger +{ + /** Updates log with the present message. */ + public void update(Message msg); +} \ No newline at end of file diff --git a/src/local/server/CallLoggerImpl.java b/src/local/server/CallLoggerImpl.java new file mode 100644 index 0000000..24fea98 --- /dev/null +++ b/src/local/server/CallLoggerImpl.java @@ -0,0 +1,153 @@ +package local.server; + + +import org.zoolu.sip.provider.SipStack; +import org.zoolu.sip.header.StatusLine; +import org.zoolu.sip.message.Message; +import org.zoolu.sip.message.SipMethods; +import org.zoolu.tools.Log; +import org.zoolu.tools.DateFormat; + +import java.util.Hashtable; +import java.util.Vector; +import java.util.Date; + + +/** CallLoggerImpl implements a simple CallLogger. + *

A CallLogger keeps trace of all processed calls. + */ +public class CallLoggerImpl implements CallLogger +{ + /** Maximum number of concurrent calls. */ + static final int MAX_SIZE=10000; + + /** Table : call_id (String) --> invite date. */ + Hashtable invite_dates; + /** Table : call_id (String) --> 2xx date (Long). */ + Hashtable accepted_dates; + /** Table : call_id (String) --> 4xx date (Long). */ + Hashtable refused_dates; + /** Table : call_id (String) --> bye date (Long). */ + Hashtable bye_dates; + + /** Table : call_id (String) --> caller (String). */ + Hashtable callers; + /** Table : call_id (String) --> callee (String). */ + Hashtable callees; + + /** Set : call_id (String). */ + Vector calls; + + /** Logger. */ + Log call_log; + + + /** Costructs a new CallLoggerImpl. + */ + public CallLoggerImpl(String filename) + { invite_dates=new Hashtable(); + accepted_dates=new Hashtable(); + refused_dates=new Hashtable(); + bye_dates=new Hashtable(); + calls=new Vector(); + callers=new Hashtable(); + callees=new Hashtable(); + + call_log=new Log(filename,null,1,-1,true); + call_log.println("Date \tCall-Id \tStatus \tCaller \tCallee \tSetup Time \tCall Time"); + } + + + /** Updates log with the present message. + */ + public void update(Message msg) + { + String method=msg.getCSeqHeader().getMethod(); + String call_id=msg.getCallIdHeader().getCallId(); + + if (method.equalsIgnoreCase(SipMethods.INVITE)) + { + if (msg.isRequest()) + { if (!invite_dates.containsKey(call_id)) + { Date time=new Date(); + String caller=msg.getFromHeader().getNameAddress().getAddress().toString(); + String callee=msg.getToHeader().getNameAddress().getAddress().toString(); + insert(invite_dates,call_id,time); + callers.put(call_id,caller); + callees.put(call_id,callee); + eventlog(time,call_id,SipMethods.INVITE,caller,callee); + } + } + else + { StatusLine status_line=msg.getStatusLine(); + int code=status_line.getCode(); + if (code>=200 && code<300 && !accepted_dates.containsKey(call_id)) + { Date time=new Date(); + insert(accepted_dates,call_id,time); + String reason=status_line.getReason(); + eventlog(time,call_id,String.valueOf(code)+" "+reason,"",""); + } + else + if (code>=300 && !refused_dates.containsKey(call_id)) + { Date time=new Date(); + insert(refused_dates,call_id,time); + String reason=status_line.getReason(); + eventlog(time,call_id,String.valueOf(code)+" "+reason,"",""); + } + } + } + else + if (method.equalsIgnoreCase(SipMethods.BYE)) + { + if (msg.isRequest()) + { if (!bye_dates.containsKey(call_id)) + { Date time=new Date(); + insert(bye_dates,call_id,time); + eventlog(time,call_id,SipMethods.BYE,"",""); + calllog(call_id); + } + } + } + } + + + /** Insters/updates a call-state table. + */ + private void insert(Hashtable table, String call_id, Date time) + { if (!invite_dates.containsKey(call_id) && !accepted_dates.containsKey(call_id) && !refused_dates.containsKey(call_id) && !bye_dates.containsKey(call_id)); + { if (calls.size()>=MAX_SIZE) + { String call_0=(String)calls.elementAt(0); + invite_dates.remove(call_0); + accepted_dates.remove(call_0); + refused_dates.remove(call_0); + bye_dates.remove(call_0); + callers.remove(call_0); + callees.remove(call_0); + calls.removeElementAt(0); + } + calls.addElement(call_id); + } + table.put(call_id,time); + } + + + /** Prints a generic event log. + */ + private void eventlog(Date time, String call_id, String event, String caller, String callee) + { //call_log.println(DateFormat.formatHHMMSS(time)+"\t"+call_id+"\t"+event+"\t"+caller+"\t"+callee); + call_log.println(DateFormat.formatYYYYMMDD(time)+"\t"+call_id+"\t"+event+"\t"+caller+"\t"+callee); + } + + + /** Prints a call report. + */ + private void calllog(String call_id) + { Date invite_time=(Date)invite_dates.get(call_id); + Date accepted_time=(Date)accepted_dates.get(call_id); + Date bye_time=(Date)bye_dates.get(call_id); + if (invite_time!=null && accepted_time!=null && bye_time!=null) + //call_log.println(DateFormat.formatHHMMSS(invite_time)+"\t"+call_id+"\tCALL \t"+callers.get(call_id)+"\t"+callees.get(call_id)+"\t"+(accepted_time.getTime()-invite_time.getTime())+"\t"+(bye_time.getTime()-accepted_time.getTime())); + call_log.println(DateFormat.formatYYYYMMDD(invite_time)+"\t"+call_id+"\tCALL \t"+callers.get(call_id)+"\t"+callees.get(call_id)+"\t"+(accepted_time.getTime()-invite_time.getTime())+"\t"+(bye_time.getTime()-accepted_time.getTime())); + } + +} \ No newline at end of file diff --git a/src/local/server/DomainRoutingRule.java b/src/local/server/DomainRoutingRule.java new file mode 100644 index 0000000..baba486 --- /dev/null +++ b/src/local/server/DomainRoutingRule.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This source code is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package local.server; + + +import org.zoolu.sip.address.SipURL; +import org.zoolu.net.SocketAddress; + + +/** DomainRoutingRule. + */ +class DomainRoutingRule implements RoutingRule +{ + + /** Matching domain. */ + String domain; + + /** Next-hop server. */ + SocketAddress nexthop; + + + /** Creates a new DomainRoutingRule. */ + public DomainRoutingRule(String domain, SocketAddress nexthop) + { this.domain=domain; + this.nexthop=nexthop; + } + + + /** Gets the proper next-hop SipURL for the selected URL. + * It return the SipURL used to reach the selected URL. + * @param sip_url the selected destination URL + * @return returns the proper next-hop SipURL for the selected URL + * if the routing rule matches the URL, otherwise it returns null. */ + public SipURL getNexthop(SipURL sip_url) + { String host=sip_url.getHost(); + if ((host.equalsIgnoreCase(domain))) + { return new SipURL(sip_url.getUserName(),nexthop.getAddress().toString(),nexthop.getPort()); + } + else return null; + } + + /** Gets the String value. */ + public String toString() + { return "{domain="+domain+","+"nexthop="+nexthop+"}"; + } +} \ No newline at end of file diff --git a/src/local/server/LocationService.java b/src/local/server/LocationService.java new file mode 100644 index 0000000..f671df6 --- /dev/null +++ b/src/local/server/LocationService.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This source code is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package local.server; + + +import org.zoolu.sip.address.*; +import java.util.Enumeration; +import java.util.Date; + + +/** LocationService is the interface used by a SIP registrar to access to a + * location repository. + *

A LocationService allows the maintinance of bindings between users and contacts. + *
For each user the LocationService should maintain information regarding: + *
- username, that is a fully qualified name for this service (e.g. alice@wonderland.net) + *
- data, that is an opaque block of data (a string), + * that can be set and fetched for any service-depending use, + *
- contacts/expires, that is the list of user contacts with the time when it expires, + *

LocationService has a set of methods for query and modifing such data. + *

Some of these methods include an optional parameter app that could be used + * to implement application-dependent mobility, i.e. lists of contacts that are specific + * for particular applications. This feature might be used by guessing the application + * by the SIP body (e.g. SDP) or by a new non-standard Application header (ref. [draft-XX.txt]) + */ +public interface LocationService extends Repository +{ + /** Whether the user has contact url. + * @param user the user name + * @param url the contact URL + * @return true if is the contact present */ + public boolean hasUserContact(String user, String url); + + /** Adds a contact. + * @param user the user name + * @param contact the contact NameAddress + * @param expire the contact expire Date + * @return this object */ + public LocationService addUserContact(String user, NameAddress contact, Date expire); + + /** Gets the user contacts that are not expired. + * @param user the user name + * @return the list of contact URLs as Enumeration of String */ + public Enumeration getUserContactURLs(String user); + + /** Removes a contact. + * @param user the user name + * @param url the contact URL + * @return this object */ + public LocationService removeUserContact(String user, String url); + + /** Gets NameAddress value of the user contact. + * @param user the user name + * @param url the contact URL + * @return the contact NameAddress */ + public NameAddress getUserContactNameAddress(String user, String url); + + /** Gets expiration date of the user contact. + * @param user the user name + * @param url the contact URL + * @return the contact expire Date */ + public Date getUserContactExpirationDate(String user, String url); + + /** Whether the contact is expired. + * @param user the user name + * @param url the contact URL + * @return true if it has expired */ + public boolean isUserContactExpired(String user, String url); + + /** Removes all contacts from the database. + * @return this object */ + public LocationService removeAllContacts(); + +} diff --git a/src/local/server/LocationServiceImpl.java b/src/local/server/LocationServiceImpl.java new file mode 100644 index 0000000..f3857e0 --- /dev/null +++ b/src/local/server/LocationServiceImpl.java @@ -0,0 +1,414 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This source code is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package local.server; + + +import org.zoolu.sip.address.*; +import org.zoolu.sip.provider.SipStack; +import org.zoolu.sip.provider.SipParser; +import org.zoolu.sip.header.SipHeaders; +import org.zoolu.sip.header.ContactHeader; +import org.zoolu.tools.Parser; +import org.zoolu.tools.LogLevel; +import java.io.*; +import java.util.*; +import java.text.*; + + +/** LocationServiceImpl is a simple implementation of a LocationService. + * LocationServiceImpl allows creation and maintainance of a + * location service for registered users. + */ +public class LocationServiceImpl implements LocationService +{ + /** LocationService name. */ + String filename=null; + + /** Whether the Location DB has been changed without saving. */ + boolean changed=false; + + /** Users bindings. Set of pairs of { (String)user , (UserBindingInfo)binding }. */ + Hashtable users; + + + /** Creates a new LocationServiceImpl */ + public LocationServiceImpl(String file_name) + { filename=file_name; + users=new Hashtable(); + load(); + } + + + // **************** Methods of interface Registry **************** + + /** Syncronizes the database. + *

Can be used, for example, to save the current memory image of the DB. */ + public void sync() + { if (changed) save(); + } + + /** Returns the numbers of users in the database. + * @return the numbers of user entries */ + public int size() + { return users.size(); + } + + /** Returns an enumeration of the users in this database. + * @return the list of user names as an Enumeration of String */ + public Enumeration getUsers() + { return users.keys(); + } + + /** Whether a user is present in the database and can be used as key. + * @param user the user name + * @return true if the user name is present as key */ + public boolean hasUser(String user) + { return (users.containsKey(user)); + } + + /** Adds a new user at the database. + * @param user the user name + * @return this object */ + public Repository addUser(String user) + { if (hasUser(user)) return this; + UserBindingInfo ur=new UserBindingInfo(user); + users.put(user,ur); + changed=true; + return this; + } + + /** Removes the user from the database. + * @param user the user name + * @return this object */ + public Repository removeUser(String user) + { if (!hasUser(user)) return this; + //else + users.remove(user); + changed=true; + return this; + } + + /** Removes all users from the database. + * @return this object */ + public Repository removeAllUsers() + { users.clear(); + changed=true; + return this; + } + + /** Gets the String value of this Object. + * @return the String value */ + public String toString() + { String str=""; + for (Enumeration i=getUserBindings(); i.hasMoreElements(); ) + { UserBindingInfo u=(UserBindingInfo)i.nextElement(); + str+=u.toString(); + } + return str; + } + + + // **************** Methods of interface LocationService **************** + + /** Whether the user has contact url. + * @param user the user name + * @param url the contact URL + * @return true if is the contact present */ + public boolean hasUserContact(String user, String url) + { if (!hasUser(user)) return false; + //else + return getUserBindingInfo(user).hasContact(url); + } + + /** Adds a contact. + * @param user the user name + * @param contact the contact NameAddress + * @param expire the contact expire Date + * @return this object */ + public LocationService addUserContact(String user, NameAddress name_addresss, Date expire) + { if (!hasUser(user)) addUser(user); + UserBindingInfo ur=getUserBindingInfo(user); + ur.addContact(name_addresss,expire); + changed=true; + return this; + } + + /** Removes a contact. + * @param user the user name + * @param url the contact URL + * @return this object */ + public LocationService removeUserContact(String user, String url) + { if (!hasUser(user)) return this; + //else + UserBindingInfo ur=getUserBindingInfo(user); + ur.removeContact(url); + changed=true; + return this; + } + + /** Gets the user contacts that are not expired. + * @param user the user name + * @return the list of contact URLs as Enumeration of String */ + public Enumeration getUserContactURLs(String user) + { if (!hasUser(user)) return null; + //else + changed=true; + return getUserBindingInfo(user).getContacts(); + } + + /** Gets NameAddress value of the user contact. + * @param user the user name + * @param url the contact URL + * @return the contact NameAddress */ + public NameAddress getUserContactNameAddress(String user, String url) + { if (!hasUser(user)) return null; + //else + return getUserBindingInfo(user).getNameAddress(url); + } + + /** Gets expiration date of the user contact. + * @param user the user name + * @param url the contact URL + * @return the contact expire Date */ + public Date getUserContactExpirationDate(String user, String url) + { if (!hasUser(user)) return null; + //else + return getUserBindingInfo(user).getExpirationDate(url); + } + + /** Whether the contact is expired. + * @param user the user name + * @param url the contact URL + * @return true if it has expired */ + public boolean isUserContactExpired(String user, String url) + { if (!hasUser(user)) return true; + //else + return getUserBindingInfo(user).isExpired(url); + } + + /** Gets the String value of user information. + * @return the String value for that user */ + /*public String userToString(String user) + { return getUserBindingInfo(user).toString(); + }*/ + + /** Removes all contacts from the database. + * @return this object */ + public LocationService removeAllContacts() + { for (Enumeration i=getUserBindings(); i.hasMoreElements(); ) + { ((UserBindingInfo)i.nextElement()).removeContacts(); + } + changed=true; + return this; + } + + + // ***************************** Private methods ***************************** + + /** Returns the name of the database. */ + private String getName() { return filename; } + + /** Whether the database is changed. */ + private boolean isChanged() { return changed; } + + + /** Adds a user record in the database */ + private void addUserBindingInfo(UserBindingInfo ur) + { if (hasUser(ur.getName())) removeUser(ur.getName()); + users.put(ur.getName(),ur); + } + + /** Adds a user record in the database */ + private UserBindingInfo getUserBindingInfo(String user) + { return (UserBindingInfo)users.get(user); + } + + /** Returns an enumeration of the values in this database */ + private Enumeration getUserBindings() + { return users.elements(); + } + + /** Loads the database */ + private void load() + { BufferedReader in=null; + changed=false; + try { in = new BufferedReader(new FileReader(filename)); } + catch (FileNotFoundException e) + { System.err.println("WARNING: file \""+filename+"\" not found: created new empty DB"); + return; + } + String user=null; + NameAddress name_address=null; + Date expire=null; + while (true) + { String line=null; + try { line=in.readLine(); } + catch (Exception e) { e.printStackTrace(); System.exit(0); } + if (line==null) + break; + if (line.startsWith("#")) + continue; + if (line.startsWith("To")) + { Parser par=new Parser(line); + user=par.skipString().getString(); + //System.out.println("add user: "+user); + addUser(user); + continue; + } + if (line.startsWith(SipHeaders.Contact)) + { SipParser par=new SipParser(line); + name_address=((SipParser)par.skipString()).getNameAddress(); + //System.out.println("DEBUG: "+name_address); + expire=(new SipParser(par.goTo("expires=").skipN(8).getStringUnquoted())).getDate(); + //System.out.println("DEBUG: "+expire); + getUserBindingInfo(user).addContact(name_address,expire); + continue; + } + } + try { in.close(); } catch (Exception e) { e.printStackTrace(); } + } + + + /** Saves the database */ + private void save() + { BufferedWriter out=null; + changed=false; + try + { out=new BufferedWriter(new FileWriter(filename)); + out.write(this.toString()); + out.close(); + } + catch (IOException e) + { System.err.println("WARNING: error trying to write on file \""+filename+"\""); + return; + } + } + +} + + +/** User's binding info. + * This class represents a user record of the location DB. + *

A UserBindingInfo contains the user name, and a set of + * contact information (i.e. contact and expire-time). + *

Method getContacts() returns an Enumeration of String values + * rapresenting the various contact SipURLs. + * Such values can be used as keys for getting for each contact + * both the contact NameAddress and the expire Date. + */ +class UserBindingInfo +{ + /** User name */ + String name; + + /** Hashtable of ContactHeader with String as key. */ + Hashtable contact_list; + + /** Costructs a new UserBindingInfo for user name. + * @param name the user name */ + public UserBindingInfo(String name) + { this.name=name; + contact_list=new Hashtable(); + } + + /** Gets the user name. + * @return the user name */ + public String getName() + { return name; + } + + /** Gets the user contacts. + * @return the user contacts as an Enumeration of String */ + public Enumeration getContacts() + { return contact_list.keys(); + } + + /** Whether the user has any registered contact. + * @param url the contact url (String) + * @return true if one or more contacts are present */ + public boolean hasContact(String url) + { return contact_list.containsKey(url); + } + + /** Adds a new contact. + * @param contact the contact address (NameAddress) + * @param expire the expire value (Date) + * @return this object */ + public UserBindingInfo addContact(NameAddress contact, Date expire) + { contact_list.put(contact.getAddress().toString(),(new ContactHeader(contact)).setExpires(expire)); + return this; + } + + /** Removes a contact. + * @param url the contact url (String) + * @return this object */ + public UserBindingInfo removeContact(String url) + { if (contact_list.containsKey(url)) contact_list.remove(url); + return this; + } + + /** Removes all contacts. + * @return this object */ + public UserBindingInfo removeContacts() + { contact_list.clear(); + return this; + } + + /** Gets NameAddress of a contact. + * @param url the contact url (String) + * @return the contact NameAddress, or null if the contact is not present */ + public NameAddress getNameAddress(String url) + { if (contact_list.containsKey(url)) return ((ContactHeader)contact_list.get(url)).getNameAddress(); + else return null; + } + + /** Whether the contact is expired. + * @param url the contact url (String) + * @return true if the contact is expired or contact does not exist */ + public boolean isExpired(String url) + { if (contact_list.containsKey(url)) return ((ContactHeader)contact_list.get(url)).isExpired(); + else return true; + } + + /** Gets expiration date. + * @param url the contact url (String) + * @return the expire Date */ + public Date getExpirationDate(String url) + { ContactHeader contact=(ContactHeader)contact_list.get(url); + //System.out.println("DEBUG: UserBindingInfo: ContactHeader: "+contact.toString()); + //System.out.println("DEBUG: UserBindingInfo: expires param: "+contact.getParameter("expires")); + if (contact_list.containsKey(url)) return ((ContactHeader)contact_list.get(url)).getExpiresDate(); + else return null; + } + + /** Gets the String value of this Object. + * @return the String value */ + public String toString() + { String str="To: "+name+"\r\n"; + for (Enumeration i=getContacts(); i.hasMoreElements(); ) + { ContactHeader ch=(ContactHeader)contact_list.get(i.nextElement()); + str+=ch.toString(); + } + return str; + } +} + diff --git a/src/local/server/PrefixRoutingRule.java b/src/local/server/PrefixRoutingRule.java new file mode 100644 index 0000000..fd5318e --- /dev/null +++ b/src/local/server/PrefixRoutingRule.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This source code is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package local.server; + + +import org.zoolu.sip.address.SipURL; +import org.zoolu.net.SocketAddress; + + +/** PrefixRoutingRule. + */ +class PrefixRoutingRule implements RoutingRule +{ + /** Prefix for the default rule. */ + public static final String DEFAULT_PREFIX="default"; + + + /** Matching prefix. */ + String prefix; + + /** Next-hop server. */ + SocketAddress nexthop; + + + /** Creates a new PrefixRoutingRule. */ + public PrefixRoutingRule(String prefix, SocketAddress nexthop) + { this.prefix=prefix; + this.nexthop=nexthop; + } + + + /** Gets the proper next-hop SipURL for the selected URL. + * It return the SipURL used to reach the selected URL. + * @param sip_url the selected destination URL + * @return returns the proper next-hop SipURL for the selected URL + * if the routing rule matches the URL, otherwise it returns null. */ + public SipURL getNexthop(SipURL sip_url) + { String username=sip_url.getUserName(); + if ((username!=null && username.startsWith(prefix)) || prefix.equalsIgnoreCase(DEFAULT_PREFIX)) + { return new SipURL(username,nexthop.getAddress().toString(),nexthop.getPort()); + } + else return null; + } + + /** Gets the String value. */ + public String toString() + { return "{prefix="+prefix+","+"nexthop="+nexthop+"}"; + } +} \ No newline at end of file diff --git a/src/local/server/Proxy.java b/src/local/server/Proxy.java new file mode 100644 index 0000000..e286634 --- /dev/null +++ b/src/local/server/Proxy.java @@ -0,0 +1,356 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This source code is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package local.server; + + +import org.zoolu.sip.address.*; +import org.zoolu.sip.provider.*; +import org.zoolu.sip.header.RequestLine; +import org.zoolu.sip.header.Header; +import org.zoolu.sip.header.ViaHeader; +import org.zoolu.sip.header.MaxForwardsHeader; +import org.zoolu.sip.header.MultipleHeader; +import org.zoolu.sip.header.RouteHeader; +import org.zoolu.sip.header.RecordRouteHeader; +import org.zoolu.sip.message.Message; +import org.zoolu.sip.message.MessageFactory; +import org.zoolu.sip.message.SipResponses; +import org.zoolu.tools.LogLevel; + + +//import java.util.Enumeration; +import java.util.Vector; +import java.io.BufferedReader; +import java.io.InputStreamReader; + + +/** Class Proxy implement a Proxy SIP Server. + * It extends class Registrar. A Proxy can work as simply SIP Proxy, + * or it can handle calls for registered users. + */ +public class Proxy extends Registrar +{ + /** Log of processed calls */ + CallLogger call_logger; + + /** Costructs a void Proxy */ + protected Proxy() {} + + /** Costructs a new Proxy that acts also as location server for registered users. */ + public Proxy(SipProvider provider, ServerProfile server_profile) + { super(provider,server_profile); + if (server_profile.call_log) call_logger=new CallLoggerImpl(SipStack.log_path+"//"+provider.getViaAddress()+"."+provider.getPort()+"_calls.log"); + } + + + /** When a new request is received for the local server. */ + public void processRequestToLocalServer(Message msg) + { printLog("inside processRequestToLocalServer(msg)",LogLevel.MEDIUM); + if (msg.isRegister()) + { super.processRequestToLocalServer(msg); + } + else + if (!msg.isAck()) + { // send a stateless error response + //int result=501; // response code 501 ("Not Implemented") + //int result=485; // response code 485 ("Ambiguous"); + int result=484; // response code 484 ("Address Incomplete"); + Message resp=MessageFactory.createResponse(msg,result,SipResponses.reasonOf(result),null); + sip_provider.sendMessage(resp); + } + } + + + /** When a new request message is received for a local user */ + public void processRequestToLocalUser(Message msg) + { printLog("inside processRequestToLocalUser(msg)",LogLevel.MEDIUM); + + if (server_profile.call_log) call_logger.update(msg); + + if (server_profile.do_proxy_authentication && !msg.isAck() && !msg.isCancel()) + { // check message authentication + Message err_resp=as.authenticateProxyRequest(msg); + if (err_resp!=null) + { sip_provider.sendMessage(err_resp); + return; + } + } + + // message targets + Vector targets=getTargets(msg); + + if (targets.isEmpty()) + { // try to treat the request-URI as a local username or phone URL with a prefix-based nexthop rule + SipURL request_uri=msg.getRequestLine().getAddress(); + SipURL new_target=getPhoneTarget(request_uri); + if (new_target!=null) targets.addElement(new_target.toString()); + } + if (targets.isEmpty()) + { printLog("No target found, message discarded",LogLevel.HIGH); + if (!msg.isAck()) sip_provider.sendMessage(MessageFactory.createResponse(msg,404,SipResponses.reasonOf(404),null)); + return; + } + + printLog("message will be forwarded to all user's contacts",LogLevel.MEDIUM); + for (int i=0; i0) msg.setRoutes(mr); + else msg.removeRoutes(); + is_on_route=true; + } + } + // add Record-Route? + if (server_profile.on_route && msg.isInvite() && !is_on_route) + { SipURL rr_url; + if (sip_provider.getPort()==SipStack.default_port) rr_url=new SipURL(sip_provider.getViaAddress()); + else rr_url=new SipURL(sip_provider.getViaAddress(),sip_provider.getPort()); + if (server_profile.loose_route) rr_url.addLr(); + RecordRouteHeader rrh=new RecordRouteHeader(new NameAddress(rr_url)); + msg.addRecordRouteHeader(rrh); + } + // which protocol? + String proto=null; + if (msg.hasRouteHeader()) + { SipURL route=msg.getRouteHeader().getNameAddress().getAddress(); + if (route.hasTransport()) proto=route.getTransport(); + } + else proto=msg.getRequestLine().getAddress().getTransport(); + if (proto==null) proto=sip_provider.getDefaultTransport(); + + // add Via + ViaHeader via=new ViaHeader(proto,sip_provider.getViaAddress(),sip_provider.getPort()); + if (sip_provider.isRportSet()) via.setRport(); + String branch=sip_provider.pickBranch(msg); + if (server_profile.loop_detection) + { String loop_tag=msg.getHeader(Loop_Tag).getValue(); + if (loop_tag!=null) + { msg.removeHeader(Loop_Tag); + branch+=loop_tag; + } + } + via.setBranch(branch); + msg.addViaHeader(via); + + // decrement Max-Forwards + MaxForwardsHeader maxfwd=msg.getMaxForwardsHeader(); + if (maxfwd!=null) maxfwd.decrement(); + else maxfwd=new MaxForwardsHeader(SipStack.max_forwards); + msg.setMaxForwardsHeader(maxfwd); + + // domain name routing + if (server_profile.domain_routing_rules!=null && server_profile.domain_routing_rules.length>0) + { RequestLine rl=msg.getRequestLine(); + SipURL request_uri=rl.getAddress(); + for (int i=0; i'9')) return false; + } + return true; + } + + + // ****************************** Logs ***************************** + + /** Adds a new string to the default Log */ + private void printLog(String str, int level) + { if (log!=null) log.println("Proxy: "+str,level+SipStack.LOG_LEVEL_UA); + } + + + // ****************************** MAIN ***************************** + + /** The main method. */ + public static void main(String[] args) + { + + String file=null; + boolean prompt_exit=false; + + for (int i=0; i(i+1)) + { file=args[++i]; + continue; + } + if (args[i].equals("--prompt")) + { prompt_exit=true; + continue; + } + if (args[i].equals("-h")) + { System.out.println("usage:\n java Proxy [options] \n"); + System.out.println(" options:"); + System.out.println(" -h this help"); + System.out.println(" -f specifies a configuration file"); + System.out.println(" --prompt prompt for exit"); + System.exit(0); + } + } + + SipStack.init(file); + SipProvider sip_provider=new SipProvider(file); + ServerProfile server_profile=new ServerProfile(file); + + new Proxy(sip_provider,server_profile); + + // promt before exit + if (prompt_exit) + try + { System.out.println("press 'enter' to exit"); + BufferedReader in=new BufferedReader(new InputStreamReader(System.in)); + in.readLine(); + System.exit(0); + } + catch (Exception e) {} + } + +} \ No newline at end of file diff --git a/src/local/server/Proxy.java.saved b/src/local/server/Proxy.java.saved new file mode 100644 index 0000000..14e4ad0 --- /dev/null +++ b/src/local/server/Proxy.java.saved @@ -0,0 +1,390 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This source code is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package local.server; + + +import org.zoolu.sip.address.*; +import org.zoolu.sip.provider.*; +import org.zoolu.sip.header.RequestLine; +import org.zoolu.sip.header.Header; +import org.zoolu.sip.header.ViaHeader; +import org.zoolu.sip.header.MaxForwardsHeader; +import org.zoolu.sip.header.MultipleHeader; +import org.zoolu.sip.header.RouteHeader; +import org.zoolu.sip.header.RecordRouteHeader; +import org.zoolu.sip.message.Message; +import org.zoolu.sip.message.MessageFactory; +import org.zoolu.sip.message.SipResponses; +import org.zoolu.tools.LogLevel; +import org.zoolu.tools.SimpleDigest; + + +//import java.util.Enumeration; +import java.util.Vector; +import java.io.BufferedReader; +import java.io.InputStreamReader; + + +/** Class Proxy implement a Proxy SIP Server. + * It extends class Registrar. A Proxy can work as simply SIP Proxy, + * or it can handle calls for registered users. + */ +public class Proxy extends Registrar +{ + /** Log of processed calls */ + CallLogger call_logger; + + /** Costructs a void Proxy */ + protected Proxy() {} + + /** Costructs a new Proxy that acts also as location server for registered users. */ + public Proxy(SipProvider provider, ServerProfile server_profile) + { super(provider,server_profile); + if (server_profile.call_log) call_logger=new CallLoggerImpl(SipStack.log_path+"//"+provider.getViaAddress()+"."+provider.getPort()+"_calls.log"); + } + + + /** When a new request is received for the local server. */ + public void processRequestToLocalServer(Message msg) + { printLog("inside processRequestToLocalServer(msg)",LogLevel.MEDIUM); + if (msg.isRegister()) + { super.processRequestToLocalServer(msg); + } + else + if (!msg.isAck()) + { // send a stateless error response + //int result=501; // response code 501 ("Not Implemented") + //int result=485; // response code 485 ("Ambiguous"); + int result=484; // response code 484 ("Address Incomplete"); + Message resp=MessageFactory.createResponse(msg,result,SipResponses.reasonOf(result),null,null); + sip_provider.sendMessage(resp); + } + } + + + /** When a new request message is received for a local user */ + public void processRequestToLocalUser(Message msg) + { printLog("inside processRequestToLocalUser(msg)",LogLevel.MEDIUM); + + if (server_profile.call_log) call_logger.update(msg); + + if (server_profile.do_proxy_authentication && !msg.isAck() && !msg.isCancel()) + { // check message authentication + Message err_resp=as.authenticateProxyRequest(msg); + if (err_resp!=null) + { sip_provider.sendMessage(err_resp); + return; + } + } + + // message targets + Vector targets=getTargets(msg); + + if (targets.isEmpty()) + { // try to treat the request-URI as a phone URL + SipURL request_uri=msg.getRequestLine().getAddress(); + SipURL new_target=getPhoneTarget(request_uri); + if (new_target!=null) targets.addElement(new_target.toString()); + } + if (targets.isEmpty()) + { printLog("No target found, message discarded",LogLevel.HIGH); + if (!msg.isAck()) sip_provider.sendMessage(MessageFactory.createResponse(msg,404,SipResponses.reasonOf(404),null,null)); + return; + } + + printLog("message will be forwarded to all user's contacts",LogLevel.MEDIUM); + for (int i=0; i0) msg.setRoutes(mr); + else msg.removeRoutes(); + is_on_route=true; + } + } + // add Record-Route? + if (server_profile.on_route && msg.isInvite() && !is_on_route) + { SipURL rr_url; + if (sip_provider.getPort()==SipStack.default_port) rr_url=new SipURL(sip_provider.getViaAddress()); + else rr_url=new SipURL(sip_provider.getViaAddress(),sip_provider.getPort()); + if (server_profile.loose_route) rr_url.addLr(); + RecordRouteHeader rrh=new RecordRouteHeader(new NameAddress(rr_url)); + msg.addRecordRouteHeader(rrh); + } + // which protocol? + String proto=null; + if (msg.hasRouteHeader()) + { SipURL route=msg.getRouteHeader().getNameAddress().getAddress(); + if (route.hasTransport()) proto=route.getTransport(); + } + else proto=msg.getRequestLine().getAddress().getTransport(); + if (proto==null) proto=sip_provider.getDefaultTransport(); + + // add Via + ViaHeader via=new ViaHeader(proto,sip_provider.getViaAddress(),sip_provider.getPort()); + if (sip_provider.isRportSet()) via.setRport(); + via.setBranch(pickBranch(msg)); + msg.addViaHeader(via); + + // decrement Max-Forwards + MaxForwardsHeader maxfwd=msg.getMaxForwardsHeader(); + if (maxfwd!=null) maxfwd.decrement(); + else maxfwd=new MaxForwardsHeader(SipStack.max_forwards); + msg.setMaxForwardsHeader(maxfwd); + + // check whether the next Route is formed according to RFC2543 + msg.rfc2543RouteAdapt(); + + return msg; + } + + + /** When a new response message is received */ + public void processResponse(Message resp) + { printLog("inside processResponse(msg)",LogLevel.MEDIUM); + + if(call_logger!=null) call_logger.update(resp); + + updateProxingResponse(resp); + + if (resp.hasViaHeader()) sip_provider.sendMessage(resp); + else + printLog("no VIA header found: message discarded",LogLevel.HIGH); + } + + + /** Processes the Proxy headers of the response. + * Such headers are: Via, .. */ + protected Message updateProxingResponse(Message resp) + { printLog("inside updateProxingResponse(resp)",LogLevel.MEDIUM); + ViaHeader vh=new ViaHeader((Header)resp.getVias().getHeaders().elementAt(0)); + if (vh.getHost().equals(sip_provider.getViaAddress())) resp.removeViaHeader(); + return resp; + } + + + /** Picks an unique branch value based on a SIP message. + * This value could also be for loop detection. */ + public String pickBranch(Message msg) + { String branch=sip_provider.pickBranch(msg); + if (server_profile.loop_detection) branch+=pickLoopBranch(msg); + return branch; + } + + + /** Picks the branch part used for loop detection. */ + private String pickLoopBranch(Message msg) + { StringBuffer sb=new StringBuffer(); + sb.append(msg.getToHeader().getTag()); + sb.append(msg.getFromHeader().getTag()); + sb.append(msg.getCallIdHeader().getCallId()); + sb.append(msg.getRequestLine().getAddress().toString()); + sb.append(msg.getCSeqHeader().getSequenceNumber()); + return (new SimpleDigest(7,sb.toString())).asHex(); + } + + + /** Whether a loop is detected for request message msg. */ + protected boolean loopDetected(Message msg) + { if (!msg.hasRouteHeader()) + { Vector v=msg.getVias().getHeaders(); + for (int i=0; i=0) return true; + } + } + } + } + // no loop detected + return false; + } + + + /** Tries to find the target for a phone URL. */ + protected SipURL getPhoneTarget(SipURL request_uri) + { String username=request_uri.getUserName(); + if (username!=null && isPhoneNumber(username)) + { printLog(username+" is a phone number",LogLevel.MEDIUM); + for (int i=0; i'9')) return false; + } + return true; + } + + + // ****************************** Logs ***************************** + + /** Adds a new string to the default Log */ + private void printLog(String str, int level) + { if (log!=null) log.println("Proxy: "+str,level+SipStack.LOG_LEVEL_UA); + } + + + // ****************************** MAIN ***************************** + + /** The main method. */ + public static void main(String[] args) + { + + String file=null; + boolean prompt_exit=false; + + for (int i=0; i(i+1)) + { file=args[++i]; + continue; + } + if (args[i].equals("--prompt")) + { prompt_exit=true; + continue; + } + if (args[i].equals("-h")) + { System.out.println("usage:\n java Proxy [options] \n"); + System.out.println(" options:"); + System.out.println(" -h this help"); + System.out.println(" -f specifies a configuration file"); + System.out.println(" --prompt prompt for exit"); + System.exit(0); + } + } + + SipStack.init(file); + SipProvider sip_provider=new SipProvider(file); + ServerProfile server_profile=new ServerProfile(file); + + new Proxy(sip_provider,server_profile); + + // promt before exit + if (prompt_exit) + try + { System.out.println("press 'enter' to exit"); + BufferedReader in=new BufferedReader(new InputStreamReader(System.in)); + in.readLine(); + System.exit(0); + } + catch (Exception e) {} + } + +} \ No newline at end of file diff --git a/src/local/server/Redirect.java b/src/local/server/Redirect.java new file mode 100644 index 0000000..ebb0e07 --- /dev/null +++ b/src/local/server/Redirect.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This source code is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package local.server; + + +import org.zoolu.sip.address.*; +import org.zoolu.sip.provider.*; +import org.zoolu.sip.message.*; +import org.zoolu.sip.header.RequestLine; +import org.zoolu.sip.header.Header; +import org.zoolu.sip.header.ViaHeader; +import org.zoolu.sip.header.ContactHeader; +import org.zoolu.sip.header.MultipleHeader; +import org.zoolu.sip.header.RouteHeader; +import org.zoolu.sip.header.SipHeaders; +import org.zoolu.tools.LogLevel; + +import java.util.Enumeration; +import java.util.Vector; + + +/** Class Redirect implement a SIP edirect server. + * It extends class Registrar. A Redirect can work as simply SIP redirect, + * or it can handle calls for registered users. + */ +public class Redirect extends Registrar +{ + /** Costructs a new Redirect that acts also as location server for registered users. */ + public Redirect(SipProvider provider, ServerProfile server_profile) + { super(provider,server_profile); + } + + /** When a new request message is received for a local user */ + public void processRequestToLocalUser(Message msg) + { printLog("inside processRequestToLocalUser(msg)",LogLevel.MEDIUM); + + // message targets + Vector contacts=getTargets(msg); + + if (contacts.isEmpty()) + { printLog("No target found, message discarded",LogLevel.HIGH); + if (!msg.isAck()) sip_provider.sendMessage(MessageFactory.createResponse(msg,404,SipResponses.reasonOf(404),null)); + return; + } + + printLog("message will be redirect to all user's contacts",LogLevel.MEDIUM); + // create the response with all contact urls, and send it + MultipleHeader mc=new MultipleHeader(SipHeaders.Contact,contacts); + mc.setCommaSeparated(true); + Message resp=MessageFactory.createResponse(msg,302,SipResponses.reasonOf(302),null); + resp.setContacts(mc); + sip_provider.sendMessage(resp); + } + + /** When a new request message is received for a remote UA */ + public void processRequestToRemoteUA(Message msg) + { printLog("inside processRequestToRemoteUA(msg)",LogLevel.MEDIUM); + printLog("request not for local server",LogLevel.HIGH); + if (!msg.isAck()) sip_provider.sendMessage(MessageFactory.createResponse(msg,404,SipResponses.reasonOf(404),null)); + else printLog("message discarded",LogLevel.HIGH); + } + + /** When a new response message is received */ + public void processResponse(Message resp) + { printLog("inside processResponse(msg)",LogLevel.MEDIUM); + printLog("request not for local server: message discarded",LogLevel.HIGH); + } + + + + // ****************************** Logs ***************************** + + /** Adds a new string to the default Log */ + private void printLog(String str, int level) + { if (log!=null) log.println("Redirect: "+str,level+SipStack.LOG_LEVEL_UA); + } + + + // ****************************** MAIN ***************************** + + /** The main method. */ + public static void main(String[] args) + { + + String file=null; + + for (int i=0; i(i+1)) + { file=args[++i]; + continue; + } + if (args[i].equals("-h")) + { System.out.println("usage:\n java Redirect [-f ] \n"); + System.exit(0); + } + } + + SipStack.init(file); + SipProvider sip_provider=new SipProvider(file); + ServerProfile server_profile=new ServerProfile(file); + + new Redirect(sip_provider,server_profile); + } + +} \ No newline at end of file diff --git a/src/local/server/Registrar.java b/src/local/server/Registrar.java new file mode 100644 index 0000000..4c4a3f2 --- /dev/null +++ b/src/local/server/Registrar.java @@ -0,0 +1,474 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This source code is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package local.server; + + +import org.zoolu.net.SocketAddress; +import org.zoolu.sip.address.*; +import org.zoolu.sip.provider.*; +import org.zoolu.sip.header.SipHeaders; +import org.zoolu.sip.header.Header; +import org.zoolu.sip.header.ToHeader; +import org.zoolu.sip.header.ViaHeader; +import org.zoolu.sip.header.ExpiresHeader; +import org.zoolu.sip.header.StatusLine; +import org.zoolu.sip.header.ContactHeader; +import org.zoolu.sip.header.MultipleHeader; +import org.zoolu.sip.header.WwwAuthenticateHeader; +import org.zoolu.sip.header.AuthorizationHeader; +import org.zoolu.sip.header.AuthenticationInfoHeader; +import org.zoolu.sip.transaction.TransactionServer; +import org.zoolu.sip.message.Message; +import org.zoolu.sip.message.MessageFactory; +import org.zoolu.sip.message.SipResponses; +import org.zoolu.tools.Parser; +import org.zoolu.tools.LogLevel; +import org.zoolu.tools.DateFormat; + +import java.util.Date; +//import java.util.Locale; +//import java.text.DateFormat; +//import java.text.SimpleDateFormat; +import java.util.Vector; +import java.util.Enumeration; + + +/** Class Registrar implements a Registrar SIP Server. + * It extends class ServerEngine. + */ +public class Registrar extends ServerEngine +{ + /** LocationService. */ + protected LocationService location_service; + + /** AuthenticationService (i.e. the repository with authentication credentials). */ + protected AuthenticationService authentication_service; + + /** AuthenticationServer. */ + protected AuthenticationServer as; + + /** List of already supported location services */ + protected static final String[] LOCATION_SERVICES={ "local", "ldap" }; + /** List of location service Classes (ordered as in LOCATION_SERVICES) */ + protected static final String[] LOCATION_SERVICE_CLASSES={ "local.server.LocationServiceImpl", "local.ldap.LdapLocationServiceImpl" }; + + /** List of already supported authentication services */ + protected static final String[] AUTHENTICATION_SERVICES={ "local", "ldap" }; + /** List of authentication service Classes (ordered as in AUTHENTICATION_SERVICES) */ + protected static final String[] AUTHENTICATION_SERVICE_CLASSES={ "local.server.AuthenticationServiceImpl", "local.ldap.LdapAuthenticationServiceImpl" }; + + /** List of already supported authentication schemes */ + protected static final String[] AUTHENTICATION_SCHEMES={ "Digest" }; + /** List of authentication server Classes (ordered as in AUTHENTICATION_SCHEMES) */ + protected static final String[] AUTHENTICATION_SERVER_CLASSES={ "local.server.AuthenticationServerImpl" }; + + + /** Costructs a void Registrar. */ + protected Registrar() {} + + + /** Costructs a new Registrar. The Location Service is stored within the file db_name */ + //public Registrar(SipProvider provider, String db_class, String db_name) + public Registrar(SipProvider provider, ServerProfile profile) + { super(provider,profile); + printLog("Domains="+getLocalDomains(),LogLevel.HIGH); + + // location service + String location_service_class=profile.location_service; + for (int i=0; iserver_profile.expires) exp_secs=server_profile.expires; + + // known user? + if (!location_service.hasUser(user)) + { if (server_profile.register_new_users) + { location_service.addUser(user); + printLog("new user '"+user+"' added.",LogLevel.HIGH); + } + else + { printLog("user '"+user+"' unknown: message discarded.",LogLevel.HIGH); + int result=404; + return MessageFactory.createResponse(msg,result,SipResponses.reasonOf(result),null); + } + } + + // Get the "device" parameter. Set device=null if not present or not supported + //String device=null; + // if (msg.hasApplicationHeader()) app=msg.getApplicationHeader().getApplication(); + SipURL to_url=msg.getToHeader().getNameAddress().getAddress(); + //if (to_url.hasParameter("device")) device=to_url.getParameter("device"); + + if (!msg.hasContactHeader()) + { //printLog("ContactHeader missed: message discarded",LogLevel.HIGH); + //int result=484; + //return MessageFactory.createResponse(msg,result,SipResponses.reasonOf(result),null,null); + printLog("no contact found: fetching bindings..",LogLevel.MEDIUM); + int result=200; + Message resp=MessageFactory.createResponse(msg,result,SipResponses.reasonOf(result),null); + // add current contacts + Vector v=new Vector(); + for (Enumeration e=location_service.getUserContactURLs(user); e.hasMoreElements(); ) + { String url=(String)e.nextElement(); + int expires=(int)(location_service.getUserContactExpirationDate(user,url).getTime()-System.currentTimeMillis())/1000; + if (expires>0) + { // not expired + ContactHeader ch=new ContactHeader(location_service.getUserContactNameAddress(user,url)); + ch.setExpires(expires); + v.addElement(ch); + } + } + if (v.size()>0) resp.setContacts(new MultipleHeader(v)); + return resp; + } + // else + + Vector contacts=msg.getContacts().getHeaders(); + int result=200; + Message resp=MessageFactory.createResponse(msg,result,SipResponses.reasonOf(result),null); + + ContactHeader contact_0=new ContactHeader((Header)contacts.elementAt(0)); + if (contact_0.isStar()) + { printLog("DEBUG: ContactHeader is star",LogLevel.LOW); + Vector resp_contacts=new Vector(); + for (Enumeration e=location_service.getUserContactURLs(user); e.hasMoreElements();) + { String url=(String)(e.nextElement()); + NameAddress name_address=location_service.getUserContactNameAddress(user,url); + // update db + location_service.removeUserContact(user,url); + printLog("contact removed: "+url,LogLevel.LOW); + if (exp_secs>0) + { Date exp_date=new Date(System.currentTimeMillis()+((long)exp_secs)*1000); + location_service.addUserContact(user,name_address,exp_date); + //DateFormat df=new SimpleDateFormat("EEE, dd MMM yyyy hh:mm:ss 'GMT'",Locale.ITALIAN); + //printLog("contact added: "+url+"; expire: "+df.format(location_service.getUserContactExpire(user,url)),LogLevel.LOW); + printLog("contact added: "+url+"; expire: "+DateFormat.formatEEEddMMM(location_service.getUserContactExpirationDate(user,url)),LogLevel.LOW); + } + ContactHeader contact_i=new ContactHeader(name_address.getAddress()); + contact_i.setExpires(exp_secs); + resp_contacts.addElement(contact_i); + } + if (resp_contacts.size()>0) resp.setContacts(new MultipleHeader(resp_contacts)); + } + else + { Vector resp_contacts=new Vector(); + for (int i=0; iserver_profile.expires) exp_secs_i=server_profile.expires; + + // update db + location_service.removeUserContact(user,url); + if (exp_secs_i>0) + { Date exp_date=new Date(System.currentTimeMillis()+((long)exp_secs)*1000); + location_service.addUserContact(user,name_address,exp_date); + printLog("registration of user "+user+" updated",LogLevel.HIGH); + } + contact_i.setExpires(exp_secs_i); + resp_contacts.addElement(contact_i); + } + if (resp_contacts.size()>0) resp.setContacts(new MultipleHeader(resp_contacts)); + } + + location_service.sync(); + return resp; + } + + + // ****************************** Logs ***************************** + + /** Adds a new string to the default Log. */ + private void printLog(String str, int level) + { if (log!=null) log.println("Registrar: "+str,level+SipStack.LOG_LEVEL_UA); + } + + /** Adds the Exception message to the default Log */ + private final void printException(Exception e, int level) + { if (log!=null) log.printException(e,level+SipStack.LOG_LEVEL_UA); + } + + + // ****************************** MAIN ***************************** + + /** The main method. */ + public static void main(String[] args) + { + + String file=null; + + for (int i=0; i(i+1)) + { file=args[++i]; + continue; + } + if (args[i].equals("-h")) + { System.out.println("usage:\n java Registrar [-f ] \n"); + System.exit(0); + } + } + + SipStack.init(file); + SipProvider sip_provider=new SipProvider(file); + ServerProfile server_profile=new ServerProfile(file); + + new Registrar(sip_provider,server_profile); + } +} \ No newline at end of file diff --git a/src/local/server/Repository.java b/src/local/server/Repository.java new file mode 100644 index 0000000..e79577f --- /dev/null +++ b/src/local/server/Repository.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This source code is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package local.server; + + +import java.util.Enumeration; + + +/** Repository is the interface used to access to a generic repository. + *

A location service or AAA service are example of such repository. + */ +public interface Repository +{ + /** Syncronizes the database. + *

Can be used, for example, to save the current memory image of the DB. */ + public void sync(); + + /** Returns the numbers of users in the database. + * @return the numbers of user entries */ + public int size(); + + /** Returns an enumeration of the users in this database. + * @return the list of user names as an Enumeration of String */ + public Enumeration getUsers(); + + /** Whether a user is present in the database and can be used as key. + * @param user the user name + * @return true if the user name is present as key */ + public boolean hasUser(String user); + + /** Adds a new user at the database. + * @param user the user name + * @return this object */ + public Repository addUser(String user); + + /** Removes the user from the database. + * @param user the user name + * @return this object */ + public Repository removeUser(String user); + + /** Removes all users from the database. + * @return this object */ + public Repository removeAllUsers(); + + /** Gets the String value of this Object. + * @return the String value */ + public String toString(); + +} diff --git a/src/local/server/RoutingRule.java b/src/local/server/RoutingRule.java new file mode 100644 index 0000000..a4b410f --- /dev/null +++ b/src/local/server/RoutingRule.java @@ -0,0 +1,20 @@ +package local.server; + + +import org.zoolu.sip.address.SipURL; + + +/** RoutingRule. + */ +public interface RoutingRule +{ + /** Gets the proper next-hop SipURL for the selected URL. + * It return the SipURL used to reach the selected URL. + * @param sip_url the selected destination URL + * @return returns the proper next-hop SipURL for the selected URL + * if the routing rule matches the URL, otherwise it returns null. */ + public SipURL getNexthop(SipURL sip_url); + + /** Gets the String value. */ + public String toString(); +} \ No newline at end of file diff --git a/src/local/server/ServerEngine.java b/src/local/server/ServerEngine.java new file mode 100644 index 0000000..b3abf97 --- /dev/null +++ b/src/local/server/ServerEngine.java @@ -0,0 +1,334 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This source code is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package local.server; + + +import org.zoolu.sip.address.SipURL; +import org.zoolu.sip.provider.*; +import org.zoolu.sip.header.MultipleHeader; +import org.zoolu.sip.header.ViaHeader; +import org.zoolu.sip.header.Header; +import org.zoolu.sip.header.RouteHeader; +import org.zoolu.sip.header.RequestLine; +import org.zoolu.sip.header.MaxForwardsHeader; +import org.zoolu.sip.header.MultipleHeader; +import org.zoolu.sip.message.Message; +import org.zoolu.sip.message.SipResponses; +import org.zoolu.sip.message.MessageFactory; +import org.zoolu.tools.Log; +import org.zoolu.tools.LogLevel; +import org.zoolu.tools.SimpleDigest; + +import java.util.Vector; + + +/** Class ServerEngine implement a stateless abstract SIP Server. + * The ServerEngine can act as SIP Proxy Server, SIP Registrar Server or both. + *

For each incoming message, the ServerEngine fires one of the following + * abstract methods: + *

    + *
  • public abstract processRequestToRemoteUA(Message),
  • + *
  • public abstract processRequestToLocalServer(Message),
  • + *
  • public abstract processRequestToLocalServer(Message),
  • + *
  • public abstract processResponse(Message).
  • + *
+ * depending of the type of received message. + */ +public abstract class ServerEngine implements SipProviderListener +{ + /** Name of the Loop-Tag header field. + * It is used as temporary filed for carry loop detection information, + * added to the via branch parameter of the forwarded requests. */ + protected static final String Loop_Tag="Loop-Tag"; + + /** Event logger. */ + protected Log log=null; + + /** ServerProfile of the server. */ + protected ServerProfile server_profile=null; + + /** SipProvider used by the server. */ + protected SipProvider sip_provider=null; + + /** Costructs a void ServerEngine */ + protected ServerEngine() {} + + + // *************************** abstract methods *************************** + + /** When a new request message is received for a remote UA */ + public abstract void processRequestToRemoteUA(Message req); + + /** When a new request message is received for a locally registered user */ + public abstract void processRequestToLocalUser(Message req); + + /** When a new request request is received for the local server */ + public abstract void processRequestToLocalServer(Message req); + + /** When a new response message is received */ + public abstract void processResponse(Message resp); + + + // **************************** public methods **************************** + + /** Costructs a new ServerEngine on SipProvider provider, + * and adds it as SipProviderListener. */ + public ServerEngine(SipProvider provider, ServerProfile profile) + { server_profile=profile; + sip_provider=provider; + log=sip_provider.getLog(); + sip_provider.addSipProviderListener(SipProvider.ANY,this); + } + + /** When a new message is received by the SipProvider. + * If the received message is a request, it cheks for loops, */ + public void onReceivedMessage(SipProvider provider, Message msg) + { printLog("message received",LogLevel.MEDIUM); + if (msg.isRequest()) // it is an INVITE or ACK or BYE or OPTIONS or REGISTER or CANCEL + { printLog("message is a request",LogLevel.MEDIUM); + + // validate the message + Message err_resp=validateRequest(msg); + if (err_resp!=null) + { // for non-ACK requests respond with an error message + if (!msg.isAck()) sip_provider.sendMessage(err_resp); + return; + } + + // target + SipURL target=msg.getRequestLine().getAddress(); + + // check if this server is the target + //boolean this_is_target=isResponsibleFor(target.getHost(),target.getPort()); + + // look if the msg sent by the previous UA is compliant with the RFC2543 Strict Route rule.. + if (isResponsibleFor(target.getHost(),target.getPort()) && msg.hasRouteHeader()) + { + //SipURL route_url=msg.getRouteHeader().getNameAddress().getAddress(); + SipURL route_url=(new RouteHeader(msg.getRoutes().getBottom())).getNameAddress().getAddress(); + if (!route_url.hasLr()) + { printLog("probably the message was compliant to RFC2543 Strict Route rule: message is updated to RFC3261",LogLevel.MEDIUM); + + // the message has been sent to this server according with RFC2543 Strict Route + // the proxy MUST replace the Request-URI in the request with the last + // value from the Route header field, and remove that value from the + // Route header field. The proxy MUST then proceed as if it received + // this modified request. + msg.rfc2543toRfc3261RouteUpdate(); + + // update the target + target=msg.getRequestLine().getAddress(); + printLog("new recipient: "+target.toString(),LogLevel.LOW); + + // check again if this server is the target + //this_is_target=matchesDomainName(target.getHost(),target.getPort()); + } + } + + // removes the local Route value, if present + /*if (msg.hasRouteHeader()) + { MultipleHeader mr=msg.getRoutes(); + SipURL top_route=(new RouteHeader(mr.getTop())).getNameAddress().getAddress(); + if (matchesDomainName(top_route.getHost(),top_route.getPort())) + { mr.removeTop(); + if (mr.size()>0) msg.setRoutes(mr); + else msg.removeRoutes(); + } + }*/ + + // check whether the request is for a domain the server is responsible for + if (isResponsibleFor(msg)) + { + printLog("the request is for the local server",LogLevel.LOW); + + if (target.hasUserName()) + { printLog("the request is for a local user",LogLevel.LOW); + processRequestToLocalUser(msg); + } + else + { printLog("no username: the request is for the local server",LogLevel.LOW); + processRequestToLocalServer(msg); + } + } + else // the request is NOT for the "local" server + { + printLog("the request is not for the local server",LogLevel.LOW); + processRequestToRemoteUA(msg); + } + } + else // the message may be a response + { + if (msg.isResponse()) + { printLog("message is a response",LogLevel.LOW); + processResponse(msg); + } + else printWarning("received message is not recognized as a request nor a response: discarded",LogLevel.HIGH); + } + } + + /** Relays the massage. + * Called after a received message has been successful processed for being relayed */ + //protected void sendMessage(Message msg) + //{ printLog("sending the successfully processed message",LogLevel.MEDIUM); + // sip_provider.sendMessage(msg); + //} + + /** Whether the server is responsible for the given domain + * (i.e. the domain is included in the local domain names list) + * and port (if >0) matches the local server port. */ + protected boolean isResponsibleFor(String domain, int port) + { // check port + if (!server_profile.domain_port_any && port>0 && port!=sip_provider.getPort()) return false; + // check host address + if (domain.equals(sip_provider.getViaAddress())) return true; + // check domain name + boolean it_is=false; + for (int i=0; ireq
. */ + protected boolean isResponsibleFor(Message req) + { SipURL target=req.getRequestLine().getAddress(); + return isResponsibleFor(target.getHost(),target.getPort()); + } + + /** Whether the request is for the local server */ + /*protected boolean isTargetOf(Message req) + { SipURL target=req.getRequestLine().getAddress(); + if (!isResponsibleFor(target.getHost(),target.getPort())) return false; + // else, request-uri matches a domain the server is responsible for + if (!req.hasRouteHeader()) return true; + // else, has route.. + MultipleHeader route=req.getRoutes(); + if (route.size()>1) return false; + // else, only 1 route, check it + target=(new RouteHeader(route.getTop())).getNameAddress().getAddress(); + if (!isResponsibleFor(target.getHost(),target.getPort())) return false; + // else + return true; + }*/ + + /** Gets a String of the list of local domain names. */ + protected String getLocalDomains() + { if (server_profile.domain_names.length>0) + { String str=""; + for (int i=0; i=0) err_code=482; + } + } + } + } + } + + // Proxy-Require + + // Proxy-Authorization + + if (err_code>0) + { String reason=SipResponses.reasonOf(err_code); + printLog("Message validation failed ("+reason+"), message discarded",LogLevel.HIGH); + return MessageFactory.createResponse(msg,err_code,reason,null); + } + else return null; + } + + /** Picks an unique branch value based on a SIP message. + * This value could also be used for loop detection. */ + /*public String pickBranch(Message msg) + { String branch=sip_provider.pickBranch(msg); + if (server_profile.loop_detection) branch+=pickLoopTag(msg); + return branch; + }*/ + + /** Picks the token used for loop detection. */ + private String pickLoopTag(Message msg) + { StringBuffer sb=new StringBuffer(); + sb.append(msg.getToHeader().getTag()); + sb.append(msg.getFromHeader().getTag()); + sb.append(msg.getCallIdHeader().getCallId()); + sb.append(msg.getRequestLine().getAddress().toString()); + sb.append(msg.getCSeqHeader().getSequenceNumber()); + MultipleHeader rr=msg.getRoutes(); + if (rr!=null) sb.append(rr.size()); + return (new SimpleDigest(7,sb.toString())).asHex(); + } + + + // ********************************* logs ********************************* + + /** Adds a new string to the default Log */ + private void printLog(String str, int level) + { if (log!=null) log.println("ServerEngine: "+str,level+SipStack.LOG_LEVEL_UA); + } + + /** Adds a Warning message to the default Log */ + private final void printWarning(String str, int level) + { printLog("WARNING: "+str,level); + } + +} \ No newline at end of file diff --git a/src/local/server/ServerProfile.java b/src/local/server/ServerProfile.java new file mode 100644 index 0000000..c6e42f2 --- /dev/null +++ b/src/local/server/ServerProfile.java @@ -0,0 +1,218 @@ +package local.server; + + +import org.zoolu.net.SocketAddress; +import org.zoolu.sip.address.*; +import org.zoolu.sip.provider.*; +import org.zoolu.tools.Configure; +import org.zoolu.tools.Parser; + +import java.io.*; +import java.net.InetAddress; +import java.util.Vector; + + +/** ServerProfile maintains the server configuration. + */ +public class ServerProfile extends Configure +{ + /** The default configuration file */ + private static String config_file="mjsip.cfg"; + + + // ********************* server configurations ******************** + + /** The domain names that the server administers. + *

It lists the domain names for which the Location Service maintains user bindings. + *
Use 'auto-configuration' for automatic configuration of the domain name. */ + public String[] domain_names=null; + /** Whether consider any port as valid local domain port + * (regardless which sip port is used). */ + public boolean domain_port_any=false; + + /** Whether the Server should act as Registrar (i.e. respond to REGISTER requests). */ + public boolean is_registrar=true; + /** Maximum expires time (in seconds). */ + public int expires=3600; + /** Whether the Registrar can register new users (i.e. REGISTER requests from unregistered users). */ + public boolean register_new_users=true; + /** Whether the Server relays requests for (or to) non-local users. */ + public boolean is_open_proxy=true; + /** The type of location service. + * You can specify the location service type (e.g. local, ldap, radius, mysql) + * or the class name (e.g. local.server.LocationServiceImpl). */ + public String location_service="local"; + /** The name of the location DB. */ + public String location_db="users.db"; + /** Whether location DB has to be cleaned at startup. */ + public boolean clean_location_db=false; + + /** Whether the Server authenticates local users. */ + public boolean do_authentication=false; + /** Whether the Proxy authenticates users. */ + public boolean do_proxy_authentication=false; + /** The authentication scheme. + * You can specify the authentication scheme name (e.g. Digest, AKA, etc.) + * or the class name (e.g. local.server.AuthenticationServerImpl). */ + public String authentication_scheme="Digest"; + /** The authentication realm. + * If not defined or equal to 'NONE' (default), the used via address is used instead. */ + public String authentication_realm=null; + /** The type of authentication service. + * You can specify the authentication service type (e.g. local, ldap, radius, mysql) + * or the class name (e.g. local.server.AuthenticationServiceImpl). */ + public String authentication_service="local"; + /** The name of the authentication DB. */ + public String authentication_db="aaa.db"; + + /** Whether maintaining a complete call log. */ + public boolean call_log=false; + /** Whether the server should stay in the signaling path (uses Record-Route/Route) */ + public boolean on_route=false; + /** Whether implementing the RFC3261 Loose Route (or RFC2543 Strict Route) rule */ + public boolean loose_route=true; + /** Whether checking for loops before forwarding a request (Loop Detection). In RFC3261 it is optional. */ + public boolean loop_detection=true; + + /** Array of RoutingRules based on pairs of username or phone prefix and corresponding nexthop address. + * It provides static rules for routing number-based SIP-URL the server is responsible for. + * Use "default" (or "*") as default prefix. + * Example, request URL sip:01234567@zoopera.com received by a server responsible for domain name 'zoopera.com'. + * phone_routing_rules={prefix=0123,nexthop=127.0.0.2:7002} {prefix=*,nexthop=127.0.0.3:7003} */ + public RoutingRule[] phone_routing_rules=null; + + /** Array of RoutingRules based on pairs of destination domain and corresponding nexthop address. + * It provides static rules for routing domain-based SIP-URL the server is NOT responsible for. + * It make the server acting (also) as 'Interrogating' Proxy, i.e. I-CSCF in the 3G networks. + * Example, domain_routing_rules={domain=wonderland.net,nexthop=neverland.net:5060} */ + public RoutingRule[] domain_routing_rules=null; + + + // ************************** costructors ************************* + + /** Costructs a new ServerProfile */ + public ServerProfile(String file) + { // load SipStack first + if (!SipStack.isInit()) SipStack.init(); + // load configuration + loadFile(file); + // post-load manipulation + if (authentication_realm!=null && authentication_realm.equals(Configure.NONE)) authentication_realm=null; + if (domain_names==null) domain_names=new String[0]; + if (phone_routing_rules==null) phone_routing_rules=new RoutingRule[0]; + if (domain_routing_rules==null) domain_routing_rules=new RoutingRule[0]; + } + + + /** Parses a single line of the file */ + protected void parseLine(String line) + { String attribute; + Parser par; + int index=line.indexOf("="); + if (index>0) { attribute=line.substring(0,index).trim(); par=new Parser(line,index+1); } + else { attribute=line; par=new Parser(""); } + + if (attribute.equals("is_registrar")) { is_registrar=(par.getString().toLowerCase().startsWith("y")); return; } + if (attribute.equals("expires")) { expires=par.getInt(); return; } + if (attribute.equals("register_new_users")) { register_new_users=(par.getString().toLowerCase().startsWith("y")); return; } + if (attribute.equals("is_open_proxy")) { is_open_proxy=(par.getString().toLowerCase().startsWith("y")); return; } + if (attribute.equals("location_service")) { location_service=par.getString(); return; } + if (attribute.equals("location_db")) { location_db=par.getString(); return; } + if (attribute.equals("clean_location_db")) { clean_location_db=(par.getString().toLowerCase().startsWith("y")); return; } + + if (attribute.equals("do_authentication")) { do_authentication=(par.getString().toLowerCase().startsWith("y")); return; } + if (attribute.equals("do_proxy_authentication")) { do_proxy_authentication=(par.getString().toLowerCase().startsWith("y")); return; } + if (attribute.equals("authentication_scheme")) { authentication_scheme=par.getString(); return; } + if (attribute.equals("authentication_realm")) { authentication_realm=par.getString(); return; } + if (attribute.equals("authentication_service")) { authentication_service=par.getString(); return; } + if (attribute.equals("authentication_db")) { authentication_db=par.getString(); return; } + + if (attribute.equals("call_log")) { call_log=(par.getString().toLowerCase().startsWith("y")); return; } + if (attribute.equals("on_route")) { on_route=(par.getString().toLowerCase().startsWith("y")); return; } + if (attribute.equals("loose_route")) { loose_route=(par.getString().toLowerCase().startsWith("y")); return; } + if (attribute.equals("loop_detection")) { loop_detection=(par.getString().toLowerCase().startsWith("y")); return; } + + if (attribute.equals("domain_port_any")) { domain_port_any=(par.getString().toLowerCase().startsWith("y")); return; } + + if (attribute.equals("domain_names")) + { char[] delim={' ',','}; + Vector aux=new Vector(); + do + { String domain=par.getWord(delim); + if (domain.equals(SipProvider.AUTO_CONFIGURATION)) + { // auto configuration + String host_addr=null; + String host_name=null; + try + { InetAddress address=java.net.InetAddress.getLocalHost(); + host_addr=address.getHostAddress(); + host_name=address.getHostName(); + } + catch (java.net.UnknownHostException e) + { if (host_addr==null) host_addr="127.0.0.1"; + if (host_name==null) host_name="localhost"; + } + aux.addElement(host_addr); + aux.addElement(host_name); + } + else + { // manual configuration + aux.addElement(domain); + } + } + while (par.hasMore()); + domain_names=new String[aux.size()]; + for (int i=0; i(i+1)) + { file=args[++i]; + continue; + } + if (args[i].equals("--prompt")) + { prompt_exit=true; + continue; + } + if (args[i].equals("-h")) + { System.out.println("usage:\n java StatefulProxy [options] \n"); + System.out.println(" options:"); + System.out.println(" -h this help"); + System.out.println(" -f specifies a configuration file"); + System.out.println(" --prompt prompt for exit"); + System.exit(0); + } + } + + SipStack.init(file); + SipProvider sip_provider=new SipProvider(file); + ServerProfile server_profile=new ServerProfile(file); + + StatefulProxy sproxy=new StatefulProxy(sip_provider,server_profile); + + // promt before exit + if (prompt_exit) + try + { System.out.println("press 'enter' to exit"); + BufferedReader in=new BufferedReader(new InputStreamReader(System.in)); + in.readLine(); + System.exit(0); + } + catch (Exception e) {} + } + +} \ No newline at end of file diff --git a/src/local/server/StatefulProxyState.java b/src/local/server/StatefulProxyState.java new file mode 100644 index 0000000..ea2eeb5 --- /dev/null +++ b/src/local/server/StatefulProxyState.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This source code is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package local.server; + + +import org.zoolu.sip.provider.SipStack; +import org.zoolu.sip.provider.TransactionIdentifier; +import org.zoolu.sip.transaction.*; +import org.zoolu.sip.message.*; +import org.zoolu.tools.LogLevel; + +import java.util.Iterator; +import java.util.HashSet; +import java.util.Hashtable; + + +/** Class StatefulProxyState allows the record and management + * of all TransactionServer-to-TransactionClient mappings in a stateful proxy */ +public class StatefulProxyState +{ + /** Table : t_client -> t_server */ + Hashtable c_server; + /** Table : t_server -> list of t_client (HashSet) */ + Hashtable s_clients; + /** Table : t_server -> resp message */ + Hashtable s_response; + + + /** Creates the StatefulProxyState */ + public StatefulProxyState() + { c_server=new Hashtable(); + s_clients=new Hashtable(); + s_response=new Hashtable(); + } + + /** Adds a new server ts */ + public void addServer(TransactionServer ts) + { //printlog("addServer(ts)",LogLevel.LOW); + if (hasServer(ts)) return; + TransactionIdentifier sid=ts.getTransactionId(); + s_clients.put(sid,new HashSet()); + Message request=new Message(ts.getRequestMessage()); + //printlog("creating a possible server 408 final response",LogLevel.LOW); + Message resp=MessageFactory.createResponse(request,408,SipResponses.reasonOf(408),null); + //printlog("DEBUG: addServer()\r\n"+resp,LogLevel.LOW); + s_response.put(sid,resp); + } + + /** Appends a new client to server ts. + * If server ts is new, adds it. */ + public void addClient(TransactionServer ts, Transaction tc) + { //printlog("addClient(ts,tc)",LogLevel.LOW); + c_server.put(tc.getTransactionId(),ts); + TransactionIdentifier sid=ts.getTransactionId(); + HashSet clients=(HashSet)s_clients.get(sid); + if (clients==null) clients=new HashSet(); + clients.add(tc); + s_clients.put(sid,clients); + Message request=new Message(ts.getRequestMessage()); + //printlog("creating a possible server 408 final response",LogLevel.LOW); + Message resp=MessageFactory.createResponse(request,408,SipResponses.reasonOf(408),null); + //printlog("DEBUG addClient():\r\n"+resp,LogLevel.LOW); + s_response.put(sid,resp); + } + + /** Removes a client. */ + public void removeClient(Transaction tc) + { TransactionIdentifier cid=tc.getTransactionId(); + TransactionServer ts=(TransactionServer)c_server.get(cid); + if (ts==null) return; + c_server.remove(cid); + TransactionIdentifier sid=ts.getTransactionId(); + HashSet clients=(HashSet)s_clients.get(sid); + if (clients==null) return; + Transaction target=null; + Transaction aux; + for (Iterator i=clients.iterator(); i.hasNext(); ) + if ((aux=(Transaction)i.next()).getTransactionId().equals(cid)) target=aux; + if (target!=null) clients.remove(target); + } + + /** Removes all clients bound to server ts. */ + public void clearClients(TransactionServer ts) + { TransactionIdentifier sid=ts.getTransactionId(); + s_clients.remove(sid); + s_clients.put(sid,new HashSet()); + } + + /** Whether there is a server ts. */ + public boolean hasServer(TransactionServer ts) + { TransactionIdentifier sid=ts.getTransactionId(); + return s_clients.containsKey(sid); + } + + /** Removes server ts. */ + public void removeServer(TransactionServer ts) + { TransactionIdentifier sid=ts.getTransactionId(); + s_clients.remove(sid); + s_response.remove(sid); + } + + /** Gets the server bound to client tc */ + public TransactionServer getServer(Transaction tc) + { return (TransactionServer)c_server.get(tc.getTransactionId()); + } + + /** Gets all clients bound to server ts. */ + public HashSet getClients(TransactionServer ts) + { return (HashSet)s_clients.get(ts.getTransactionId()); + } + + /** Sets the final response for server ts. */ + public void setFinalResponse(TransactionServer ts, Message resp) + { TransactionIdentifier sid=ts.getTransactionId(); + s_response.remove(sid); + s_response.put(sid,resp); + } + + /** Gets the final response for server ts. */ + public Message getFinalResponse(TransactionServer ts) + { return (Message)s_response.get(ts.getTransactionId()); + } + + /** Gets the number of active servers. */ + public int numOfServers() + { return s_clients.size(); + } + + /** Gets the number of active clients. */ + public int numOfClients() + { return c_server.size(); + } + +} \ No newline at end of file diff --git a/src/local/ua/CommandLineMA.java b/src/local/ua/CommandLineMA.java new file mode 100644 index 0000000..7cadb4b --- /dev/null +++ b/src/local/ua/CommandLineMA.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This source code is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package local.ua; + + +import org.zoolu.sip.address.*; +import org.zoolu.sip.provider.*; +import org.zoolu.tools.Log; +import org.zoolu.tools.LogLevel; + +import java.io.*; + + +/** Simple command-line short-message UA. + * It allows a user to send and receive short messages, using a command-line interface. + */ +public class CommandLineMA implements RegisterAgentListener, MessageAgentListener +{ + /** Event logger. */ + Log log; + + /** Message Agent */ + MessageAgent ma; + + /** Register Agent */ + RegisterAgent ra; + + /** Remote user. */ + NameAddress remote_user; + + + /** Costructs a new CommandLineMA. */ + public CommandLineMA(SipProvider sip_provider, UserAgentProfile user_profile) + { log=sip_provider.getLog(); + ma=new MessageAgent(sip_provider,user_profile,this); + ma.receive(); + ra=new RegisterAgent(sip_provider,user_profile.from_url,user_profile.contact_url,this); + } + + + /** Gets the remote peer of the last received/sent message. */ + public String getRemoteUser() + { return remote_user.toString(); + } + + + /** Register with the registrar server. */ + public void register(int expire_time) + { ra.register(expire_time); + } + + + /** Unregister with the registrar server */ + public void unregister() + { ra.unregister(); + } + + + /** Unregister all contacts with the registrar server */ + public void unregisterall() + { ra.unregisterall(); + } + + + /** Sends a new message. */ + public void send(String recipient, String subject, String text) + { ma.send(recipient,subject,text); + } + + + // *********************** callback functions ********************* + + /** When a new Message is received. */ + public void onMaReceivedMessage(MessageAgent ma, NameAddress sender, NameAddress recipient, String subject, String content_type, String content) + { remote_user=sender; + printLog("NEW MESSAGE:"); + printLog("From: "+sender); + if (subject!=null) printLog("Subject: "+subject); + printLog("Content: "+content); + } + + /** When a message delivery successes. */ + public void onMaDeliverySuccess(MessageAgent ma, NameAddress recipient, String subject, String result) + { //printLog("Delivery success: "+result,LogLevel.HIGH); + } + + /** When a message delivery fails. */ + public void onMaDeliveryFailure(MessageAgent ma, NameAddress recipient, String subject, String result) + { //printLog("Delivery failure: "+result,LogLevel.HIGH); + } + + /** When a UA has been successfully (un)registered. */ + public void onUaRegistrationSuccess(RegisterAgent ra, NameAddress target, NameAddress contact, String result) + { printLog("Registration success: "+result,LogLevel.HIGH); + } + + /** When a UA failed on (un)registering. */ + public void onUaRegistrationFailure(RegisterAgent ra, NameAddress target, NameAddress contact, String result) + { printLog("Registration failure: "+result,LogLevel.HIGH); + } + + + // ***************************** MAIN ***************************** + + /** The main method. */ + public static void main(String[] args) + { + + String file=null; + String remote_user=null; + boolean opt_regist=false; + boolean opt_unregist=false; + boolean opt_unregist_all=false; + int opt_expires=0; + + for (int i=0; i(i+1)) + { file=args[++i]; + continue; + } + if (args[i].equals("-c")) + { remote_user=args[++i]; + continue; + } + if (args[i].equals("-g") && args.length>(i+1)) // registrate the contact url + { opt_regist=true; + String time=args[++i]; + if (time.charAt(time.length()-1)=='h') opt_expires=Integer.parseInt(time.substring(0,time.length()-1))*3600; + else opt_expires=Integer.parseInt(time); + continue; + } + if (args[i].equals("-u")) // unregistrate the contact url + { opt_unregist=true; + continue; + } + if (args[i].equals("-z")) // unregistrate all contact urls + { opt_unregist_all=true; + continue; + } + // else, do: + if (!args[i].equals("-h")) + System.out.println("unrecognized param '"+args[i]+"'\n"); + + System.out.println("usage:\n java CommandLineMA [options]"); + System.out.println(" options:"); + System.out.println(" -h this help"); + System.out.println(" -f specifies a configuration file"); + System.out.println(" -c the corresponding user"); + System.out.println(" -g

It can use external audio/video tools as media applications. + * Currently only RAT (Robust Audio Tool) and VIC are supported as external applications. + */ +public class CommandLineUA implements UserAgentListener, RegisterAgentListener +{ + + /** Event logger. */ + Log log; + + /** User Agent */ + UserAgent ua; + + /** Register Agent */ + RegisterAgent ra; + + /** UserAgentProfile */ + UserAgentProfile user_profile; + + /** Standard input */ + BufferedReader stdin=null; + + /** Standard output */ + PrintStream stdout=null; + + + /** Costructs a UA with a default media port */ + public CommandLineUA(SipProvider sip_provider, UserAgentProfile user_profile) + { log=sip_provider.getLog(); + this.user_profile=user_profile; + + ua=new UserAgent(sip_provider,user_profile,this); + ra=new RegisterAgent(sip_provider,user_profile.from_url,user_profile.contact_url,user_profile.username,user_profile.realm,user_profile.passwd,this); + + if (!user_profile.no_prompt) stdin=new BufferedReader(new InputStreamReader(System.in)); + if (!user_profile.no_prompt) stdout=System.out; + + run(); + } + + + /** Register with the registrar server. + * @param expire_time expiration time in seconds */ + public void register(int expire_time) + { if (ra.isRegistering()) ra.halt(); + ra.register(expire_time); + } + + + /** Periodically registers the contact address with the registrar server. + * @param expire_time expiration time in seconds + * @param renew_time renew time in seconds + * @param keepalive_time keep-alive packet rate (inter-arrival time) in milliseconds */ + public void loopRegister(int expire_time, int renew_time, long keepalive_time) + { if (ra.isRegistering()) ra.halt(); + ra.loopRegister(expire_time,renew_time,keepalive_time); + } + + + /** Unregister with the registrar server */ + public void unregister() + { if (ra.isRegistering()) ra.halt(); + ra.unregister(); + } + + + /** Unregister all contacts with the registrar server */ + public void unregisterall() + { if (ra.isRegistering()) ra.halt(); + ra.unregisterall(); + } + + + /** Makes a new call */ + public void call(String target_url) + { ua.hangup(); + ua.printLog("UAC: CALLING "+target_url); + if (!ua.user_profile.audio && !ua.user_profile.video) ua.printLog("ONLY SIGNALING, NO MEDIA"); + ua.call(target_url); + } + + + /** Receives incoming calls (auto accept) */ + public void listen() + { ua.printLog("UAS: WAITING FOR INCOMING CALL"); + if (!ua.user_profile.audio && !ua.user_profile.video) ua.printLog("ONLY SIGNALING, NO MEDIA"); + ua.listen(); + printOut("digit the callee's URL to make a call or press 'enter' to exit"); + } + + + /** Starts the UA */ + void run() + { + try + { // Set the re-invite + if (user_profile.re_invite_time>0) + { ua.reInvite(user_profile.contact_url,user_profile.re_invite_time); + } + + // Set the transfer (REFER) + if (user_profile.transfer_to!=null && user_profile.transfer_time>0) + { ua.callTransfer(user_profile.transfer_to,user_profile.transfer_time); + } + + if (user_profile.do_unregister_all) + // ########## unregisters ALL contact URLs + { ua.printLog("UNREGISTER ALL contact URLs"); + unregisterall(); + } + + if (user_profile.do_unregister) + // unregisters the contact URL + { ua.printLog("UNREGISTER the contact URL"); + unregister(); + } + + if (user_profile.do_register) + // ########## registers the contact URL with the registrar server + { ua.printLog("REGISTRATION"); + loopRegister(user_profile.expires,user_profile.expires/2,user_profile.keepalive_time); + } + + if (user_profile.call_to!=null) + { // UAC + call(user_profile.call_to); + printOut("press 'enter' to hangup"); + readLine(); + ua.hangup(); + exit(); + } + else + { // UAS + if (user_profile.accept_time>=0) ua.printLog("UAS: AUTO ACCEPT MODE"); + listen(); + while (stdin!=null) + { String line=readLine(); + if (ua.statusIs(UserAgent.UA_INCOMING_CALL)) + { if (line.toLowerCase().startsWith("n")) + { ua.hangup(); + } + else + { ua.accept(); + } + } + else + if (ua.statusIs(UserAgent.UA_IDLE)) + { if (line!=null && line.length()>0) + { call(line); + } + else + { exit(); + } + } + else + if (ua.statusIs(UserAgent.UA_ONCALL)) + { ua.hangup(); + } + } + } + } + catch (Exception e) { e.printStackTrace(); System.exit(0); } + } + + + /** Exits */ + public void exit() + { try { Thread.sleep(1000); } catch (Exception e) {} + System.exit(0); + } + + + // ******************* UserAgent callback functions ****************** + + /** When a new call is incoming */ + public void onUaCallIncoming(UserAgent ua, NameAddress callee, NameAddress caller) + { if (ua.user_profile.redirect_to!=null) // redirect the call + { ua.redirect(ua.user_profile.redirect_to); + printOut("call redirected to "+ua.user_profile.redirect_to); + } + else + if (ua.user_profile.accept_time>=0) // automatically accept the call + { //ua.accept(); + //printOut("press 'enter' to hangup"); + ua.automaticAccept(ua.user_profile.accept_time); + } + else + { printOut("incoming call from "+caller.toString()); + printOut("accept? [yes/no]"); + } + } + + /** When an ougoing call is remotly ringing */ + public void onUaCallRinging(UserAgent ua) + { + } + + /** When an ougoing call has been accepted */ + public void onUaCallAccepted(UserAgent ua) + { + } + + /** When a call has been trasferred */ + public void onUaCallTrasferred(UserAgent ua) + { + } + + /** When an incoming call has been cancelled */ + public void onUaCallCancelled(UserAgent ua) + { listen(); + } + + /** When an ougoing call has been refused or timeout */ + public void onUaCallFailed(UserAgent ua) + { if (ua.user_profile.call_to!=null) exit(); + else listen(); + } + + /** When a call has been locally or remotely closed */ + public void onUaCallClosed(UserAgent ua) + { if (ua.user_profile.call_to!=null) exit(); + else listen(); + } + + + // **************** RegisterAgent callback functions ***************** + + /** When a UA has been successfully (un)registered. */ + public void onUaRegistrationSuccess(RegisterAgent ra, NameAddress target, NameAddress contact, String result) + { ua.printLog("Registration success: "+result,LogLevel.HIGH); + } + + /** When a UA failed on (un)registering. */ + public void onUaRegistrationFailure(RegisterAgent ra, NameAddress target, NameAddress contact, String result) + { ua.printLog("Registration failure: "+result,LogLevel.HIGH); + } + + + // ***************************** MAIN ***************************** + + + /** The main method. */ + public static void main(String[] args) + { + String file=null; + boolean opt_regist=false; + boolean opt_unregist=false; + boolean opt_unregist_all=false; + int opt_expires=-1; + long opt_keepalive_time=-1; + boolean opt_no_offer=false; + String opt_call_to=null; + int opt_accept_time=-1; + int opt_hangup_time=-1; + String opt_redirect_to=null; + String opt_transfer_to=null; + int opt_transfer_time=-1; + int opt_re_invite_time=-1; + boolean opt_audio=false; + boolean opt_video=false; + int opt_media_port=0; + boolean opt_recv_only=false; + boolean opt_send_only=false; + boolean opt_send_tone=false; + String opt_send_file=null; + String opt_recv_file=null; + boolean opt_no_prompt=false; + + String opt_from_url=null; + String opt_contact_url=null; + String opt_username=null; + String opt_realm=null; + String opt_passwd=null; + + int opt_debug_level=-1; + String opt_log_path=null; + String opt_outbound_proxy=null; + String opt_via_addr=SipProvider.AUTO_CONFIGURATION; + int opt_host_port=SipStack.default_port; + + try + { + for (int i=0; i(i+1)) + { file=args[++i]; + continue; + } + if (args[i].equals("-g") && args.length>(i+1)) // registrate the contact url + { opt_regist=true; + String time=args[++i]; + if (time.charAt(time.length()-1)=='h') opt_expires=Integer.parseInt(time.substring(0,time.length()-1))*3600; + else opt_expires=Integer.parseInt(time); + continue; + } + if (args[i].equals("-u")) // unregistrate the contact url + { opt_unregist=true; + continue; + } + if (args[i].equals("-z")) // unregistrate all contact urls + { opt_unregist_all=true; + continue; + } + if (args[i].equals("-n")) // no offer in the invite + { opt_no_offer=true; + continue; + } + if (args[i].equals("-c") && args.length>(i+1)) // make a call with a remote user (url) + { opt_call_to=args[++i]; + continue; + } + if (args[i].equals("-y") && args.length>(i+1)) // set automatic accept time + { opt_accept_time=Integer.parseInt(args[++i]); + continue; + } + if (args[i].equals("-t") && args.length>(i+1)) // set the call duration + { opt_hangup_time=Integer.parseInt(args[++i]); + continue; + } + if (args[i].equals("-i") && args.length>(i+1)) // set the re-invite time + { opt_re_invite_time=Integer.parseInt(args[++i]); + continue; + } + if (args[i].equals("-r") && args.length>(i+1)) // redirect the call to a new url + { opt_accept_time=0; + opt_redirect_to=args[++i]; + continue; + } + if (args[i].equals("-q") && args.length>(i+1)) // transfers the call to a new user (REFER) + { opt_transfer_to=args[++i]; + opt_transfer_time=Integer.parseInt(args[++i]); + continue; + } + if (args[i].equals("-a")) // use audio + { opt_audio=true; + continue; + } + if (args[i].equals("-v")) // use video + { opt_video=true; + continue; + } + if (args[i].equals("-m") && args.length>(i+1)) // set the local media port + { opt_media_port=Integer.parseInt(args[++i]); + continue; + } + if (args[i].equals("-o") && args.length>(i+1)) // outbound proxy + { opt_outbound_proxy=args[++i]; + continue; + } + if (args[i].equals("--via-addr") && args.length>(i+1)) // via addr + { opt_via_addr=args[++i]; + continue; + } + if (args[i].equals("-p") && args.length>(i+1)) // set the local sip port + { opt_host_port=Integer.parseInt(args[++i]); + continue; + } + if (args[i].equals("--keep-alive") && args.length>(i+1)) // keep-alive + { opt_keepalive_time=Integer.parseInt(args[++i]); + continue; + } + if (args[i].equals("--from-url") && args.length>(i+1)) // user's AOR + { opt_from_url=args[++i]; + continue; + } + if (args[i].equals("--contact-url") && args.length>(i+1)) // user's contact_url + { opt_contact_url=args[++i]; + continue; + } + if (args[i].equals("--username") && args.length>(i+1)) // username + { opt_username=args[++i]; + continue; + } + if (args[i].equals("--realm") && args.length>(i+1)) // realm + { opt_realm=args[++i]; + continue; + } + if (args[i].equals("--passwd") && args.length>(i+1)) // passwd + { opt_passwd=args[++i]; + continue; + } + if (args[i].equals("--recv-only")) // receive only mode + { opt_recv_only=true; + continue; + } + if (args[i].equals("--send-only")) // send only mode + { opt_send_only=true; + continue; + } + if (args[i].equals("--send-tone")) // send only mode + { opt_send_only=true; + opt_send_tone=true; + continue; + } + if (args[i].equals("--send-file") && args.length>(i+1)) // send audio file + { opt_send_file=args[++i]; + continue; + } + if (args[i].equals("--recv-file") && args.length>(i+1)) // receive audio file + { opt_recv_file=args[++i]; + continue; + } + if (args[i].equals("--debug-level") && args.length>(i+1)) // debug level + { opt_debug_level=Integer.parseInt(args[++i]); + continue; + } + if (args[i].equals("--log-path") && args.length>(i+1)) // log path + { opt_log_path=args[++i]; + continue; + } + if (args[i].equals("--no-prompt")) // do not prompt + { opt_no_prompt=true; + continue; + } + + // else, do: + if (!args[i].equals("-h")) + System.out.println("unrecognized param '"+args[i]+"'\n"); + + System.out.println("usage:\n java CommandLineUA [options]"); + System.out.println(" options:"); + System.out.println(" -h this help"); + System.out.println(" -f specifies a configuration file"); + System.out.println(" -t auto hangup time (0 means manual hangup)"); + System.out.println(" -g

attributes is a Vector of the sdp media attributes */ + public RATLauncher(String rat_comm, int local_port, String remote_addr, int remote_port, Log logger) + { log=logger; + command=rat_comm; + localport=local_port; + remoteport=remote_port; + remoteaddr=remote_addr; + } + + /** Starts media application */ + public boolean startMedia() + { // udp flow adaptation for RAT application + if (localport!=remoteport) + { printLog("UDP local relay: src_port="+localport+", dest_port="+remoteport); + printLog("UDP local relay: src_port="+(localport+1)+", dest_port="+(remoteport+1)); + new UdpRelay(localport,"127.0.0.1",remoteport,null); + new UdpRelay(localport+1,"127.0.0.1",remoteport+1,null); + } + else + { printLog("local_port==remote_port --> no UDP relay is needed"); + } + + //debug... + printLog("launching RAT-Audio..."); + + String cmds[] = {"","",""}; + cmds[0] = command; + cmds[1] = remoteaddr+"/"+remoteport; + + // try to start the RAT + try + { media_process=Runtime.getRuntime().exec(cmds); + return true; + } + catch (Exception e) + { e.printStackTrace(); + return false; + } + } + + /** Stops media application */ + public boolean stopMedia() + { if (media_process!=null) media_process.destroy(); + return true; + } + + + // ****************************** Logs ***************************** + + /** Adds a new string to the default Log */ + private void printLog(String str) + { if (log!=null) log.println("RATLauncher: "+str,SipStack.LOG_LEVEL_UA+LogLevel.HIGH); + //if (LOG_LEVEL<=LogLevel.HIGH) System.out.println("RATLauncher: "+str); + System.out.println("RATLauncher: "+str); + } + +} \ No newline at end of file diff --git a/src/local/ua/RegisterAgent.java b/src/local/ua/RegisterAgent.java new file mode 100644 index 0000000..64d51a0 --- /dev/null +++ b/src/local/ua/RegisterAgent.java @@ -0,0 +1,328 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This source code is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package local.ua; + + +import local.net.KeepAliveSip; +import org.zoolu.net.SocketAddress; +import org.zoolu.sip.address.*; +import org.zoolu.sip.provider.SipStack; +import org.zoolu.sip.provider.SipProvider; +import org.zoolu.sip.header.*; +import org.zoolu.sip.message.*; +import org.zoolu.sip.transaction.TransactionClient; +import org.zoolu.sip.transaction.TransactionClientListener; +import org.zoolu.sip.authentication.DigestAuthentication; +import org.zoolu.tools.Log; +import org.zoolu.tools.LogLevel; + +import java.util.Vector; + + +/** Register User Agent. + * It registers (one time or periodically) a contact address with a registrar server. + */ +public class RegisterAgent implements Runnable, TransactionClientListener +{ + /** Max number of registration attempts. */ + static final int MAX_ATTEMPTS=3; + + /** RegisterAgent listener */ + RegisterAgentListener listener; + + /** SipProvider */ + SipProvider sip_provider; + + /** User's URI with the fully qualified domain name of the registrar server. */ + NameAddress target; + + /** User name. */ + String username; + + /** User name. */ + String realm; + + /** User's passwd. */ + String passwd; + + /** Nonce for the next authentication. */ + String next_nonce; + + /** Qop for the next authentication. */ + String qop; + + /** User's contact address. */ + NameAddress contact; + + /** Expiration time. */ + int expire_time; + + /** Renew time. */ + int renew_time; + + /** Whether keep on registering. */ + boolean loop; + + /** Whether the thread is running. */ + boolean is_running; + + /** Event logger. */ + Log log; + + /** Number of registration attempts. */ + int attempts; + + /** KeepAliveSip daemon. */ + KeepAliveSip keep_alive; + + + /** Creates a new RegisterAgent. */ + public RegisterAgent(SipProvider sip_provider, String target_url, String contact_url, RegisterAgentListener listener) + { init(sip_provider,target_url,contact_url,listener); + } + + + /** Creates a new RegisterAgent with authentication credentials (i.e. username, realm, and passwd). */ + public RegisterAgent(SipProvider sip_provider, String target_url, String contact_url, String username, String realm, String passwd, RegisterAgentListener listener) + { init(sip_provider,target_url,contact_url,listener); + // authentication + this.username=username; + this.realm=realm; + this.passwd=passwd; + } + + /** Inits the RegisterAgent. */ + private void init(SipProvider sip_provider, String target_url, String contact_url, RegisterAgentListener listener) + { this.listener=listener; + this.sip_provider=sip_provider; + this.log=sip_provider.getLog(); + this.target=new NameAddress(target_url); + this.contact=new NameAddress(contact_url); + this.expire_time=SipStack.default_expires; + this.renew_time=0; + this.is_running=false; + this.keep_alive=null; + // authentication + this.username=null; + this.realm=null; + this.passwd=null; + this.next_nonce=null; + this.qop=null; + this.attempts=0; + } + + + /** Whether it is periodically registering. */ + public boolean isRegistering() + { return is_running; + } + + + /** Registers with the registrar server. */ + public void register() + { register(expire_time); + } + + + /** Registers with the registrar server for expire_time seconds. */ + public void register(int expire_time) + { attempts=0; + if (expire_time>0) this.expire_time=expire_time; + Message req=MessageFactory.createRegisterRequest(sip_provider,target,target,contact); + req.setExpiresHeader(new ExpiresHeader(String.valueOf(expire_time))); + if (next_nonce!=null) + { AuthorizationHeader ah=new AuthorizationHeader("Digest"); + SipURL target_url=target.getAddress(); + ah.addUsernameParam(username); + ah.addRealmParam(realm); + ah.addNonceParam(next_nonce); + ah.addUriParam(req.getRequestLine().getAddress().toString()); + ah.addQopParam(qop); + String response=(new DigestAuthentication(SipMethods.REGISTER,ah,null,passwd)).getResponse(); + ah.addResponseParam(response); + req.setAuthorizationHeader(ah); + } + if (expire_time>0) printLog("Registering contact "+contact+" (it expires in "+expire_time+" secs)",LogLevel.HIGH); + else printLog("Unregistering contact "+contact,LogLevel.HIGH); + TransactionClient t=new TransactionClient(sip_provider,req,this); + t.request(); + } + + + /** Unregister with the registrar server */ + public void unregister() + { register(0); + } + + + /** Unregister all contacts with the registrar server */ + public void unregisterall() + { attempts=0; + NameAddress user=new NameAddress(target); + Message req=MessageFactory.createRegisterRequest(sip_provider,target,target,null); + //ContactHeader contact_star=new ContactHeader(); // contact is * + //req.setContactHeader(contact_star); + req.setExpiresHeader(new ExpiresHeader(String.valueOf(0))); + printLog("Unregistering all contacts",LogLevel.HIGH); + TransactionClient t=new TransactionClient(sip_provider,req,this); + t.request(); + } + + + /** Periodically registers with the registrar server. + * @param expire_time expiration time in seconds + * @param renew_time renew time in seconds */ + public void loopRegister(int expire_time, int renew_time) + { this.expire_time=expire_time; + this.renew_time=renew_time; + loop=true; + if (!is_running) (new Thread(this)).start(); + } + + + /** Periodically registers with the registrar server. + * @param expire_time expiration time in seconds + * @param renew_time renew time in seconds + * @param keepalive_time keep-alive packet rate (inter-arrival time) in milliseconds */ + public void loopRegister(int expire_time, int renew_time, long keepalive_time) + { loopRegister(expire_time,renew_time); + // keep-alive + if (keepalive_time>0) + { SipURL target_url=target.getAddress(); + String target_host=target_url.getHost(); + int targe_port=target_url.getPort(); + if (targe_port<0) targe_port=SipStack.default_port; + new KeepAliveSip(sip_provider,new SocketAddress(target_host,targe_port),null,keepalive_time); + } + } + + + /** Halts the periodic registration. */ + public void halt() + { if (is_running) loop=false; + if (keep_alive!=null) keep_alive.halt(); + } + + + // ***************************** run() ***************************** + + /** Run method */ + public void run() + { + is_running=true; + try + { while (loop) + { register(); + Thread.sleep(renew_time*1000); + } + } + catch (Exception e) { printException(e,LogLevel.HIGH); } + is_running=false; + } + + + // **************** Transaction callback functions ***************** + + /** Callback function called when client sends back a failure response. */ + + /** Callback function called when client sends back a provisional response. */ + public void onTransProvisionalResponse(TransactionClient transaction, Message resp) + { // do nothing.. + } + + /** Callback function called when client sends back a success response. */ + public void onTransSuccessResponse(TransactionClient transaction, Message resp) + { if (transaction.getTransactionMethod().equals(SipMethods.REGISTER)) + { if (resp.hasAuthenticationInfoHeader()) + { next_nonce=resp.getAuthenticationInfoHeader().getNextnonceParam(); + } + StatusLine status=resp.getStatusLine(); + String result=status.getCode()+" "+status.getReason(); + + // update the renew_time + int expires=0; + if (resp.hasExpiresHeader()) + { expires=resp.getExpiresHeader().getDeltaSeconds(); + } + else + if (resp.hasContactHeader()) + { Vector contacts=resp.getContacts().getHeaders(); + for (int i=0; i0 && (expires==0 || exp_i0 && expiresfile */ + public StringList(String file) + { list=new Vector(); + file_name=file; + load(); + } + + + /** Loads list */ + public void load() + { loadFile(file_name); + } + + + /** Saves list */ + public void save() + { saveFile(file_name); + } + + + /** Gets elements */ + public Vector getElements() + { return list; + } + + + /** Gets the element at positon i */ + public String elementAt(int i) + { return (String)list.elementAt(i); + } + + + /** Inserts element at positon i */ + public void insertElementAt(String elem, int i) + { list.insertElementAt(elem,i); + } + + + /** Removes element at positon i */ + public void removeElementAt(int i) + { list.removeElementAt(i); + } + + + /** Adds element */ + public void addElement(String elem) + { list.addElement(elem); + } + + + /** Whether the element is present */ + public boolean contains(String elem) + { return (indexOf(elem)>=0); + } + + + /** Index of the element (if present) */ + public int indexOf(String elem) + { return list.indexOf(elem); + } + + + /** Whether an element that containg subelem*/ + /*public boolean containsSubElement(String subelem) + { return indexOfSubElement(subelem)>=0; + }*/ + + + /** Whether an element that containg subelem*/ + /*public int indexOfSubElement(String subelem) + { for (int i=0; i=0 && index + * It can use external audio/video tools as media applications. + * Currently only RAT (Robust Audio Tool) and VIC are supported as external applications. + */ +public class UserAgent extends CallListenerAdapter +{ + /** Event logger. */ + Log log; + + /** UserAgentProfile */ + protected UserAgentProfile user_profile; + + /** SipProvider */ + protected SipProvider sip_provider; + + /** Call */ + //Call call; + protected ExtendedCall call; + + /** Call transfer */ + protected ExtendedCall call_transfer; + + /** Audio application */ + protected MediaLauncher audio_app=null; + /** Video application */ + protected MediaLauncher video_app=null; + + /** Local sdp */ + protected String local_session=null; + + /** UserAgent listener */ + protected UserAgentListener listener=null; + + /** Media file path */ + final String MEDIA_PATH="media/local/ua/"; + + /** On wav file */ + final String CLIP_ON=MEDIA_PATH+"on.wav"; + /** Off wav file */ + final String CLIP_OFF=MEDIA_PATH+"off.wav"; + /** Ring wav file */ + final String CLIP_RING=MEDIA_PATH+"ring.wav"; + + /** Ring sound */ + AudioClipPlayer clip_ring; + /** On sound */ + AudioClipPlayer clip_on; + /** Off sound */ + AudioClipPlayer clip_off; + + + // *********************** Startup Configuration *********************** + + /** UA_IDLE=0 */ + static final String UA_IDLE="IDLE"; + /** UA_INCOMING_CALL=1 */ + static final String UA_INCOMING_CALL="INCOMING_CALL"; + /** UA_OUTGOING_CALL=2 */ + static final String UA_OUTGOING_CALL="OUTGOING_CALL"; + /** UA_ONCALL=3 */ + static final String UA_ONCALL="ONCALL"; + + /** Call state + *

UA_IDLE=0,
UA_INCOMING_CALL=1,
UA_OUTGOING_CALL=2,
UA_ONCALL=3 */ + String call_state=UA_IDLE; + + + + // *************************** Basic methods *************************** + + /** Changes the call state */ + protected void changeStatus(String state) + { call_state=state; + //printLog("state: "+call_state,LogLevel.MEDIUM); + } + + /** Checks the call state */ + protected boolean statusIs(String state) + { return call_state.equals(state); + } + + /** Gets the call state */ + protected String getStatus() + { return call_state; + } + + /** Sets the automatic answer time (default is -1 that means no auto accept mode) */ + public void setAcceptTime(int accept_time) + { user_profile.accept_time=accept_time; + } + + /** Sets the automatic hangup time (default is 0, that corresponds to manual hangup mode) */ + public void setHangupTime(int time) + { user_profile.hangup_time=time; + } + + /** Sets the redirection url (default is null, that is no redircetion) */ + public void setRedirection(String url) + { user_profile.redirect_to=url; + } + + /** Sets the no offer mode for the invite (default is false) */ + public void setNoOfferMode(boolean nooffer) + { user_profile.no_offer=nooffer; + } + + /** Enables audio */ + public void setAudio(boolean enable) + { user_profile.audio=enable; + } + + /** Enables video */ + public void setVideo(boolean enable) + { user_profile.video=enable; + } + + /** Sets the receive only mode */ + public void setReceiveOnlyMode(boolean r_only) + { user_profile.recv_only=r_only; + } + + /** Sets the send only mode */ + public void setSendOnlyMode(boolean s_only) + { user_profile.send_only=s_only; + } + + /** Sets the send tone mode */ + public void setSendToneMode(boolean s_tone) + { user_profile.send_tone=s_tone; + } + + /** Sets the send file */ + public void setSendFile(String file_name) + { user_profile.send_file=file_name; + } + + /** Sets the recv file */ + public void setRecvFile(String file_name) + { user_profile.recv_file=file_name; + } + + /** Gets the local SDP */ + public String getSessionDescriptor() + { return local_session; + } + + /** Sets the local SDP */ + public void setSessionDescriptor(String sdp) + { local_session=sdp; + } + + /** Inits the local SDP (no media spec) */ + public void initSessionDescriptor() + { SessionDescriptor sdp=new SessionDescriptor(user_profile.from_url,sip_provider.getViaAddress()); + local_session=sdp.toString(); + } + + /** Adds a media to the SDP */ + public void addMediaDescriptor(String media, int port, int avp, String codec, int rate) + { if (local_session==null) initSessionDescriptor(); + SessionDescriptor sdp=new SessionDescriptor(local_session); + String attr_param=String.valueOf(avp); + if (codec!=null) attr_param+=" "+codec+"/"+rate; + sdp.addMedia(new MediaField(media,port,0,"RTP/AVP",String.valueOf(avp)),new AttributeField("rtpmap",attr_param)); + local_session=sdp.toString(); + } + + + // *************************** Public Methods ************************** + + /** Costructs a UA with a default media port */ + public UserAgent(SipProvider sip_provider, UserAgentProfile user_profile, UserAgentListener listener) + { this.sip_provider=sip_provider; + log=sip_provider.getLog(); + this.listener=listener; + this.user_profile=user_profile; + // if no contact_url and/or from_url has been set, create it now + user_profile.initContactAddress(sip_provider); + + // load sounds + + // ################# patch to make audio working with javax.sound.. ################# + // currently AudioSender must be started before any AudioClipPlayer is initialized, + // since there is a problem with the definition of the audio format + if (!user_profile.use_rat && !user_profile.use_jmf) + { if (user_profile.audio && !user_profile.recv_only && user_profile.send_file==null && !user_profile.send_tone) local.media.AudioInput.initAudioLine(); + if (user_profile.audio && !user_profile.send_only && user_profile.recv_file==null) local.media.AudioOutput.initAudioLine(); + } + // ################# patch to make rat working.. ################# + // in case of rat, do not load and play audio clips + if (!user_profile.use_rat) + { try + { String jar_file=user_profile.ua_jar; + clip_on=new AudioClipPlayer(Archive.getAudioInputStream(Archive.getJarURL(jar_file,CLIP_ON)),null); + clip_off=new AudioClipPlayer(Archive.getAudioInputStream(Archive.getJarURL(jar_file,CLIP_OFF)),null); + clip_ring=new AudioClipPlayer(Archive.getAudioInputStream(Archive.getJarURL(jar_file,CLIP_RING)),null); + } + catch (Exception e) + { printException(e,LogLevel.HIGH); + } + //clip_ring=new AudioClipPlayer(CLIP_RING,null); + //clip_on=new AudioClipPlayer(CLIP_ON,null); + //clip_off=new AudioClipPlayer(CLIP_OFF,null); + } + + // set local sdp + initSessionDescriptor(); + if (user_profile.audio || !user_profile.video) addMediaDescriptor("audio",user_profile.audio_port,user_profile.audio_avp,user_profile.audio_codec,user_profile.audio_sample_rate); + if (user_profile.video) addMediaDescriptor("video",user_profile.video_port,user_profile.video_avp,null,0); + } + + + /** Creates a new session descriptor */ + /*private void newSession(int media_port) + { SessionDescriptor local_sdp=new SessionDescriptor(user_profile.from_url,sip_provider.getAddress()); + int audio_port=media_port; + int video_port=media_port+2; + //PATCH [040902] if (audio || !video) local_sdp.addMedia(new MediaField("audio",audio_port,0,"RTP/AVP","0"),new AttributeField("rtpmap","0 PCMU/8000")); + //PATCH [040902] if (video || !(audio || video)) local_sdp.addMedia(new MediaField("video",video_port,0,"RTP/AVP","7"),new AttributeField("rtpmap","17")); + local_sdp.addMedia(new MediaField("audio",audio_port,0,"RTP/AVP","0"),new AttributeField("rtpmap","0 PCMU/8000")); + local_session=local_sdp.toString(); + }*/ + + + /** Makes a new call (acting as UAC). */ + public void call(String target_url) + { changeStatus(UA_OUTGOING_CALL); + call=new ExtendedCall(sip_provider,user_profile.from_url,user_profile.contact_url,user_profile.username,user_profile.realm,user_profile.passwd,this); + // in case of incomplete url (e.g. only 'user' is present), try to complete it + target_url=sip_provider.completeNameAddress(target_url).toString(); + if (user_profile.no_offer) call.call(target_url); + else call.call(target_url,local_session); + } + + + /** Waits for an incoming call (acting as UAS). */ + public void listen() + { changeStatus(UA_IDLE); + call=new ExtendedCall(sip_provider,user_profile.from_url,user_profile.contact_url,user_profile.username,user_profile.realm,user_profile.passwd,this); + call.listen(); + } + + + /** Closes an ongoing, incoming, or pending call */ + public void hangup() + { if (clip_ring!=null) clip_ring.stop(); + closeMediaApplication(); + if (call!=null) call.hangup(); + changeStatus(UA_IDLE); + } + + + /** Closes an ongoing, incoming, or pending call */ + public void accept() + { if (clip_ring!=null) clip_ring.stop(); + if (call!=null) call.accept(local_session); + } + + + /** Redirects an incoming call */ + public void redirect(String redirection) + { if (clip_ring!=null) clip_ring.stop(); + if (call!=null) call.redirect(redirection); + } + + + /** Launches the Media Application (currently, the RAT audio tool) */ + protected void launchMediaApplication() + { + // exit if the Media Application is already running + if (audio_app!=null || video_app!=null) + { printLog("DEBUG: media application is already running",LogLevel.HIGH); + return; + } + SessionDescriptor local_sdp=new SessionDescriptor(call.getLocalSessionDescriptor()); + String local_media_address=(new Parser(local_sdp.getConnection().toString())).skipString().skipString().getString(); + int local_audio_port=0; + int local_video_port=0; + // parse local sdp + for (Enumeration e=local_sdp.getMediaDescriptors().elements(); e.hasMoreElements(); ) + { MediaField media=((MediaDescriptor)e.nextElement()).getMedia(); + if (media.getMedia().equals("audio")) + local_audio_port=media.getPort(); + if (media.getMedia().equals("video")) + local_video_port=media.getPort(); + } + // parse remote sdp + SessionDescriptor remote_sdp=new SessionDescriptor(call.getRemoteSessionDescriptor()); + String remote_media_address=(new Parser(remote_sdp.getConnection().toString())).skipString().skipString().getString(); + int remote_audio_port=0; + int remote_video_port=0; + for (Enumeration e=remote_sdp.getMediaDescriptors().elements(); e.hasMoreElements(); ) + { MediaField media=((MediaDescriptor)e.nextElement()).getMedia(); + if (media.getMedia().equals("audio")) + remote_audio_port=media.getPort(); + if (media.getMedia().equals("video")) + remote_video_port=media.getPort(); + } + + // select the media direction (send_only, recv_ony, fullduplex) + int dir=0; + if (user_profile.recv_only) dir=-1; + else + if (user_profile.send_only) dir=1; + + if (user_profile.audio && local_audio_port!=0 && remote_audio_port!=0) + { // create an audio_app and start it + if (user_profile.use_rat) + { audio_app=new RATLauncher(user_profile.bin_rat,local_audio_port,remote_media_address,remote_audio_port,log); + } + else + if (user_profile.use_jmf) + { // try to use JMF audio app + try + { Class myclass=Class.forName("local.ua.JMFAudioLauncher"); + Class[] parameter_types={ java.lang.Integer.TYPE, Class.forName("java.lang.String"), java.lang.Integer.TYPE, java.lang.Integer.TYPE, Class.forName("org.zoolu.tools.Log") }; + Object[] parameters={ new Integer(local_audio_port), remote_media_address, new Integer(remote_audio_port), new Integer(dir), log }; + java.lang.reflect.Constructor constructor=myclass.getConstructor(parameter_types); + audio_app=(MediaLauncher)constructor.newInstance(parameters); + } + catch (Exception e) + { printException(e,LogLevel.HIGH); + printLog("Error trying to create the JMFAudioLauncher",LogLevel.HIGH); + } + } + // else + if (audio_app==null) + { // for testing.. + String audio_in=null; + if (user_profile.send_tone) audio_in=JAudioLauncher.TONE; + else if (user_profile.send_file!=null) audio_in=user_profile.send_file; + String audio_out=null; + if (user_profile.recv_file!=null) audio_out=user_profile.recv_file; + //audio_app=new JAudioLauncher(local_audio_port,remote_media_address,remote_audio_port,dir,log); + audio_app=new JAudioLauncher(local_audio_port,remote_media_address,remote_audio_port,dir,audio_in,audio_out,user_profile.audio_sample_rate,user_profile.audio_sample_size,user_profile.audio_frame_size,log); + } + audio_app.startMedia(); + } + if (user_profile.video && local_video_port!=0 && remote_video_port!=0) + { // create a video_app and start it + if (user_profile.use_vic) + { video_app=new VICLauncher(user_profile.bin_vic,local_video_port,remote_media_address,remote_video_port,log); + } + else + if (user_profile.use_jmf) + { // try to use JMF video app + try + { Class myclass=Class.forName("local.ua.JMFVideoLauncher"); + Class[] parameter_types={ java.lang.Integer.TYPE, Class.forName("java.lang.String"), java.lang.Integer.TYPE, java.lang.Integer.TYPE, Class.forName("org.zoolu.tools.Log") }; + Object[] parameters={ new Integer(local_video_port), remote_media_address, new Integer(remote_video_port), new Integer(dir), log }; + java.lang.reflect.Constructor constructor=myclass.getConstructor(parameter_types); + video_app=(MediaLauncher)constructor.newInstance(parameters); + } + catch (Exception e) + { printException(e,LogLevel.HIGH); + printLog("Error trying to create the JMFVideoLauncher",LogLevel.HIGH); + } + } + // else + if (video_app==null) + { printLog("No external video application nor JMF has been provided: Video not started",LogLevel.HIGH); + return; + } + video_app.startMedia(); + } + } + + + /** Close the Media Application */ + protected void closeMediaApplication() + { if (audio_app!=null) + { audio_app.stopMedia(); + audio_app=null; + } + if (video_app!=null) + { video_app.stopMedia(); + video_app=null; + } + } + + + // ********************** Call callback functions ********************** + + /** Callback function called when arriving a new INVITE method (incoming call) */ + public void onCallIncoming(Call call, NameAddress callee, NameAddress caller, String sdp, Message invite) + { printLog("onCallIncoming()",LogLevel.LOW); + if (call!=this.call) { printLog("NOT the current call",LogLevel.LOW); return; } + printLog("INCOMING",LogLevel.HIGH); + //System.out.println("DEBUG: inside UserAgent.onCallIncoming(): sdp=\n"+sdp); + changeStatus(UA_INCOMING_CALL); + call.ring(); + if (sdp!=null) + { // Create the new SDP + SessionDescriptor remote_sdp=new SessionDescriptor(sdp); + SessionDescriptor local_sdp=new SessionDescriptor(local_session); + SessionDescriptor new_sdp=new SessionDescriptor(remote_sdp.getOrigin(),remote_sdp.getSessionName(),local_sdp.getConnection(),local_sdp.getTime()); + new_sdp.addMediaDescriptors(local_sdp.getMediaDescriptors()); + new_sdp=SdpTools.sdpMediaProduct(new_sdp,remote_sdp.getMediaDescriptors()); + new_sdp=SdpTools.sdpAttirbuteSelection(new_sdp,"rtpmap"); + local_session=new_sdp.toString(); + } + // play "ring" sound + if (clip_ring!=null) clip_ring.loop(); + if (listener!=null) listener.onUaCallIncoming(this,callee,caller); + } + + + /** Callback function called when arriving a new Re-INVITE method (re-inviting/call modify) */ + public void onCallModifying(Call call, String sdp, Message invite) + { printLog("onCallModifying()",LogLevel.LOW); + if (call!=this.call) { printLog("NOT the current call",LogLevel.LOW); return; } + printLog("RE-INVITE/MODIFY",LogLevel.HIGH); + // to be implemented. + // currently it simply accepts the session changes (see method onCallModifying() in CallListenerAdapter) + super.onCallModifying(call,sdp,invite); + } + + + /** Callback function that may be overloaded (extended). Called when arriving a 180 Ringing */ + public void onCallRinging(Call call, Message resp) + { printLog("onCallRinging()",LogLevel.LOW); + if (call!=this.call && call!=call_transfer) { printLog("NOT the current call",LogLevel.LOW); return; } + printLog("RINGING",LogLevel.HIGH); + // play "on" sound + if (clip_on!=null) clip_on.replay(); + if (listener!=null) listener.onUaCallRinging(this); + } + + + /** Callback function called when arriving a 2xx (call accepted) */ + public void onCallAccepted(Call call, String sdp, Message resp) + { printLog("onCallAccepted()",LogLevel.LOW); + if (call!=this.call && call!=call_transfer) { printLog("NOT the current call",LogLevel.LOW); return; } + printLog("ACCEPTED/CALL",LogLevel.HIGH); + changeStatus(UA_ONCALL); + if (user_profile.no_offer) + { // Create the new SDP + SessionDescriptor remote_sdp=new SessionDescriptor(sdp); + SessionDescriptor local_sdp=new SessionDescriptor(local_session); + SessionDescriptor new_sdp=new SessionDescriptor(remote_sdp.getOrigin(),remote_sdp.getSessionName(),local_sdp.getConnection(),local_sdp.getTime()); + new_sdp.addMediaDescriptors(local_sdp.getMediaDescriptors()); + new_sdp=SdpTools.sdpMediaProduct(new_sdp,remote_sdp.getMediaDescriptors()); + new_sdp=SdpTools.sdpAttirbuteSelection(new_sdp,"rtpmap"); + + + // update the local SDP + local_session=new_sdp.toString(); + // answer with the local sdp + call.ackWithAnswer(local_session); + } + // play "on" sound + if (clip_on!=null) clip_on.replay(); + if (listener!=null) listener.onUaCallAccepted(this); + + launchMediaApplication(); + + if (call==call_transfer) + { StatusLine status_line=resp.getStatusLine(); + int code=status_line.getCode(); + String reason=status_line.getReason(); + this.call.notify(code,reason); + } + } + + + /** Callback function called when arriving an ACK method (call confirmed) */ + public void onCallConfirmed(Call call, String sdp, Message ack) + { printLog("onCallConfirmed()",LogLevel.LOW); + if (call!=this.call) { printLog("NOT the current call",LogLevel.LOW); return; } + printLog("CONFIRMED/CALL",LogLevel.HIGH); + changeStatus(UA_ONCALL); + // play "on" sound + if (clip_on!=null) clip_on.replay(); + launchMediaApplication(); + if (user_profile.hangup_time>0) this.automaticHangup(user_profile.hangup_time); + } + + + /** Callback function called when arriving a 2xx (re-invite/modify accepted) */ + public void onCallReInviteAccepted(Call call, String sdp, Message resp) + { printLog("onCallReInviteAccepted()",LogLevel.LOW); + if (call!=this.call) { printLog("NOT the current call",LogLevel.LOW); return; } + printLog("RE-INVITE-ACCEPTED/CALL",LogLevel.HIGH); + } + + + /** Callback function called when arriving a 4xx (re-invite/modify failure) */ + public void onCallReInviteRefused(Call call, String reason, Message resp) + { printLog("onCallReInviteRefused()",LogLevel.LOW); + if (call!=this.call) { printLog("NOT the current call",LogLevel.LOW); return; } + printLog("RE-INVITE-REFUSED ("+reason+")/CALL",LogLevel.HIGH); + if (listener!=null) listener.onUaCallFailed(this); + } + + + /** Callback function called when arriving a 4xx (call failure) */ + public void onCallRefused(Call call, String reason, Message resp) + { printLog("onCallRefused()",LogLevel.LOW); + if (call!=this.call) { printLog("NOT the current call",LogLevel.LOW); return; } + printLog("REFUSED ("+reason+")",LogLevel.HIGH); + changeStatus(UA_IDLE); + if (call==call_transfer) + { StatusLine status_line=resp.getStatusLine(); + int code=status_line.getCode(); + //String reason=status_line.getReason(); + this.call.notify(code,reason); + call_transfer=null; + } + // play "off" sound + if (clip_off!=null) clip_off.replay(); + if (listener!=null) listener.onUaCallFailed(this); + } + + + /** Callback function called when arriving a 3xx (call redirection) */ + public void onCallRedirection(Call call, String reason, Vector contact_list, Message resp) + { printLog("onCallRedirection()",LogLevel.LOW); + if (call!=this.call) { printLog("NOT the current call",LogLevel.LOW); return; } + printLog("REDIRECTION ("+reason+")",LogLevel.HIGH); + call.call(((String)contact_list.elementAt(0))); + } + + + /** Callback function that may be overloaded (extended). Called when arriving a CANCEL request */ + public void onCallCanceling(Call call, Message cancel) + { printLog("onCallCanceling()",LogLevel.LOW); + if (call!=this.call) { printLog("NOT the current call",LogLevel.LOW); return; } + printLog("CANCEL",LogLevel.HIGH); + changeStatus(UA_IDLE); + // stop ringing + if (clip_ring!=null) clip_ring.stop(); + // play "off" sound + if (clip_off!=null) clip_off.replay(); + if (listener!=null) listener.onUaCallCancelled(this); + } + + + /** Callback function called when arriving a BYE request */ + public void onCallClosing(Call call, Message bye) + { printLog("onCallClosing()",LogLevel.LOW); + if (call!=this.call && call!=call_transfer) { printLog("NOT the current call",LogLevel.LOW); return; } + if (call!=call_transfer && call_transfer!=null) + { printLog("CLOSE PREVIOUS CALL",LogLevel.HIGH); + this.call=call_transfer; + call_transfer=null; + return; + } + // else + printLog("CLOSE",LogLevel.HIGH); + closeMediaApplication(); + // play "off" sound + if (clip_off!=null) clip_off.replay(); + if (listener!=null) listener.onUaCallClosed(this); + changeStatus(UA_IDLE); + } + + + /** Callback function called when arriving a response after a BYE request (call closed) */ + public void onCallClosed(Call call, Message resp) + { printLog("onCallClosed()",LogLevel.LOW); + if (call!=this.call) { printLog("NOT the current call",LogLevel.LOW); return; } + printLog("CLOSE/OK",LogLevel.HIGH); + if (listener!=null) listener.onUaCallClosed(this); + changeStatus(UA_IDLE); + } + + /** Callback function called when the invite expires */ + public void onCallTimeout(Call call) + { printLog("onCallTimeout()",LogLevel.LOW); + if (call!=this.call) { printLog("NOT the current call",LogLevel.LOW); return; } + printLog("NOT FOUND/TIMEOUT",LogLevel.HIGH); + changeStatus(UA_IDLE); + if (call==call_transfer) + { int code=408; + String reason="Request Timeout"; + this.call.notify(code,reason); + call_transfer=null; + } + // play "off" sound + if (clip_off!=null) clip_off.replay(); + if (listener!=null) listener.onUaCallFailed(this); + } + + + + // ****************** ExtendedCall callback functions ****************** + + /** Callback function called when arriving a new REFER method (transfer request) */ + public void onCallTransfer(ExtendedCall call, NameAddress refer_to, NameAddress refered_by, Message refer) + { printLog("onCallTransfer()",LogLevel.LOW); + if (call!=this.call) { printLog("NOT the current call",LogLevel.LOW); return; } + printLog("Transfer to "+refer_to.toString(),LogLevel.HIGH); + call.acceptTransfer(); + call_transfer=new ExtendedCall(sip_provider,user_profile.from_url,user_profile.contact_url,this); + call_transfer.call(refer_to.toString(),local_session); + } + + /** Callback function called when a call transfer is accepted. */ + public void onCallTransferAccepted(ExtendedCall call, Message resp) + { printLog("onCallTransferAccepted()",LogLevel.LOW); + if (call!=this.call) { printLog("NOT the current call",LogLevel.LOW); return; } + printLog("Transfer accepted",LogLevel.HIGH); + } + + /** Callback function called when a call transfer is refused. */ + public void onCallTransferRefused(ExtendedCall call, String reason, Message resp) + { printLog("onCallTransferRefused()",LogLevel.LOW); + if (call!=this.call) { printLog("NOT the current call",LogLevel.LOW); return; } + printLog("Transfer refused",LogLevel.HIGH); + } + + /** Callback function called when a call transfer is successfully completed */ + public void onCallTransferSuccess(ExtendedCall call, Message notify) + { printLog("onCallTransferSuccess()",LogLevel.LOW); + if (call!=this.call) { printLog("NOT the current call",LogLevel.LOW); return; } + printLog("Transfer successed",LogLevel.HIGH); + call.hangup(); + if (listener!=null) listener.onUaCallTrasferred(this); + } + + /** Callback function called when a call transfer is NOT sucessfully completed */ + public void onCallTransferFailure(ExtendedCall call, String reason, Message notify) + { printLog("onCallTransferFailure()",LogLevel.LOW); + if (call!=this.call) { printLog("NOT the current call",LogLevel.LOW); return; } + printLog("Transfer failed",LogLevel.HIGH); + } + + + // ************************* Schedule events *********************** + + /** Schedules a re-inviting event after delay_time secs. */ + void reInvite(final String contact_url, final int delay_time) + { SessionDescriptor sdp=new SessionDescriptor(local_session); + final SessionDescriptor new_sdp=new SessionDescriptor(sdp.getOrigin(),sdp.getSessionName(),new ConnectionField("IP4","0.0.0.0"),new TimeField()); + new_sdp.addMediaDescriptors(sdp.getMediaDescriptors()); + (new Thread() { public void run() { runReInvite(contact_url,new_sdp.toString(),delay_time); } }).start(); + } + + /** Re-invite. */ + private void runReInvite(String contact, String body, int delay_time) + { try + { if (delay_time>0) Thread.sleep(delay_time*1000); + printLog("RE-INVITING/MODIFING"); + if (call!=null && call.isOnCall()) + { printLog("REFER/TRANSFER"); + call.modify(contact,body); + } + } + catch (Exception e) { e.printStackTrace(); } + } + + + /** Schedules a call-transfer event after delay_time secs. */ + void callTransfer(final String transfer_to, final int delay_time) + { (new Thread() { public void run() { runCallTransfer(transfer_to,delay_time); } }).start(); + } + + /** Call-transfer. */ + private void runCallTransfer(String transfer_to, int delay_time) + { try + { if (delay_time>0) Thread.sleep(delay_time*1000); + if (call!=null && call.isOnCall()) + { printLog("REFER/TRANSFER"); + call.transfer(transfer_to); + } + } + catch (Exception e) { e.printStackTrace(); } + } + + + /** Schedules an automatic answer event after delay_time secs. */ + void automaticAccept(final int delay_time) + { (new Thread() { public void run() { runAutomaticAccept(delay_time); } }).start(); + } + + /** Automatic answer. */ + private void runAutomaticAccept(int delay_time) + { try + { if (delay_time>0) Thread.sleep(delay_time*1000); + if (call!=null) + { printLog("AUTOMATIC-ANSWER"); + accept(); + } + } + catch (Exception e) { e.printStackTrace(); } + } + + + /** Schedules an automatic hangup event after delay_time secs. */ + void automaticHangup(final int delay_time) + { (new Thread() { public void run() { runAutomaticHangup(delay_time); } }).start(); + } + + /** Automatic hangup. */ + private void runAutomaticHangup(int delay_time) + { try + { if (delay_time>0) Thread.sleep(delay_time*1000); + if (call!=null && call.isOnCall()) + { printLog("AUTOMATIC-HANGUP"); + hangup(); + listen(); + } + } + catch (Exception e) { e.printStackTrace(); } + } + + + // ****************************** Logs ***************************** + + /** Adds a new string to the default Log */ + void printLog(String str) + { printLog(str,LogLevel.HIGH); + } + + /** Adds a new string to the default Log */ + void printLog(String str, int level) + { if (log!=null) log.println("UA: "+str,level+SipStack.LOG_LEVEL_UA); + if ((user_profile==null || !user_profile.no_prompt) && level<=LogLevel.HIGH) System.out.println("UA: "+str); + } + + /** Adds the Exception message to the default Log */ + void printException(Exception e,int level) + { if (log!=null) log.printException(e,level+SipStack.LOG_LEVEL_UA); + } + +} diff --git a/src/local/ua/UserAgentListener.java b/src/local/ua/UserAgentListener.java new file mode 100644 index 0000000..3e58c3a --- /dev/null +++ b/src/local/ua/UserAgentListener.java @@ -0,0 +1,31 @@ +package local.ua; + + +import org.zoolu.sip.address.NameAddress; + + +/** Listener of UserAgent */ +public interface UserAgentListener +{ + /** When a new call is incoming */ + public void onUaCallIncoming(UserAgent ua, NameAddress callee, NameAddress caller); + + /** When an incoming call is cancelled */ + public void onUaCallCancelled(UserAgent ua); + + /** When an ougoing call is remotly ringing */ + public void onUaCallRinging(UserAgent ua); + + /** When an ougoing call has been accepted */ + public void onUaCallAccepted(UserAgent ua); + + /** When a call has been trasferred */ + public void onUaCallTrasferred(UserAgent ua); + + /** When an ougoing call has been refused or timeout */ + public void onUaCallFailed(UserAgent ua); + + /** When a call has been locally or remotly closed */ + public void onUaCallClosed(UserAgent ua); + +} \ No newline at end of file diff --git a/src/local/ua/UserAgentProfile.java b/src/local/ua/UserAgentProfile.java new file mode 100644 index 0000000..77249da --- /dev/null +++ b/src/local/ua/UserAgentProfile.java @@ -0,0 +1,245 @@ +package local.ua; + + +import org.zoolu.sip.address.*; +import org.zoolu.sip.provider.SipStack; +import org.zoolu.sip.provider.SipProvider; +import org.zoolu.tools.Configure; +import org.zoolu.tools.Parser; + + +/** UserProfile maintains the user configuration + */ +public class UserAgentProfile extends Configure +{ + /** The default configuration file */ + private static String config_file="mjsip.cfg"; + + + // ********************** user configurations ********************* + + /** User's AOR (Address Of Record), used also as From URL. + *

+ * The AOR is the SIP address used to register with the user's registrar server + * (if requested). + *
The address of the registrar is taken from the hostport field of the AOR, + * i.e. the value(s) host[:port] after the '@' character. + *

+ * If not defined (default), it equals the contact_url attribute. */ + public String from_url=null; + /** Contact URL. + * If not defined (default), it is formed by sip:local_user@host_address:host_port */ + public String contact_url=null; + /** User's name (used to build the contact_url if not explitely defined) */ + public String username=null; + /** User's realm. */ + public String realm=null; + /** User's passwd. */ + public String passwd=null; + /** Path for the 'ua.jar' lib, used to retrive various UA media (gif, wav, etc.) + * By default, it is used the "lib/ua.jar" folder */ + public static String ua_jar="lib/ua.jar"; + /** Path for the 'contacts.lst' file where save and load the VisualUA contacts + * By default, it is used the "config/contacts.lst" folder */ + public static String contacts_file="contacts.lst"; + + /** Whether registering with the registrar server */ + public boolean do_register=false; + /** Whether unregistering the contact address */ + public boolean do_unregister=false; + /** Whether unregistering all contacts beafore registering the contact address */ + public boolean do_unregister_all=false; + /** Expires time (in seconds). */ + public int expires=3600; + + /** Rate of keep-alive packets sent toward the registrar server (in milliseconds). + * Set keepalive_time=0 to disable the sending of keep-alive datagrams. */ + public long keepalive_time=0; + + + /** Automatic call a remote user secified by the 'call_to' value. + * Use value 'NONE' for manual calls (or let it undefined). */ + public String call_to=null; + /** Automatic answer time in seconds; time<0 corresponds to manual answer mode. */ + public int accept_time=-1; + /** Automatic hangup time (call duartion) in seconds; time<=0 corresponds to manual hangup mode. */ + public int hangup_time=-1; + /** Automatic call transfer time in seconds; time<0 corresponds to no auto transfer mode. */ + public int transfer_time=-1; + /** Automatic re-inviting time in seconds; time<0 corresponds to no auto re-invite mode. */ + public int re_invite_time=-1; + + /** Redirect incoming call to the secified url. + * Use value 'NONE' for not redirecting incoming calls (or let it undefined). */ + public String redirect_to=null; + + /** Transfer calls to the secified url. + * Use value 'NONE' for not transferring calls (or let it undefined). */ + public String transfer_to=null; + + /** No offer in the invite */ + public boolean no_offer=false; + /** Do not use prompt */ + public boolean no_prompt=false; + + /** Whether using audio */ + public boolean audio=false; + /** Whether using video */ + public boolean video=false; + + /** Whether playing in receive only mode */ + public boolean recv_only=false; + /** Whether playing in send only mode */ + public boolean send_only=false; + /** Whether playing a test tone in send only mode */ + public boolean send_tone=false; + /** Audio file to be played */ + public String send_file=null; + /** Audio file to be recorded */ + public String recv_file=null; + + /** Audio port */ + public int audio_port=21000; + /** Audio avp */ + public int audio_avp=0; + /** Audio codec */ + public String audio_codec="PCMU"; + /** Audio sample rate */ + public int audio_sample_rate=8000; + /** Audio sample size */ + public int audio_sample_size=1; + /** Audio frame size */ + public int audio_frame_size=160; + + /** Video port */ + public int video_port=21070; + /** Video avp */ + public int video_avp=17; + + /** Whether using JMF for audio/video streaming */ + public boolean use_jmf=false; + /** Whether using RAT (Robust Audio Tool) as audio sender/receiver */ + public boolean use_rat=false; + /** Whether using VIC (Video Conferencing Tool) as video sender/receiver */ + public boolean use_vic=false; + /** RAT command-line executable */ + public String bin_rat="rat"; + /** VIC command-line executable */ + public String bin_vic="vic"; + + // ************************** Costructors ************************* + + /** Costructs a void UserProfile */ + public UserAgentProfile() + { init(); + } + + /** Costructs a new UserAgentProfile */ + public UserAgentProfile(String file) + { // load configuration + loadFile(file); + // post-load manipulation + init(); + } + + /** Inits the UserAgentProfile */ + private void init() + { if (realm==null && contact_url!=null) realm=new NameAddress(contact_url).getAddress().getHost(); + if (username==null) username=(contact_url!=null)? new NameAddress(contact_url).getAddress().getUserName() : "user"; + if (call_to!=null && call_to.equalsIgnoreCase(Configure.NONE)) call_to=null; + if (redirect_to!=null && redirect_to.equalsIgnoreCase(Configure.NONE)) redirect_to=null; + if (transfer_to!=null && transfer_to.equalsIgnoreCase(Configure.NONE)) transfer_to=null; + if (send_file!=null && send_file.equalsIgnoreCase(Configure.NONE)) send_file=null; + if (recv_file!=null && recv_file.equalsIgnoreCase(Configure.NONE)) recv_file=null; + } + + + // ************************ Public methods ************************ + + /** Sets contact_url and from_url with transport information. + *

+ * This method actually sets contact_url and from_url only if + * they haven't still been explicitly initilized. + */ + public void initContactAddress(SipProvider sip_provider) + { // contact_url + if (contact_url==null) + { contact_url="sip:"+username+"@"+sip_provider.getViaAddress(); + if (sip_provider.getPort()!=SipStack.default_port) contact_url+=":"+sip_provider.getPort(); + if (!sip_provider.getDefaultTransport().equals(SipProvider.PROTO_UDP)) contact_url+=";transport="+sip_provider.getDefaultTransport(); + } + // from_url + if (from_url==null) from_url=contact_url; + } + + + // *********************** Protected methods ********************** + + /** Parses a single line (loaded from the config file) */ + protected void parseLine(String line) + { String attribute; + Parser par; + int index=line.indexOf("="); + if (index>0) { attribute=line.substring(0,index).trim(); par=new Parser(line,index+1); } + else { attribute=line; par=new Parser(""); } + + if (attribute.equals("from_url")) { from_url=par.getRemainingString().trim(); return; } + if (attribute.equals("contact_url")) { contact_url=par.getRemainingString().trim(); return; } + if (attribute.equals("username")) { username=par.getString(); return; } + if (attribute.equals("realm")) { realm=par.getRemainingString().trim(); return; } + if (attribute.equals("passwd")) { passwd=par.getRemainingString().trim(); return; } + if (attribute.equals("ua_jar")) { ua_jar=par.getStringUnquoted(); return; } + if (attribute.equals("contacts_file")) { contacts_file=par.getStringUnquoted(); return; } + + if (attribute.equals("do_register")) { do_register=(par.getString().toLowerCase().startsWith("y")); return; } + if (attribute.equals("do_unregister")) { do_unregister=(par.getString().toLowerCase().startsWith("y")); return; } + if (attribute.equals("do_unregister_all")) { do_unregister_all=(par.getString().toLowerCase().startsWith("y")); return; } + if (attribute.equals("expires")) { expires=par.getInt(); return; } + if (attribute.equals("keepalive_time")) { keepalive_time=par.getInt(); return; } + + if (attribute.equals("call_to")) { call_to=par.getRemainingString().trim(); return; } + if (attribute.equals("accept_time")) { accept_time=par.getInt(); return; } + if (attribute.equals("hangup_time")) { hangup_time=par.getInt(); return; } + if (attribute.equals("transfer_time")) { transfer_time=par.getInt(); return; } + if (attribute.equals("re_invite_time")) { re_invite_time=par.getInt(); return; } + if (attribute.equals("redirect_to")) { redirect_to=par.getRemainingString().trim(); return; } + if (attribute.equals("transfer_to")) { transfer_to=par.getRemainingString().trim(); return; } + if (attribute.equals("no_offer")) { no_offer=(par.getString().toLowerCase().startsWith("y")); return; } + if (attribute.equals("no_prompt")) { no_prompt=(par.getString().toLowerCase().startsWith("y")); return; } + + if (attribute.equals("audio")) { audio=(par.getString().toLowerCase().startsWith("y")); return; } + if (attribute.equals("video")) { video=(par.getString().toLowerCase().startsWith("y")); return; } + if (attribute.equals("recv_only")) { recv_only=(par.getString().toLowerCase().startsWith("y")); return; } + if (attribute.equals("send_only")) { send_only=(par.getString().toLowerCase().startsWith("y")); return; } + if (attribute.equals("send_tone")) { send_tone=(par.getString().toLowerCase().startsWith("y")); return; } + if (attribute.equals("send_file")) { send_file=par.getRemainingString().trim(); return; } + if (attribute.equals("recv_file")) { recv_file=par.getRemainingString().trim(); return; } + + if (attribute.equals("audio_port")) { audio_port=par.getInt(); return; } + if (attribute.equals("audio_avp")) { audio_avp=par.getInt(); return; } + if (attribute.equals("audio_codec")) { audio_codec=par.getString(); return; } + if (attribute.equals("audio_sample_rate")) { audio_sample_rate=par.getInt(); return; } + if (attribute.equals("audio_sample_size")) { audio_sample_size=par.getInt(); return; } + if (attribute.equals("audio_frame_size")) { audio_frame_size=par.getInt(); return; } + if (attribute.equals("video_port")) { video_port=par.getInt(); return; } + if (attribute.equals("video_avp")) { video_avp=par.getInt(); return; } + + if (attribute.equals("use_jmf")) { use_jmf=(par.getString().toLowerCase().startsWith("y")); return; } + if (attribute.equals("use_rat")) { use_rat=(par.getString().toLowerCase().startsWith("y")); return; } + if (attribute.equals("bin_rat")) { bin_rat=par.getStringUnquoted(); return; } + if (attribute.equals("use_vic")) { use_vic=(par.getString().toLowerCase().startsWith("y")); return; } + if (attribute.equals("bin_vic")) { bin_vic=par.getStringUnquoted(); return; } + + // for backward compatibily + if (attribute.equals("contact_user")) { username=par.getString(); return; } + if (attribute.equals("auto_accept")) { accept_time=((par.getString().toLowerCase().startsWith("y")))? 0 : -1; return; } + } + + + /** Converts the entire object into lines (to be saved into the config file) */ + protected String toLines() + { // currently not implemented.. + return contact_url; + } + +} diff --git a/src/local/ua/VICLauncher.java b/src/local/ua/VICLauncher.java new file mode 100644 index 0000000..34e08e2 --- /dev/null +++ b/src/local/ua/VICLauncher.java @@ -0,0 +1,89 @@ +package local.ua; + + +import local.net.UdpRelay; +import org.zoolu.sip.provider.SipStack; +import org.zoolu.tools.Log; +import org.zoolu.tools.LogLevel; +//import tools.Parser; + +//import java.util.Iterator; +//import java.util.Vector; +//import java.io.*; + + +/** VIC launcher */ +public class VICLauncher implements MediaLauncher +{ + /** Event logger. */ + Log log=null; + + /** Runtime media process (VIC application) */ + Process media_process=null; + + int localport; + int remoteport; + String remoteaddr; + + /** Media application command */ + String command; + + /** Costructs the VIC launcher + *

attributes is a Vector of the sdp media attributes */ + public VICLauncher(String vic_comm, int local_port, String remote_addr, int remote_port, Log logger) + { log=logger; + command=vic_comm; + localport=local_port; + remoteport=remote_port; + remoteaddr=remote_addr; + } + + /** Starts media application */ + public boolean startMedia() + { // udp flow adaptation for VIC application + if (localport!=remoteport) + { printLog("UDP local relay: src_port="+localport+", dest_port="+remoteport); + printLog("UDP local relay: src_port="+(localport+1)+", dest_port="+(remoteport+1)); + new UdpRelay(localport,"127.0.0.1",remoteport,null); + new UdpRelay(localport+1,"127.0.0.1",remoteport+1,null); + } + else + { printLog("local_port==remote_port --> no UDP relay is needed"); + } + + //debug... + printLog("launching VIC-Audio..."); + + String cmds[] = {"","",""}; + cmds[0] = command; + cmds[1] = remoteaddr+"/"+remoteport; + + // try to start the VIC + try + { media_process=Runtime.getRuntime().exec(cmds); + return true; + } + catch (Exception e) + { e.printStackTrace(); + return false; + } + } + + /** Stops media application */ + public boolean stopMedia() + { if (media_process!=null) media_process.destroy(); + return true; + } + + + + // ****************************** Logs ***************************** + + /** Adds a new string to the default Log */ + private void printLog(String str) + { if (log!=null) log.println("VICLauncher: "+str,SipStack.LOG_LEVEL_UA+LogLevel.HIGH); + //if (LOG_LEVEL<=LogLevel.HIGH) System.out.println("VICLauncher: "+str); + System.out.println("VICLauncher: "+str); + } + +} \ No newline at end of file diff --git a/src/org/zoolu/net/IpAddress.java b/src/org/zoolu/net/IpAddress.java new file mode 100644 index 0000000..cf87602 --- /dev/null +++ b/src/org/zoolu/net/IpAddress.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.net; + + + +import java.net.InetAddress; + + + +/** IpAddress is an IP address. + */ +public class IpAddress +{ + + /** The host address/name */ + String address; + + /** The InetAddress */ + InetAddress inet_address; + + + // ********************* Protected ********************* + + /** Creates an IpAddress */ + IpAddress(InetAddress iaddress) + { init(null,iaddress); + } + + /** Inits the IpAddress */ + private void init(String address, InetAddress iaddress) + { this.address=address; + this.inet_address=iaddress; + } + + /** Gets the InetAddress */ + InetAddress getInetAddress() + { if (inet_address==null) try { inet_address=InetAddress.getByName(address); } catch (java.net.UnknownHostException e) {} + return inet_address; + } + + + // ********************** Public *********************** + + /** Creates an IpAddress */ + public IpAddress(String address) + { init(address,null); + } + + /** Creates an IpAddress */ + public IpAddress(IpAddress ipaddr) + { init(ipaddr.address,ipaddr.inet_address); + } + + /** Gets the host address */ + /*public String getAddress() + { if (address==null) address=inet_address.getHostAddress(); + return address; + }*/ + + /** Makes a copy */ + public Object clone() + { return new IpAddress(this); + } + + /** Wthether it is equal to Object obj */ + public boolean equals(Object obj) + { try + { IpAddress ipaddr=(IpAddress)obj; + if (!toString().equals(ipaddr.toString())) return false; + return true; + } + catch (Exception e) { return false; } + } + + /** Gets a String representation of the Object */ + public String toString() + { if (address==null && inet_address!=null) address=inet_address.getHostAddress(); + return address; + } + + + // *********************** Static *********************** + + /** Gets the IpAddress for a given fully-qualified host name. */ + public static IpAddress getByName(String host_addr) throws java.net.UnknownHostException + { InetAddress iaddr=InetAddress.getByName(host_addr); + return new IpAddress(iaddr); + } + + + /** Detects the default IP address of this host. */ + public static IpAddress getLocalHostAddress() + { try + { return new IpAddress(java.net.InetAddress.getLocalHost()); + } + catch (java.net.UnknownHostException e) + { return new IpAddress("127.0.0.1"); + } + } + +} diff --git a/src/org/zoolu/net/SocketAddress.java b/src/org/zoolu/net/SocketAddress.java new file mode 100644 index 0000000..9794b46 --- /dev/null +++ b/src/org/zoolu/net/SocketAddress.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.net; + + + + +/** A SocketAddress is a pair { address, port }. + */ +public class SocketAddress +{ + /** The InetAddress */ + IpAddress ipaddr; + + /** The port */ + int port; + + + /** Creates a SocketAddress. */ + public SocketAddress(IpAddress ipaddr, int port) + { init(ipaddr,port); + } + + /** Creates a SocketAddress. */ + public SocketAddress(String addr, int port) + { init(new IpAddress(addr),port); + } + + /** Creates a SocketAddress. */ + public SocketAddress(String soaddr) + { String addr=null; + int port=-1; + int colon=soaddr.indexOf(':'); + if (colon<0) addr=soaddr; + else + { addr=soaddr.substring(0,colon); + try { port=Integer.parseInt(soaddr.substring(colon+1)); } catch (Exception e) {} + } + init(new IpAddress(addr),port); + } + + /** Creates a SocketAddress. */ + public SocketAddress(SocketAddress soaddr) + { init(soaddr.ipaddr,soaddr.port); + } + + /** Inits the SocketAddress. */ + private void init(IpAddress ipaddr, int port) + { this.ipaddr=ipaddr; + this.port=port; + } + + /** Gets the host address. */ + public IpAddress getAddress() + { return ipaddr; + } + + /** Gets the port. */ + public int getPort() + { return port; + } + + /** Makes a copy. */ + public Object clone() + { return new SocketAddress(this); + } + + /** Wthether it is equal to Object obj. */ + public boolean equals(Object obj) + { try + { SocketAddress saddr=(SocketAddress)obj; + if (port!=saddr.port) return false; + if (!ipaddr.equals(saddr.ipaddr)) return false; + return true; + } + catch (Exception e) { return false; } + } + + /** Gets a String representation of the Object. */ + public String toString() + { return (ipaddr.toString()+":"+port); + } + +} diff --git a/src/org/zoolu/net/TcpConnection.java b/src/org/zoolu/net/TcpConnection.java new file mode 100644 index 0000000..522b047 --- /dev/null +++ b/src/org/zoolu/net/TcpConnection.java @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.net; + + +//import java.net.InetAddress; +import java.io.*; + + +/** TcpConnection provides a TCP connection oriented transport service. + */ +public class TcpConnection extends Thread +{ + /** The reading buffer size */ + static final int BUFFER_SIZE=65535; + + /** Default value for the maximum time that the tcp connection can remain active after been halted (in milliseconds) */ + public static final int DEFAULT_SOCKET_TIMEOUT=2000; // 2sec + + /** The TCP socket */ + TcpSocket socket; + + /** Maximum time that the connection can remain active after been halted (in milliseconds) */ + int socket_timeout; + + /** Maximum time that the connection remains active without receiving data (in milliseconds) */ + long alive_time; + + /** The InputStream */ + InputStream istream; + + /** The OutputStream */ + OutputStream ostream; + + /** InputStream/OutputStream error */ + Exception error; + + /** Whether it has been halted */ + boolean stop; + + /** Whether it is running */ + boolean is_running; + + /** TcpConnection listener */ + TcpConnectionListener listener; + + + /** Costructs a new TcpConnection */ + public TcpConnection(TcpSocket socket, TcpConnectionListener listener) + { init(socket,0,listener); + start(); + } + + + /** Costructs a new TcpConnection */ + public TcpConnection(TcpSocket socket, long alive_time, TcpConnectionListener listener) + { init(socket,alive_time,listener); + start(); + } + + + /** Inits the TcpConnection */ + private void init(TcpSocket socket, long alive_time, TcpConnectionListener listener) + { this.listener=listener; + this.socket=socket; + this.socket_timeout=DEFAULT_SOCKET_TIMEOUT; + this.alive_time=alive_time; + this.stop=false; + this.is_running=true; + + this.istream=null; + this.ostream=null; + this.error=null; + try + { istream=new BufferedInputStream(socket.getInputStream()); + ostream=new BufferedOutputStream(socket.getOutputStream()); + } + catch (Exception e) + { error=e; + } + } + + + /** Whether the service is running */ + public boolean isRunning() + { return is_running; + } + + + /** Gets the TcpSocket */ + public TcpSocket getSocket() + { return socket; + } + + + /** Gets the remote IP address */ + public IpAddress getRemoteAddress() + { return socket.getAddress(); + } + + + /** Gets the remote port */ + public int getRemotePort() + { return socket.getPort(); + } + + + /** Stops running */ + public void halt() + { stop=true; + } + + + /** Sends data */ + public void send(byte[] buff, int offset, int len) throws IOException + { if (!stop && ostream!=null) + { ostream.write(buff,offset,len); + ostream.flush(); + } + } + + + /** Sends data */ + public void send(byte[] buff) throws IOException + { send(buff,0,buff.length); + } + + + /** Runs the tcp receiver */ + public void run() + { + byte[] buff=new byte[BUFFER_SIZE]; + long expire=0; + if (alive_time>0) expire=System.currentTimeMillis()+alive_time; + try + { if (error!=null) throw error; + socket.setSoTimeout(socket_timeout); + // loop + while(!stop) + { int len=0; + if (istream!=null) + { try + { len=istream.read(buff); + } + catch (InterruptedIOException ie) + { if (alive_time>0 && System.currentTimeMillis()>expire) halt(); + continue; + } + } + if (len<0) + { //error=new Exception("TCP connection closed"); + stop=true; + } + else + if (len>0) + { if (listener!=null) listener.onReceivedData(this,buff,len); + if (alive_time>0) expire=System.currentTimeMillis()+alive_time; + } + } + } + catch (Exception e) + { error=e; + stop=true; + } + is_running=false; + if (istream!=null) try { istream.close(); } catch (Exception e) {} + if (ostream!=null) try { ostream.close(); } catch (Exception e) {} + if (listener!=null) listener.onConnectionTerminated(this,error); + listener=null; + } + + + /** Gets a String representation of the Object */ + public String toString() + { return "tcp:"+socket.getLocalAddress()+":"+socket.getLocalPort()+"<->"+socket.getAddress()+":"+socket.getPort(); + } + +} diff --git a/src/org/zoolu/net/TcpConnectionListener.java b/src/org/zoolu/net/TcpConnectionListener.java new file mode 100644 index 0000000..f817684 --- /dev/null +++ b/src/org/zoolu/net/TcpConnectionListener.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.net; + + + +/** Listener for TcpConnection events. + */ +public interface TcpConnectionListener +{ + /** When new data is received through the TcpConnection. */ + public void onReceivedData(TcpConnection tcp_conn, byte[] data, int len); + + /** When TcpConnection terminates. */ + public void onConnectionTerminated(TcpConnection tcp_conn, Exception error); +} diff --git a/src/org/zoolu/net/TcpServer.java b/src/org/zoolu/net/TcpServer.java new file mode 100644 index 0000000..395d773 --- /dev/null +++ b/src/org/zoolu/net/TcpServer.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.net; + + +import java.net.ServerSocket; +import java.io.InterruptedIOException; + + + +/** TcpServer implements a TCP server wainting for incoming connection. + */ +public class TcpServer extends Thread +{ + /** Default value for the maximum time that the tcp server can remain active after been halted (in milliseconds) */ + public static final int DEFAULT_SOCKET_TIMEOUT=5000; // 5sec + + /** Default ServerSocket backlog value */ + static int socket_backlog=50; + + /** The protocol type */ + //protected static final String PROTO="tcp"; + + /** The TCP server socket */ + ServerSocket server_socket; + + /** Maximum time that the connection can remain active after been halted (in milliseconds) */ + int socket_timeout; + + /** Maximum time that the server remains active without incoming connections (in milliseconds) */ + long alive_time; + + /** Whether it has been halted */ + boolean stop; + + /** Whether it is running */ + boolean is_running; + + /** TcpServer listener */ + TcpServerListener listener; + + + /** Costructs a new TcpServer */ + public TcpServer(int port, TcpServerListener listener) throws java.io.IOException + { init(port,null,0,listener); + start(); + } + + + /** Costructs a new TcpServer */ + public TcpServer(int port, IpAddress bind_ipaddr, TcpServerListener listener) throws java.io.IOException + { init(port,bind_ipaddr,0,listener); + start(); + } + + + /** Costructs a new TcpServer */ + public TcpServer(int port, IpAddress bind_ipaddr, long alive_time, TcpServerListener listener) throws java.io.IOException + { init(port,bind_ipaddr,alive_time,listener); + start(); + } + + + /** Inits the TcpServer */ + private void init(int port, IpAddress bind_ipaddr, long alive_time, TcpServerListener listener) throws java.io.IOException + { this.listener=listener; + if (bind_ipaddr==null) server_socket=new ServerSocket(port); + else server_socket=new ServerSocket(port,socket_backlog,bind_ipaddr.getInetAddress()); + this.socket_timeout=DEFAULT_SOCKET_TIMEOUT; + this.alive_time=alive_time; + this.stop=false; + this.is_running=true; + } + + + /** Whether the service is running */ + public boolean isRunning() + { return is_running; + } + + + /** Stops running */ + public void halt() + { stop=true; + } + + + /** Runs the server */ + public void run() + { Exception error=null; + try + { server_socket.setSoTimeout(socket_timeout); + long expire=0; + if (alive_time>0) expire=System.currentTimeMillis()+alive_time; + // loop + while (!stop) + { TcpSocket socket=null; + try + { socket=new TcpSocket(server_socket.accept()); + } + catch (InterruptedIOException ie) + { if (alive_time>0 && System.currentTimeMillis()>expire) halt(); + continue; + } + if (listener!=null) listener.onIncomingConnection(this,socket); + if (alive_time>0) expire=System.currentTimeMillis()+alive_time; + } + } + catch (Exception e) + { error=e; + stop=true; + } + is_running=false; + try + { server_socket.close(); + } + catch (java.io.IOException e) {} + server_socket=null; + + if (listener!=null) listener.onServerTerminated(this,error); + listener=null; + } + + + /** Gets a String representation of the Object */ + public String toString() + { return "tcp:"+server_socket.getInetAddress()+":"+server_socket.getLocalPort(); + } + +} diff --git a/src/org/zoolu/net/TcpServerListener.java b/src/org/zoolu/net/TcpServerListener.java new file mode 100644 index 0000000..bdaef84 --- /dev/null +++ b/src/org/zoolu/net/TcpServerListener.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.net; + + + + +/** Listener for TcpServer events. + */ +public interface TcpServerListener +{ + /** When a new incoming connection is established */ + public void onIncomingConnection(TcpServer tcp_server, TcpSocket socket); + + /** When ConnectionServer terminates. */ + public void onServerTerminated(TcpServer tcp_server, Exception error); +} diff --git a/src/org/zoolu/net/TcpSocket.java b/src/org/zoolu/net/TcpSocket.java new file mode 100644 index 0000000..e8529f4 --- /dev/null +++ b/src/org/zoolu/net/TcpSocket.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.net; + + +import java.net.Socket; +//import java.net.InetAddress; +import java.io.InputStream; +import java.io.OutputStream; + + +/** TcpSocket provides a uniform interface to TCP transport protocol, + * regardless J2SE or J2ME is used. + */ +public class TcpSocket +{ + /** Socket */ + Socket socket; + + /** Creates a new TcpSocket */ + TcpSocket() + { socket=null; + } + + /** Creates a new TcpSocket */ + TcpSocket(Socket sock) + { socket=sock; + } + + /** Creates a new TcpSocket */ + public TcpSocket(String host, int port) throws java.io.IOException + { socket=new Socket(host,port); + } + + /** Creates a new TcpSocket */ + public TcpSocket(String host, int port, IpAddress local_ipaddr, int local_port) throws java.io.IOException + { socket=new Socket(host,port,local_ipaddr.getInetAddress(),local_port); + } + + /** Creates a new UdpSocket */ + public TcpSocket(IpAddress ipaddr, int port) throws java.io.IOException + { socket=new Socket(ipaddr.getInetAddress(),port); + } + + /** Creates a new UdpSocket */ + public TcpSocket(IpAddress ipaddr, int port, IpAddress local_ipaddr, int local_port) throws java.io.IOException + { socket=new Socket(ipaddr.getInetAddress(),port,local_ipaddr.getInetAddress(),local_port); + } + + /** Closes this socket. */ + public void close() throws java.io.IOException + { socket.close(); + } + + /** Gets the address to which the socket is connected. */ + public IpAddress getAddress() + { return new IpAddress(socket.getInetAddress()); + } + + /** Gets an input stream for this socket. */ + public InputStream getInputStream() throws java.io.IOException + { return socket.getInputStream(); + } + + /** Gets the local address to which the socket is bound. */ + public IpAddress getLocalAddress() + { return new IpAddress(socket.getLocalAddress()); + } + + /** Gets the local port to which this socket is bound. */ + public int getLocalPort() + { return socket.getLocalPort(); + } + + /** Gets an output stream for this socket. */ + public OutputStream getOutputStream() throws java.io.IOException + { return socket.getOutputStream(); + } + + /** Gets the remote port to which this socket is connected. */ + public int getPort() + { return socket.getPort(); + } + + /** Gets the socket timeout. */ + public int getSoTimeout() throws java.net.SocketException + { return socket.getSoTimeout(); + } + + /** Enables/disables the socket timeou, in milliseconds. */ + public void setSoTimeout(int timeout) throws java.net.SocketException + { socket.setSoTimeout(timeout); + } + + /** Converts this object to a String. */ + public String toString() + { return socket.toString(); + } + +} diff --git a/src/org/zoolu/net/UdpPacket.java b/src/org/zoolu/net/UdpPacket.java new file mode 100644 index 0000000..c8f2df7 --- /dev/null +++ b/src/org/zoolu/net/UdpPacket.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.net; + + +import java.net.DatagramPacket; +//import java.net.InetAddress; + + +/** UdpPacket provides a uniform interface to UDP packets, + * regardless J2SE or J2ME is used. + */ +public class UdpPacket +{ + /** The DatagramPacket */ + DatagramPacket packet; + + + /** Creates a new UdpPacket */ + UdpPacket(DatagramPacket packet) + { this.packet=packet; + } + /**Gets the DatagramPacket */ + DatagramPacket getDatagramPacket() + { return packet; + } + /**Sets the DatagramPacket */ + void setDatagramPacket(DatagramPacket packet) + { this.packet=packet; + } + + + /** Creates a new UdpPacket */ + public UdpPacket(byte[] buf, int length) + { packet=new DatagramPacket(buf,length); + } + + /** Creates a new UdpPacket */ + public UdpPacket(byte[] buf, int length, IpAddress ipaddr, int port) + { packet=new DatagramPacket(buf,length,ipaddr.getInetAddress(),port); + } + + /** Creates a new UdpPacket */ + public UdpPacket(byte[] buf, int offset, int length) + { packet=new DatagramPacket(buf,offset,length); + } + + /** Creates a new UdpPacket */ + public UdpPacket(byte[] buf, int offset, int length, IpAddress ipaddr, int port) + { packet=new DatagramPacket(buf,offset,length,ipaddr.getInetAddress(),port); + } + + /** Gets the IP address of the machine to which this datagram is being sent or from which the datagram was received. */ + public IpAddress getIpAddress() + { return new IpAddress(packet.getAddress()); + } + + /** Gets the data received or the data to be sent. */ + public byte[] getData() + { return packet.getData(); + } + + /** Gets the length of the data to be sent or the length of the data received. */ + public int getLength() + { return packet.getLength(); + } + + /** Gets the offset of the data to be sent or the offset of the data received. */ + public int getOffset() + { return packet.getOffset(); + } + + /** Gets the port number on the remote host to which this datagram is being sent or from which the datagram was received. */ + public int getPort() + { return packet.getPort(); + } + + /** Sets the IP address of the machine to which this datagram is being sent. */ + public void setIpAddress(IpAddress ipaddr) + { packet.setAddress(ipaddr.getInetAddress()); + } + + /** Sets the data buffer for this packet. */ + public void setData(byte[] buf) + { packet.setData(buf); + } + + /** Sets the data buffer for this packet. */ + public void setData(byte[] buf, int offset, int length) + { packet.setData(buf,offset,length); + } + + /** Sets the length for this packet. */ + public void setLength(int length) + { packet.setLength(length); + } + + /** Sets the port number on the remote host to which this datagram is being sent. */ + public void setPort(int iport) + { packet.setPort(iport); + } + +} diff --git a/src/org/zoolu/net/UdpProvider.java b/src/org/zoolu/net/UdpProvider.java new file mode 100644 index 0000000..c2d9a24 --- /dev/null +++ b/src/org/zoolu/net/UdpProvider.java @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.net; + + +import java.io.IOException; +import java.io.InterruptedIOException; + + +/** UdpProvider provides an UDP send/receive service. + * On the receiver side it waits for UDP datagrams and passes them + * to the UdpProviderListener. + *

If the attribute alive_time has a non-zero value, the UdpProvider stops + * after alive_time milliseconds of inactivity. + *

When a new packet is received, the onReceivedPacket(UdpProvider,DatagramPacket) + * method is fired. + *

Method onServiceTerminated(UdpProvider) is fired when the the UdpProvider stops + * receiving packets. + */ +public class UdpProvider extends Thread +{ + /** The reading buffer size */ + public static final int BUFFER_SIZE=65535; + + /** Default value for the maximum time that the UDP receiver can remain active after been halted (in milliseconds) */ + public static final int DEFAULT_SOCKET_TIMEOUT=2000; // 2sec + + /** UDP socket */ + UdpSocket socket; + + /** Maximum time that the UDP receiver can remain active after been halted (in milliseconds) */ + int socket_timeout; + + /** Maximum time that the UDP receiver remains active without receiving UDP datagrams (in milliseconds) */ + long alive_time; + + /** Minimum size for received packets. Shorter packets are silently discarded. */ + int minimum_length; + + /** Whether it has been halted */ + boolean stop; + + /** Whether it is running */ + boolean is_running; + + /** UdpProvider listener */ + UdpProviderListener listener; + + + /** Creates a new UdpProvider */ + public UdpProvider(UdpSocket socket, UdpProviderListener listener) + { init(socket,0,listener); + start(); + } + + + /** Creates a new UdpProvider */ + public UdpProvider(UdpSocket socket, long alive_time, UdpProviderListener listener) + { init(socket,alive_time,listener); + start(); + } + + + /** Inits the UdpProvider */ + private void init(UdpSocket socket, long alive_time, UdpProviderListener listener) + { this.listener=listener; + this.socket=socket; + this.socket_timeout=DEFAULT_SOCKET_TIMEOUT; + this.alive_time=alive_time; + this.minimum_length=0; + this.stop=false; + this.is_running=true; + } + + + /** Gets the UdpSocket */ + public UdpSocket getUdpSocket() + { return socket; + } + + + /** Sets a new UdpSocket */ + /*public void setUdpSocket(UdpSocket socket) + { this.socket=socket; + }*/ + + + /** Whether the service is running */ + public boolean isRunning() + { return is_running; + } + + + /** Sets the maximum time that the UDP service can remain active after been halted */ + public void setSoTimeout(int timeout) + { socket_timeout=timeout; + } + + + /** Gets the maximum time that the UDP service can remain active after been halted */ + public int getSoTimeout() + { return socket_timeout; + } + + + /** Sets the minimum size for received packets. + * Packets shorter than that are silently discarded. */ + public void setMinimumReceivedDataLength(int len) + { minimum_length=len; + } + + + /** Gets the minimum size for received packets. + * Packets shorter than that are silently discarded. */ + public int getMinimumReceivedDataLength() + { return minimum_length; + } + + + /** Sends a UdpPacket */ + public void send(UdpPacket packet) throws IOException + { if (!stop) socket.send(packet); + } + + + /** Stops running */ + public void halt() + { stop=true; + } + + + /** The main thread */ + public void run() + { + byte[] buf=new byte[BUFFER_SIZE]; + UdpPacket packet=new UdpPacket(buf, buf.length); + + Exception error=null; + long expire=0; + if (alive_time>0) expire=System.currentTimeMillis()+alive_time; + try + { socket.setSoTimeout(socket_timeout); + // loop + while(!stop) + { try + { socket.receive(packet); + } + catch (InterruptedIOException ie) + { if (alive_time>0 && System.currentTimeMillis()>expire) halt(); + continue; + } + if (packet.getLength()>=minimum_length) + { if (listener!=null) listener.onReceivedPacket(this,packet); + if (alive_time>0) expire=System.currentTimeMillis()+alive_time; + } + packet=new UdpPacket(buf, buf.length); + } + } + catch (Exception e) + { error=e; + stop=true; + } + is_running=false; + if (listener!=null) listener.onServiceTerminated(this,error); + listener=null; + } + + + /** Gets a String representation of the Object */ + public String toString() + { return "udp:"+socket.getLocalAddress()+":"+socket.getLocalPort(); + } + +} diff --git a/src/org/zoolu/net/UdpProviderListener.java b/src/org/zoolu/net/UdpProviderListener.java new file mode 100644 index 0000000..56cfed8 --- /dev/null +++ b/src/org/zoolu/net/UdpProviderListener.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.net; + + + + +/** Listener for UdpProvider events. + */ +public interface UdpProviderListener +{ + /** When a new UDP datagram is received. */ + public void onReceivedPacket(UdpProvider udp, UdpPacket packet); + + /** When UdpProvider terminates. */ + public void onServiceTerminated(UdpProvider udp, Exception error); +} diff --git a/src/org/zoolu/net/UdpSocket.java b/src/org/zoolu/net/UdpSocket.java new file mode 100644 index 0000000..244dd94 --- /dev/null +++ b/src/org/zoolu/net/UdpSocket.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.net; + + +import java.net.DatagramSocket; +import java.net.DatagramPacket; +import java.net.InetAddress; + + +/** UdpSocket provides a uniform interface to UDP transport protocol, + * regardless J2SE or J2ME is used. + */ +public class UdpSocket +{ + + /** DatagramSocket */ + DatagramSocket socket; + + /** Creates a new UdpSocket */ + public UdpSocket() throws java.net.SocketException + { socket=new DatagramSocket(); + } + + /** Creates a new UdpSocket */ + public UdpSocket(int port) throws java.net.SocketException + { socket=new DatagramSocket(port); + } + + /** Creates a new UdpSocket */ + UdpSocket(DatagramSocket sock) + { socket=sock; + } + + /** Creates a new UdpSocket */ + public UdpSocket(int port, IpAddress ipaddr) throws java.net.SocketException + { socket=new DatagramSocket(port,ipaddr.getInetAddress()); + } + + /** Closes this datagram socket. */ + public void close() + { socket.close(); + } + + /** Gets the local address to which the socket is bound. */ + public IpAddress getLocalAddress() + { return new IpAddress(socket.getInetAddress()); + } + + /** Gets the port number on the local host to which this socket is bound. */ + public int getLocalPort() + { return socket.getLocalPort(); + } + + /** Gets the socket timeout. */ + public int getSoTimeout() throws java.net.SocketException + { return socket.getSoTimeout(); + } + + /** Enables/disables socket timeout with the specified timeout, in milliseconds. */ + public void setSoTimeout(int timeout) throws java.net.SocketException + { socket.setSoTimeout(timeout); + } + + /** Receives a datagram packet from this socket. */ + public void receive(UdpPacket pkt) throws java.io.IOException + { DatagramPacket dgram=pkt.getDatagramPacket(); + socket.receive(dgram); + pkt.setDatagramPacket(dgram); + } + + /** Sends an UDP packet from this socket. */ + public void send(UdpPacket pkt) throws java.io.IOException + { socket.send(pkt.getDatagramPacket()); + } + + /** Converts this object to a String. */ + public String toString() + { return socket.toString(); + } + +} diff --git a/src/org/zoolu/sdp/AttributeField.java b/src/org/zoolu/sdp/AttributeField.java new file mode 100644 index 0000000..1b2fa73 --- /dev/null +++ b/src/org/zoolu/sdp/AttributeField.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sdp; + + +import org.zoolu.tools.Parser; + + +/** SDP attribute field. + *

+ *

+  *    attribute-fields = "a=" (att-field ":" att-value) | att-field CRLF
+  * 
+ */ +public class AttributeField extends SdpField +{ + /** Creates a new AttributeField. */ + public AttributeField(String attribute) + { super('a',attribute); + } + + /** Creates a new AttributeField. */ + public AttributeField(String attribute, String a_value) + { super('a',attribute+":"+a_value); + } + + /** Creates a new AttributeField. */ + public AttributeField(SdpField sf) + { super(sf); + } + + /** Gets the attribute name. */ + public String getAttributeName() + { int i=value.indexOf(":"); + if (i<0) return value; else return value.substring(0,i); + } + + /** Gets the attribute value. */ + public String getAttributeValue() + { int i=value.indexOf(":"); + if (i<0) return null; else return value.substring(i+1); + } + +} diff --git a/src/org/zoolu/sdp/ConnectionField.java b/src/org/zoolu/sdp/ConnectionField.java new file mode 100644 index 0000000..535ab52 --- /dev/null +++ b/src/org/zoolu/sdp/ConnectionField.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sdp; + + +import org.zoolu.tools.Parser; + + +/** SDP connection field. + *

+ *

+  *    connection-field = "c=" nettype SP addrtype SP connection-address CRLF
+  *                       ;a connection field must be present
+  *                       ;in every media description or at the
+  *                       ;session-level
+  * 
+ */ +public class ConnectionField extends SdpField +{ + /** Creates a new ConnectionField. */ + public ConnectionField(String connection_field) + { super('c',connection_field); + } + + /** Creates a new ConnectionField. */ + public ConnectionField(String address_type, String address, int ttl, int num) + { super('c',null); + value="IN "+address_type+" "+address; + if (ttl>0) value+="/"+ttl; + if (num>0) value+="/"+num; + } + + /** Creates a new ConnectionField. */ + public ConnectionField(String address_type, String address) + { super('c',"IN "+address_type+" "+address); + } + + /** Creates a new ConnectionField. */ + public ConnectionField(SdpField sf) + { super(sf); + } + + /** Gets the connection address. */ + public String getAddressType() + { String type=(new Parser(value)).skipString().getString(); + return type; + } + + /** Gets the connection address. */ + public String getAddress() + { String address=(new Parser(value)).skipString().skipString().getString(); + int i=address.indexOf("/"); + if (i<0) return address; else return address.substring(0,i); + } + + /** Gets the TTL. */ + public int getTTL() + { String address=(new Parser(value)).skipString().skipString().getString(); + int i=address.indexOf("/"); + if (i<0) return 0; + int j=address.indexOf("/",i); + if (j<0) return Integer.parseInt(address.substring(i)); else return Integer.parseInt(address.substring(i,j)); + } + + /** Gets the number of addresses. */ + public int getNum() + { String address=(new Parser(value)).skipString().skipString().getString(); + int i=address.indexOf("/"); + if (i<0) return 0; + int j=address.indexOf("/",i); + if (j<0) return 0; + return Integer.parseInt(address.substring(j)); + } + +} diff --git a/src/org/zoolu/sdp/MediaDescriptor.java b/src/org/zoolu/sdp/MediaDescriptor.java new file mode 100644 index 0000000..6ef5c66 --- /dev/null +++ b/src/org/zoolu/sdp/MediaDescriptor.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sdp; + + +import java.util.Vector; + + +/** Class MediaDescriptor handles SDP media descpriptions. + *

A MediaDescriptor can be part of a SessionDescriptor, and contains + * details that apply onto to a single media stream. + *

A single SessionDescriptor may convey zero or more MediaDescriptors. + *

In the current implementation, the MediaDescriptor consists of + * the m (media) and c (connection information) fields, followed by zero + * or more a (attribute) fields. + * The m field is mandatory for a MediaDescriptor. + */ +public class MediaDescriptor +{ + /** Media field ('m'). */ + MediaField m; + /** Connection field ('c') */ + ConnectionField c; + /** Vector of attribute fileds ('a') */ + Vector av; + + /** Creates a new MediaDescriptor. + * @param md the cloned MediaDescriptor */ + public MediaDescriptor(MediaDescriptor md) + { m=new MediaField(md.m); + if (md.c!=null) c=new ConnectionField(md.c); else c=null; + av=new Vector(); + for (int i=0; imedia and c connection. + * No attribute is set by default. + * @param media the MediaField + * @param connection the ConnectionField, or null if no ConnectionField + * is present in the MediaDescriptor */ + public MediaDescriptor(MediaField media, ConnectionField connection) + { m=media; + c=connection; + av=new Vector(); + } + + /** Creates a new MediaDescriptor with m media, c connection, + * and a attribute. + * @param media the MediaField + * @param connection the ConnectionField, or null if no ConnectionField + * is present in the MediaDescriptor + * @param attribute the first AttributeField */ + public MediaDescriptor(MediaField media, ConnectionField connection, AttributeField attribute) + { m=media; + c=connection; + av=new Vector(); + if (attribute!=null) av.addElement(attribute); + } + + /** Creates a new MediaDescriptor with m=media and c=connection, + * with attributes 'a' equals to attributes (Vector of AttributeField). + * @param media the MediaField + * @param connection the ConnectionField, or null if no ConnectionField + * is present in the MediaDescriptor + * @param attributes the Vector of AttributeField */ + public MediaDescriptor(MediaField media, ConnectionField connection, Vector attributes) + { m=media; + c=connection; + av=new Vector(attributes.size()); + av.setSize(attributes.size()); + for (int i=0; imedia, c connection, + * and a attribute. + * @param media the media field vaule + * @param connection the connection field vaule, or null if no connection field + * is present in the MediaDescriptor + * @param attribute the first media attribute alue */ + public MediaDescriptor(String media, String connection, String attribute) + { m=new MediaField(media); + if (connection!=null) c=new ConnectionField(connection); + av=new Vector(); + if (attribute!=null) av.addElement(new AttributeField(attribute)); + } + + /** Creates a new MediaDescriptor from String str. + * @param str the media field line */ + /*public MediaDescriptor(String str) + { SdpParser par=new SdpParser(str); + m=par.parseMediaField(); + c=par.parseConnectionField(); + av=new Vector(); + AttributeField a=par.parseAttributeField(); + while (a!=null) + { av.addElement(a); + a=par.parseAttributeField(); + } + }*/ + + /** Gets media. + * @return the MediaField */ + public MediaField getMedia() + { return m; + } + + /** Gets connection information. + * @return the ConnectionField */ + public ConnectionField getConnection() + { return c; + } + + /** Gets a Vector of attribute values. + * @return a Vector of AttributeField */ + public Vector getAttributes() + { Vector v=new Vector(av.size()); + for (int i=0; i + *

+  *      media-field = "m=" media SP port ["/" integer] SP proto 1*(SP fmt) CRLF
+  * 
+ */ +public class MediaField extends SdpField +{ + /** Creates a new MediaField. */ + public MediaField(String media_field) + { super('m',media_field); + } + + /** Creates a new MediaField. */ + public MediaField(String media, int port, int num, String transport, String formats) + { super('m',null); + value=media+" "+port; + if (num>0) value+="/"+num; + value+=" "+transport+" "+formats; + } + + /** Creates a new MediaField. + * @param formatlist a Vector of media formats (properly a Vector of Strings) */ + public MediaField(String media, int port, int num, String transport, Vector formatlist) + { super('m',null); + value=media+" "+port; + if (num>0) value+="/"+num; + value+=" "+transport; + for (int i=0; i0) formatlist.addElement(fmt); + } + return formatlist; + } + +} diff --git a/src/org/zoolu/sdp/OriginField.java b/src/org/zoolu/sdp/OriginField.java new file mode 100644 index 0000000..5ed8df9 --- /dev/null +++ b/src/org/zoolu/sdp/OriginField.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sdp; + + +import org.zoolu.tools.Parser; + + +/** SDP origin field. + *

+ *

+  *    origin-field = "o=" username SP sess-id SP sess-version SP
+  *                        nettype SP addrtype SP unicast-address CRLF
+  * 
+ */ +public class OriginField extends SdpField +{ + /** Creates a new OriginField. */ + public OriginField(String origin) + { super('o',origin); + } + + /** Creates a new OriginField. */ + public OriginField(String username, String sess_id, String sess_version, String addrtype, String address) + { super('o',username+" "+sess_id+" "+sess_version+" IN "+addrtype+" "+address); + } + + /** Creates a new OriginField. */ + public OriginField(String username, String sess_id, String sess_version, String address) + { super('o',username+" "+sess_id+" "+sess_version+" IN IP4 "+address); + } + + /** Creates a new OriginField. */ + public OriginField(SdpField sf) + { super(sf); + } + + /** Gets the user name. */ + public String getUserName() + { return (new Parser(value)).getString(); + } + + /** Gets the session id. */ + public String getSessionId() + { return (new Parser(value)).skipString().getString(); + } + + /** Gets the session version. */ + public String getSessionVersion() + { return (new Parser(value)).skipString().skipString().getString(); + } + + /** Gets the address type. */ + public String getAddressType() + { return (new Parser(value)).skipString().skipString().skipString().skipString().getString(); + } + + /** Gets the address. */ + public String getAddress() + { return (new Parser(value)).skipString().skipString().skipString().skipString().skipString().getString(); + } + +} diff --git a/src/org/zoolu/sdp/SdpField.java b/src/org/zoolu/sdp/SdpField.java new file mode 100644 index 0000000..c9ac301 --- /dev/null +++ b/src/org/zoolu/sdp/SdpField.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sdp; + + + + +/** SdpField rapresents a SDP line field. + * It is formed by a 'type' (char) and a 'value' (String). + *

A SDP line field is of the form <type> = <value> + */ +public class SdpField +{ char type; + String value; + + /** Creates a new SdpField. + * @param s_type the field type + * @param s_value the field value */ + public SdpField(char s_type, String s_value) + { type=s_type; + value=s_value; + } + + /** Creates a new SdpField. + * @param sf the SdpField clone */ + public SdpField(SdpField sf) + { type=sf.type; + value=sf.value; + } + + /** Creates a new SdpField based on a SDP line of the form =. + * The SDP value terminats with the end of the String or with the first CR or LF char. + * @param str the <type> = <value> line */ + public SdpField(String str) + { SdpParser par=new SdpParser(str); + SdpField sf=par.parseSdpField(); + type=sf.type; + value=sf.value; + } + + /** Creates and returns a copy of the SdpField. + * @return a SdpField clone */ + public Object clone() + { return new SdpField(this); + } + + /** Whether the SdpField is equal to Object obj + * @return true if equal */ + public boolean equals(Object obj) + { try + { SdpField sf=(SdpField)obj; + if (type!=sf.type) return false; + if (value!=sf.value) return false; + return true; + } + catch (Exception e) { return false; } + } + + /** Gets the type of field + * @return the field type */ + public char getType() + { return type; + } + + /** Gets the value + * @return the field value */ + public String getValue() + { return value; + } + + /** Gets string representation of the SdpField + * @return the string representation */ + public String toString() + { return type+"="+value+"\r\n"; + } + +} diff --git a/src/org/zoolu/sdp/SdpParser.java b/src/org/zoolu/sdp/SdpParser.java new file mode 100644 index 0000000..200c3ce --- /dev/null +++ b/src/org/zoolu/sdp/SdpParser.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sdp; + + +import org.zoolu.sdp.*; +import org.zoolu.tools.Parser; +import java.util.Vector; + + +/** Class SdpParser extends class Parser for parsing of SDP strings. + */ +class SdpParser extends Parser +{ + /** Creates a SdpParser based on String s */ + public SdpParser(String s) + { super(s); + } + + /** Creates a SdpParser based on String s and starting from position i */ + public SdpParser(String s, int i) + { super(s,i); + } + + /** Returns the SdpField at the current position. + * The SDP value terminates with the end of the String or with the first CR or LF char. + *

Returns null if no SdpField is recognized. */ + /*public SdpField parseSdpField() + { Parser par=new Parser(str,index); + while (par.length()>1 && par.charAt(1)!='=') par.goToNextLine(); + if (!par.hasMore()) return null; + char type=par.getChar(); + par.skipChar(); + String value=par.skipChar().getLine(); + if (value==null) return null; + index=par.getPos(); + // for DEBUG + //System.out.println("DEBUG: "+type+"="+value); + return new SdpField(type,value); + }*/ + + /** Returns the first SdpField. + * The SDP value terminates with the end of the String or with the first CR or LF char. + * @return the first SdpField, or null if no SdpField is recognized. */ + public SdpField parseSdpField() + { int begin=index; + while (begin>=0 && begin0 && CR0 && LF A session description consists of a session-level description + * (details that apply to the whole session and all media streams) and + * zero or more media-level descriptions (details that apply onto + * to a single media stream). + *

The session-level part starts with a + * `v=' line and continues to the first media-level section. The media + * description starts with an `m=' line and continues to the next media + * description or end of the whole session description. In general, + * session-level values are the default for all media unless overridden + * by an equivalent media-level value. + *

In the current implementation, the session-level description consists + * of the v, o, s, c, and t SDP fields (lines). + */ +public class SessionDescriptor +{ + /** Version filed. */ + SdpField v; + /** Origin filed. */ + OriginField o; + /** Session-name filed. */ + SessionNameField s; + /** Connection filed. */ + ConnectionField c; + /** Time filed. */ + TimeField t; + + /** Vector of session attributes (as Vector of SdpFields). */ + Vector av; + + /** Vector of MediaDescriptors. */ + Vector media; + + /*private void init(String owner, String session, String connection, String time) + { v=new SdpField('v',"0"); + o=new SdpField('o',owner); + s=new SdpField('s',session); + c=new SdpField('c',connection); + t=new SdpField('t',time); + media=new HashSet(); + }*/ + + /** Inits the SessionDescriptor. */ + private void init(OriginField origin, SessionNameField session, ConnectionField connection, TimeField time) + { v=new SdpField('v',"0"); + o=origin; + s=session; + c=connection; + t=time; + av=new Vector(); + media=new Vector(); + } + + /** Creates a new SessionDescriptor. + * @param sd the SessionDescriptor clone */ + public SessionDescriptor(SessionDescriptor sd) + { init(new OriginField(sd.o),new SessionNameField(sd.s),new ConnectionField(sd.c),new TimeField(sd.t)); + for (int i=0; i with: + *
o=owner + *
s=Session SIP/SDP + *
c=IP4 address + *
t=0 0 + *

if address==null, '127.0.0.1' is used + *
if owner==null, 'user@'address is used + * @param owner the owner + * @param address the IPv4 address */ + public SessionDescriptor(String owner, String address) + { if (address==null) address="127.0.0.1"; + if (owner==null) owner="user@"+address; + init(new OriginField(owner,"0","0",address),new SessionNameField("Session SIP/SDP"),new ConnectionField("IP4",address),new TimeField()); + } + + /** Creates a default SessionDescriptor. + *

o=user@127.0.0.1 + * s=Session SIP/SDP + * c=127.0.0.1 + * t=0 0 */ + public SessionDescriptor() + { String address="127.0.0.1"; + String owner="user@"+address; + init(new OriginField(owner,"0","0",address),new SessionNameField("Session SIP/SDP"),new ConnectionField("IP4",address),new TimeField()); + } + + /** Creates a new SessionDescriptor from String sdp + * @param sdp the entire SDP */ + public SessionDescriptor(String sdp) + { SdpParser par=new SdpParser(sdp); + // parse mandatory fields + v=par.parseSdpField('v'); + if (v==null) v=new SdpField('v',"0"); + o=par.parseOriginField(); + if (o==null) o=new OriginField("unknown"); + s=par.parseSessionNameField(); + if (s==null) s=new SessionNameField(); + c=par.parseConnectionField(); + if (c==null) c=new ConnectionField("IP4","0.0.0.0"); + t=par.parseTimeField(); + if (t==null) t=new TimeField(); + while (par.hasMore() && (!par.startsWith("a=") && !par.startsWith("m="))) + { // skip unknown lines.. + par.goToNextLine(); + } + // parse session attributes + av=new Vector(); + while (par.hasMore() && par.startsWith("a=")) + { AttributeField attribute=par.parseAttributeField(); + av.addElement(attribute); + } + // parse media descriptors + media=new Vector(); + MediaDescriptor md; + while ((md=par.parseMediaDescriptor())!=null) + { addMediaDescriptor(md); + } + } + + /** Sets the origin 'o' field. + * @param origin the OriginField + * @return this SessionDescriptor */ + public SessionDescriptor setOrigin(OriginField origin) + { o=origin; + return this; + } + + /** Gets the origin 'o' field */ + public OriginField getOrigin() + { //System.out.println("DEBUG: inside SessionDescriptor.getOwner(): sdp=\n"+toString()); + return o; + } + + /** Sets the session-name 's' field. + * @param session the SessionNameField + * @return this SessionDescriptor */ + public SessionDescriptor setSessionName(SessionNameField session) + { s=session; + return this; + } + + /** Gets the session-name 's' field */ + public SessionNameField getSessionName() + { return s; + } + + /** Sets the connection-information 'c' field. + * @param connection the ConnectionField + * @return this SessionDescriptor */ + public SessionDescriptor setConnection(ConnectionField connection) + { c=connection; + return this; + } + + /** Gets the connection-information 'c' field */ + public ConnectionField getConnection() + { return c; + } + + /** Sets the time 't' field. + * @param time the TimeField + * @return this SessionDescriptor */ + public SessionDescriptor setTime(TimeField time) + { t=time; + return this; + } + + /** Gets the time 't' field */ + public TimeField getTime() + { return t; + } + + /** Adds a new attribute for a particular media + * @param media the MediaField + * @param attribute an AttributeField + * @return this SessionDescriptor */ + public SessionDescriptor addMedia(MediaField media, AttributeField attribute) + { //printlog("DEBUG: media: "+media,5); + //printlog("DEBUG: attribute: "+attribute,5); + addMediaDescriptor(new MediaDescriptor(media,null,attribute)); + return this; + } + + /** Adds a new media. + * @param media the MediaField + * @param attributes Vector of AttributeField + * @return this SessionDescriptor */ + public SessionDescriptor addMedia(MediaField media, Vector attributes) + { //printlog("DEBUG: media: "+media,5); + //printlog("DEBUG: attribute: "+attributes,5); + addMediaDescriptor(new MediaDescriptor(media,null,attributes)); + return this; + } + + /** Adds a new MediaDescriptor + * @param media_desc a MediaDescriptor + * @return this SessionDescriptor */ + public SessionDescriptor addMediaDescriptor(MediaDescriptor media_desc) + { //printlog("DEBUG: media desc: "+media_desc,5); + media.addElement(media_desc); + return this; + } + + /** Adds a Vector of MediaDescriptors + * @param media_descs Vector if MediaDescriptor + * @return this SessionDescriptor */ + public SessionDescriptor addMediaDescriptors(Vector media_descs) + { //media.addAll(media_descs); // not supported by J2ME.. + for (int i=0; i=0; i--) + if (((MediaDescriptor)media.elementAt(i)).getMedia().getMedia().equals(media_type)) media.removeElementAt(i); + return this; + } + + /** Removes all MediaDescriptors */ + public SessionDescriptor removeMediaDescriptors() + { //media.clear(); // not supported by J2ME.. + media.setSize(0); + return this; + } + + /** Gets the first MediaDescriptor of a particular media. + * @param media_type the media type + * @return the MediaDescriptor */ + public MediaDescriptor getMediaDescriptor(String media_type) + { for (int i=0; i + *

+  *    session-name-field = "s=" text CRLF
+  * 
+ */ +public class SessionNameField extends SdpField +{ + /** Creates a new SessionNameField. */ + public SessionNameField(String session_name) + { super('s',session_name); + } + + /** Creates a new void SessionNameField. */ + public SessionNameField() + { super('s'," "); + } + + /** Creates a new SessionNameField. */ + public SessionNameField(SdpField sf) + { super(sf); + } + + /** Gets the session name. */ + public String getSession() + { return value; + } + +} diff --git a/src/org/zoolu/sdp/TimeField.java b/src/org/zoolu/sdp/TimeField.java new file mode 100644 index 0000000..ce56987 --- /dev/null +++ b/src/org/zoolu/sdp/TimeField.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sdp; + + +import org.zoolu.tools.Parser; + + +/** SDP attribute field. + *

+ *

+  *    time-fields = 1*( "t=" start-time SP stop-time *(CRLF repeat-fields) CRLF) [zone-adjustments CRLF]
+  * 
+ */ +public class TimeField extends SdpField +{ + /** Creates a new TimeField. */ + public TimeField(String time_field) + { super('t',time_field); + } + + /** Creates a new TimeField. */ + public TimeField(String start, String stop) + { super('t',start+" "+stop); + } + + /** Creates a new void TimeField. */ + public TimeField() + { super('t',"0 0"); + } + + /** Creates a new TimeField. */ + public TimeField(SdpField sf) + { super(sf); + } + + /** Gets the start time. */ + public String getStartTime() + { return (new Parser(value)).getString(); + } + + /** Gets the stop time. */ + public String getStopTime() + { return (new Parser(value)).skipString().getString(); + } + +} diff --git a/src/org/zoolu/sip/address/NameAddress.java b/src/org/zoolu/sip/address/NameAddress.java new file mode 100644 index 0000000..dde065c --- /dev/null +++ b/src/org/zoolu/sip/address/NameAddress.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.address; + + +import org.zoolu.sip.provider.SipParser; + + +/** Class NameAddress is used to rapresent any valid SIP Name Address. + * It contains a SIP URI and optionally a display name. + *
A SIP Name Address is a string of the form of: + *
   [ display-name ] address
+  * 
   where address can be a valid SIP URL
+*/ +public class NameAddress +{ + String name; + SipURL url; + + + public NameAddress(String displayname, SipURL sipurl) + { name=displayname; + url=sipurl; + } + + public NameAddress(SipURL sipurl) + { name=null; + url=sipurl; + } + + public NameAddress(NameAddress name_address) + { name=name_address.getDisplayName(); + url=name_address.getAddress(); + } + + public NameAddress(String naddr) + { SipParser par=new SipParser(naddr); + NameAddress na=par.getNameAddress(); + //DEBUG + //if (na==null) + //{ System.out.println("DEBUG: NameAddress: par:\r\n"+par.getWholeString()); + // System.exit(0); + //} + name=na.name; + url=na.url; + } + + /** Creates and returns a copy of NameAddress */ + public Object clone() + { return new NameAddress(this); + } + + /** Indicates whether some other Object is "equal to" this NameAddress */ + public boolean equals(Object obj) + { NameAddress naddr=(NameAddress)obj; + return url.equals(naddr.getAddress()); + } + + /** Gets address of NameAddress */ + public SipURL getAddress() + { return url; + } + + /** Gets display name of NameAddress (Returns null id display name does not exist) */ + public String getDisplayName() + { return name; + } + + /** Gets boolean value to indicate if NameAddress has display name */ + public boolean hasDisplayName() + { return name!=null; + } + + /** Removes display name from NameAddress (if it exists) */ + public void removeDisplayName() + { name=null; + } + + /** Sets address of NameAddress */ + public void setAddress(SipURL address) + { url=address; + } + + /** Sets display name of Header */ + public void setDisplayName(String displayName) + { name=displayName; + } + + /** Whether two NameAddresses are equals */ + public boolean equals(NameAddress naddr) + { return (name==naddr.name && url==naddr.url); + } + + /** Gets string representation of NameAddress */ + public String toString() + { String str; + if (hasDisplayName()) + str="\""+name+"\" <"+url+">"; + else str="<"+url+">"; + + return str; + } + +} diff --git a/src/org/zoolu/sip/address/SipURL.java b/src/org/zoolu/sip/address/SipURL.java new file mode 100644 index 0000000..4e2b00c --- /dev/null +++ b/src/org/zoolu/sip/address/SipURL.java @@ -0,0 +1,265 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.address; + + +import org.zoolu.sip.provider.SipParser; +import org.zoolu.tools.Parser; +import java.util.Vector; + + +/** +

Class SipURL implements SIP URLs. +

A SIP URL is a string of the form of: +

   sip:[user@]hostname[:port][;parameters] 
+

If port number is ommitted, -1 is returned +*/ +public class SipURL +{ + protected String url; + + protected static final String transport_param="transport"; + protected static final String maddr_param="maddr"; + protected static final String ttl_param="ttl"; + protected static final String lr_param="lr"; + + /** Creates a new SipURL based on a hostname or on a sip url as sip:[user@]hostname[:port][;param1=value1].. */ + public SipURL(String sipurl) + { if (sipurl.startsWith("sip:")) url=new String(sipurl); + else url="sip:"+sipurl; + } + + /** Creates a new SipURL */ + public SipURL(String username, String hostname) + { init(username,hostname,-1); + } + + /** Creates a new SipURL */ + public SipURL(String hostname, int portnumber) + { init(null,hostname,portnumber); + } + + /** Creates a new SipURL */ + public SipURL(String username, String hostname, int portnumber) + { init(username,hostname,portnumber); + } + + /** Inits the SipURL */ + private void init(String username, String hostname, int portnumber) + { StringBuffer sb=new StringBuffer("sip:"); + if (username!=null) sb.append(username).append('@'); + sb.append(hostname); + if (portnumber>0) sb.append(":"+portnumber); + url=sb.toString(); + } + + /** Creates and returns a copy of the URL */ + public Object clone() + { return new SipURL(url); + } + + /** Indicates whether some other Object is "equal to" this URL */ + public boolean equals(Object obj) + { SipURL newurl=(SipURL)obj; + return url.toString().equals(newurl.toString()); + } + + /** Gets user name of SipURL (Returns null if user name does not exist) */ + public String getUserName() + { int begin=4; // skip "sip:" + int end=url.indexOf('@',begin); + if (end<0) return null; + else return url.substring(begin,end); + } + + /** Gets host of SipURL */ + public String getHost() + { char[] host_terminators={':',';','?'}; + Parser par=new Parser(url); + int begin=par.indexOf('@'); // skip "sip:user@" + if (begin<0) begin=4; // skip "sip:" + else begin++; // skip "@" + par.setPos(begin); + int end=par.indexOf(host_terminators); + if (end<0) return url.substring(begin); + else return url.substring(begin,end); + } + + /** Gets port of SipURL; returns -1 if port is not specidfied */ + public int getPort() + { char[] port_terminators={';','?'}; + Parser par=new Parser(url,4); // skip "sip:" + int begin=par.indexOf(':'); + if (begin<0) return -1; + else + { begin++; + par.setPos(begin); + int end=par.indexOf(port_terminators); + if (end<0) return Integer.parseInt(url.substring(begin)); + else return Integer.parseInt(url.substring(begin,end)); + } + } + + /** Gets boolean value to indicate if SipURL has user name */ + public boolean hasUserName() + { return getUserName()!=null; + } + + /** Gets boolean value to indicate if SipURL has port */ + public boolean hasPort() + { return getPort()>=0; + } + + /** Whether two SipURLs are equals */ + public boolean equals(SipURL sip_url) + { return (url==sip_url.url); + } + + + /** Gets string representation of URL */ + public String toString() + { return url; + } + + /** Gets the value of specified parameter. + * @return null if parameter does not exist. */ + public String getParameter(String name) + { SipParser par=new SipParser(url); + return ((SipParser)par.goTo(';').skipChar()).getParameter(name); + } + + + /** Gets a String Vector of parameter names. + * @return null if no parameter is present */ + public Vector getParameters() + { SipParser par=new SipParser(url); + return ((SipParser)par.goTo(';').skipChar()).getParameters(); + } + + /** Whether there is the specified parameter */ + public boolean hasParameter(String name) + { SipParser par=new SipParser(url); + return ((SipParser)par.goTo(';').skipChar()).hasParameter(name); + } + + /** Whether there are any parameters */ + public boolean hasParameters() + { if (url!=null && url.indexOf(';')>=0) return true; + else return false; + } + + /** Adds a new parameter without a value */ + public void addParameter(String name) + { url=url+";"+name; + } + + /** Adds a new parameter with value */ + public void addParameter(String name, String value) + { if (value!=null) url=url+";"+name+"="+value; + else url=url+";"+name; + } + + /** Removes all parameters (if any) */ + public void removeParameters() + { int index=url.indexOf(';'); + if (index>=0) url=url.substring(0,index); + } + + /** Removes specified parameter (if present) */ + public void removeParameter(String name) + { int index=url.indexOf(';'); + if (index<0) return; + Parser par=new Parser(url,index); + while (par.hasMore()) + { int begin_param=par.getPos(); + par.skipChar(); + if (par.getWord(SipParser.param_separators).equals(name)) + { String top=url.substring(0,begin_param); + par.goToSkippingQuoted(';'); + String bottom=""; + if (par.hasMore()) bottom=url.substring(par.getPos()); + url=top.concat(bottom); + return; + } + par.goTo(';'); + } + } + + + /** Gets the value of transport parameter. + * @return null if no transport parameter is present. */ + public String getTransport() + { return getParameter(transport_param); + } + /** Whether transport parameter is present */ + public boolean hasTransport() + { return hasParameter(transport_param); + } + /** Adds transport parameter */ + public void addTransport(String proto) + { addParameter(transport_param,proto.toLowerCase()); + } + + + /** Gets the value of maddr parameter. + * @return null if no maddr parameter is present. */ + public String getMaddr() + { return getParameter(maddr_param); + } + /** Whether maddr parameter is present */ + public boolean hasMaddr() + { return hasParameter(maddr_param); + } + /** Adds maddr parameter */ + public void addMaddr(String maddr) + { addParameter(maddr_param,maddr); + } + + + /** Gets the value of ttl parameter. + * @return 1 if no ttl parameter is present. */ + public int getTtl() + { try { return Integer.parseInt(getParameter(ttl_param)); } catch (Exception e) { return 1; } + } + /** Whether ttl parameter is present */ + public boolean hasTtl() + { return hasParameter(ttl_param); + } + /** Adds ttl parameter */ + public void addTtl(int ttl) + { addParameter(ttl_param,Integer.toString(ttl)); + } + + + /** Whether lr (loose-route) parameter is present */ + public boolean hasLr() + { return hasParameter(lr_param); + } + /** Adds lr parameter */ + public void addLr() + { addParameter(lr_param); + } +} + + diff --git a/src/org/zoolu/sip/authentication/DigestAuthentication.java b/src/org/zoolu/sip/authentication/DigestAuthentication.java new file mode 100644 index 0000000..4ba71ea --- /dev/null +++ b/src/org/zoolu/sip/authentication/DigestAuthentication.java @@ -0,0 +1,288 @@ +package org.zoolu.sip.authentication; + + +import org.zoolu.sip.header.AuthenticationHeader; +import org.zoolu.sip.header.AuthorizationHeader; +import org.zoolu.sip.header.ProxyAuthorizationHeader; +import org.zoolu.sip.header.WwwAuthenticateHeader; +import org.zoolu.tools.MD5; + + +/** The HTTP Digest Authentication as defined in RFC2617. + * It can be used to i) calculate an authentication response + * from an authentication request, or ii) validate an authentication response. + *
in the former case the DigestAuthentication is created based on + * a WwwAuthenticationHeader (or ProxyAuthenticationHeader), + * while in the latter case it is created based on an AuthorizationHeader + * (or ProxyAuthorizationHeader). + */ +public class DigestAuthentication +{ + protected String method; + protected String username; + protected String passwd; + + protected String realm; + protected String nonce; // e.g. base 64 encoding of time-stamp H(time-stamp ":" ETag ":" private-key) + //protected String[] domain; + protected String opaque; + //protected boolean stale; // "true" | "false" + protected String algorithm; // "MD5" | "MD5-sess" | token + + protected String qop; // "auth" | "auth-int" | token + + protected String uri; + protected String cnonce; + protected String nc; + protected String response; + + protected String body; + + + /** Costructs a new DigestAuthentication. */ + protected DigestAuthentication() + { + } + + /** Costructs a new DigestAuthentication. */ + public DigestAuthentication(String method, AuthorizationHeader ah, String body, String passwd) + { init(method,ah,body,passwd); + } + + /** Costructs a new DigestAuthentication. */ + public DigestAuthentication(String method, String uri, WwwAuthenticateHeader ah, String qop, String body, String username, String passwd) + { init(method,ah,body,passwd); + this.uri=uri; + this.qop=qop; + this.username=username; + } + + /** Costructs a new DigestAuthentication. */ + private void init(String method, AuthenticationHeader ah, String body, String passwd) + { this.method=method; + this.username=ah.getUsernameParam(); + this.passwd=passwd; + this.realm=ah.getRealmParam(); + this.opaque=ah.getOpaqueParam(); + this.nonce=ah.getNonceParam(); + this.algorithm=ah.getAlgorithParam(); + this.qop=ah.getQopParam(); + this.uri=ah.getUriParam(); + this.cnonce=ah.getCnonceParam(); + this.nc=ah.getNcParam(); + this.response=ah.getResponseParam(); + this.body=body; + } + + + /** Gets a String representation of the object. */ + public String toString() + { StringBuffer sb=new StringBuffer(); + sb.append("method=").append(method).append("\n"); + sb.append("username=").append(username).append("\n"); + sb.append("passwd=").append(passwd).append("\n"); + sb.append("realm=").append(realm).append("\n"); + sb.append("nonce=").append(nonce).append("\n"); + sb.append("opaque=").append(opaque).append("\n"); + sb.append("algorithm=").append(algorithm).append("\n"); + sb.append("qop=").append(qop).append("\n"); + sb.append("uri=").append(uri).append("\n"); + sb.append("cnonce=").append(cnonce).append("\n"); + sb.append("nc=").append(nc).append("\n"); + sb.append("response=").append(response).append("\n"); + sb.append("body=").append(body).append("\n"); + return sb.toString(); + } + + + /** Whether the digest-response in the 'response' parameter in correct. */ + public boolean checkResponse() + { if (response==null) return false; + else return response.equals(getResponse()); + } + + + /** Gets a new AuthorizationHeader based on current authentication attributes. */ + public AuthorizationHeader getAuthorizationHeader() + { AuthorizationHeader ah=new AuthorizationHeader("Digest"); + ah.addUsernameParam(username); + ah.addRealmParam(realm); + ah.addNonceParam(nonce); + ah.addUriParam(uri); + if (algorithm!=null) ah.addAlgorithParam(algorithm); + if (opaque!=null) ah.addOpaqueParam(opaque); + if (qop!=null) ah.addQopParam(qop); + if (nc!=null) ah.addNcParam(nc); + String response=getResponse(); + ah.addResponseParam(response); + return ah; + } + + + /** Gets a new ProxyAuthorizationHeader based on current authentication attributes. */ + public ProxyAuthorizationHeader getProxyAuthorizationHeader() + { return new ProxyAuthorizationHeader(getAuthorizationHeader().getValue()); + } + + + /** Calculates the digest-response. + *

If the "qop" value is "auth" or "auth-int": + *
KD ( H(A1), unq(nonce) ":" nc ":" unq(cnonce) ":" unq(qop) ":" H(A2) ) + * + *

If the "qop" directive is not present: + *
KD ( H(A1), unq(nonce) ":" H(A2) ) + */ + public String getResponse() + { String secret=HEX(MD5(A1())); + StringBuffer sb=new StringBuffer(); + if (nonce!=null) sb.append(nonce); + sb.append(":"); + if (qop!=null) + { if (nc!=null) sb.append(nc); + sb.append(":"); + if (cnonce!=null) sb.append(cnonce); + sb.append(":"); + sb.append(qop); + sb.append(":"); + } + sb.append(HEX(MD5(A2()))); + String data=sb.toString(); + return HEX(KD(secret,data)); + } + + + /** Calculates KD() value. + *

KD(secret, data) = H(concat(secret, ":", data)) + */ + private byte[] KD(String secret, String data) + { StringBuffer sb=new StringBuffer(); + sb.append(secret).append(":").append(data); + return MD5(sb.toString()); + } + + + /** Calculates A1 value. + *

If the "algorithm" directive's value is "MD5" or is unspecified: + *
A1 = unq(username) ":" unq(realm) ":" passwd + * + *

If the "algorithm" directive's value is "MD5-sess": + *
A1 = H( unq(username) ":" unq(realm) ":" passwd ) ":" unq(nonce) ":" unq(cnonce) + */ + private byte[] A1() + { StringBuffer sb=new StringBuffer(); + if (username!=null) sb.append(username); + sb.append(":"); + if (realm!=null) sb.append(realm); + sb.append(":"); + if (passwd!=null) sb.append(passwd); + + if (algorithm==null || !algorithm.equalsIgnoreCase("MD5-sess")) + { return sb.toString().getBytes(); + } + else + { StringBuffer sb2=new StringBuffer(); + sb2.append(":"); + if (nonce!=null) sb2.append(nonce); + sb2.append(":"); + if (cnonce!=null) sb2.append(cnonce); + return cat(MD5(sb.toString()),sb2.toString().getBytes()); + } + } + + + /** Calculates A2 value. + *

If the "qop" directive's value is "auth" or is unspecified: + *
A2 = Method ":" digest-uri + * + *

If the "qop" value is "auth-int": + *
A2 = Method ":" digest-uri ":" H(entity-body) + */ + private String A2() + { StringBuffer sb=new StringBuffer(); + sb.append(method); + sb.append(":"); + if (uri!=null) sb.append(uri); + + if (qop!=null && qop.equalsIgnoreCase("auth-int")) + { sb.append(":"); + if (body==null) sb.append(HEX(MD5(""))); + else sb.append(HEX(MD5(body))); + } + return sb.toString(); + } + + + /** Concatenates two arrays of bytes. */ + private static byte[] cat(byte[] a, byte[] b) + { int len=a.length+b.length; + byte[ ] c=new byte[len]; + for (int i=0; iIt handles both outgoing or incoming calls. + *

Both offer/answer models are supported, that is: + *
i) offer/answer in invite/2xx, or + *
ii) offer/answer in 2xx/ack + */ +public class Call implements InviteDialogListener +{ + /** Event logger. */ + Log log; + + /** The SipProvider used for the call */ + protected SipProvider sip_provider; + + /** The invite dialog (sip.dialog.InviteDialog) */ + protected InviteDialog dialog; + + /** The user url */ + protected String from_url; + + /** The user contact url */ + protected String contact_url; + + /** The local sdp */ + protected String local_sdp; + + /** The remote sdp */ + protected String remote_sdp; + + /** The call listener (sipx.call.CallListener) */ + CallListener listener; + + + /** Creates a new Call. */ + public Call(SipProvider sip_provider, String from_url, String contact_url, CallListener call_listener) + { this.sip_provider=sip_provider; + this.log=sip_provider.getLog(); + this.listener=call_listener; + this.from_url=from_url; + this.contact_url=contact_url; + this.dialog=null; + this.local_sdp=null; + this.remote_sdp=null; + } + + /** Creates a new Call specifing the sdp */ + /*public Call(SipProvider sip_provider, String from_url, String contact_url, String sdp, CallListener call_listener) + { this.sip_provider=sip_provider; + this.log=sip_provider.getLog(); + this.listener=call_listener; + this.from_url=from_url; + this.contact_url=contact_url; + local_sdp=sdp; + }*/ + + /** Gets the current invite dialog */ + /*public InviteDialog getInviteDialog() + { return dialog; + }*/ + + /** Gets the current local session descriptor */ + public String getLocalSessionDescriptor() + { return local_sdp; + } + + /** Sets a new local session descriptor */ + public void setLocalSessionDescriptor(String sdp) + { local_sdp=sdp; + } + + /** Gets the current remote session descriptor */ + public String getRemoteSessionDescriptor() + { return remote_sdp; + } + + /** Whether the call is on (active). */ + public boolean isOnCall() + { return dialog.isSessionActive(); + } + + /** Waits for an incoming call */ + public void listen() + { dialog=new InviteDialog(sip_provider,this); + dialog.listen(); + } + + /** Starts a new call, inviting a remote user (callee) */ + public void call(String callee) + { call(callee,null,null,null); + } + + /** Starts a new call, inviting a remote user (callee) */ + public void call(String callee, String sdp) + { call(callee,null,null,sdp); + } + + /** Starts a new call, inviting a remote user (callee) */ + public void call(String callee, String from, String contact, String sdp) + { printLog("calling "+callee,LogLevel.HIGH); + if (from==null) from=from_url; + if (contact==null) contact=contact_url; + if (sdp!=null) local_sdp=sdp; + dialog=new InviteDialog(sip_provider,this); + if (local_sdp!=null) + dialog.invite(callee,from,contact,local_sdp); + else dialog.inviteWithoutOffer(callee,from,contact); + } + + /** Starts a new call with the invite message request */ + public void call(Message invite) + { dialog=new InviteDialog(sip_provider,this); + local_sdp=invite.getBody(); + if (local_sdp!=null) + dialog.invite(invite); + else dialog.inviteWithoutOffer(invite); + } + + /** Answers at the 2xx/offer (in the ack message) */ + public void ackWithAnswer(String sdp) + { local_sdp=sdp; + dialog.ackWithAnswer(contact_url,sdp); + } + + /** Rings back for the incoming call */ + public void ring() + { if (dialog!=null) dialog.ring(); + } + + /** Respond to a incoming call (invite) with resp */ + public void respond(Message resp) + { if (dialog!=null) dialog.respond(resp); + } + + /** Accepts the incoming call */ + /*public void accept() + { accept(local_sdp); + }*/ + + /** Accepts the incoming call */ + public void accept(String sdp) + { local_sdp=sdp; + if (dialog!=null) dialog.accept(contact_url,local_sdp); + } + + /** Redirects the incoming call */ + public void redirect(String redirect_url) + { if (dialog!=null) dialog.redirect(302,"Moved Temporarily",redirect_url); + } + + /** Refuses the incoming call */ + public void refuse() + { if (dialog!=null) dialog.refuse(); + } + + /** Cancels the outgoing call */ + public void cancel() + { if (dialog!=null) dialog.cancel(); + } + + /** Close the ongoing call */ + public void bye() + { if (dialog!=null) dialog.bye(); + } + + /** Modify the current call */ + public void modify(String contact, String sdp) + { local_sdp=sdp; + if (dialog!=null) dialog.reInvite(contact,local_sdp); + } + + /** Closes an ongoing or incoming/outgoing call + *

It trys to fires refuse(), cancel(), and bye() methods */ + public void hangup() + { if (dialog!=null) + { // try dialog.refuse(), cancel(), and bye() methods.. + dialog.refuse(); + dialog.cancel(); + dialog.bye(); + } + } + + + // ************** Inherited from InviteDialogListener ************** + + /** Inherited from class InviteDialogListener and called by an InviteDialag. Normally you should not use it. Use specific callback methods instead (e.g. onCallIncoming()). */ + public void onDlgInvite(InviteDialog d, NameAddress callee, NameAddress caller, String sdp, Message msg) + { if (d!=dialog) { printLog("NOT the current dialog",LogLevel.HIGH); return; } + if (sdp!=null && sdp.length()!=0) remote_sdp=sdp; + if (listener!=null) listener.onCallIncoming(this,callee,caller,sdp,msg); + } + + /** Inherited from class InviteDialogListener and called by an InviteDialag. Normally you should not use it. Use specific callback methods instead (e.g. onCallModifying()). */ + public void onDlgReInvite(InviteDialog d, String sdp, Message msg) + { if (d!=dialog) { printLog("NOT the current dialog",LogLevel.HIGH); return; } + if (sdp!=null && sdp.length()!=0) remote_sdp=sdp; + if (listener!=null) listener.onCallModifying(this,sdp,msg); + } + + /** Inherited from class InviteDialogListener and called by an InviteDialag. Normally you should not use it. Use specific callback methods instead (e.g. onCallRinging()). */ + public void onDlgInviteProvisionalResponse(InviteDialog d, int code, String reason, String sdp, Message msg) + { if (d!=dialog) { printLog("NOT the current dialog",LogLevel.HIGH); return; } + if (sdp!=null && sdp.length()!=0) remote_sdp=sdp; + if (code==180) if (listener!=null) listener.onCallRinging(this,msg); + } + + /** Inherited from class InviteDialogListener and called by an InviteDialag. Normally you should not use it. Use specific callback methods instead (e.g. onCallAccepted()). */ + public void onDlgInviteSuccessResponse(InviteDialog d, int code, String reason, String sdp, Message msg) + { if (d!=dialog) { printLog("NOT the current dialog",LogLevel.HIGH); return; } + if (sdp!=null && sdp.length()!=0) remote_sdp=sdp; + if (listener!=null) listener.onCallAccepted(this,sdp,msg); + } + + /** Inherited from class InviteDialogListener and called by an InviteDialag. Normally you should not use it. Use specific callback methods instead (e.g. onCallRedirection()). */ + public void onDlgInviteRedirectResponse(InviteDialog d, int code, String reason, MultipleHeader contacts, Message msg) + { if (d!=dialog) { printLog("NOT the current dialog",LogLevel.HIGH); return; } + if (listener!=null) listener.onCallRedirection(this,reason,contacts.getValues(),msg); + } + + /** Inherited from class InviteDialogListener and called by an InviteDialag. Normally you should not use it. Use specific callback methods instead (e.g. onCallRefused()). */ + public void onDlgInviteFailureResponse(InviteDialog d, int code, String reason, Message msg) + { if (d!=dialog) { printLog("NOT the current dialog",LogLevel.HIGH); return; } + if (listener!=null) listener.onCallRefused(this,reason,msg); + } + + /** Inherited from class InviteDialogListener and called by an InviteDialag. Normally you should not use it. Use specific callback methods instead (e.g. onCallTimeout()). */ + public void onDlgTimeout(InviteDialog d) + { if (d!=dialog) { printLog("NOT the current dialog",LogLevel.HIGH); return; } + if (listener!=null) listener.onCallTimeout(this); + } + + /** Inherited from class InviteDialogListener and called by an InviteDialag. Normally you should not use it. */ + public void onDlgReInviteProvisionalResponse(InviteDialog d, int code, String reason, String sdp, Message msg) + { if (d!=dialog) { printLog("NOT the current dialog",LogLevel.HIGH); return; } + if (sdp!=null && sdp.length()!=0) remote_sdp=sdp; + } + + /** Inherited from class InviteDialogListener and called by an InviteDialag. Normally you should not use it. Use specific callback methods instead (e.g. onCallReInviteAccepted()). */ + public void onDlgReInviteSuccessResponse(InviteDialog d, int code, String reason, String sdp, Message msg) + { if (d!=dialog) { printLog("NOT the current dialog",LogLevel.HIGH); return; } + if (sdp!=null && sdp.length()!=0) remote_sdp=sdp; + if (listener!=null) listener.onCallReInviteAccepted(this,sdp,msg); + } + + /** Inherited from class InviteDialogListener and called by an InviteDialag. Normally you should not use it. Use specific callback methods instead (e.g. onCallReInviteRedirection()). */ + //public void onDlgReInviteRedirectResponse(InviteDialog d, int code, String reason, MultipleHeader contacts, Message msg) + //{ if (d!=dialog) { printLog("NOT the current dialog",LogLevel.HIGH); return; } + // if (listener!=null) listener.onCallReInviteRedirection(this,reason,contacts.getValues(),msg); + //} + + /** Inherited from class InviteDialogListener and called by an InviteDialag. Normally you should not use it. Use specific callback methods instead (e.g. onCallReInviteRefused()). */ + public void onDlgReInviteFailureResponse(InviteDialog d, int code, String reason, Message msg) + { if (d!=dialog) { printLog("NOT the current dialog",LogLevel.HIGH); return; } + if (listener!=null) listener.onCallReInviteRefused(this,reason,msg); + } + + /** Inherited from class InviteDialogListener and called by an InviteDialag. Normally you should not use it. Use specific callback methods instead (e.g. onCallReInviteTimeout()). */ + public void onDlgReInviteTimeout(InviteDialog d) + { if (d!=dialog) { printLog("NOT the current dialog",LogLevel.HIGH); return; } + if (listener!=null) listener.onCallReInviteTimeout(this); + } + + /** Inherited from class InviteDialogListener and called by an InviteDialag. Normally you should not use it. Use specific callback methods instead (e.g. onCallConfirmed()). */ + public void onDlgAck(InviteDialog d, String sdp, Message msg) + { if (d!=dialog) { printLog("NOT the current dialog",LogLevel.HIGH); return; } + if (sdp!=null && sdp.length()!=0) remote_sdp=sdp; + if (listener!=null) listener.onCallConfirmed(this,sdp,msg); + } + + /** Inherited from class InviteDialogListener and called by an InviteDialag. Normally you should not use it. Use specific callback methods instead (e.g. onCallClosing()). */ + public void onDlgCancel(InviteDialog d, Message msg) + { if (d!=dialog) { printLog("NOT the current dialog",LogLevel.HIGH); return; } + if (listener!=null) listener.onCallCanceling(this,msg); + } + + /** Inherited from class InviteDialogListener and called by an InviteDialag. Normally you should not use it. Use specific callback methods instead (e.g. onClosing()). */ + public void onDlgBye(InviteDialog d, Message msg) + { if (d!=dialog) { printLog("NOT the current dialog",LogLevel.HIGH); return; } + if (listener!=null) listener.onCallClosing(this,msg); + } + + /** Inherited from class InviteDialogListener and called by an InviteDialag. Normally you should not use it. Use specific callback methods instead (e.g. onClosed()). */ + public void onDlgByeFailureResponse(InviteDialog d, int code, String reason, Message msg) + { if (d!=dialog) { printLog("NOT the current dialog",LogLevel.HIGH); return; } + if (listener!=null) listener.onCallClosed(this,msg); + } + + /** Inherited from class InviteDialogListener and called by an InviteDialag. Normally you should not use it. Use specific callback methods instead (e.g. onClosed()). */ + public void onDlgByeSuccessResponse(InviteDialog d, int code, String reason, Message msg) + { if (d!=dialog) { printLog("NOT the current dialog",LogLevel.HIGH); return; } + if (listener!=null) listener.onCallClosed(this,msg); + } + + // ----------------------------------------------------- + + /** When an incoming INVITE is accepted */ + //public void onDlgAccepted(InviteDialog dialog) {} + + /** When an incoming INVITE is refused */ + //public void onDlgRefused(InviteDialog dialog) {} + + /** When the INVITE handshake is successful terminated */ + public void onDlgCall(InviteDialog dialog) {} + + /** When an incoming Re-INVITE is accepted */ + //public void onDlgReInviteAccepted(InviteDialog dialog) {} + + /** When an incoming Re-INVITE is refused */ + //public void onDlgReInviteRefused(InviteDialog dialog) {} + + /** When a BYE request traqnsaction has been started */ + //public void onDlgByeing(InviteDialog dialog) {} + + /** When the dialog is finally closed */ + public void onDlgClose(InviteDialog dialog) {} + + + //**************************** Logs ****************************/ + + /** Adds a new string to the default Log */ + protected void printLog(String str, int level) + { if (log!=null) log.println("Call: "+str,level+SipStack.LOG_LEVEL_CALL); + } +} + diff --git a/src/org/zoolu/sip/call/CallListener.java b/src/org/zoolu/sip/call/CallListener.java new file mode 100644 index 0000000..7094a52 --- /dev/null +++ b/src/org/zoolu/sip/call/CallListener.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.call; + +import org.zoolu.sip.message.*; +import org.zoolu.sip.address.NameAddress; +import org.zoolu.sdp.*; +import java.util.Vector; + + +/** Interface CallListener can be implemented to manage SIP calls (sipx.call.Call). + *

Objects of class Call use CallListener callback methods to signal + * specific call events. + */ +public interface CallListener +{ + /** Callback function called when arriving a new INVITE method (incoming call) */ + public void onCallIncoming(Call call, NameAddress callee, NameAddress caller, String sdp, Message invite); + + /** Callback function called when arriving a new Re-INVITE method (re-inviting/call modify) */ + public void onCallModifying(Call call, String sdp, Message invite); + + /** Callback function called when arriving a 180 Ringing */ + public void onCallRinging(Call call, Message resp); + + /** Callback function called when arriving a 2xx (call accepted) */ + public void onCallAccepted(Call call, String sdp, Message resp); + + /** Callback function called when arriving a 4xx (call failure) */ + public void onCallRefused(Call call, String reason, Message resp); + + /** Callback function called when arriving a 3xx (call redirection) */ + public void onCallRedirection(Call call, String reason, Vector contact_list, Message resp); + + /** Callback function called when arriving an ACK method (call confirmed) */ + public void onCallConfirmed(Call call, String sdp, Message ack); + + /** Callback function called when the invite expires */ + public void onCallTimeout(Call call); + + /** Callback function called when arriving a 2xx (re-invite/modify accepted) */ + public void onCallReInviteAccepted(Call call, String sdp, Message resp); + + /** Callback function called when arriving a 4xx (re-invite/modify failure) */ + public void onCallReInviteRefused(Call call, String reason, Message resp); + + /** Callback function called when a re-invite expires */ + public void onCallReInviteTimeout(Call call); + + /** Callback function called when arriving a 3xx (call redirection) */ + //public void onCallReInviteRedirection(Call call, String reason, Vector contact_list, Message resp); + + /** Callback function called when arriving a CANCEL request */ + public void onCallCanceling(Call call, Message cancel); + + /** Callback function called when arriving a BYE request */ + public void onCallClosing(Call call, Message bye); + + /** Callback function called when arriving a response for the BYE request (call closed) */ + public void onCallClosed(Call call, Message resp); +} + diff --git a/src/org/zoolu/sip/call/CallListenerAdapter.java b/src/org/zoolu/sip/call/CallListenerAdapter.java new file mode 100644 index 0000000..bdd1b2f --- /dev/null +++ b/src/org/zoolu/sip/call/CallListenerAdapter.java @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.call; + + +import org.zoolu.sip.call.*; +import org.zoolu.sip.provider.SipStack; +import org.zoolu.sip.address.NameAddress; +import org.zoolu.sip.message.Message; +import org.zoolu.tools.Log; +import org.zoolu.tools.LogLevel; +import org.zoolu.sdp.*; +//import java.util.Iterator; +import java.util.Enumeration; +import java.util.Vector; + + +/** Class CallListenerAdapter implements CallListener interface + * providing a dummy implementation of all Call callback functions used to capture Call events. + *

CallListenerAdapter can be extended to manage basic SIP calls. + * The callback methods defined in this class have basically a void implementation. + * This class exists as convenience for creating call listener objects. + *
You can extend this class overriding only methods corresponding to events + * you want to handle. + *

onCallIncoming(NameAddress,String) is the only non-empty method. + * It signals the receiver the ring status (by using method Call.ring()), + * adapts the sdp body and accepts the call (by using method Call.accept(sdp)). + */ +public abstract class CallListenerAdapter implements ExtendedCallListener +{ + + // ************************** Costructors *************************** + + /** Creates a new dummy call listener */ + protected CallListenerAdapter() + { + } + + + // ************************* Static methods ************************* + + /** Changes the current session descriptor specifing the receiving RTP/UDP port number, the AVP format, the codec, and the clock rate */ + /*public static String audioSession(int port, int avp, String codec, int rate) + { SessionDescriptor sdp=new SessionDescriptor(); + sdp.addMedia(new MediaField("audio ",port,0,"RTP/AVP",String.valueOf(avp)),new AttributeField("rtpmap",avp+" "+codec+"/"+rate)); + return sdp.toString(); + }*/ + + /** Changes the current session descriptor specifing the receiving RTP/UDP port number, the AVP format, the codec, and the clock rate */ + /*public static String audioSession(int port) + { return audioSession(port,0,"PCMU",8000); + }*/ + + + // *********************** Callback functions *********************** + + /** Accepts an incoming call. + * Callback function called when arriving a new INVITE method (incoming call) */ + public void onCallIncoming(Call call, NameAddress callee, NameAddress caller, String sdp, Message invite) + { //printLog("INCOMING"); + call.ring(); + String local_session; + if (sdp!=null && sdp.length()>0) + { SessionDescriptor remote_sdp=new SessionDescriptor(sdp); + SessionDescriptor local_sdp=new SessionDescriptor(call.getLocalSessionDescriptor()); + SessionDescriptor new_sdp=new SessionDescriptor(remote_sdp.getOrigin(),remote_sdp.getSessionName(),local_sdp.getConnection(),local_sdp.getTime()); + new_sdp.addMediaDescriptors(local_sdp.getMediaDescriptors()); + new_sdp=SdpTools.sdpMediaProduct(new_sdp,remote_sdp.getMediaDescriptors()); + new_sdp=SdpTools.sdpAttirbuteSelection(new_sdp,"rtpmap"); + local_session=new_sdp.toString(); + } + else local_session=call.getLocalSessionDescriptor(); + // accept immediatly + call.accept(local_session); + } + + /** Changes the call when remotly requested. + * Callback function called when arriving a new Re-INVITE method (re-inviting/call modify) */ + public void onCallModifying(Call call, String sdp, Message invite) + { //printLog("RE-INVITE/MODIFY"); + String local_session; + if (sdp!=null && sdp.length()>0) + { SessionDescriptor remote_sdp=new SessionDescriptor(sdp); + SessionDescriptor local_sdp=new SessionDescriptor(call.getLocalSessionDescriptor()); + SessionDescriptor new_sdp=new SessionDescriptor(remote_sdp.getOrigin(),remote_sdp.getSessionName(),local_sdp.getConnection(),local_sdp.getTime()); + new_sdp.addMediaDescriptors(local_sdp.getMediaDescriptors()); + new_sdp=SdpTools.sdpMediaProduct(new_sdp,remote_sdp.getMediaDescriptors()); + new_sdp=SdpTools.sdpAttirbuteSelection(new_sdp,"rtpmap"); + local_session=new_sdp.toString(); + } + else local_session=call.getLocalSessionDescriptor(); + // accept immediatly + call.accept(local_session); + } + + /** Does nothing. + * Callback function called when arriving a 180 Ringing */ + public void onCallRinging(Call call, Message resp) + { //printLog("RINGING"); + } + + /** Does nothing. + * Callback function called when arriving a 2xx (call accepted) */ + public void onCallAccepted(Call call, String sdp, Message resp) + { //printLog("ACCEPTED/CALL"); + } + + /** Does nothing. + * Callback function called when arriving a 4xx (call failure) */ + public void onCallRefused(Call call, String reason, Message resp) + { //printLog("REFUSED ("+reason+")"); + } + + /** Redirects the call when remotly requested. + * Callback function called when arriving a 3xx (call redirection) */ + public void onCallRedirection(Call call, String reason, Vector contact_list, Message resp) + { //printLog("REDIRECTION ("+reason+")"); + call.call((String)contact_list.elementAt(0)); + } + + /** Does nothing. + * Callback function called when arriving an ACK method (call confirmed) */ + public void onCallConfirmed(Call call, String sdp, Message ack) + { //printLog("CONFIRMED/CALL"); + } + + /** Does nothing. + * Callback function called when the invite expires */ + public void onCallTimeout(Call call) + { //printLog("TIMEOUT/CLOSE"); + } + + /** Does nothing. + * Callback function called when arriving a 2xx (re-invite/modify accepted) */ + public void onCallReInviteAccepted(Call call, String sdp, Message resp) + { //printLog("RE-INVITE-ACCEPTED/CALL"); + } + + /** Does nothing. + * Callback function called when arriving a 4xx (re-invite/modify failure) */ + public void onCallReInviteRefused(Call call, String reason, Message resp) + { //printLog("RE-INVITE-REFUSED ("+reason+")/CALL"); + } + + /** Does nothing. + * Callback function called when a re-invite expires */ + public void onCallReInviteTimeout(Call call) + { //printLog("RE-INVITE-TIMEOUT/CALL"); + } + + /** Does nothing. + * Callback function called when arriving a CANCEL request */ + public void onCallCanceling(Call call, Message cancel) + { //printLog("CANCELING"); + } + + /** Does nothing. + * Callback function that may be overloaded (extended). Called when arriving a BYE request */ + public void onCallClosing(Call call, Message bye) + { //printLog("CLOSING"); + } + + /** Does nothing. + * Callback function that may be overloaded (extended). Called when arriving a response for a BYE request (call closed) */ + public void onCallClosed(Call call, Message resp) + { //printLog("CLOSED"); + } + + + /** Does nothing. + * Callback function called when arriving a new REFER method (transfer request) */ + public void onCallTransfer(ExtendedCall call, NameAddress refer_to, NameAddress refered_by, Message refer) + { //printLog("REFER-TO/TRANSFER"); + } + + /** Does nothing. + * Callback function called when a call transfer is accepted. */ + public void onCallTransferAccepted(ExtendedCall call, Message resp) + { + } + + /** Does nothing. + * Callback function called when a call transfer is refused. */ + public void onCallTransferRefused(ExtendedCall call, String reason, Message resp) + { + } + + /** Does nothing. + * Callback function called when a call transfer is successfully completed */ + public void onCallTransferSuccess(ExtendedCall call, Message notify) + { //printLog("TRANSFER SUCCESS"); + } + + /** Does nothing. + * Callback function called when a call transfer is NOT sucessfully completed */ + public void onCallTransferFailure(ExtendedCall call, String reason, Message notify) + { //printLog("TRANSFER FAILURE"); + } + +} + diff --git a/src/org/zoolu/sip/call/ExtendedCall.java b/src/org/zoolu/sip/call/ExtendedCall.java new file mode 100644 index 0000000..51259cd --- /dev/null +++ b/src/org/zoolu/sip/call/ExtendedCall.java @@ -0,0 +1,225 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.call; + + +import org.zoolu.sip.call.*; +import org.zoolu.sip.provider.*; +import org.zoolu.sip.message.*; +//import org.zoolu.sip.dialog.*; +import org.zoolu.sip.header.StatusLine; +import org.zoolu.sip.address.NameAddress; +import org.zoolu.sip.dialog.ExtendedInviteDialog; +import org.zoolu.sip.dialog.ExtendedInviteDialogListener; +import org.zoolu.tools.Log; +import org.zoolu.tools.LogLevel; +import org.zoolu.sdp.*; +import java.util.Vector; + + +/** Class ExtendedCall extends basic SIP calls. + *

It implements: + *
- call transfer (REFER/NOTIFY methods) + */ +public class ExtendedCall extends Call implements ExtendedInviteDialogListener +{ + + ExtendedCallListener xcall_listener; + + Message refer; + + + /** User name. */ + String username; + + /** User name. */ + String realm; + + /** User's passwd. */ + String passwd; + + /** Nonce for the next authentication. */ + String next_nonce; + + /** Qop for the next authentication. */ + String qop; + + + /** Creates a new ExtendedCall. */ + public ExtendedCall(SipProvider sip_provider, String from_url, String contact_url, ExtendedCallListener call_listener) + { super(sip_provider,from_url,contact_url,call_listener); + this.xcall_listener=call_listener; + this.refer=null; + this.username=null; + this.realm=null; + this.passwd=null; + this.next_nonce=null; + this.qop=null; + } + + + /** Creates a new ExtendedCall specifing the sdp. */ + /*public ExtendedCall(SipProvider sip_provider, String from_url, String contact_url, String sdp, ExtendedCallListener call_listener) + { super(sip_provider,from_url,contact_url,sdp,call_listener); + xcall_listener=call_listener; + }*/ + + + /** Creates a new ExtendedCall. */ + public ExtendedCall(SipProvider sip_provider, String from_url, String contact_url, String username, String realm, String passwd, ExtendedCallListener call_listener) + { super(sip_provider,from_url,contact_url,call_listener); + this.xcall_listener=call_listener; + this.refer=null; + this.username=username; + this.realm=realm; + this.passwd=passwd; + this.next_nonce=null; + this.qop=null; + } + + + /** Waits for an incoming call */ + public void listen() + { if (username!=null) dialog=new ExtendedInviteDialog(sip_provider,username,realm,passwd,this); + else dialog=new ExtendedInviteDialog(sip_provider,this); + dialog.listen(); + } + + + /** Starts a new call, inviting a remote user (r_user) */ + public void call(String r_user, String from, String contact, String sdp) + { printLog("calling "+r_user,LogLevel.MEDIUM); + if (username!=null) dialog=new ExtendedInviteDialog(sip_provider,username,realm,passwd,this); + else dialog=new ExtendedInviteDialog(sip_provider,this); + if (from==null) from=from_url; + if (contact==null) contact=contact_url; + if (sdp!=null) local_sdp=sdp; + if (local_sdp!=null) + dialog.invite(r_user,from,contact,local_sdp); + else dialog.inviteWithoutOffer(r_user,from,contact); + } + + + /** Starts a new call with the invite message request */ + public void call(Message invite) + { dialog=new ExtendedInviteDialog(sip_provider,this); + local_sdp=invite.getBody(); + if (local_sdp!=null) + dialog.invite(invite); + else dialog.inviteWithoutOffer(invite); + } + + + /** Requests a call transfer */ + public void transfer(String transfer_to) + { ((ExtendedInviteDialog)dialog).refer(new NameAddress(transfer_to)); + } + + /** Accepts a call transfer request */ + public void acceptTransfer() + { ((ExtendedInviteDialog)dialog).acceptRefer(refer); + } + + + /** Refuses a call transfer request */ + public void refuseTransfer() + { ((ExtendedInviteDialog)dialog).refuseRefer(refer); + } + + + /** Notifies the satus of an other call */ + public void notify(int code, String reason) + { ((ExtendedInviteDialog)dialog).notify(code,reason); + } + + + // ************** Inherited from InviteDialogListener ************** + + + /** When an incoming REFER request is received within the dialog */ + public void onDlgRefer(org.zoolu.sip.dialog.InviteDialog d, NameAddress refer_to, NameAddress referred_by, Message msg) + { if (d!=dialog) { printLog("NOT the current dialog",LogLevel.HIGH); return; } + printLog("onDlgRefer("+refer_to.toString()+")",LogLevel.LOW); + refer=msg; + if (xcall_listener!=null) xcall_listener.onCallTransfer(this,refer_to,referred_by,msg); + } + + /** When a response is received for a REFER request within the dialog */ + public void onDlgReferResponse(org.zoolu.sip.dialog.InviteDialog d, int code, String reason, Message msg) + { if (d!=dialog) { printLog("NOT the current dialog",LogLevel.HIGH); return; } + printLog("onDlgReferResponse("+code+" "+reason+")",LogLevel.LOW); + if (code>=200 && code <300) + { if(xcall_listener!=null) xcall_listener.onCallTransferAccepted(this,msg); + } + else + if (code>=300) + { if(xcall_listener!=null) xcall_listener.onCallTransferRefused(this,reason,msg); + } + } + + /** When an incoming NOTIFY request is received within the dialog */ + public void onDlgNotify(org.zoolu.sip.dialog.InviteDialog d, String event, String sipfragment, Message msg) + { if (d!=dialog) { printLog("NOT the current dialog",LogLevel.HIGH); return; } + printLog("onDlgNotify()",LogLevel.LOW); + if (event.equals("refer")) + { Message fragment=new Message(sipfragment); + printLog("Notify: "+sipfragment,LogLevel.HIGH); + if (fragment.isResponse()) + { StatusLine status_line=fragment.getStatusLine(); + int code=status_line.getCode(); + String reason=status_line.getReason(); + if (code>=200 && code<300) + { printLog("Call successfully transferred",LogLevel.MEDIUM); + if(xcall_listener!=null) xcall_listener.onCallTransferSuccess(this,msg); + } + else + if (code>=300) + { printLog("Call NOT transferred",LogLevel.MEDIUM); + if(xcall_listener!=null) xcall_listener.onCallTransferFailure(this,reason,msg); + } + } + } + } + + /** When an incoming request is received within the dialog + * different from INVITE, CANCEL, ACK, BYE */ + public void onDlgAltRequest(org.zoolu.sip.dialog.InviteDialog d, String method, String body, Message msg) + { + } + + /** When a response is received for a request within the dialog + * different from INVITE, CANCEL, ACK, BYE */ + public void onDlgAltResponse(org.zoolu.sip.dialog.InviteDialog d, String method, int code, String reason, String body, Message msg) + { + } + + + //**************************** Logs ****************************/ + + /** Adds a new string to the default Log */ + protected void printLog(String str, int level) + { if (log!=null) log.println("ExtendedCall: "+str,level+SipStack.LOG_LEVEL_CALL); + } +} + diff --git a/src/org/zoolu/sip/call/ExtendedCallListener.java b/src/org/zoolu/sip/call/ExtendedCallListener.java new file mode 100644 index 0000000..810f98e --- /dev/null +++ b/src/org/zoolu/sip/call/ExtendedCallListener.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.call; + +import org.zoolu.sip.call.*; +import org.zoolu.sip.message.*; +import org.zoolu.sip.address.NameAddress; +import org.zoolu.sdp.*; +import java.util.Vector; + + +/** Interface ExtendedCallListener can be implemented to manage exteded SIP calls (sipx.call.ExtendedCall). + *

Objects of class ExtendedCall use ExtendedCallListener callback methods to signal + * specific call events. + */ +public interface ExtendedCallListener extends CallListener +{ + /** Callback function called when arriving a new REFER method (transfer request). */ + public void onCallTransfer(ExtendedCall call, NameAddress refer_to, NameAddress refered_by, Message refer); + + /** Callback function called when a call transfer is accepted. */ + public void onCallTransferAccepted(ExtendedCall call, Message resp); + + /** Callback function called when a call transfer is refused. */ + public void onCallTransferRefused(ExtendedCall call, String reason, Message resp); + + /** Callback function called when a call transfer is successfully completed. */ + public void onCallTransferSuccess(ExtendedCall call, Message notify); + + /** Callback function called when a call transfer is NOT sucessfully completed. */ + public void onCallTransferFailure(ExtendedCall call, String reason, Message notify); + +} + diff --git a/src/org/zoolu/sip/call/SdpTools.java b/src/org/zoolu/sip/call/SdpTools.java new file mode 100644 index 0000000..80930dd --- /dev/null +++ b/src/org/zoolu/sip/call/SdpTools.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.call; + + +import org.zoolu.sdp.*; +import java.util.Enumeration; +import java.util.Vector; + + +/** Class SdpTools collects some static methods for managing SDP materials. + */ +public class SdpTools +{ + /** Costructs a new SessionDescriptor from a given SessionDescriptor + * with olny media types and attribute values specified by a MediaDescriptor Vector. + *

If no attribute is specified for a particular media, all present attributes are kept. + *
If no attribute is present for a selected media, the media is kept (regardless any sepcified attributes). + * @param sdp the given SessionDescriptor + * @param m_descs Vector of MediaDescriptor with the selecting media types and attributes + * @return this SessionDescriptor */ + public static SessionDescriptor sdpMediaProduct(SessionDescriptor sdp, Vector m_descs) + { Vector new_media=new Vector(); + if (m_descs!=null) + { for (Enumeration e=m_descs.elements(); e.hasMoreElements(); ) + { MediaDescriptor spec_md=(MediaDescriptor)e.nextElement(); + //System.out.print("DEBUG: SDP: sdp_select: "+spec_md.toString()); + MediaDescriptor prev_md=sdp.getMediaDescriptor(spec_md.getMedia().getMedia()); + //System.out.print("DEBUG: SDP: sdp_origin: "+prev_md.toString()); + if (prev_md!=null) + { Vector spec_attributes=spec_md.getAttributes(); + Vector prev_attributes=prev_md.getAttributes(); + if (spec_attributes.size()==0 || prev_attributes.size()==0) + { new_media.addElement(prev_md); + } + else + { Vector new_attributes=new Vector(); + for (Enumeration i=spec_attributes.elements(); i.hasMoreElements(); ) + { AttributeField spec_attr=(AttributeField)i.nextElement(); + String spec_name=spec_attr.getAttributeName(); + String spec_value=spec_attr.getAttributeValue(); + for (Enumeration k=prev_attributes.elements(); k.hasMoreElements(); ) + { AttributeField prev_attr=(AttributeField)k.nextElement(); + String prev_name=prev_attr.getAttributeName(); + String prev_value=prev_attr.getAttributeValue(); + if (prev_name.equals(spec_name) && prev_value.equalsIgnoreCase(spec_value)) + { new_attributes.addElement(prev_attr); + break; + } + } + } + if (new_attributes.size()>0) new_media.addElement(new MediaDescriptor(prev_md.getMedia(),prev_md.getConnection(),new_attributes)); + } + } + } + } + SessionDescriptor new_sdp=new SessionDescriptor(sdp); + new_sdp.removeMediaDescriptors(); + new_sdp.addMediaDescriptors(new_media); + return new_sdp; + } + + /** Costructs a new SessionDescriptor from a given SessionDescriptor + * with olny the first specified media attribute. + /** Keeps only the fisrt attribute of the specified type for each media. + *

If no attribute is present for a media, the media is dropped. + * @param sdp the given SessionDescriptor + * @param a_name the attribute name + * @return this SessionDescriptor */ + public static SessionDescriptor sdpAttirbuteSelection(SessionDescriptor sdp, String a_name) + { Vector new_media=new Vector(); + for (Enumeration e=sdp.getMediaDescriptors().elements(); e.hasMoreElements(); ) + { MediaDescriptor md=(MediaDescriptor)e.nextElement(); + AttributeField attr=md.getAttribute(a_name); + if (attr!=null) + { new_media.addElement(new MediaDescriptor(md.getMedia(),md.getConnection(),attr)); + } + } + SessionDescriptor new_sdp=new SessionDescriptor(sdp); + new_sdp.removeMediaDescriptors(); + new_sdp.addMediaDescriptors(new_media); + return new_sdp; + } + +} diff --git a/src/org/zoolu/sip/dialog/Dialog.java b/src/org/zoolu/sip/dialog/Dialog.java new file mode 100644 index 0000000..db41304 --- /dev/null +++ b/src/org/zoolu/sip/dialog/Dialog.java @@ -0,0 +1,277 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.dialog; + + +import org.zoolu.sip.address.*; +import org.zoolu.sip.message.*; +import org.zoolu.sip.header.*; +import org.zoolu.sip.provider.*; +import org.zoolu.tools.Log; +import org.zoolu.tools.LogLevel; +import org.zoolu.tools.AssertException; + + +import java.util.Vector; + + +/** Class Dialog maintains a complete information status of a generic SIP dialog. + * It has the following attributes: + *

    + *
  • sip-provider
  • + *
  • call-id
  • + *
  • local and remote URLs
  • + *
  • local and remote contact URLs
  • + *
  • local and remote cseqs
  • + *
  • local and remote tags
  • + *
  • dialog-id
  • + *
  • route set
  • + *
+ */ +public abstract class Dialog extends DialogInfo implements SipProviderListener +{ + + // ************************ Static attributes ************************* + + /** Dialogs counter */ + private static int dialog_counter=0; + + + /** Identifier for the transaction client side of a dialog (UAC). */ + public final static int UAC=0; + /** Identifier for the transaction server side of a dialog (UAS). */ + public final static int UAS=1; + + + // *********************** Protected attributes *********************** + + /** Dialog sequence number */ + protected int dialog_sqn; + + /** Event logger. */ + protected Log log; + + /** SipProvider */ + protected SipProvider sip_provider; + + /** Internal dialog state. */ + protected int status; + + /** Dialog identifier */ + protected DialogIdentifier dialog_id; + + + // ************************* Abstract methods ************************* + + /** Gets the dialog state */ + abstract protected String getStatus(); + + /** Whether the dialog is in "early" state. */ + abstract public boolean isEarly(); + + /** Whether the dialog is in "confirmed" state. */ + abstract public boolean isConfirmed(); + + /** Whether the dialog is in "terminated" state. */ + abstract public boolean isTerminated(); + + /** When a new Message is received by the SipProvider. */ + abstract public void onReceivedMessage(SipProvider provider, Message message); + + + // **************************** Costructors *************************** + + /** Creates a new empty Dialog */ + protected Dialog(SipProvider provider) + { super(); + this.sip_provider=provider; + this.log=sip_provider.getLog(); + this.dialog_sqn=dialog_counter++; + this.status=0; + this.dialog_id=null; + } + + + // ************************* Protected methods ************************ + + /** Changes the internal dialog state */ + protected void changeStatus(int newstatus) + { status=newstatus; + printLog("changed dialog state: "+getStatus(),LogLevel.MEDIUM); + + // remove the sip_provider listener when going to "terminated" state + if (isTerminated()) + { if (dialog_id!=null && sip_provider.getListeners().containsKey(dialog_id)) sip_provider.removeSipProviderListener(dialog_id); + } + else + // add sip_provider listener when going to "early" or "confirmed" state + if (isEarly() || isConfirmed()) + { if (dialog_id!=null && !sip_provider.getListeners().containsKey(dialog_id)) sip_provider.addSipProviderListener(dialog_id,this); + } + } + + + /** Whether the dialog state is equal to st */ + protected boolean statusIs(int st) + { return status==st; + } + + + // ************************** Public methods ************************** + + /** Gets the SipProvider of this Dialog. */ + public SipProvider getSipProvider() + { return sip_provider; + } + + + /** Gets the inique Dialog-ID */ + public DialogIdentifier getDialogID() + { return dialog_id; + } + + + /** Updates empty attributes (tags, route set) and mutable attributes (cseqs, contacts), based on a new message. + * @param side indicates whether the Dialog is acting as transaction client or server for the current message (use constant values Dialog.UAC or Dialog.UAS) + * @param msg the message that is used to update the Dialog state */ + public void update(int side, Message msg) + { + if (isTerminated()) + { printWarning("trying to update a terminated dialog: do nothing.",LogLevel.HIGH); + return; + } + // else + + // update call_id + if (call_id==null) call_id=msg.getCallIdHeader().getCallId(); + + // update names and tags + if (side==UAC) + { if (remote_name==null || remote_tag==null) + { ToHeader to=msg.getToHeader(); + if (remote_name==null) remote_name=to.getNameAddress(); + if (remote_tag==null) remote_tag=to.getTag(); + } + if (local_name==null || local_tag==null) + { FromHeader from=msg.getFromHeader(); + if (local_name==null) local_name=from.getNameAddress(); + if (local_tag==null) local_tag=from.getTag(); + } + local_cseq=msg.getCSeqHeader().getSequenceNumber(); + //if (remote_cseq==-1) remote_cseq=SipProvider.pickInitialCSeq()-1; + } + else + { if (local_name==null || local_tag==null) + { ToHeader to=msg.getToHeader(); + if (local_name==null) local_name=to.getNameAddress(); + if (local_tag==null) local_tag=to.getTag(); + } + if (remote_name==null || remote_tag==null) + { FromHeader from=msg.getFromHeader(); + if (remote_name==null) remote_name=from.getNameAddress(); + if (remote_tag==null) remote_tag=from.getTag(); + } + remote_cseq=msg.getCSeqHeader().getSequenceNumber(); + if (local_cseq==-1) local_cseq=SipProvider.pickInitialCSeq()-1; + } + // update contact + if (msg.hasContactHeader()) + { if ((side==UAC && msg.isRequest()) || (side==UAS && msg.isResponse())) + local_contact=msg.getContactHeader().getNameAddress(); + else + remote_contact=msg.getContactHeader().getNameAddress(); + } + // update route or record-route + if (side==UAC) + { if (msg.isRequest() && msg.hasRouteHeader() && route==null) + { route=msg.getRoutes().getValues(); + } + if (side==UAC && msg.isResponse() && msg.hasRecordRouteHeader()) + { Vector rr=msg.getRecordRoutes().getHeaders(); + int size=rr.size(); + route=new Vector(size); + for (int i=0; i + *
  • sip-provider
  • + *
  • call-id
  • + *
  • local and remote URLs
  • + *
  • local and remote contact URLs
  • + *
  • local and remote cseqs
  • + *
  • local and remote tags
  • + *
  • dialog-id
  • + *
  • route set
  • + * + */ +public class DialogInfo +{ + + // ************************ Private attributes ************************ + + /** Local name */ + NameAddress local_name; + + /** Remote name */ + NameAddress remote_name; + + /** Local contact url */ + NameAddress local_contact; + + /** Remote contact url */ + NameAddress remote_contact; + + /** Call-id */ + String call_id; + + /** Local tag */ + String local_tag; + + /** Remote tag */ + String remote_tag; + /** Sets the remote tag */ + + /** Local CSeq number */ + long local_cseq; + + /** Remote CSeq number */ + long remote_cseq; + + /** Route set (Vector of NameAddresses) */ + Vector route; + + + // **************************** Costructors *************************** + + /** Creates a new empty DialogInfo */ + public DialogInfo() + { this.local_name=null; + this.remote_name=null; + this.local_contact=null; + this.remote_contact=null; + this.call_id=null; + this.local_tag=null; + this.remote_tag=null; + this.local_cseq=-1; + this.remote_cseq=-1; + this.route=null; + } + + + // ************************** Public methods ************************** + + /** Sets the local name */ + public void setLocalName(NameAddress url) { local_name=url; } + /** Gets the local name */ + public NameAddress getLocalName() { return local_name; } + + + /** Sets the remote name */ + public void setRemoteName(NameAddress url) { remote_name=url; } + /** Gets the remote name */ + public NameAddress getRemoteName() { return remote_name; } + + + /** Sets the local contact url */ + public void setLocalContact(NameAddress name_address) { local_contact=name_address; } + /** Gets the local contact url */ + public NameAddress getLocalContact() { return local_contact; } + + + /** Sets the remote contact url */ + public void setRemoteContact(NameAddress name_address) { remote_contact=name_address; } + /** Gets the remote contact url */ + public NameAddress getRemoteContact() { return remote_contact; } + + + /** Sets the call-id */ + public void setCallID(String id) { call_id=id; } + /** Gets the call-id */ + public String getCallID() { return call_id; } + + + /** Sets the local tag */ + public void setLocalTag(String tag) { local_tag=tag; } + /** Gets the local tag */ + public String getLocalTag() { return local_tag; } + + + public void setRemoteTag(String tag) { remote_tag=tag; } + /** Gets the remote tag */ + public String getRemoteTag() { return remote_tag; } + + + /** Sets the local CSeq number */ + public void setLocalCSeq(long cseq) { local_cseq=cseq; } + /** Increments the local CSeq number */ + public void incLocalCSeq() { local_cseq++; } + /** Gets the local CSeq number */ + public long getLocalCSeq() { return local_cseq; } + + + /** Sets the remote CSeq number */ + public void setRemoteCSeq(long cseq) { remote_cseq=cseq; } + /** Increments the remote CSeq number */ + public void incRemoteCSeq() { remote_cseq++; } + /** Gets the remote CSeq number */ + public long getRemoteCSeq() { return remote_cseq; } + + + /** Sets the route set */ + public void setRoute(Vector r) { route=r; } + /** Gets the route set */ + public Vector getRoute() { return route; } + +} diff --git a/src/org/zoolu/sip/dialog/ExtendedInviteDialog.java b/src/org/zoolu/sip/dialog/ExtendedInviteDialog.java new file mode 100644 index 0000000..3a21c5d --- /dev/null +++ b/src/org/zoolu/sip/dialog/ExtendedInviteDialog.java @@ -0,0 +1,313 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.dialog; + + +import org.zoolu.sip.provider.*; +import org.zoolu.sip.address.NameAddress; +import org.zoolu.sip.header.StatusLine; +import org.zoolu.sip.header.RequestLine; +import org.zoolu.sip.header.AuthorizationHeader; +import org.zoolu.sip.header.WwwAuthenticateHeader; +import org.zoolu.sip.header.ProxyAuthenticateHeader; +import org.zoolu.sip.transaction.*; +import org.zoolu.sip.message.*; +import org.zoolu.sip.authentication.DigestAuthentication; +import org.zoolu.tools.LogLevel; + +import java.util.Hashtable; + + +/** Class ExtendedInviteDialog can be used to manage extended invite dialogs. + *

    + * An ExtendedInviteDialog allows the user: + *
    - to handle authentication + *
    - to handle refer/notify + *
    - to capture all methods within the dialog + */ +public class ExtendedInviteDialog extends org.zoolu.sip.dialog.InviteDialog +{ + /** Max number of registration attempts. */ + static final int MAX_ATTEMPTS=3; + + /** ExtendedInviteDialog listener. */ + ExtendedInviteDialogListener dialog_listener; + + /** Acive transactions. */ + Hashtable transactions; + + + /** User name. */ + String username; + + /** User name. */ + String realm; + + /** User's passwd. */ + String passwd; + + /** Nonce for the next authentication. */ + String next_nonce; + + /** Qop for the next authentication. */ + String qop; + + /** Number of authentication attempts. */ + int attempts; + + + + + /** Creates a new ExtendedInviteDialog. */ + public ExtendedInviteDialog(SipProvider provider, ExtendedInviteDialogListener listener) + { super(provider,listener); + init(listener); + } + + /** Creates a new ExtendedInviteDialog. */ + public ExtendedInviteDialog(SipProvider provider, String username, String realm, String passwd, ExtendedInviteDialogListener listener) + { super(provider,listener); + init(listener); + this.username=username; + this.realm=realm; + this.passwd=passwd; + } + + /** Inits the ExtendedInviteDialog. */ + private void init(ExtendedInviteDialogListener listener) + { this.dialog_listener=listener; + this.transactions=new Hashtable(); + this.username=null; + this.realm=null; + this.passwd=null; + this.next_nonce=null; + this.qop=null; + this.attempts=0; + } + + + /** Sends a new request within the dialog */ + public void request(Message req) + { TransactionClient t=new TransactionClient(sip_provider,req,this); + transactions.put(t.getTransactionId(),t); + t.request(); + } + + + /** Sends a new REFER within the dialog */ + public void refer(NameAddress refer_to) + { refer(refer_to,null); + } + + /** Sends a new REFER within the dialog */ + public void refer(NameAddress refer_to, NameAddress referred_by) + { Message req=MessageFactory.createReferRequest(this,refer_to,referred_by); + request(req); + } + + + /** Sends a new NOTIFY within the dialog */ + public void notify(int code, String reason) + { notify((new StatusLine(code,reason)).toString()); + } + + /** Sends a new NOTIFY within the dialog */ + public void notify(String sipfragment) + { Message req=MessageFactory.createNotifyRequest(this,"refer",null,sipfragment); + request(req); + } + + + /** Responds with resp */ + public void respond(Message resp) + { printLog("inside respond(resp)",LogLevel.MEDIUM); + String method=resp.getCSeqHeader().getMethod(); + if (method.equals(SipMethods.INVITE) || method.equals(SipMethods.CANCEL) || method.equals(SipMethods.BYE)) + { super.respond(resp); + } + else + { TransactionIdentifier transaction_id=resp.getTransactionId(); + printLog("transaction-id="+transaction_id,LogLevel.MEDIUM); + if (transactions.containsKey(transaction_id)) + { printLog("responding",LogLevel.LOW); + TransactionServer t=(TransactionServer)transactions.get(transaction_id); + t.respondWith(resp); + } + else + printLog("transaction server not found; message discarded",LogLevel.MEDIUM); + } + } + + + /** Accept a REFER */ + public void acceptRefer(Message req) + { printLog("inside acceptRefer(refer)",LogLevel.MEDIUM); + Message resp=MessageFactory.createResponse(req,202,SipResponses.reasonOf(200),null); + respond(resp); + } + + + /** Refuse a REFER */ + public void refuseRefer(Message req) + { printLog("inside refuseRefer(refer)",LogLevel.MEDIUM); + Message resp=MessageFactory.createResponse(req,603,SipResponses.reasonOf(603),null); + respond(resp); + } + + + /** Inherited from class SipProviderListener. */ + public void onReceivedMessage(SipProvider provider, Message msg) + { printLog("Message received: "+msg.getFirstLine().substring(0,msg.toString().indexOf('\r')),LogLevel.LOW); + if (msg.isResponse()) + { super.onReceivedMessage(provider,msg); + } + else + if (msg.isInvite() || msg.isAck() || msg.isCancel() || msg.isBye()) + { super.onReceivedMessage(provider,msg); + } + else + { TransactionServer t=new TransactionServer(sip_provider,msg,this); + transactions.put(t.getTransactionId(),t); + //t.listen(); + + if (msg.isRefer()) + { //Message resp=MessageFactory.createResponse(msg,202,"Accepted",null,null); + //respond(resp); + NameAddress refer_to=msg.getReferToHeader().getNameAddress(); + NameAddress referred_by=null; + if (msg.hasReferredByHeader()) referred_by=msg.getReferredByHeader().getNameAddress(); + dialog_listener.onDlgRefer(this,refer_to,referred_by,msg); + } + else + if (msg.isNotify()) + { Message resp=MessageFactory.createResponse(msg,200,SipResponses.reasonOf(200),null); + respond(resp); + String event=msg.getEventHeader().getValue(); + String sipfragment=msg.getBody(); + dialog_listener.onDlgNotify(this,event,sipfragment,msg); + } + else + { printLog("Received alternative request "+msg.getRequestLine().getMethod(),LogLevel.MEDIUM); + dialog_listener.onDlgAltRequest(this,msg.getRequestLine().getMethod(),msg.getBody(),msg); + } + } + } + + + /** Inherited from TransactionClientListener. + * When the TransactionClientListener goes into the "Completed" state, receiving a failure response */ + public void onTransFailureResponse(TransactionClient tc, Message msg) + { printLog("inside onTransFailureResponse("+tc.getTransactionId()+",msg)",LogLevel.LOW); + String method=tc.getTransactionMethod(); + StatusLine status_line=msg.getStatusLine(); + int code=status_line.getCode(); + String reason=status_line.getReason(); + + // AUTHENTICATION-BEGIN + if ((code==401 && attempts + * An InviteDialog can be in state inviting/waiting/invited, accepted/refused, call, + * byed/byeing, and close. + *

    + * InviteDialog supports the offer/answer model for the sip body, with the following rules: + *
    - both INVITE-offer/2xx-answer and 2xx-offer/ACK-answer modes for incoming calls + *
    - INVITE-offer/2xx-answer mode for outgoing calls. + */ +public class InviteDialog extends Dialog implements TransactionClientListener, InviteTransactionServerListener, AckTransactionServerListener, SipProviderListener +{ + /** The last invite message */ + Message invite_req; + /** The last ack message */ + Message ack_req; + + /** The InviteTransactionServer. */ + InviteTransactionServer invite_ts; + /** The AckTransactionServer. */ + AckTransactionServer ack_ts; + /** The BYE TransactionServer. */ + TransactionServer bye_ts; + + /** The InviteDialog listener */ + InviteDialogListener listener; + + + /** Whether offer/answer are in INVITE/200_OK */ + boolean invite_offer; + + protected static final int D_INIT=0; + protected static final int D_WAITING=1; + protected static final int D_INVITING=2; + protected static final int D_INVITED=3; + protected static final int D_REFUSED=4; + protected static final int D_ACCEPTED=5; + protected static final int D_CALL=6; + + protected static final int D_ReWAITING=11; + protected static final int D_ReINVITING=12; + protected static final int D_ReINVITED=13; + protected static final int D_ReREFUSED=14; + protected static final int D_ReACCEPTED=15; + + protected static final int D_BYEING=7; + protected static final int D_BYED=8; + protected static final int D_CLOSE=9; + + /** Gets the dialog state */ + protected String getStatus() + { switch (status) + { case D_INIT : return "D_INIT"; + case D_WAITING : return "D_WAITING"; + case D_INVITING : return "D_INVITING"; + case D_INVITED : return "D_INVITED"; + case D_REFUSED : return "D_REFUSED"; + case D_ACCEPTED : return "D_ACCEPTED"; + case D_CALL : return "D_CALL"; + case D_ReWAITING : return "D_ReWAITING"; + case D_ReINVITING : return "D_ReINVITING"; + case D_ReINVITED : return "D_ReINVITED"; + case D_ReREFUSED : return "D_ReREFUSED"; + case D_ReACCEPTED : return "D_ReACCEPTED"; + case D_BYEING : return "D_BYEING"; + case D_BYED : return "D_BYED"; + case D_CLOSE : return "D_CLOSE"; + default : return null; + } + } + + // ************************** Public methods ************************** + + /** Whether the dialog is in "early" state. */ + public boolean isEarly() + { return status=D_ACCEPTED && statusinvite. */ + public InviteDialog(SipProvider sip_provider, Message invite, InviteDialogListener listener) + { super(sip_provider); + init(listener); + + changeStatus(D_INVITED); + invite_req=invite; + invite_ts=new InviteTransactionServer(sip_provider,invite_req,this); + update(Dialog.UAS,invite_req); + } + + /** Inits the InviteDialog. */ + private void init(InviteDialogListener listener) + { log=sip_provider.getLog(); + this.listener=listener; + this.invite_req=null; + this.ack_req=null; + this.invite_offer=true; + changeStatus(D_INIT); + } + + /** Starts a new InviteTransactionServer. */ + public void listen() + { if (!statusIs(D_INIT)) return; + //else + changeStatus(D_WAITING); + invite_ts=new InviteTransactionServer(sip_provider,this); + invite_ts.listen(); + } + + /** Starts a new InviteTransactionClient + * and initializes the dialog state information. + * @param callee the callee url (and display name) + * @param caller the caller url (and display name) + * @param contact the contact url OR the contact username + * @param session_descriptor SDP body + */ + public void invite(String callee, String caller, String contact, String session_descriptor) + { printLog("inside invite(callee,caller,contact,sdp)",LogLevel.MEDIUM); + if (!statusIs(D_INIT)) return; + // else + NameAddress to_url=new NameAddress(callee); + NameAddress from_url=new NameAddress(caller); + SipURL request_uri=to_url.getAddress(); + + NameAddress contact_url=null; + if (contact!=null) + { if (contact.indexOf("sip:")>=0) contact_url=new NameAddress(contact); + else contact_url=new NameAddress(new SipURL(contact,sip_provider.getViaAddress(),sip_provider.getPort())); + } + else contact_url=from_url; + + Message invite=MessageFactory.createInviteRequest(sip_provider,request_uri,to_url,from_url,contact_url,session_descriptor); + // do invite + invite(invite); + } + + /** Starts a new InviteTransactionClient + * and initializes the dialog state information + * @param invite the INVITE message + */ + public void invite(Message invite) + { printLog("inside invite(invite)",LogLevel.MEDIUM); + if (!statusIs(D_INIT)) return; + // else + changeStatus(D_INVITING); + invite_req=invite; + update(Dialog.UAC,invite_req); + InviteTransactionClient invite_tc=new InviteTransactionClient(sip_provider,invite_req,this); + invite_tc.request(); + } + + + /** Starts a new InviteTransactionClient with offer/answer in 2xx/ack + * and initializes the dialog state information */ + public void inviteWithoutOffer(String callee, String caller, String contact) + { invite_offer=false; + invite(callee,caller,contact,null); + } + + /** Starts a new InviteTransactionClient with offer/answer in 2xx/ack + * and initializes the dialog state information */ + public void inviteWithoutOffer(Message invite) + { invite_offer=false; + invite(invite); + } + + + /** Re-invites the remote user. + *

    Starts a new InviteTransactionClient and changes the dialog state information + *

    Parameters: + *
    - contact : the contact url OR the contact username; if null, the previous contact is used + *
    - session_descriptor : the message body + */ + public void reInvite(String contact, String session_descriptor) + { printLog("inside reInvite(contact,sdp)",LogLevel.MEDIUM); + if (!statusIs(D_CALL)) return; + // else + Message invite=MessageFactory.createInviteRequest(this,session_descriptor); + if (contact!=null) + { NameAddress contact_url; + if (contact.indexOf("sip:")>=0) contact_url=new NameAddress(contact); + else contact_url=new NameAddress(new SipURL(contact,sip_provider.getViaAddress(),sip_provider.getPort())); + invite.setContactHeader(new ContactHeader(contact_url)); + } + reInvite(invite); + } + + + /** Re-invites the remote user. + *

    Starts a new InviteTransactionClient and changes the dialog state information */ + public void reInvite(Message invite) + { printLog("inside reInvite(invite)",LogLevel.MEDIUM); + if (!statusIs(D_CALL)) return; + // else + changeStatus(D_ReINVITING); + invite_req=invite; + update(Dialog.UAC,invite_req); + InviteTransactionClient invite_tc=new InviteTransactionClient(sip_provider,invite_req,this); + invite_tc.request(); + } + + /** Re-invites the remote user with offer/answer in 2xx/ack + *

    Starts a new InviteTransactionClient and changes the dialog state information */ + public void reInviteWithoutOffer(Message invite) + { invite_offer=false; + reInvite(invite); + } + + /** Re-invites the remote user with offer/answer in 2xx/ack + *

    Starts a new InviteTransactionClient and changes the dialog state information */ + public void reInviteWithoutOffer(String contact, String session_descriptor) + { invite_offer=false; + reInvite(contact,session_descriptor); + } + + /** Sends the ack when offer/answer is in 2xx/ack */ + public void ackWithAnswer(String contact, String session_descriptor) + { if (contact!=null) setLocalContact(new NameAddress(contact)); + Message ack=MessageFactory.create2xxAckRequest(this,session_descriptor); + ackWithAnswer(ack); + } + + /** Sends the ack when offer/answer is in 2xx/ack */ + public void ackWithAnswer(Message ack) + { ack_req=ack; + // reset the offer/answer flag to the default value + invite_offer=true; + AckTransactionClient ack_tc=new AckTransactionClient(sip_provider,ack,null); + ack_tc.request(); + } + + + /** Responds with resp. + * This method can be called when the InviteDialog is in D_INVITED or D_BYED states. + *

    + * If the CSeq method is INVITE and the response is 2xx, + * it moves to state D_ACCEPTED, adds a new listener to the SipProviderListener, + * and creates new AckTransactionServer + *

    + * If the CSeq method is INVITE and the response is not 2xx, + * it moves to state D_REFUSED, and sends the response. */ + public void respond(Message resp) + //private void respond(Message resp) + { printLog("inside respond(resp)",LogLevel.MEDIUM); + String method=resp.getCSeqHeader().getMethod(); + if (method.equals(SipMethods.INVITE)) + { if (!verifyStatus(statusIs(D_INVITED)||statusIs(D_ReINVITED))) + { printLog("respond(): InviteDialog not in (re)invited state: No response now",LogLevel.HIGH); + return; + } + + int code=resp.getStatusLine().getCode(); + // 1xx provisional responses + if (code>=100 && code<200) + { invite_ts.respondWith(resp); + return; + } + // For all final responses establish the dialog + if (code>=200) + { //changeStatus(D_ACCEPTED); + update(Dialog.UAS,resp); + } + // 2xx success responses + if (code>=200 && code<300) + { if(statusIs(D_INVITED)) changeStatus(D_ACCEPTED); else changeStatus(D_ReACCEPTED); + // terminates the INVITE Transaction server and activates an ACK Transaction server + invite_ts.terminate(); + ConnectionIdentifier conn_id=invite_ts.getConnectionId(); + ack_ts=new AckTransactionServer(sip_provider,conn_id,resp,this); + ack_ts.respond(); + //if (statusIs(D_ReACCEPTED)) listener.onDlgReInviteAccepted(this); + //else listener.onDlgAccepted(this); + return; + } + else + // 300-699 failure responses + //if (code>=300) + { if(statusIs(D_INVITED)) changeStatus(D_REFUSED); else changeStatus(D_ReREFUSED); + invite_ts.respondWith(resp); + //if (statusIs(D_ReREFUSED)) listener.onDlgReInviteRefused(this); + //else listener.onDlgRefused(this); + return; + } + } + if (method.equals(SipMethods.BYE)) + { if (!verifyStatus(statusIs(D_BYED))) return; + bye_ts.respondWith(resp); + } + } + + /** Responds with code and reason. + * This method can be called when the InviteDialog is in D_INVITED, D_ReINVITED states */ + public void respond(int code, String reason, String contact, String sdp) + { printLog("inside respond("+code+","+reason+")",LogLevel.MEDIUM); + if (statusIs(D_INVITED) || statusIs(D_ReINVITED)) + { NameAddress contact_address=null; + if (contact!=null) contact_address=new NameAddress(contact); + Message resp=MessageFactory.createResponse(invite_req,code,reason,contact_address); + resp.setBody(sdp); + respond(resp); + } + else + printWarning("Dialog isn't in \"invited\" state: cannot respond ("+code+"/"+getStatus()+"/"+getDialogID()+")",LogLevel.MEDIUM); + } + + /** Signals that the phone is ringing. + * This method should be called when the InviteDialog is in D_INVITED or D_ReINVITED state */ + public void ring() + { printLog("inside ring()",LogLevel.MEDIUM); + respond(180,SipResponses.reasonOf(180),null,null); + } + + /** Accepts the incoming call. + * This method should be called when the InviteDialog is in D_INVITED or D_ReINVITED state */ + public void accept(String contact, String sdp) + { printLog("inside accept(sdp)",LogLevel.MEDIUM); + respond(200,SipResponses.reasonOf(200),contact,sdp); + } + + /** Refuses the incoming call. + * This method should be called when the InviteDialog is in D_INVITED or D_ReINVITED state */ + public void refuse(int code, String reason) + { printLog("inside refuse("+code+","+reason+")",LogLevel.MEDIUM); + respond(code,reason,null,null); + } + + /** Refuses the incoming call. + * This method should be called when the InviteDialog is in D_INVITED or D_ReINVITED state */ + public void refuse() + { printLog("inside refuse()",LogLevel.MEDIUM); + //refuse(480,"Temporarily Unavailable"); + //refuse(603,"Decline"); + refuse(403,SipResponses.reasonOf(403)); + } + + /** Termiante the call. + * This method should be called when the InviteDialog is in D_CALL state + *

    + * Increments the Cseq, moves to state D_BYEING, and creates new BYE TransactionClient */ + public void bye() + { printLog("inside bye()",LogLevel.MEDIUM); + if (statusIs(D_CALL)) + { Message bye=MessageFactory.createByeRequest(this); + bye(bye); + } + } + + /** Termiante the call. + * This method should be called when the InviteDialog is in D_CALL state + *

    + * Increments the Cseq, moves to state D_BYEING, and creates new BYE TransactionClient */ + public void bye(Message bye) + { printLog("inside bye(bye)",LogLevel.MEDIUM); + if (statusIs(D_CALL)) + { changeStatus(D_BYEING); + //dialog_state.incLocalCSeq(); // done by MessageFactory.createRequest() + TransactionClient tc=new TransactionClient(sip_provider,bye,this); + tc.request(); + //listener.onDlgByeing(this); + } + } + + /** Cancel the ongoing call request or a call listening. + * This method should be called when the InviteDialog is in D_INVITING or D_ReINVITING state + * or in the D_WAITING state */ + public void cancel() + { printLog("inside cancel()",LogLevel.MEDIUM); + if (statusIs(D_INVITING) || statusIs(D_ReINVITING)) + { Message cancel=MessageFactory.createCancelRequest(invite_req); + cancel(cancel); + } + else + if (statusIs(D_WAITING) || statusIs(D_ReWAITING)) + { invite_ts.terminate(); + } + } + + /** Cancel the ongoing call request or a call listening. + * This method should be called when the InviteDialog is in D_INVITING or D_ReINVITING state + * or in the D_WAITING state */ + public void cancel(Message cancel) + { printLog("inside cancel(cancel)",LogLevel.MEDIUM); + if (statusIs(D_INVITING) || statusIs(D_ReINVITING)) + { //changeStatus(D_CANCELING); + TransactionClient tc=new TransactionClient(sip_provider,cancel,null); + tc.request(); + } + else + if (statusIs(D_WAITING) || statusIs(D_ReWAITING)) + { invite_ts.terminate(); + } + } + + /** Redirects the incoming call + * , specifing the code and reason. + * This method can be called when the InviteDialog is in D_INVITED or D_ReINVITED state */ + public void redirect(int code, String reason, String contact) + { printLog("inside redirect("+code+","+reason+","+contact+")",LogLevel.MEDIUM); + respond(code,reason,contact,null); + } + + // ************** Inherited from SipProviderListener ************** + + /** Inherited from class SipProviderListener. + * Called when a new message is received (out of any ongoing transaction) + * for the current InviteDialog. + * Always checks for out-of-date methods (CSeq header sequence number). + *

    + * If the message is ACK(2xx/INVITE) request, it moves to D_CALL state, and fires onDlgAck(this,body,msg). + *

    + * If the message is 2xx(INVITE) response, it create a new AckTransactionClient + *

    + * If the message is BYE, + * it moves to D_BYED state, removes the listener from SipProvider, fires onDlgBye(this,msg) + * then it responds with 200 OK, moves to D_CLOSE state and fires onDlgClose(this) + */ + public void onReceivedMessage(SipProvider sip_provider, Message msg) + { printLog("inside onReceivedMessage(sip_provider,message)",LogLevel.MEDIUM); + if (msg.isRequest() && !(msg.isAck() || msg.isCancel()) && msg.getCSeqHeader().getSequenceNumber()<=getRemoteCSeq()) + { printLog("Request message is too late (CSeq too small): Message discarded",LogLevel.HIGH); + return; + } + // invite received + if (msg.isRequest() && msg.isInvite()) + { verifyStatus(statusIs(D_INIT)||statusIs(D_CALL)); + // NOTE: if the invite_ts.listen() is used, you should not arrive here with the D_INIT state.. + // however state D_INIT has been included for robustness against further changes. + if (statusIs(D_INIT)) changeStatus(D_INVITED); else changeStatus(D_ReINVITED); + invite_req=msg; + invite_ts=new InviteTransactionServer(sip_provider,invite_req,this); + //((TransactionServer)transaction).listen(); + update(Dialog.UAS,invite_req); + if (statusIs(D_INVITED)) listener.onDlgInvite(this,invite_req.getToHeader().getNameAddress(),invite_req.getFromHeader().getNameAddress(),invite_req.getBody(),invite_req); + else listener.onDlgReInvite(this,invite_req.getBody(),invite_req); + } + else + // ack (of 2xx of INVITE) + if (msg.isRequest() && msg.isAck()) + { if (!verifyStatus(statusIs(D_ACCEPTED)||statusIs(D_ReACCEPTED))) return; + changeStatus(D_CALL); + // terminates the AckTransactionServer + ack_ts.terminate(); + listener.onDlgAck(this,msg.getBody(),msg); + listener.onDlgCall(this); + } + else + // keep sending ACK (if already sent) for any "200 OK" received + if (msg.isResponse()) + { if (!verifyStatus(statusIs(D_CALL))) return; + int code=msg.getStatusLine().getCode(); + verifyThat(code>=200 && code<300,"code 2xx was expected"); + if (ack_req!=null) + { AckTransactionClient ack_tc=new AckTransactionClient(sip_provider,ack_req,null); + ack_tc.request(); + } + } + else + // bye received + if (msg.isRequest() && msg.isBye()) + { if (!verifyStatus(statusIs(D_CALL)||statusIs(D_BYEING))) return; + changeStatus(D_BYED); + bye_ts=new TransactionServer(sip_provider,msg,this); + // automatically sends a 200 OK + Message resp=MessageFactory.createResponse(msg,200,SipResponses.reasonOf(200),null); + respond(resp); + listener.onDlgBye(this,msg); + changeStatus(D_CLOSE); + listener.onDlgClose(this); + } + else + // cancel received + if (msg.isRequest() && msg.isCancel()) + { if (!verifyStatus(statusIs(D_INVITED)||statusIs(D_ReINVITED))) return; + // create a CANCEL TransactionServer and send a 200 OK (CANCEL) + TransactionServer ts=new TransactionServer(sip_provider,msg,null); + //ts.listen(); + ts.respondWith(MessageFactory.createResponse(msg,200,SipResponses.reasonOf(200),null)); + // automatically sends a 487 Cancelled + Message resp=MessageFactory.createResponse(invite_req,487,SipResponses.reasonOf(487),null); + respond(resp); + listener.onDlgCancel(this,msg); + } + else + // any other request received + if (msg.isRequest()) + { TransactionServer ts=new TransactionServer(sip_provider,msg,null); + //ts.listen(); + ts.respondWith(MessageFactory.createResponse(msg,405,SipResponses.reasonOf(405),null)); + } + } + + // ************** Inherited from InviteTransactionClientListener ************** + + /** Inherited from TransactionClientListener. + * When the TransactionClientListener is in "Proceeding" state and receives a new 1xx response + *

    + * For INVITE transaction it fires onFailureResponse(this,code,reason,body,msg). */ + public void onTransProvisionalResponse(TransactionClient tc, Message msg) + { printLog("inside onTransProvisionalResponse(tc,mdg)",LogLevel.LOW); + if (tc.getTransactionMethod().equals(SipMethods.INVITE)) + { StatusLine statusline=msg.getStatusLine(); + listener.onDlgInviteProvisionalResponse(this,statusline.getCode(),statusline.getReason(),msg.getBody(),msg); + } + } + + /** Inherited from TransactionClientListener. + * When the TransactionClientListener goes into the "Completed" state, receiving a failure response + *

    + * If called for a INVITE transaction, it moves to D_CLOSE state, removes the listener from SipProvider. + *

    + * If called for a BYE transaction, it moves to D_CLOSE state, + * removes the listener from SipProvider, and fires onClose(this,msg). */ + public void onTransFailureResponse(TransactionClient tc, Message msg) + { printLog("inside onTransFailureResponse("+tc.getTransactionId()+",msg)",LogLevel.LOW); + if (tc.getTransactionMethod().equals(SipMethods.INVITE)) + { if (!verifyStatus(statusIs(D_INVITING)||statusIs(D_ReINVITING))) return; + StatusLine statusline=msg.getStatusLine(); + int code=statusline.getCode(); + verifyThat(code>=300 && code <700,"error code was expected"); + if (statusIs(D_ReINVITING)) + { changeStatus(D_CALL); + listener.onDlgReInviteFailureResponse(this,code,statusline.getReason(),msg); + } + else + { changeStatus(D_CLOSE); + if (code>=300 && code<400) listener.onDlgInviteRedirectResponse(this,code,statusline.getReason(),msg.getContacts(),msg); + else listener.onDlgInviteFailureResponse(this,code,statusline.getReason(),msg); + listener.onDlgClose(this); + } + } + else + if (tc.getTransactionMethod().equals(SipMethods.BYE)) + { if (!verifyStatus(statusIs(D_BYEING))) return; + StatusLine statusline=msg.getStatusLine(); + int code=statusline.getCode(); + verifyThat(code>=300 && code <700,"error code was expected"); + changeStatus(this.D_CALL); + listener.onDlgByeFailureResponse(this,code,statusline.getReason(),msg); + } + } + + /** Inherited from TransactionClientListener. + * When an TransactionClientListener goes into the "Terminated" state, receiving a 2xx response + *

    + * If called for a INVITE transaction, it updates the dialog information, moves to D_CALL state, + * add a listener to the SipProvider, creates a new AckTransactionClient(ack,this), + * and fires onSuccessResponse(this,code,body,msg). + *

    + * If called for a BYE transaction, it moves to D_CLOSE state, + * removes the listener from SipProvider, and fires onClose(this,msg). */ + public void onTransSuccessResponse(TransactionClient tc, Message msg) + { printLog("inside onTransSuccessResponse(tc,msg)",LogLevel.LOW); + if (tc.getTransactionMethod().equals(SipMethods.INVITE)) + { if (!verifyStatus(statusIs(D_INVITING)||statusIs(D_ReINVITING))) return; + StatusLine statusline=msg.getStatusLine(); + int code=statusline.getCode(); + if (!verifyThat(code>=200 && code <300 && msg.getTransactionMethod().equals(SipMethods.INVITE),"2xx for invite was expected")) return; + boolean re_inviting=statusIs(D_ReINVITING); + changeStatus(D_CALL); + update(Dialog.UAC,msg); + if (invite_offer) + { //invite_req=MessageFactory.createRequest(SipMethods.ACK,dialog_state,sdp.toString()); + //ack=MessageFactory.createRequest(this,SipMethods.ACK,null); + ack_req=MessageFactory.create2xxAckRequest(this,null); + AckTransactionClient ack_tc=new AckTransactionClient(sip_provider,ack_req,null); + ack_tc.request(); + } + if (!re_inviting) + { listener.onDlgInviteSuccessResponse(this,code,statusline.getReason(),msg.getBody(),msg); + listener.onDlgCall(this); + } + else + listener.onDlgReInviteSuccessResponse(this,code,statusline.getReason(),msg.getBody(),msg); + } + else + if (tc.getTransactionMethod().equals(SipMethods.BYE)) + { if (!verifyStatus(statusIs(D_BYEING))) return; + StatusLine statusline=msg.getStatusLine(); + int code=statusline.getCode(); + verifyThat(code>=200 && code <300,"2xx for bye was expected"); + changeStatus(D_CLOSE); + listener.onDlgByeSuccessResponse(this,code,statusline.getReason(),msg); + listener.onDlgClose(this); + } + } + + /** Inherited from TransactionClientListener. + * When the TransactionClient goes into the "Terminated" state, caused by transaction timeout */ + public void onTransTimeout(TransactionClient tc) + { printLog("inside onTransTimeout(tc,msg)",LogLevel.LOW); + if (tc.getTransactionMethod().equals(SipMethods.INVITE)) + { if (!verifyStatus(statusIs(D_INVITING)||statusIs(D_ReINVITING))) return; + changeStatus(D_CLOSE); + listener.onDlgTimeout(this); + listener.onDlgClose(this); + } + else + if (tc.getTransactionMethod().equals(SipMethods.BYE)) + { if (!verifyStatus(statusIs(D_BYEING))) return; + changeStatus(D_CLOSE); + listener.onDlgClose(this); + } + } + + + // ************** Inherited from InviteTransactionServerListener ************** + + /** Inherited from TransactionServerListener. + * When the TransactionServer goes into the "Trying" state receiving a request + *

    + * If called for a INVITE transaction, it initializes the dialog information, + *
    moves to D_INVITED state, and add a listener to the SipProvider, + *
    and fires onInvite(caller,body,msg). */ + public void onTransRequest(TransactionServer ts, Message req) + { printLog("inside onTransRequest(ts,msg)",LogLevel.LOW); + if (ts.getTransactionMethod().equals(SipMethods.INVITE)) + { if (!verifyStatus(statusIs(D_WAITING))) return; + changeStatus(D_INVITED); + invite_req=req; + update(Dialog.UAS,invite_req); + listener.onDlgInvite(this,invite_req.getToHeader().getNameAddress(),invite_req.getFromHeader().getNameAddress(),invite_req.getBody(),invite_req); + } + } + + /** Inherited from InviteTransactionServerListener. + * When an InviteTransactionServer goes into the "Confirmed" state receining an ACK for NON-2xx response + *

    + * It moves to D_CLOSE state and removes the listener from SipProvider. */ + public void onTransFailureAck(InviteTransactionServer ts, Message msg) + { printLog("inside onTransFailureAck(ts,msg)",LogLevel.LOW); + if (!verifyStatus(statusIs(D_REFUSED)||statusIs(D_ReREFUSED))) return; + if (statusIs(D_ReREFUSED)) + { changeStatus(D_CALL); + } + else + { changeStatus(D_CLOSE); + listener.onDlgClose(this); + } + } + + + // ************ Inherited from AckTransactionServerListener ************ + + /** When the AckTransactionServer goes into the "Terminated" state, caused by transaction timeout */ + public void onTransAckTimeout(AckTransactionServer ts) + { printLog("inside onAckSrvTimeout(ts)",LogLevel.LOW); + if (!verifyStatus(statusIs(D_ACCEPTED)||statusIs(D_ReACCEPTED)||statusIs(D_REFUSED)||statusIs(D_ReREFUSED))) return; + printLog("No ACK received..",LogLevel.HIGH); + changeStatus(D_CLOSE); + listener.onDlgClose(this); + } + + + //**************************** Logs ****************************/ + + /** Adds a new string to the default Log */ + protected void printLog(String str, int level) + { if (log!=null) log.println("InviteDialog#"+dialog_sqn+": "+str,level+SipStack.LOG_LEVEL_DIALOG); + } + +} diff --git a/src/org/zoolu/sip/dialog/InviteDialogListener.java b/src/org/zoolu/sip/dialog/InviteDialogListener.java new file mode 100644 index 0000000..f102e1f --- /dev/null +++ b/src/org/zoolu/sip/dialog/InviteDialogListener.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.dialog; + + +import org.zoolu.sip.address.NameAddress; +import org.zoolu.sip.message.Message; +import org.zoolu.sip.header.MultipleHeader; + + +/** An InviteDialogListener listens for InviteDialog events. + * It collects all InviteDialog callback functions. + */ +public interface InviteDialogListener +{ + /** When an incoming INVITE is received */ + public void onDlgInvite(InviteDialog dialog, NameAddress callee, NameAddress caller, String body, Message msg); + + /** When an incoming Re-INVITE is received */ + public void onDlgReInvite(InviteDialog dialog, String body, Message msg); + + + + /** When a 1xx response response is received for an INVITE transaction */ + public void onDlgInviteProvisionalResponse(InviteDialog dialog, int code, String reason, String body, Message msg); + + /** When a 2xx successfull final response is received for an INVITE transaction */ + public void onDlgInviteSuccessResponse(InviteDialog dialog, int code, String reason, String body, Message msg); + + /** When a 3xx redirection response is received for an INVITE transaction */ + public void onDlgInviteRedirectResponse(InviteDialog dialog, int code, String reason, MultipleHeader contacts, Message msg); + + /** When a 400-699 failure response is received for an INVITE transaction */ + public void onDlgInviteFailureResponse(InviteDialog dialog, int code, String reason, Message msg); + + /** When INVITE transaction expires */ + public void onDlgTimeout(InviteDialog dialog); + + + + /** When a 1xx response response is received for a Re-INVITE transaction */ + public void onDlgReInviteProvisionalResponse(InviteDialog dialog, int code, String reason, String body, Message msg); + + /** When a 2xx successfull final response is received for a Re-INVITE transaction */ + public void onDlgReInviteSuccessResponse(InviteDialog dialog, int code, String reason, String body, Message msg); + + /** When a 3xx redirection response is received for a Re-INVITE transaction */ + //public void onDlgReInviteRedirectResponse(InviteDialog dialog, int code, String reason, MultipleHeader contacts, Message msg); + + /** When a 400-699 failure response is received for a Re-INVITE transaction */ + public void onDlgReInviteFailureResponse(InviteDialog dialog, int code, String reason, Message msg); + + /** When a Re-INVITE transaction expires */ + public void onDlgReInviteTimeout(InviteDialog dialog); + + + + /** When an incoming INVITE is accepted */ + //public void onDlgAccepted(InviteDialog dialog); + + /** When an incoming INVITE is refused */ + //public void onDlgRefused(InviteDialog dialog); + + /** When an incoming Re-INVITE is accepted */ + //public void onDlgReInviteAccepted(InviteDialog dialog); + + /** When an incoming Re-INVITE is refused */ + //public void onDlgReInviteRefused(InviteDialog dialog); + + + + /** When an incoming ACK is received for an INVITE transaction */ + public void onDlgAck(InviteDialog dialog, String body, Message msg); + + /** When the INVITE handshake is successful terminated */ + public void onDlgCall(InviteDialog dialog); + + + + /** When an incoming CANCEL is received for an INVITE transaction */ + public void onDlgCancel(InviteDialog dialog, Message msg); + + /** When an incoming BYE is received*/ + public void onDlgBye(InviteDialog dialog, Message msg); + + /** When a BYE request traqnsaction has been started */ + //public void onDlgByeing(InviteDialog dialog); + + /** When a success response is received for a Bye request */ + public void onDlgByeSuccessResponse(InviteDialog dialog, int code, String reason, Message msg); + + /** When a failure response is received for a Bye request */ + public void onDlgByeFailureResponse(InviteDialog dialog, int code, String reason, Message msg); + + /** When the dialog is finally closed */ + public void onDlgClose(InviteDialog dialog); +} diff --git a/src/org/zoolu/sip/dialog/NotifierDialog.java b/src/org/zoolu/sip/dialog/NotifierDialog.java new file mode 100644 index 0000000..76a587b --- /dev/null +++ b/src/org/zoolu/sip/dialog/NotifierDialog.java @@ -0,0 +1,359 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +/* Modified by: + * Daina Interrante (daina.interrante@studenti.unipr.it) + */ + +package org.zoolu.sip.dialog; + + +import org.zoolu.sip.address.*; +import org.zoolu.sip.transaction.*; +import org.zoolu.sip.dialog.*; +import org.zoolu.sip.message.*; +import org.zoolu.sip.header.*; +import org.zoolu.sip.header.*; +import org.zoolu.sip.provider.*; +import org.zoolu.tools.LogLevel; + +import java.util.Date; + + +/** NotifierDialog. + */ +public class NotifierDialog extends Dialog implements TransactionClientListener/*, TransactionServerListener*/ +{ + /** String "active" */ + protected static final String ACTIVE="active"; + /** String "pending" */ + protected static final String PENDING="pending"; + /** String "terminated" */ + protected static final String TERMINATED="terminated"; + + /** The SubscriberDialog listener */ + NotifierDialogListener listener; + + /** The current subscribe method */ + Message subscribe_req; + + /** The current subscribe transaction */ + TransactionServer subscribe_transaction; + + /** The current notify transaction */ + TransactionClient notify_transaction; + + /** The event name */ + String event; + + /** The subscription id */ + String id; + + /** Internal state D_INIT (the starting point) */ + protected static final int D_INIT=0; + /** Internal state D_WAITING (listening for the first subscription request) */ + protected static final int D_WAITING=1; + /** Internal state D_SUBSCRIBED (first subscription request arrived) */ + protected static final int D_SUBSCRIBED=2; + /** Internal state D_PENDING (first subscription request has been accepted) */ + protected static final int D_PENDING=3; + /** Internal state D_ACTIVE (subscription has been activated) */ + protected static final int D_ACTIVE=4; + /** Internal state D_TERMINATED (first subscription request has been refused or subscription has been terminated) */ + protected static final int D_TERMINATED=9; + + + // ************************* Protected methods ************************ + + /** Gets the dialog state */ + protected String getStatus() + { switch (status) + { case D_INIT : return "D_INIT"; + case D_WAITING : return "D_WAITING"; + case D_SUBSCRIBED : return "D_SUBSCRIBED"; + case D_PENDING : return "D_PENDING"; + case D_ACTIVE : return "D_ACTIVE"; + case D_TERMINATED : return "D_TERMINATED"; + default : return null; + } + } + + // ************************** Public methods ************************** + + /** Whether the dialog is in "early" state. */ + public boolean isEarly() + { return (status=D_PENDING && status=D_SUBSCRIBED && statussubscribe. */ + public NotifierDialog(SipProvider sip_provider, Message subscribe, NotifierDialogListener listener) + { super(sip_provider); + init(listener); + + changeStatus(D_SUBSCRIBED); + subscribe_req=subscribe; + subscribe_transaction=new TransactionServer(sip_provider,subscribe,null); + update(Dialog.UAS,subscribe); + EventHeader eh=subscribe.getEventHeader(); + if (eh!=null) + { event=eh.getEvent(); + id=eh.getId(); + } + } + + /** Inits the NotifierDialog. */ + private void init(NotifierDialogListener listener) + { this.listener=listener; + this.subscribe_transaction=null; + this.notify_transaction=null; + this.subscribe_req=null; + this.event=null; + this.id=null; + changeStatus(D_INIT); + } + + + // *************************** Public methods ************************** + + /** Listen for the first subscription request. */ + public void listen() + { printLog("inside method listen()",LogLevel.MEDIUM); + if (!statusIs(D_INIT)) + { printLog("first subscription already received",LogLevel.MEDIUM); + return; + } + // else + changeStatus(D_WAITING); + // listen for the first SUBSCRIBE request + sip_provider.addSipProviderListener(new MethodIdentifier(SipMethods.SUBSCRIBE),this); + } + + /** Accepts the subscription request (sends a "202 Accepted" response). */ + public void accept(int expires, String contact) + { printLog("inside accept()",LogLevel.MEDIUM); + respond(202,SipResponses.reasonOf(202),expires,contact,null,null); + } + + /** Refuses the subscription request. */ + public void refuse() + { printLog("inside refuse()",LogLevel.MEDIUM); + respond(403,SipResponses.reasonOf(403),-1,null,null,null); + } + + /** Responds with code and reason. + * This method can be called when the InviteDialog is in D_INVITED, D_ReINVITED states */ + public void respond(int code, String reason, int expires, String contact, String content_type, String body) + { printLog("inside respond("+code+","+reason+")",LogLevel.MEDIUM); + NameAddress contact_url=null; + if (contact!=null) contact_url=new NameAddress(contact); + Message resp=MessageFactory.createResponse(subscribe_req,code,SipResponses.reasonOf(code),contact_url); + if (expires>=0) resp.setExpiresHeader(new ExpiresHeader(expires)); + if (body!=null) resp.setBody(content_type,body); + respond(resp); + } + + /** Responds with resp. */ + public void respond(Message resp) + { printLog("inside respond(resp)",LogLevel.MEDIUM); + if (resp.getStatusLine().getCode()>=200) update(UAS,resp); + subscribe_transaction.respondWith(resp); + } + + /** Activates the subscription (subscription goes into 'active' state). */ + public void activate() + { activate(SipStack.default_expires); + } + + /** Activates the subscription (subscription goes into 'active' state). */ + public void activate(int expires) + { notify(ACTIVE,expires,null,null); + } + + /** Makes the subscription pending (subscription goes into 'pending' state). */ + public void pending() + { pending(SipStack.default_expires); + } + + /** Makes the subscription pending (subscription goes into 'pending' state). */ + public void pending(int expires) + { notify(PENDING,expires,null,null); + } + + /** Terminates the subscription (subscription goes into 'terminated' state). */ + public void terminate() + { terminate(null); + } + + /** Terminates the subscription (subscription goes into 'terminated' state). */ + public void terminate(String reason) + { Message req=MessageFactory.createNotifyRequest(this,event,id,null,null); + SubscriptionStateHeader sh=new SubscriptionStateHeader(TERMINATED); + if (reason!=null) sh.setReason(reason); + //sh.setExpires(0); + req.setSubscriptionStateHeader(sh); + notify(req); + } + + /** Sends a NOTIFY. */ + public void notify(String state, int expires, String content_type, String body) + { Message req=MessageFactory.createNotifyRequest(this,event,id,content_type,body); + if (state!=null) + { SubscriptionStateHeader sh=new SubscriptionStateHeader(state); + if (expires>=0) sh.setExpires(expires); + req.setSubscriptionStateHeader(sh); + } + notify(req); + } + + /** Sends a NOTIFY. */ + public void notify(Message req) + { String subscription_state=req.getSubscriptionStateHeader().getState(); + if (subscription_state.equalsIgnoreCase(ACTIVE) && (statusIs(D_SUBSCRIBED) || statusIs(D_PENDING))) changeStatus(D_ACTIVE); + else + if (subscription_state.equalsIgnoreCase(PENDING) && statusIs(D_SUBSCRIBED)) changeStatus(D_PENDING); + else + if (subscription_state.equalsIgnoreCase(TERMINATED) && !statusIs(D_TERMINATED)) changeStatus(D_TERMINATED); + + TransactionClient notify_transaction=new TransactionClient(sip_provider,req,this); + notify_transaction.request(); + } + + + // ************** Inherited from TransactionClientListener ************** + + /** When the TransactionClient is (or goes) in "Proceeding" state and receives a new 1xx provisional response */ + public void onTransProvisionalResponse(TransactionClient tc, Message resp) + { printLog("onTransProvisionalResponse()",LogLevel.MEDIUM); + // do nothing. + } + + /** When the TransactionClient goes into the "Completed" state receiving a 2xx response */ + public void onTransSuccessResponse(TransactionClient tc, Message resp) + { printLog("onTransSuccessResponse()",LogLevel.MEDIUM); + StatusLine status_line=resp.getStatusLine(); + if (listener!=null) listener.onDlgNotificationSuccess(this,status_line.getCode(),status_line.getReason(),resp); + } + + /** When the TransactionClient goes into the "Completed" state receiving a 300-699 response */ + public void onTransFailureResponse(TransactionClient tc, Message resp) + { printLog("onTransFailureResponse()",LogLevel.MEDIUM); + StatusLine status_line=resp.getStatusLine(); + if (listener!=null) listener.onDlgNotificationFailure(this,status_line.getCode(),status_line.getReason(),resp); + } + + /** When the TransactionClient goes into the "Terminated" state, caused by transaction timeout */ + public void onTransTimeout(TransactionClient tc) + { printLog("onTransTimeout()",LogLevel.MEDIUM); + if (!statusIs(D_TERMINATED)) + { changeStatus(D_TERMINATED); + if (listener!=null) listener.onDlgNotifyTimeout(this); + } + } + + + // ************** Inherited from SipProviderListener ************** + + /** When a new Message is received by the SipProvider. */ + public void onReceivedMessage(SipProvider provider, Message msg) + { printLog("onReceivedMessage()",LogLevel.MEDIUM); + if (statusIs(D_TERMINATED)) + { printLog("subscription already terminated: message discarded",LogLevel.MEDIUM); + return; + } + // else + if(msg.isRequest() && msg.isSubscribe()) + { if (statusIs(this.D_WAITING)) + { // the first SUBSCRIBE request + changeStatus(D_SUBSCRIBED); + sip_provider.removeSipProviderListener(new MethodIdentifier(SipMethods.SUBSCRIBE)); + } + subscribe_req=msg; + NameAddress target=msg.getToHeader().getNameAddress(); + NameAddress subscriber=msg.getFromHeader().getNameAddress(); + EventHeader eh=msg.getEventHeader(); + if (eh!=null) + { event=eh.getEvent(); + id=eh.getId(); + } + update(UAS,msg); + subscribe_transaction=new TransactionServer(sip_provider,msg,null); + if (listener!=null) listener.onDlgSubscribe(this,target,subscriber,event,id,msg); + } + else + { printLog("message is not a SUBSCRIBE: message discarded",LogLevel.HIGH); + } + } + + + //**************************** Logs ****************************/ + + /** Adds a new string to the default Log */ + protected void printLog(String str, int level) + { if (log!=null) log.println("NotifierDialog#"+dialog_sqn+": "+str,level+SipStack.LOG_LEVEL_DIALOG); + } + +} diff --git a/src/org/zoolu/sip/dialog/NotifierDialogListener.java b/src/org/zoolu/sip/dialog/NotifierDialogListener.java new file mode 100644 index 0000000..46ea1c1 --- /dev/null +++ b/src/org/zoolu/sip/dialog/NotifierDialogListener.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.dialog; + + +import org.zoolu.sip.message.Message; +import org.zoolu.sip.address.SipURL; +import org.zoolu.sip.address.NameAddress; + + +/** A NotifierDialogListener listens for NotifierDialog events. + * It collects all NOTIFY callback functions. + */ +public interface NotifierDialogListener +{ + /** When an incoming SUBSCRIBE is received. */ + public void onDlgSubscribe(NotifierDialog dialog, NameAddress target, NameAddress subscriber, String event, String id, Message msg); + + /** When a re-SUBSCRIBE is received. */ + //public void onDlgReSubscribe(NotifierDialog dialog, Message msg); + + /** When NOTIFY transaction expires without a final response. */ + public void onDlgNotifyTimeout(NotifierDialog dialog); + + /** When a 300-699 response is received for a NOTIFY transaction. */ + public void onDlgNotificationFailure(NotifierDialog dialog, int code, String reason, Message msg); + + /** When a 2xx successfull final response is received for a NOTIFY transaction. */ + public void onDlgNotificationSuccess(NotifierDialog dialog, int code, String reason, Message msg); + +} diff --git a/src/org/zoolu/sip/dialog/SubscriberDialog.java b/src/org/zoolu/sip/dialog/SubscriberDialog.java new file mode 100644 index 0000000..85bcfa0 --- /dev/null +++ b/src/org/zoolu/sip/dialog/SubscriberDialog.java @@ -0,0 +1,304 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +/* Modified by: + * Daina Interrante (daina.interrante@studenti.unipr.it) + */ + +package org.zoolu.sip.dialog; + + +import org.zoolu.sip.address.*; +import org.zoolu.sip.transaction.*; +import org.zoolu.sip.dialog.*; +import org.zoolu.sip.message.*; +import org.zoolu.sip.header.*; +import org.zoolu.sip.provider.*; +import org.zoolu.tools.LogLevel; + +import java.util.Date; + + +/** SubscriberDialog. + */ +public class SubscriberDialog extends Dialog implements TransactionClientListener +{ + /** String "active" */ + protected static final String ACTIVE="active"; + /** String "pending" */ + protected static final String PENDING="pending"; + /** String "terminated" */ + protected static final String TERMINATED="terminated"; + + /** The current subscribe method */ + //Message subscribe=null; + + /** The subscribe transaction */ + TransactionClient subscribe_transaction; + + /** The notify transaction */ + //TransactionServer notify_transaction=null; + + /** The SubscriberDialog listener */ + SubscriberDialogListener listener; + + /** The event package name */ + String event; + + /** The subscription id */ + String id; + + /** Internal state D_INIT */ + protected static final int D_INIT=0; + /** Internal state D_SUBSCRIBING */ + protected static final int D_SUBSCRIBING=1; + /** Internal state D_SUBSCRIBED */ + protected static final int D_ACCEPTED=2; + /** Internal state D_PENDING */ + protected static final int D_PENDING=3; + /** Internal state D_ACTIVE */ + protected static final int D_ACTIVE=4; + /** Internal state D_TERMINATED */ + protected static final int D_TERMINATED=9; + + /** Gets the dialog state */ + protected String getStatus() + { switch (status) + { case D_INIT : return "D_INIT"; + case D_SUBSCRIBING: return "D_SUBSCRIBING"; + case D_ACCEPTED : return "D_ACCEPTED"; + case D_PENDING : return "D_PENDING"; + case D_ACTIVE : return "D_ACTIVE"; + case D_TERMINATED : return "D_TERMINATED"; + default : return null; + } + } + + + // *************************** Public methods ************************** + + + /** Whether the dialog is in "early" state. */ + public boolean isEarly() + { return (status=D_ACCEPTED && status=D_ACCEPTED && status"); + if (begin<0) begin=0; else begin++; + if (end<0) end=value.length(); + return value.substring(begin,end); + } + + /** Sets the absoluteURI */ + public void setAbsoluteURI(String absolute_uri) + { absolute_uri=absolute_uri.trim(); + if (absolute_uri.indexOf("<")<0) absolute_uri="<"+absolute_uri; + if (absolute_uri.indexOf(">")<0) absolute_uri=absolute_uri+">"; + value=absolute_uri; + } +} diff --git a/src/org/zoolu/sip/header/AllowEventsHeader.java b/src/org/zoolu/sip/header/AllowEventsHeader.java new file mode 100644 index 0000000..543e49a --- /dev/null +++ b/src/org/zoolu/sip/header/AllowEventsHeader.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.header; + + +import java.util.Vector; + + +/** SIP Header Allow-Events */ +public class AllowEventsHeader extends ListHeader +{ + public AllowEventsHeader(String hvalue) + { super(SipHeaders.Allow_Events,hvalue); + } + + public AllowEventsHeader(Header hd) + { super(hd); + } + + /** Gets list of events (as Vector of Strings). */ + public Vector getEvents() + { return super.getElements(); + } + + /** Sets the list of events. */ + public void setEvents(Vector events) + { super.setElements(events); + } + + /** Adds a new event to the event list. */ + public void addEvent(String event) + { super.addElement(event); + } +} diff --git a/src/org/zoolu/sip/header/AllowHeader.java b/src/org/zoolu/sip/header/AllowHeader.java new file mode 100644 index 0000000..2be9b4d --- /dev/null +++ b/src/org/zoolu/sip/header/AllowHeader.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.header; + + +import java.util.Vector; + + +/** SIP Header Allow */ +public class AllowHeader extends ListHeader +{ + public AllowHeader(String hvalue) + { super(SipHeaders.Allow,hvalue); + } + + public AllowHeader(Header hd) + { super(hd); + } + + /** Gets list of methods (as Vector of Strings). */ + public Vector getMethods() + { return super.getElements(); + } + + /** Sets the list of methods. */ + public void setMethod(Vector methods) + { super.setElements(methods); + } + + /** Adds a new method to the methods list. */ + public void addMethod(String method) + { super.addElement(method); + } +} diff --git a/src/org/zoolu/sip/header/AuthenticationHeader.java b/src/org/zoolu/sip/header/AuthenticationHeader.java new file mode 100644 index 0000000..8c03c62 --- /dev/null +++ b/src/org/zoolu/sip/header/AuthenticationHeader.java @@ -0,0 +1,418 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.header; + + +import org.zoolu.sip.provider.SipParser; +import org.zoolu.tools.Parser; +import java.util.Vector; + + +/** Abstract header for various authentication schemes + *

    It is inherited by WwwAuthenticateHeader, AuthorizationHeader, etc. + */ +public abstract class AuthenticationHeader extends Header +{ + + /** Lienar white space separator inserted bethween parameters. */ + //public static String LWS_SEPARATOR="\r\n "; + public static String LWS_SEPARATOR=" "; + + /** Array of parameters that are quoted. */ + public static String[] QUOTED_PARAMETERS={ "auts", "cnonce", "nextnonce", "nonce", "opaque", "realm", "response", "rspauth", "uri", "username" }; + + + + /** Whether is a quoted parameter (i.e. belongs to QUOTED_PARAMETERS). */ + private static boolean isQuotedParameter(String param_name) + { for (int i=0; iauth_scheme and the vector of authentication parameters. + *

    auth_params is a vector of String of the form parm_name "=" parm_value */ + public AuthenticationHeader(String hname, String auth_scheme, Vector auth_params) + { super(hname,auth_scheme); + if (auth_params.size()>0) value+=" "+(String)auth_params.elementAt(0); + for (int i=1; iparam_name belongs to QUOTED_PARAMETERS, param_value is quoted (if already not). */ + public void addParameter(String param_name, String param_value) + { if (param_value.indexOf('"')<0 && isQuotedParameter(param_name)) addQuotedParameter(param_name,param_value); + else addUnquotedParameter(param_name,param_value); + } + + + /** Adds a parameter without inserting quotes. */ + public void addUnquotedParameter(String param_name, String param_value) + { if (value.indexOf('=')<0) value+=" "; else value+=","+LWS_SEPARATOR; + value+=param_name+"="+param_value; + } + + + /** Adds a parameter with quotes. */ + public void addQuotedParameter(String param_name, String param_value) + { if (value.indexOf('=')<0) value+=" "; else value+=","+LWS_SEPARATOR; + if (param_value.indexOf('"')>=0) value+=param_name+"="+param_value; + else value+=param_name+"=\""+param_value+"\""; + } + + + /** Whether has parameter param_name */ + public boolean hasParameter(String param_name) + { char[] name_separators={'=', ' ', '\t', '\r', '\n'}; + SipParser par=new SipParser(value); + par.skipString(); // skip the auth_scheme + par.skipWSPCRLF(); + while (par.hasMore()) + { String name=par.getWord(name_separators); + if (name.equals(param_name)) return true; + par.goToCommaHeaderSeparator().skipChar().skipWSPCRLF(); + } + return false; + } + + + /** Returns the parameter param_name, without quotes. */ + public String getParameter(String param_name) + { char[] name_separators={'=', ' ', '\t'}; + SipParser par=new SipParser(value); + par.skipString(); // skip the auth_scheme + par.skipWSPCRLF(); + while (par.hasMore()) + { String name=par.getWord(name_separators); + if (name.equals(param_name)) + { par.goTo('=').skipChar().skipWSP(); + int comma=par.indexOfCommaHeaderSeparator(); + if (comma>=0) + par=new SipParser(par.getString(comma-par.getPos())); + return par.getStringUnquoted(); + } + else par.goToCommaHeaderSeparator().skipChar().skipWSPCRLF(); + } + return null; + } + + + /** Gets a String Vector of parameter names. + * @returns a Vector of String. */ + public Vector getParameters() + { char[] name_separators={'=', ' ', '\t'}; + SipParser par=new SipParser(value); + par.skipString(); // skip the auth_scheme + par.skipWSPCRLF(); + Vector names=new Vector(); + while (par.hasMore()) + { String name=par.getWord(name_separators); + names.addElement(name); + par.goToCommaHeaderSeparator().skipChar().skipWSPCRLF(); + } + return names; + } + + /** Gets the athentication scheme (i.e. the first token). */ + public String getAuthScheme() + { SipParser par=new SipParser(value); + return par.getString(); + } + + + + // ***************** quoted parameters ***************** + + /** Whether has realm */ + public boolean hasRealmParam() + { return hasParameter("realm"); + } + + /** Returns the realm (unquoted) */ + public String getRealmParam() + { return getParameter("realm"); + } + + /** Adds the realm */ + public void addRealmParam(String unquoted_realm) + { addQuotedParameter("realm",unquoted_realm); + } + + + + /** Whether has nonce */ + public boolean hasNonceParam() + { return hasParameter("nonce"); + } + + /** Returns the nonce (unquoted) */ + public String getNonceParam() + { return getParameter("nonce"); + } + + /** Adds the nonce */ + public void addNonceParam(String unquoted_nonce) + { addQuotedParameter("nonce",unquoted_nonce); + } + + + + /** Whether has opaque */ + public boolean hasOpaqueParam() + { return hasParameter("opaque"); + } + + /** Returns the opaque (unquoted) */ + public String getOpaqueParam() + { return getParameter("opaque"); + } + + /** Adds the opaque */ + public void addOpaqueParam(String unquoted_opaque) + { addQuotedParameter("opaque",unquoted_opaque); + } + + + + /** Whether has username */ + public boolean hasUsernameParam() + { return hasParameter("username"); + } + + /** Returns the username (unquoted) */ + public String getUsernameParam() + { return getParameter("username"); + } + + /** Adds the username */ + public void addUsernameParam(String unquoted_username) + { addQuotedParameter("username",unquoted_username); + } + + + + /** Whether has uri */ + public boolean hasUriParam() + { return hasParameter("uri"); + } + + /** Returns the uri (unquoted) */ + public String getUriParam() + { return getParameter("uri"); + } + + /** Adds the uri */ + public void addUriParam(String unquoted_uri) + { addQuotedParameter("uri",unquoted_uri); + } + + + + /** Whether has response */ + public boolean hasResponseParam() + { return hasParameter("response"); + } + + /** Returns the response (unquoted) */ + public String getResponseParam() + { return getParameter("response"); + } + + /** Adds the response */ + public void addResponseParam(String unquoted_response) + { addQuotedParameter("response",unquoted_response); + } + + + + /** Whether has cnonce */ + public boolean hasCnonceParam() + { return hasParameter("cnonce"); + } + + /** Returns the cnonce (unquoted) */ + public String getCnonceParam() + { return getParameter("cnonce"); + } + + /** Adds the cnonce */ + public void addCnonceParam(String unquoted_cnonce) + { addQuotedParameter("cnonce",unquoted_cnonce); + } + + + + /** Whether has rspauth */ + public boolean hasRspauthParam() + { return hasParameter("rspauth"); + } + + /** Returns the rspauth (unquoted) */ + public String getRspauthParam() + { return getParameter("rspauth"); + } + + /** Adds the rspauth */ + public void addRspauthParam(String unquoted_rspauth) + { addQuotedParameter("rspauth",unquoted_rspauth); + } + + + + /** Whether has auts */ + public boolean hasAutsParam() + { return hasParameter("auts"); + } + + /** Returns the auts */ + public String getAutsParam() + { return getParameter("auts"); + } + + /** Adds the auts */ + public void addAutsParam(String unquoted_auts) + { addQuotedParameter("auts",unquoted_auts); + } + + + /** Whether has nextnonce */ + public boolean hasNextnonceParam() + { return hasParameter("nextnonce"); + } + + /** Returns the nextnonce */ + public String getNextnonceParam() + { return getParameter("nextnonce"); + } + + /** Adds the nextnonce */ + public void addNextnonceParam(String unquoted_nextnonce) + { addQuotedParameter("nextnonce",unquoted_nextnonce); + } + + + /** Whether has qop-options */ + public boolean hasQopOptionsParam() + { return hasParameter("qop"); + } + + /** Gets the qop-options */ + /*public String[] getQopOptionsParam() + { Vector aux=new Vector(); + Parser par=new Parser(getParameter("qop")); + char[] separators={','}; + String qop=null; + while ((qop=par.getWord(separators))!=null) aux.addElement(qop); + if (aux.size()==0) return null; + String[] qop_options=new String[aux.size()]; + for (int i=0; i0) sb.append(","); + sb.append(qop_options[i]); + } + addQuotedParameter("qop",sb.toString()); + }*/ + /** Adds the qop-options */ + public void addQopOptionsParam(String unquoted_qop_options) + { addQuotedParameter("qop",unquoted_qop_options); + } + + + // **************** unquoted parameters **************** + + /** Whether has qop */ + public boolean hasQopParam() + { return hasParameter("qop"); + } + + /** Returns the qop */ + public String getQopParam() + { return getParameter("qop"); + } + + /** Adds the qop */ + public void addQopParam(String qop) + { addUnquotedParameter("qop",qop); + } + + + /** Whether has nc */ + public boolean hasNcParam() + { return hasParameter("nc"); + } + + /** Returns the nc */ + public String getNcParam() + { return getParameter("nc"); + } + + /** Adds the nc */ + public void addNcParam(String nc) + { addUnquotedParameter("nc",nc); + } + + + + /** Whether has algorithm */ + public boolean hasAlgorithmParam() + { return hasParameter("algorithm"); + } + + /** Returns the algorithm */ + public String getAlgorithParam() + { return getParameter("algorithm"); + } + + /** Adds the algorithm */ + public void addAlgorithParam(String algorithm) + { addUnquotedParameter("algorithm",algorithm); + } + + +} diff --git a/src/org/zoolu/sip/header/AuthenticationInfoHeader.java b/src/org/zoolu/sip/header/AuthenticationInfoHeader.java new file mode 100644 index 0000000..622c55a --- /dev/null +++ b/src/org/zoolu/sip/header/AuthenticationInfoHeader.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.header; + + +import org.zoolu.sip.provider.SipParser; +import java.util.Vector; + + +/** SIP AuthenticationInfo header */ +public class AuthenticationInfoHeader extends AuthenticationHeader +{ + /** Creates a new AuthenticationInfoHeader */ + public AuthenticationInfoHeader() + { super(SipHeaders.Authentication_Info,""); + } + + /** Creates a new AuthenticationInfoHeader */ + public AuthenticationInfoHeader(String hvalue) + { super(SipHeaders.Authentication_Info,hvalue); + } + + /** Creates a new AuthenticationInfoHeader */ + public AuthenticationInfoHeader(Header hd) + { super(hd); + } + + /** Creates a new AuthenticationInfoHeader + * specifing the auth_scheme and the vector of authentication parameters. + *

    auth_param is a vector of String of the form parm_name "=" parm_value */ + public AuthenticationInfoHeader(Vector auth_params) + { super(SipHeaders.Authentication_Info,"",auth_params); + } + + /** Whether has parameter param_name */ + public boolean hasParameter(String param_name) + { char[] name_separators={'=', ' ', '\t', '\r', '\n'}; + SipParser par=new SipParser(value); + //par.skipString(); // skip the auth_scheme + par.skipWSPCRLF(); + while (par.hasMore()) + { String name=par.getWord(name_separators); + if (name.equals(param_name)) return true; + par.goToCommaHeaderSeparator().skipChar().skipWSPCRLF(); + } + return false; + } + + /** Returns the parameter param_name, in case removing quotes. */ + public String getParameter(String param_name) + { char[] name_separators={'=', ' ', '\t'}; + SipParser par=new SipParser(value); + //par.skipString(); // skip the auth_scheme + par.skipWSPCRLF(); + while (par.hasMore()) + { String name=par.getWord(name_separators); + if (name.equals(param_name)) + { par.goTo('=').skipChar().skipWSP(); + int comma=par.indexOfCommaHeaderSeparator(); + if (comma>=0) + par=new SipParser(par.getString(comma-par.getPos())); + return par.getStringUnquoted(); + } + else par.goToCommaHeaderSeparator().skipChar().skipWSPCRLF(); + } + return null; + } + + /** Gets a String Vector of parameter names. + * @returns a Vector of String. */ + public Vector getParameters() + { char[] name_separators={'=', ' ', '\t'}; + SipParser par=new SipParser(value); + //par.skipString(); // skip the auth_scheme + par.skipWSPCRLF(); + Vector names=new Vector(); + while (par.hasMore()) + { String name=par.getWord(name_separators); + names.addElement(name); + par.goToCommaHeaderSeparator().skipChar().skipWSPCRLF(); + } + return names; + } + + /** Gets the athentication scheme. Note that for AuthenticationInfoHeader it always return null. */ + public String getAuthScheme() + { return null; + } +} diff --git a/src/org/zoolu/sip/header/AuthorizationHeader.java b/src/org/zoolu/sip/header/AuthorizationHeader.java new file mode 100644 index 0000000..3fdf3b3 --- /dev/null +++ b/src/org/zoolu/sip/header/AuthorizationHeader.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.header; + + +import java.util.Vector; + + +/** SIP Authorization header */ +public class AuthorizationHeader extends AuthenticationHeader +{ + /** Creates a new AuthorizationHeader */ + public AuthorizationHeader(String hvalue) + { super(SipHeaders.Authorization,hvalue); + } + + /** Creates a new AuthorizationHeader */ + public AuthorizationHeader(Header hd) + { super(hd); + } + + /** Creates a new AuthorizationHeader + * specifing the auth_scheme and the vector of authentication parameters. + *

    auth_param is a vector of String of the form parm_name "=" parm_value */ + public AuthorizationHeader(String auth_scheme, Vector auth_params) + { super(SipHeaders.Authorization,auth_scheme,auth_params); + } +} diff --git a/src/org/zoolu/sip/header/BaseSipHeaders.java b/src/org/zoolu/sip/header/BaseSipHeaders.java new file mode 100644 index 0000000..6bed14f --- /dev/null +++ b/src/org/zoolu/sip/header/BaseSipHeaders.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.header; + + + + +/** SipHeaders simply collects all standard SIP header names. */ +public abstract class BaseSipHeaders +{ + /** String "Accept" */ + public static final String Accept="Accept"; + /** String "Alert-Info" */ + public static final String Alert_Info="Alert-Info"; + /** String "Allow" */ + public static final String Allow="Allow"; + /** String "Authentication-Info" */ + public static final String Authentication_Info="Authentication-Info"; + /** String "Authorization" */ + public static final String Authorization="Authorization"; + /** String "Call-ID" */ + public static final String Call_ID="Call-ID"; + /** String "i" */ + public static final String Call_ID_short="i"; + /** String "Contact" */ + public static final String Contact="Contact"; + /** String "m" */ + public static final String Contact_short="m"; + /** String "Content-Length" */ + public static final String Content_Length="Content-Length"; + /** String "l" */ + public static final String Content_Length_short="l"; + /** String "Content-Type" */ + public static final String Content_Type="Content-Type"; + /** String "c" */ + public static final String Content_Type_short="c"; + /** String "CSeq" */ + public static final String CSeq="CSeq"; + /** String "Date" */ + public static final String Date="Date"; + /** String "Expires" */ + public static final String Expires="Expires"; + /** String "From" */ + public static final String From="From"; + /** String "f" */ + public static final String From_short="f"; + /** String "User-Agent" */ + public static final String User_Agent="User-Agent"; + /** String "Max-Forwards" */ + public static final String Max_Forwards="Max-Forwards"; + /** String "Proxy-Authenticate" */ + public static final String Proxy_Authenticate="Proxy-Authenticate"; + /** String "Proxy-Authorization" */ + public static final String Proxy_Authorization="Proxy-Authorization"; + /** String "Proxy-Require" */ + public static final String Proxy_Require="Proxy-Require"; + /** String "Record-Route" */ + public static final String Record_Route="Record-Route"; + /** String "Require" */ + public static final String Require="Require"; + /** String "Route" */ + public static final String Route="Route"; + /** String "Server" */ + public static final String Server="Server"; + /** String "Subject" */ + public static final String Subject="Subject"; + /** String "s" */ + public static final String Subject_short="s"; + /** String "Supported" */ + public static final String Supported="Supported"; + /** String "k" */ + public static final String Supported_short="k"; + /** String "To" */ + public static final String To="To"; + /** String "t" */ + public static final String To_short="t"; + /** String "Unsupported" */ + public static final String Unsupported="Unsupported"; + /** String "Via" */ + public static final String Via="Via"; + /** String "v" */ + public static final String Via_short="v"; + /** String "WWW-Authenticate" */ + public static final String WWW_Authenticate="WWW-Authenticate"; + + /** Whether s1 and s2 are case-unsensitive-equal. */ + protected static boolean same(String s1, String s2) + { //return s1.compareToIgnoreCase(s2)==0; + return s1.equalsIgnoreCase(s2); + } + + /** Whether str is a Accept field */ + public static boolean isAccept(String str) { return same(str,Accept); } + /** Whether str is a Alert_Info field */ + public static boolean isAlert_Info(String str) { return same(str,Alert_Info); } + /** Whether str is a Allow field */ + public static boolean isAllow(String str) { return same(str,Allow); } + /** Whether str is a Authentication_Info field */ + public static boolean isAuthentication_Info(String str) { return same(str,Authentication_Info); } + /** Whether str is a Authorization field */ + public static boolean isAuthorization(String str) { return same(str,Authorization); } + /** Whether str is a Call-ID field */ + public static boolean isCallId(String str) { return same(str,Call_ID) || same(str,Call_ID_short); } + /** Whether str is a Contact field */ + public static boolean isContact(String str) { return same(str,Contact) || same(str,Contact_short); } + /** Whether str is a Content_Length field */ + public static boolean isContent_Length(String str) { return same(str,Content_Length) || same(str,Content_Length_short); } + /** Whether str is a Content_Type field */ + public static boolean isContent_Type(String str) { return same(str,Content_Type) || same(str,Content_Type_short); } + /** Whether str is a CSeq field */ + public static boolean isCSeq(String str) { return same(str,CSeq); } + /** Whether str is a Date field */ + public static boolean isDate(String str) { return same(str,Date); } + /** Whether str is a Expires field */ + public static boolean isExpires(String str) { return same(str,Expires); } + /** Whether str is a From field */ + public static boolean isFrom(String str) { return same(str,From) || same(str,From_short); } + /** Whether str is a User_Agent field */ + public static boolean isUser_Agent(String str) { return same(str,User_Agent); } + /** Whether str is a Max_Forwards field */ + public static boolean isMax_Forwards(String str) { return same(str,Max_Forwards); } + /** Whether str is a Proxy_Authenticate field */ + public static boolean isProxy_Authenticate(String str) { return same(str,Proxy_Authenticate); } + /** Whether str is a Proxy_Authorization field */ + public static boolean isProxy_Authorization(String str) { return same(str,Proxy_Authorization); } + /** Whether str is a Proxy_Require field */ + public static boolean isProxy_Require(String str) { return same(str,Proxy_Require); } + /** Whether str is a Record_Route field */ + public static boolean isRecord_Route(String str) { return same(str,Record_Route); } + /** Whether str is a Require field */ + public static boolean isRequire(String str) { return same(str,Require); } + /** Whether str is a Route field */ + public static boolean isRoute(String str) { return same(str,Route); } + /** Whether str is a Server field */ + public static boolean isServer(String str) { return same(str,Server); } + /** Whether str is a Subject field */ + public static boolean isSubject(String str) { return same(str,Subject) || same(str,Subject_short); } + /** Whether str is a Supported field */ + public static boolean isSupported(String str) { return same(str,Supported) || same(str,Supported_short); } + /** Whether str is a To field */ + public static boolean isTo(String str) { return same(str,To) || same(str,To_short); } + /** Whether str is a Unsupported field */ + public static boolean isUnsupported(String str) { return same(str,Unsupported); } + /** Whether str is a Via field */ + public static boolean isVia(String str) { return same(str,Via) || same(str,Via_short); } + /** Whether str is a WWW_Authenticate field */ + public static boolean isWWW_Authenticate(String str) { return same(str,WWW_Authenticate); } + +} diff --git a/src/org/zoolu/sip/header/CSeqHeader.java b/src/org/zoolu/sip/header/CSeqHeader.java new file mode 100644 index 0000000..e10d5f8 --- /dev/null +++ b/src/org/zoolu/sip/header/CSeqHeader.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.header; + + +import org.zoolu.sip.provider.SipParser; + + +/** SIP Header CSeq. + * The CSeq header field serves as a way to identify and order + * transactions. It consists of a sequence number and a method. The + * method MUST match that of the request. For non-REGISTER requests + * outside of a dialog, the sequence number value is arbitrary. + */ +public class CSeqHeader extends Header +{ + //public CSeqHeader() + //{ super(SipHeaders.CSeq); + //} + + public CSeqHeader(String hvalue) + { super(SipHeaders.CSeq,hvalue); + } + + public CSeqHeader(Header hd) + { super(hd); + } + + public CSeqHeader(long seq, String method) + { super(SipHeaders.CSeq,String.valueOf(seq)+" "+method); + } + + /** Gets method of CSeqHeader */ + public String getMethod() + { SipParser par=new SipParser(value); + par.skipString(); // skip sequence number + return par.getString(); + } + + /** Gets sequence number of CSeqHeader */ + public long getSequenceNumber() + { return (new SipParser(value)).getInt(); + } + + /** Sets method of CSeqHeader */ + public void setMethod(String method) + { value=getSequenceNumber()+" "+method; + } + + /** Sets sequence number of CSeqHeader */ + public void setSequenceNumber(long sequenceNumber) + { value=String.valueOf(sequenceNumber)+" "+getMethod(); + } + + /** Increments sequence number of CSeqHeader */ + public CSeqHeader incSequenceNumber() + { value=String.valueOf(getSequenceNumber()+1)+" "+getMethod(); + return this; + } +} + diff --git a/src/org/zoolu/sip/header/CallIdHeader.java b/src/org/zoolu/sip/header/CallIdHeader.java new file mode 100644 index 0000000..6fd3857 --- /dev/null +++ b/src/org/zoolu/sip/header/CallIdHeader.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.header; + + +import org.zoolu.tools.Parser; + + +/** SIP Header Call-ID. + * The Call-ID header field acts as a unique identifier to group + * together a series of messages. It MUST be the same for all requests + * and responses sent by either UA in a dialog. It SHOULD be the same + * in each registration from a UA. + *
    In a new request created by a UAC outside of any dialog, the Call-ID + * header field MUST be selected by the UAC as a globally unique + * identifier over space and time unless overridden by method-specific + * behavior. + *
    Use of cryptographically random identifiers in the + * generation of Call-IDs is RECOMMENDED. Implementations MAY use the + * form "localid@host". Call-IDs are case-sensitive and are simply + * compared byte-by-byte. + */ +public class CallIdHeader extends Header +{ + /** Creates a CallIdHeader */ + //public CallIdHeader() + //{ super(SipHeaders.Call_ID); + //} + + /** Creates a CallIdHeader with value hvalue */ + public CallIdHeader(String hvalue) + { super(SipHeaders.Call_ID,hvalue); + } + + /** Creates a new CallIdHeader equal to CallIdHeader hd */ + public CallIdHeader(Header hd) + { super(hd); + } + + /** Gets Call-Id of CallIdHeader */ + public String getCallId() + { return (new Parser(value)).getString(); + } + + /** Sets Call-Id of CallIdHeader */ + public void setCallId(String callId) + { value=callId; + } +} diff --git a/src/org/zoolu/sip/header/ContactHeader.java b/src/org/zoolu/sip/header/ContactHeader.java new file mode 100644 index 0000000..a0f6f95 --- /dev/null +++ b/src/org/zoolu/sip/header/ContactHeader.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.header; + + +import org.zoolu.sip.address.*; +import org.zoolu.sip.provider.SipParser; +import org.zoolu.tools.Parser; +import org.zoolu.tools.DateFormat; +import java.util.Date; +//import java.text.DateFormat; +//import java.text.SimpleDateFormat; + + +/** SIP Header Contact. + * The Contact header field provides a SIP or SIPS URI that can be used + * to contact that specific instance of the UA for subsequent requests. + * The Contact header field MUST be present and contain exactly one SIP + * URI in any request that can result in the establishment of a + * dialog (i.e. INVITEs). + *

    Note: for backward compatibility with legacy implementations + * the date format in 'expires' parameter is still supported + * although it has been deprecated in RFC 3261. + */ +public class ContactHeader extends EndPointHeader +{ + /** Creates a ContactHeader with '*' as contact value */ + public ContactHeader() + { super(new Header(SipHeaders.Contact,null)); + value="*"; + } + + public ContactHeader(NameAddress nameaddr) + { super(SipHeaders.Contact,nameaddr); + } + + public ContactHeader(SipURL url) + { super(SipHeaders.Contact,url); + } + + public ContactHeader(Header hd) + { super(hd); + } + + //public void setStar() + //{ value="*"; + //} + + public ContactHeader setExpires(Date expire) + { setParameter("expires","\""+DateFormat.formatEEEddMMM(expire)+"\""); + return this; + } + + public ContactHeader setExpires(int secs) + { setParameter("expires",Integer.toString(secs)); + return this; + } + + public boolean isStar() + { if (value.indexOf('*')>=0) return true; + else return false; + } + + public boolean hasExpires() + { return hasParameter("expires"); + } + + public boolean isExpired() + { if (getExpires()==0) return true; + else return false; + } + + public int getExpires() + { int secs=-1; + String exp_param=getParameter("expires"); + if (exp_param!=null) + { if (exp_param.indexOf("GMT")>=0) + { Date date=(new SipParser((new Parser(exp_param)).getStringUnquoted())).getDate(); + secs=(int)((date.getTime()-System.currentTimeMillis())/1000); + if (secs<0) secs=0; + } + else secs=(new SipParser(exp_param)).getInt(); + } + return secs; + } + + public Date getExpiresDate() + { Date date=null; + String exp_param=getParameter("expires"); + if (exp_param!=null) + { if (exp_param.indexOf("GMT")>=0) + { date=(new SipParser((new Parser(exp_param)).getStringUnquoted())).getDate(); + } + else + { long secs=(new SipParser(exp_param)).getInt(); + if (secs>=0) date=new Date(System.currentTimeMillis()+secs*1000); + } + } + return date; + } + + public ContactHeader removeExpires() + { removeParameter("expires"); + return this; + } +/* + public static String toString(Vector clist) + { String str="Contact: "; + for (int i=0; i It extends the NameAddressHeader.getNameAddress() method, by removing + * eventual EndPointHeader field parameters (e.g. 'tag' param) from the returnerd NameAddress. + * @return the end point NameAddress or null if NameAddress does not exist + * (that leads to the wildcard in case of ContactHeader) */ + public NameAddress getNameAddress() + { NameAddress naddr=(new SipParser(value)).getNameAddress(); + // patch for removing eventual 'tag' or other EndPointHeader parameters from NameAddress + SipURL url=naddr.getAddress(); + for (int i=0; i< ENDPOINT_PARAMS.length; i++) + { if (url.hasParameter(ENDPOINT_PARAMS[i])) + { url.removeParameter(ENDPOINT_PARAMS[i]); + naddr=new NameAddress(naddr.getDisplayName(),url); + } + } + return naddr; + } + +} diff --git a/src/org/zoolu/sip/header/EventHeader.java b/src/org/zoolu/sip/header/EventHeader.java new file mode 100644 index 0000000..1fc5061 --- /dev/null +++ b/src/org/zoolu/sip/header/EventHeader.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.header; + + +import org.zoolu.sip.address.*; +import org.zoolu.tools.Parser; + + +/** SIP Event header (RFC 3265). + *

    Event is a request header field (request-header). + * It appears in SUBSCRIBE and NOTIFY requests. It provides a event-package name. */ +public class EventHeader extends ParametricHeader +{ + /** State delimiters. */ + private static final char [] delim={',', ';', ' ', '\t', '\n', '\r'}; + + /** Costructs a new EventHeader. */ + public EventHeader(String event_package) + { super(SipHeaders.Event,event_package); + } + + /** Costructs a new EventHeader. */ + public EventHeader(String event_package, String id) + { super(SipHeaders.Event,event_package); + if (id!=null) this.setParameter("id",id); + } + + /** Costructs a new EventHeader. */ + public EventHeader(Header hd) + { super(hd); + } + + /** Gets the event name. */ + public String getEvent() + { return new Parser(value).getWord(delim); + } + + /** Gets 'id' parameter. */ + public String getId() + { return this.getParameter("id"); + } + + /** Whether it has 'id' parameter. */ + public boolean hasId() + { return this.hasParameter("id"); + } + +} + diff --git a/src/org/zoolu/sip/header/ExpiresHeader.java b/src/org/zoolu/sip/header/ExpiresHeader.java new file mode 100644 index 0000000..3b56cbc --- /dev/null +++ b/src/org/zoolu/sip/header/ExpiresHeader.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.header; + + +import org.zoolu.sip.provider.SipParser; +import org.zoolu.tools.Parser; +import java.util.Date; + + +/** SIP Header Expires. + *

    Note: for backward compatibility with legacy implementations + * the date format is still supported + * although it has been deprecated in RFC 3261. + */ +public class ExpiresHeader extends SipDateHeader +{ + //public ExpiresHeader() + //{ super(SipHeaders.Expires); + //} + + public ExpiresHeader(String hvalue) + { super(SipHeaders.Expires,hvalue); + } + + /** Creates a new ExpiresHeader based on a Date value. */ + public ExpiresHeader(Date date) + { super(SipHeaders.Expires,date); + } + + /** Creates a new ExpiresHeader with delta-seconds as value. */ + public ExpiresHeader(int seconds) + { super(SipHeaders.Expires,(String)null); + value=String.valueOf(seconds); + } + + + public ExpiresHeader(Header hd) + { super(hd); + } + + /** Gets boolean value to indicate if expiry value of ExpiresHeader is in date format. */ + public boolean isDate() + { if (value.indexOf("GMT")>=0) return true; + return false; + } + + /** Gets value of ExpiresHeader as delta-seconds */ + public int getDeltaSeconds() + { int secs=-1; + if (isDate()) + { Date date=(new SipParser((new Parser(value)).getStringUnquoted())).getDate(); + secs=(int)((date.getTime()-System.currentTimeMillis())/1000); + if (secs<0) secs=0; + } + else secs=(new SipParser(value)).getInt(); + + return secs; + } + + /** Gets value of ExpiresHeader as absolute date */ + public Date getDate() + { Date date=null; + if (isDate()) + { date=(new SipParser((new Parser(value)).getStringUnquoted())).getDate(); + } + else + { long secs=getDeltaSeconds(); + if (secs>=0) date=new Date(System.currentTimeMillis()+secs*1000); + } + return date; + } + + /** Sets expires of ExpiresHeader as delta-seconds */ + //public void setDeltaSeconds(long seconds) + //{ value=String.valueOf(seconds); + //} + + /** Gets value of ExpiresHeader */ + /* + public static void getExpires(ExpiresHeader eh) + { + if (eh.isDate()) eh.getDate(); + else eh.getDeltaSeconds(); + } + */ +} + diff --git a/src/org/zoolu/sip/header/FromHeader.java b/src/org/zoolu/sip/header/FromHeader.java new file mode 100644 index 0000000..74daea1 --- /dev/null +++ b/src/org/zoolu/sip/header/FromHeader.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.header; + + +import org.zoolu.sip.address.*; + + +/** SIP Header From. + * The From header field indicates the logical identity of the initiator + * of the request, possibly the user's address-of-record. Like the To + * header field, it contains a URI and optionally a display name. + *
    The From field MUST contain a new "tag" parameter, chosen by the UAC. + */ +public class FromHeader extends EndPointHeader +{ + //public FromHeader() + //{ super(SipHeaders.From); + //} + + public FromHeader(NameAddress nameaddr) + { super(SipHeaders.From,nameaddr); + } + + public FromHeader(SipURL url) + { super(SipHeaders.From,url); + } + + public FromHeader(NameAddress nameaddr, String tag) + { super(SipHeaders.From,nameaddr,tag); + } + + public FromHeader(SipURL url, String tag) + { super(SipHeaders.From,url,tag); + } + + public FromHeader(Header hd) + { super(hd); + } +} + diff --git a/src/org/zoolu/sip/header/Header.java b/src/org/zoolu/sip/header/Header.java new file mode 100644 index 0000000..a82839a --- /dev/null +++ b/src/org/zoolu/sip/header/Header.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.header; + + + +/** Header is the base Class for all SIP Headers + */ +public class Header +{ + /** The header type */ + protected String name; + /** The header string, without terminating CRLF */ + protected String value; + + /** Creates a void Header. */ + protected Header() + { name=null; + value=null; + } + + /** Creates a new Header. */ + public Header(String hname, String hvalue) + { name=hname; + value=hvalue; + } + + /** Creates a new Header. */ + public Header(Header hd) + { name=hd.getName(); + value=hd.getValue(); + } + + /** Creates and returns a copy of the Header */ + public Object clone() + { return new Header(getName(),getValue()); + } + + /** Whether the Header is equal to Object obj */ + public boolean equals(Object obj) + { try + { Header hd=(Header)obj; + if (hd.getName().equals(this.getName()) && hd.getValue().equals(this.getValue())) return true; + else return false; + } + catch (Exception e) { return false; } + } + + /** Gets name of Header */ + public String getName() + { return name; + } + + /** Gets value of Header */ + public String getValue() + { return value; + } + + /** Sets value of Header */ + public void setValue(String hvalue) + { value=hvalue; + } + + /** Gets string representation of Header */ + public String toString() + { return name+": "+value+"\r\n"; + } +} diff --git a/src/org/zoolu/sip/header/ListHeader.java b/src/org/zoolu/sip/header/ListHeader.java new file mode 100644 index 0000000..f376098 --- /dev/null +++ b/src/org/zoolu/sip/header/ListHeader.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.header; + + +import org.zoolu.tools.Parser; +import java.util.Vector; + + +/** Generic SIP Header containing a list of tokens (Strings). */ +public abstract class ListHeader extends Header +{ + public ListHeader(String hname, String hvalue) + { super(hname,hvalue); + } + + public ListHeader(Header hd) + { super(hd); + } + + /** Gets list of tokens (as Vector of Strings). */ + public Vector getElements() + { Vector elements=new Vector(); + Parser par=new Parser(value); + char[] delim={ ',' }; + while (par.hasMore()) + { String elem=par.getWord(delim).trim(); + if (elem!=null && elem.length()>0) elements.addElement(elem); + par.skipChar(); + } + return elements; + } + + /** Sets the list of tokens. */ + public void setElements(Vector elements) + { StringBuffer sb=new StringBuffer(); + for (int i=0; i0) sb.append(", "); + sb.append((String)elements.elementAt(i)); + } + value=sb.toString(); + } + + /** Adds a new token to the elements list. */ + public void addElement(String elem) + { if (value==null || value.length()==0) value=elem; + else value+=", "+elem; + } +} diff --git a/src/org/zoolu/sip/header/MaxForwardsHeader.java b/src/org/zoolu/sip/header/MaxForwardsHeader.java new file mode 100644 index 0000000..bebb9c1 --- /dev/null +++ b/src/org/zoolu/sip/header/MaxForwardsHeader.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.header; + + +import org.zoolu.tools.Parser; +//import org.zoolu.sip.provider.SipStack; + + +/** SIP Header Max-Forwards + * The Max-Forwards header field serves to limit the number of hops a + * request can transit on the way to its destination. It consists of an + * integer that is decremented by one at each hop. If the Max-Forwards + * value reaches 0 before the request reaches its destination, it will + * be rejected with a 483(Too Many Hops) error response. + * A default Max-Forwards value 70 is used. + */ +public class MaxForwardsHeader extends Header +{ + /** Creates a MaxForwardsHeader with value=SipStack.max_forwards + * (the default value is 70, as recommended in RFC3261) */ + //public MaxForwardsHeader() + //{ super("Max-Forwards",String.valueOf(SipStack.max_forwards)); + //} + + /** Creates a MaxForwardsHeader with value=n */ + public MaxForwardsHeader(int n) + { super(SipHeaders.Max_Forwards,String.valueOf(n)); + } + + public MaxForwardsHeader(String hvalue) + { super(SipHeaders.Max_Forwards,hvalue); + } + + public MaxForwardsHeader(Header hd) + { super(hd); + } + + /** Sets Max-Forwards number */ + public void setNumber(int n) + { value=String.valueOf(n); + } + + /** Gets Max-Forwards number */ + public int getNumber() + { return (new Parser(value)).getInt(); + } + + /** Decrements the Max-Forwards number */ + public void decrement() + { value=String.valueOf(getNumber()-1); + } +} + diff --git a/src/org/zoolu/sip/header/MultipleHeader.java b/src/org/zoolu/sip/header/MultipleHeader.java new file mode 100644 index 0000000..6554306 --- /dev/null +++ b/src/org/zoolu/sip/header/MultipleHeader.java @@ -0,0 +1,247 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.header; + + +import org.zoolu.sip.provider.SipParser; +import java.util.Vector; + + +/** MultipleHeader can be used to handle SIP headers that support comma-separated (multiple-header) rapresentation, + * as explaned in section 7.3.1 of RFC 3261. + */ +public class MultipleHeader +{ + /** The header type */ + protected String name; + /** Vector of header values (as Strings) */ + protected Vector values; + /** whether to be rapresented with a comma-separated(compact) header line or multiple header lines */ + protected boolean compact; + + protected MultipleHeader() + { name=null; + values=new Vector(); + compact=true; + } + + /** Costructs a MultipleHeader named hname */ + public MultipleHeader(String hname) + { name=hname; + values=new Vector(); + compact=true; + } + + /** Costructs a MultipleHeader named hname from a Vector of header values (as Strings). */ + public MultipleHeader(String hname, Vector hvalues) + { name=hname; + values=hvalues; + compact=true; + } + + /** Costructs a MultipleHeader from a Vector of Headers. Each Header can be a single header or a multiple-comma-separated header. */ + public MultipleHeader(Vector headers) + { name=((Header)headers.elementAt(0)).getName(); + values=new Vector(headers.size()); + for (int i=0; i=0) + { values.addElement(par.getString(comma-par.getPos()).trim()); + par.skipChar(); //skip comma + comma=par.indexOfCommaHeaderSeparator(); + } + values.addElement(par.getRemainingString().trim()); + compact=true; + } + + /** Costructs a MultipleHeader from a MultipleHeader */ + public MultipleHeader(MultipleHeader mhd) + { name=mhd.getName(); + values=mhd.getValues(); + compact=mhd.isCommaSeparated(); + } + + /** Checks if Header hd contains comma-separated multi-header */ + public static boolean isCommaSeparated(Header hd) + { SipParser par=new SipParser(hd.getValue()); + return par.indexOfCommaHeaderSeparator()>=0; + } + + /** Sets the MultipleHeader rappresentation as comma-separated or multiple headers */ + public void setCommaSeparated(boolean comma_separated) + { compact=comma_separated; + } + + /** Whether the MultipleHeader rappresentation is comma-separated or multiple headers */ + public boolean isCommaSeparated() + { return compact; + } + + /** Gets the size of th MultipleHeader */ + public int size() + { return values.size(); + } + + /** Whether it is empty */ + public boolean isEmpty() + { return values.isEmpty(); + } + + /** Creates and returns a copy of Header */ + public Object clone() + { return new MultipleHeader(getName(),getValues()); + } + + /** Indicates whether some other Object is "equal to" this Header */ + public boolean equals(Object obj) + { MultipleHeader hd=(MultipleHeader)obj; + if (hd.getName().equals(this.getName()) && hd.getValues().equals(this.getValues())) return true; + else return false; + } + + /** Gets name of Header */ + public String getName() + { return name; + } + + /** Gets a vector of header values */ + public Vector getValues() + { return values; + } + + /** Sets header values */ + public void setValues(Vector v) + { values=v; + } + + /** Gets a vector of headers */ + public Vector getHeaders() + { Vector v=new Vector(values.size()); + for (int i=0; i0) str+=values.elementAt(values.size()-1); + return new Header(name,str); + } + + /** Gets comma-separated(compact) or multi-headers(extended) representation.
    + * Note that an empty header is rapresentated as:
    + * - empty String (i.e. ""), for multi-headers(extended) rapresentation, + * - empty-value Header (i.e. "HeaderName: \r\n"), for comma-separated(compact) rapresentation. + */ + public String toString() + { if (compact) + { String str=name+": "; + for (int i=0; i0) str+=values.elementAt(values.size()-1); + return str+"\r\n"; + } + else + { String str=""; + for (int i=0; i'); + if (par.getPos()==value.length()) par.setPos(0); + par.goToSkippingQuoted(';'); + if (par.getPos()=value.length())? -1 : index; + } + + /** Gets the value of specified parameter. + * @returns the parameter value or null if parameter does not exist or doesn't have a value (i.e. in case of flag parameter). */ + public String getParameter(String name) + { int index=indexOfFirstSemi(); + if (index<0) return null; + return (new SipParser((new Parser(getValue(),index)).skipChar().skipWSP())).getParameter(name); + } + + /** Gets a String Vector of parameter names. + * @returns a Vector of String */ + public Vector getParameterNames() + { int index=indexOfFirstSemi(); + if (index<0) return new Vector(); + return (new SipParser((new Parser(getValue(),index)).skipChar().skipWSP())).getParameters(); + } + + + /** Whether there is the specified parameter */ + public boolean hasParameter(String name) + { int index=indexOfFirstSemi(); + if (index<0) return false; + return (new SipParser((new Parser(getValue(),index)).skipChar().skipWSP())).hasParameter(name); + } + + + /** Whether there are any parameters */ + public boolean hasParameters() + { return indexOfFirstSemi()>=0; + } + + + /** Removes all parameters (if any) */ + public void removeParameters() + { if (!hasParameters()) return; + String header=getValue(); + //System.out.println(header); + int i=header.indexOf(';'); + header=header.substring(0,i); + //System.out.println(header); + setValue(header); + } + + + /** Removes specified parameter (if present) */ + public void removeParameter(String name) + { int index=indexOfFirstSemi(); + if (index<0) return; + String header=getValue(); + Parser par=new Parser(header,index); + while (par.hasMore()) + { int begin_param=par.getPos(); + par.skipChar(); + if (par.getWord(SipParser.param_separators).equals(name)) + { String top=header.substring(0,begin_param); + par.goToSkippingQuoted(';'); + String bottom=""; + if (par.hasMore()) bottom=header.substring(par.getPos()); + header=top.concat(bottom); + setValue(header); + return; + //par=new Parser(header,par.getPos()); + } + par.goTo(';'); + } + } + + + /** Sets the value of a specified parameter. + * Zero-length String is returned in case of flag parameter (without value). */ + public void setParameter(String name, String value) + { if (getValue()==null) setValue(""); + if (hasParameter(name)) removeParameter(name); + String header=getValue(); + header=header.concat(";"+name); + if (value!=null) header=header.concat("="+value); + setValue(header); + } +} diff --git a/src/org/zoolu/sip/header/ProxyAuthenticateHeader.java b/src/org/zoolu/sip/header/ProxyAuthenticateHeader.java new file mode 100644 index 0000000..693fe8c --- /dev/null +++ b/src/org/zoolu/sip/header/ProxyAuthenticateHeader.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.header; + + +import java.util.Vector; + + +/** SIP Proxy-Authenticate header */ +public class ProxyAuthenticateHeader extends WwwAuthenticateHeader +{ + /** Creates a new ProxyAuthenticateHeader */ + public ProxyAuthenticateHeader(String hvalue) + { super(hvalue); + name=SipHeaders.Proxy_Authenticate; + } + + /** Creates a new ProxyAuthenticateHeader */ + public ProxyAuthenticateHeader(Header hd) + { super(hd); + } + + /** Creates a new ProxyAuthenticateHeader + * specifing the auth_scheme and the vector of authentication parameters. + *

    auth_param is a vector of String of the form parm_name "=" parm_value */ + public ProxyAuthenticateHeader(String auth_scheme, Vector auth_params) + { super(auth_scheme,auth_params); + name=SipHeaders.Proxy_Authenticate; + } +} diff --git a/src/org/zoolu/sip/header/ProxyAuthorizationHeader.java b/src/org/zoolu/sip/header/ProxyAuthorizationHeader.java new file mode 100644 index 0000000..e874113 --- /dev/null +++ b/src/org/zoolu/sip/header/ProxyAuthorizationHeader.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.header; + + +import java.util.Vector; + + +/** SIP Proxy-Authorization header */ +public class ProxyAuthorizationHeader extends AuthorizationHeader +{ + /** Creates a new ProxyAuthorizationHeader */ + public ProxyAuthorizationHeader(String hvalue) + { super(hvalue); + name=SipHeaders.Proxy_Authorization; + } + + /** Creates a new ProxyAuthorizationHeader */ + public ProxyAuthorizationHeader(Header hd) + { super(hd); + } + + /** Creates a new ProxyAuthorizationHeader + * specifing the auth_scheme and the vector of authentication parameters. + *

    auth_param is a vector of String of the form parm_name "=" parm_value */ + public ProxyAuthorizationHeader(String auth_scheme, Vector auth_params) + { super(auth_scheme,auth_params); + name=SipHeaders.Proxy_Authorization; + } +} diff --git a/src/org/zoolu/sip/header/ProxyRequireHeader.java b/src/org/zoolu/sip/header/ProxyRequireHeader.java new file mode 100644 index 0000000..f371167 --- /dev/null +++ b/src/org/zoolu/sip/header/ProxyRequireHeader.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.header; + + + + +/** SIP Header Proxy-Require */ +public class ProxyRequireHeader extends OptionHeader +{ + public ProxyRequireHeader(String option) + { super(SipHeaders.Proxy_Require,option); + } + + public ProxyRequireHeader(Header hd) + { super(hd); + } +} diff --git a/src/org/zoolu/sip/header/RecordRouteHeader.java b/src/org/zoolu/sip/header/RecordRouteHeader.java new file mode 100644 index 0000000..bd41533 --- /dev/null +++ b/src/org/zoolu/sip/header/RecordRouteHeader.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.header; + + +import org.zoolu.sip.address.NameAddress; + + +/** SIP Header Record-Route */ +public class RecordRouteHeader extends NameAddressHeader +{ + //public RecordRouteHeader() + //{ super(SipHeaders.Record_Route); + //} + + public RecordRouteHeader(NameAddress nameaddr) + { super(SipHeaders.Record_Route,nameaddr); + } + + public RecordRouteHeader(Header hd) + { super(hd); + } +} diff --git a/src/org/zoolu/sip/header/ReferToHeader.java b/src/org/zoolu/sip/header/ReferToHeader.java new file mode 100644 index 0000000..135aa7c --- /dev/null +++ b/src/org/zoolu/sip/header/ReferToHeader.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.header; + + +import org.zoolu.sip.address.*; + + +/** SIP ReferTo header (RFC 3515). + *

    Refer-To is a request header field (request-header). + * It appears in REFER requests. It provides a URL to reference. */ +public class ReferToHeader extends NameAddressHeader +{ + + public ReferToHeader(NameAddress nameaddr) + { super(SipHeaders.Refer_To,nameaddr); + } + + public ReferToHeader(SipURL url) + { super(SipHeaders.Refer_To,url); + } + + public ReferToHeader(Header hd) + { super(hd); + } +} + diff --git a/src/org/zoolu/sip/header/ReferredByHeader.java b/src/org/zoolu/sip/header/ReferredByHeader.java new file mode 100644 index 0000000..3253e2b --- /dev/null +++ b/src/org/zoolu/sip/header/ReferredByHeader.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.header; + + +import org.zoolu.sip.address.*; + + +/** SIP Referred-By header (draft-ietf-sip-referredby). + *

    Referred-By is a request header field (request-header). + * It appears in REFER requests. It provides the URL of the referrer. */ +public class ReferredByHeader extends NameAddressHeader +{ + + /** Costructs a new ReferredByHeader. */ + public ReferredByHeader(NameAddress nameaddr) + { super(SipHeaders.Referred_By,nameaddr); + } + + /** Costructs a new ReferredByHeader. */ + public ReferredByHeader(SipURL url) + { super(SipHeaders.Referred_By,url); + } + + /** Costructs a new ReferredByHeader. */ + public ReferredByHeader(Header hd) + { super(hd); + } +} + diff --git a/src/org/zoolu/sip/header/RequestLine.java b/src/org/zoolu/sip/header/RequestLine.java new file mode 100644 index 0000000..61f20a5 --- /dev/null +++ b/src/org/zoolu/sip/header/RequestLine.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.header; + + +import org.zoolu.sip.address.*; + + +/** SIP Request-line, i.e. the first line of a request message + *
    The initial Request-URI of the message SHOULD be set to the value of + * the URI in the To field. + */ +public class RequestLine +{ + protected String method; + protected SipURL url; + + /** Construct RequestLine request with sipurl as recipient */ + public RequestLine(String request, String sipUrl) + { method=request; + url=new SipURL(sipUrl); + } + + public RequestLine(String request, SipURL sipUrl) + { method=request; + url=sipUrl; + } + + /** Create a new copy of the RequestLine*/ + public Object clone() + { return new RequestLine(getMethod(),getAddress()); + } + + /** Indicates whether some other Object is "equal to" this RequestLine */ + public boolean equals(Object obj) + { //if (o.getClass().getSuperclass()!=this.getClass().getSuperclass()) return false; + try + { RequestLine r=(RequestLine)obj; + if (r.getMethod().equals(this.getMethod()) && r.getAddress().equals(this.getAddress())) return true; + else return false; + } + catch (Exception e) { return false; } + } + + public String toString() + { return method+" "+url+" SIP/2.0\r\n"; + } + + public String getMethod() + { return method; + } + + public SipURL getAddress() + { return url; + } +} diff --git a/src/org/zoolu/sip/header/RequireHeader.java b/src/org/zoolu/sip/header/RequireHeader.java new file mode 100644 index 0000000..deb6c83 --- /dev/null +++ b/src/org/zoolu/sip/header/RequireHeader.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.header; + + + + +/** SIP Header Require */ +public class RequireHeader extends OptionHeader +{ + public RequireHeader(String option) + { super(SipHeaders.Require,option); + } + + public RequireHeader(Header hd) + { super(hd); + } +} diff --git a/src/org/zoolu/sip/header/RouteHeader.java b/src/org/zoolu/sip/header/RouteHeader.java new file mode 100644 index 0000000..66d155f --- /dev/null +++ b/src/org/zoolu/sip/header/RouteHeader.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.header; + + +import org.zoolu.sip.address.NameAddress; + + +/** SIP Header Route */ +public class RouteHeader extends NameAddressHeader +{ + //public RouteHeader() + //{ super(SipHeaders.Route); + //} + + public RouteHeader(NameAddress nameaddr) + { super(SipHeaders.Route,nameaddr); + } + + public RouteHeader(Header hd) + { super(hd); + } +} diff --git a/src/org/zoolu/sip/header/ServerHeader.java b/src/org/zoolu/sip/header/ServerHeader.java new file mode 100644 index 0000000..2025afe --- /dev/null +++ b/src/org/zoolu/sip/header/ServerHeader.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.header; + + + + +/** Server header that carries information about the UAS */ +public class ServerHeader extends Header +{ + public ServerHeader(String info) + { super(SipHeaders.Server,info); + } + + public ServerHeader(Header hd) + { super(hd); + } + + /** Gets UAS information */ + public String getInfo() + { return value; + } + + /** Sets the UAS information */ + public void setInfo(String info) + { value=info; + } +} diff --git a/src/org/zoolu/sip/header/SipDateHeader.java b/src/org/zoolu/sip/header/SipDateHeader.java new file mode 100644 index 0000000..db2f387 --- /dev/null +++ b/src/org/zoolu/sip/header/SipDateHeader.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.header; + + +import org.zoolu.sip.provider.SipParser; +import org.zoolu.tools.DateFormat; +import java.util.Date; +//import java.text.DateFormat; +//import java.text.SimpleDateFormat; + + +/** SIP Header Date */ +public abstract class SipDateHeader extends Header +{ + + //public SipDateHeader(String hname) + //{ super(hname); + //} + + public SipDateHeader(String hname, String hvalue) + { super(hname,hvalue); + } + + public SipDateHeader(String hname, Date date) + { super(hname,null); + //DateFormat df=new SimpleDateFormat("EEE, dd MMM yyyy hh:mm:ss 'GMT'",Locale.US); + //value=df.format(date); + value=DateFormat.formatEEEddMMM(date); + } + + public SipDateHeader(Header hd) + { super(hd); + } + + /** Gets date value of DateHeader */ + public Date getDate() + { SipParser par=new SipParser(value); + return par.getDate(); + } + + /** Sets date of DateHeader */ + //public void setDate(Date date) + //{ DateFormat df=new SimpleDateFormat("EEE, dd MMM yyyy hh:mm:ss 'GMT'",Locale.US); + // value=df.format(date); + //} + + /** Sets date in string format of DateHeader */ + //public void setDate(String date) + //{ value=date; + //} + +} diff --git a/src/org/zoolu/sip/header/SipHeaders.java b/src/org/zoolu/sip/header/SipHeaders.java new file mode 100644 index 0000000..01fba72 --- /dev/null +++ b/src/org/zoolu/sip/header/SipHeaders.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.header; + +/** SipHeaders extends class sip.header.SipHeaders by adding new SIP header names. */ +public class SipHeaders extends BaseSipHeaders +{ + + //****************************** Extensions *******************************/ + + /** String "Refer-To" */ + public static final String Refer_To="Refer-To"; + /** Whether str is "Refer-To" */ + public static boolean isReferTo(String str) { return same(str,Refer_To); } + + /** String "Referred-By" */ + public static final String Referred_By="Referred-By"; + /** Whether str is "Referred-By" */ + public static boolean isReferredBy(String str) { return same(str,Referred_By); } + + /** String "Event" */ + public static final String Event="Event"; + /** String "o" */ + public static final String Event_short="o"; + /** Whether str is an Event field */ + public static boolean isEvent(String str) { return same(str,Event) || same(str,Event_short); } + + /** String "Allow-Events" */ + public static final String Allow_Events="Allow-Events"; + /** Whether str is "Allow-Events" */ + public static boolean isAllowEvents(String str) { return same(str,Allow_Events); } + + /** String "Subscription-State" */ + public static final String Subscription_State="Subscription-State"; + /** Whether str is an Subscription_State field */ + public static boolean isSubscriptionState(String str) { return same(str,Subscription_State); } + +} diff --git a/src/org/zoolu/sip/header/StatusLine.java b/src/org/zoolu/sip/header/StatusLine.java new file mode 100644 index 0000000..199003b --- /dev/null +++ b/src/org/zoolu/sip/header/StatusLine.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.header; + + + + +/** SIP Status-line, i.e. the first line of a response message */ +public class StatusLine +{ + protected int code; + protected String reason; + + /** Construct StatusLine */ + public StatusLine(int c, String r) + { code=c; + reason=r; + } + + /** Create a new copy of the request-line*/ + public Object clone() + { return new StatusLine(getCode(),getReason()); + } + + /** Indicates whether some other Object is "equal to" this StatusLine */ + public boolean equals(Object obj) + { //if (o.getClass().getSuperclass()!=this.getClass().getSuperclass()) return false; + try + { StatusLine r=(StatusLine)obj; + if (r.getCode()==(this.getCode()) && r.getReason().equals(this.getReason())) return true; + else return false; + } + catch (Exception e) { return false; } + } + + public String toString() + { return "SIP/2.0 "+code+" "+reason+"\r\n"; + } + + public int getCode() + { return code; + } + + public String getReason() + { return reason; + } +} diff --git a/src/org/zoolu/sip/header/SubjectHeader.java b/src/org/zoolu/sip/header/SubjectHeader.java new file mode 100644 index 0000000..76046a9 --- /dev/null +++ b/src/org/zoolu/sip/header/SubjectHeader.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.header; + + +import org.zoolu.tools.Parser; + + +/** SIP Header Subject. + */ +public class SubjectHeader extends Header +{ + /** Creates a SubjectHeader */ + //public SubjectHeader() + //{ super(SipHeaders.Subject); + //} + + /** Creates a SubjectHeader with value hvalue */ + public SubjectHeader(String hvalue) + { super(SipHeaders.Subject,hvalue); + } + + /** Creates a new SubjectHeader equal to SubjectHeader hd */ + public SubjectHeader(Header hd) + { super(hd); + } + + /** Gets the subject */ + public String getSubject() + { return value; + } +} diff --git a/src/org/zoolu/sip/header/SubscriptionStateHeader.java b/src/org/zoolu/sip/header/SubscriptionStateHeader.java new file mode 100644 index 0000000..48852d8 --- /dev/null +++ b/src/org/zoolu/sip/header/SubscriptionStateHeader.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.header; + + +import org.zoolu.tools.Parser; + + +/** Subscription-State header (see RFC3265 for details). */ +public class SubscriptionStateHeader extends ParametricHeader +{ + /** State "active" */ + public static final String ACTIVE="active"; + + /** State "pending" */ + public static final String PENDING="pending"; + + /** State "terminated" */ + public static final String TERMINATED="terminated"; + + /** State delimiters. */ + private static final char [] delim={',', ';', ' ', '\t', '\n', '\r'}; + + + /** Costructs a new SubscriptionStateHeader. */ + public SubscriptionStateHeader(String state) + { super(SipHeaders.Subscription_State,state); + } + + /** Costructs a new SubscriptionStateHeader. */ + public SubscriptionStateHeader(Header hd) + { super(hd); + } + + /** Gets the subscription state. */ + public String getState() + { return new Parser(value).getWord(delim); + } + + /** Whether the subscription is active. */ + public boolean isActive() + { return getState().equals(ACTIVE); + } + + /** Whether the subscription is pending. */ + public boolean isPending() + { return getState().equals(PENDING); + } + + /** Whether the subscription is terminated. */ + public boolean isTerminated() + { return getState().equals(TERMINATED); + } + + /** Sets the 'expires' param. */ + public SubscriptionStateHeader setExpires(int secs) + { setParameter("expires",Integer.toString(secs)); + return this; + } + + /** Whether there is the 'expires' param. */ + public boolean hasExpires() + { return hasParameter("expires"); + } + + /** Gets the 'expires' param. */ + public int getExpires() + { String exp=getParameter("expires"); + if (exp!=null) return Integer.parseInt(exp); + else return -1; + } + + /** Sets the 'reason' param. */ + public SubscriptionStateHeader setReason(String reason) + { setParameter("reason",reason); + return this; + } + + /** Whether there is the 'reason' param. */ + public boolean hasReason() + { return hasParameter("reason"); + } + + /** Gets the 'reason' param. */ + public String getReason() + { return getParameter("reason"); + } + +} + + diff --git a/src/org/zoolu/sip/header/SupportedHeader.java b/src/org/zoolu/sip/header/SupportedHeader.java new file mode 100644 index 0000000..41d248f --- /dev/null +++ b/src/org/zoolu/sip/header/SupportedHeader.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.header; + + + + +/** SIP Header Supported */ +public class SupportedHeader extends OptionHeader +{ + public SupportedHeader(String option) + { super(SipHeaders.Supported,option); + } + + public SupportedHeader(Header hd) + { super(hd); + } +} diff --git a/src/org/zoolu/sip/header/ToHeader.java b/src/org/zoolu/sip/header/ToHeader.java new file mode 100644 index 0000000..b8d8e19 --- /dev/null +++ b/src/org/zoolu/sip/header/ToHeader.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.header; + + +import org.zoolu.sip.address.*; + + +/** SIP Header To. + * The To header field specifies the desired + * "logical" recipient of the request, or the address-of-record of the + * user or resource that is the target of this request. This may or may + * not be the ultimate recipient of the request. Like the From + * header field, it contains a URI and optionally a display name. + *
    A request outside of a dialog MUST NOT contain a To tag; the tag in + * the To field of a request identifies the peer of the dialog. Since + * no dialog is established, no tag is present. + * The original + * recipient may or may not be the UAS processing the request, due to + * call forwarding or other proxy operations. A UAS MAY apply any + * policy it wishes to determine whether to accept requests when the To + * header field is not the identity of the UAS. However, it is + * RECOMMENDED that a UAS accept requests even if they do not recognize + * the URI scheme (for example, a tel: URI) in the To header field, or + * if the To header field does not address a known or current user of + * this UAS. If, on the other hand, the UAS decides to reject the + * request, it SHOULD generate a response with a 403 (Forbidden) status + * code and pass it to the server transaction for transmission. */ +public class ToHeader extends EndPointHeader +{ + //public ToHeader() + //{ super(sip.header.SipHeaders.To); + //} + + + public ToHeader(NameAddress nameaddr) + { super(SipHeaders.To,nameaddr); + } + + public ToHeader(SipURL url) + { super(SipHeaders.To,url); + } + + public ToHeader(NameAddress nameaddr, String tag) + { super(SipHeaders.To,nameaddr,tag); + } + + public ToHeader(SipURL url, String tag) + { super(SipHeaders.To,url,tag); + } + + public ToHeader(Header hd) + { super(hd); + } +} + diff --git a/src/org/zoolu/sip/header/UnsupportedHeader.java b/src/org/zoolu/sip/header/UnsupportedHeader.java new file mode 100644 index 0000000..c7cbfb6 --- /dev/null +++ b/src/org/zoolu/sip/header/UnsupportedHeader.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.header; + + + +/** SIP Header Unsupported */ +public class UnsupportedHeader extends OptionHeader +{ + public UnsupportedHeader(String option) + { super(SipHeaders.Unsupported,option); + } + + public UnsupportedHeader(Header hd) + { super(hd); + } +} diff --git a/src/org/zoolu/sip/header/UserAgentHeader.java b/src/org/zoolu/sip/header/UserAgentHeader.java new file mode 100644 index 0000000..fbbfebb --- /dev/null +++ b/src/org/zoolu/sip/header/UserAgentHeader.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.header; + + + + +/** User-Agent header that carries information about the UAC */ +public class UserAgentHeader extends Header +{ + public UserAgentHeader(String info) + { super(SipHeaders.User_Agent,info); + } + + public UserAgentHeader(Header hd) + { super(hd); + } + + /** Gets UAC information */ + public String getInfo() + { return value; + } + + /** Sets the UAC information */ + public void setInfo(String info) + { value=info; + } +} diff --git a/src/org/zoolu/sip/header/ViaHeader.java b/src/org/zoolu/sip/header/ViaHeader.java new file mode 100644 index 0000000..db83356 --- /dev/null +++ b/src/org/zoolu/sip/header/ViaHeader.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.header; + + +import org.zoolu.sip.address.SipURL; +import org.zoolu.sip.provider.SipParser; + + +/** SIP Header Via. + * The Via header field indicates the transport used for the transaction + * and identifies the location where the response is to be sent. + *
    When the UAC creates a request, it MUST insert a Via into that + * request. The protocol name and protocol version in the header field + * is SIP and 2.0, respectively. + *
    The Via header field value MUST + * contain a branch parameter. This parameter is used to identify the + * transaction created by that request. This parameter is used by both + * the client and the server. + *
    The branch parameter value MUST be unique across space and time for + * all requests sent by the UA. The exceptions to this rule are CANCEL + * and ACK for non-2xx responses. A CANCEL request + * will have the same value of the branch parameter as the request it + * cancels. An ACK for a non-2xx + * response will also have the same branch ID as the INVITE whose + * response it acknowledges. + */ +public class ViaHeader extends ParametricHeader +{ + protected static final String received_param="received"; + protected static final String rport_param="rport"; + protected static final String branch_param="branch"; + protected static final String maddr_param="maddr"; + protected static final String ttl_param="ttl"; + + //public ViaHeader() + //{ super(SipHeaders.Via); + //} + + public ViaHeader(String hvalue) + { super(SipHeaders.Via,hvalue); + } + + public ViaHeader(Header hd) + { super(hd); + } + + public ViaHeader(String host, int port) + { super(SipHeaders.Via,"SIP/2.0/UDP "+host+":"+port); + } + + /*public ViaHeader(String host, int port, String branch) + { super(SipHeaders.Via,"SIP/2.0/UDP "+host+":"+port+";branch="+branch); + }*/ + + public ViaHeader(String proto, String host, int port) + { super(SipHeaders.Via,"SIP/2.0/"+proto.toUpperCase()+" "+host+":"+port); + } + + /*public ViaHeader(String proto, String host, int port, String branch) + { super(SipHeaders.Via,"SIP/2.0/"+proto.toUpperCase()+" "+host+":"+port+";branch="+branch); + }*/ + + /** Gets the transport protocol */ + public String getProtocol() + { SipParser par=new SipParser(value); + return par.goTo('/').skipChar().goTo('/').skipChar().skipWSP().getString(); + } + + /** Gets "sent-by" parameter */ + public String getSentBy() + { SipParser par=new SipParser(value); + par.goTo('/').skipChar().goTo('/').skipString().skipWSP(); + if (!par.hasMore()) return null; + String sentby=value.substring(par.getPos(),par.indexOfSeparator()); + return sentby; + } + + /** Gets host of ViaHeader */ + public String getHost() + { String sentby=getSentBy(); + SipParser par=new SipParser(sentby); + par.goTo(':'); + if (par.hasMore()) return sentby.substring(0,par.getPos()); + else return sentby; + } + + /** Returns boolean value indicating if ViaHeader has port */ + public boolean hasPort() + { String sentby=getSentBy(); + if (sentby.indexOf(":")>0) return true; + return false; + } + + /** Gets port of ViaHeader */ + public int getPort() + { SipParser par=new SipParser(getSentBy()); + par.goTo(':'); + if (par.hasMore()) return par.skipChar().getInt(); + return -1; + } + + /** Makes a SipURL from ViaHeader */ + public SipURL getSipURL() + { return new SipURL(getHost(),getPort()); + } + + /** Checks if "branch" parameter is present */ + public boolean hasBranch() + { return hasParameter(branch_param); + } + /** Gets "branch" parameter */ + public String getBranch() + { return getParameter(branch_param); + } + /** Sets "branch" parameter */ + public void setBranch(String value) + { setParameter(branch_param,value); + } + + /** Checks if "received" parameter is present */ + public boolean hasReceived() + { return hasParameter(received_param); + } + /** Gets "received" parameter */ + public String getReceived() + { return getParameter(received_param); + } + /** Sets "received" parameter */ + public void setReceived(String value) + { setParameter(received_param,value); + } + + /** Checks if "rport" parameter is present */ + public boolean hasRport() + { return hasParameter(rport_param); + } + /** Gets "rport" parameter */ + public int getRport() + { String value=getParameter(rport_param); + if (value!=null) return Integer.parseInt(value); + else return -1; + } + /** Sets "rport" parameter */ + public void setRport() + { setParameter(rport_param,null); + } + /** Sets "rport" parameter */ + public void setRport(int port) + { if (port<0) setParameter(rport_param,null); + else setParameter(rport_param,Integer.toString(port)); + } + + /** Checks if "maddr" parameter is present */ + public boolean hasMaddr() + { return hasParameter(maddr_param); + } + /** Gets "maddr" parameter */ + public String getMaddr() + { return getParameter(maddr_param); + } + /** Sets "maddr" parameter */ + public void setMaddr(String value) + { setParameter(maddr_param,value); + } + + /** Checks if "ttl" parameter is present */ + public boolean hasTtl() + { return hasParameter(ttl_param); + } + /** Gets "ttl" parameter */ + public int getTtl() + { String value=getParameter(ttl_param); + if (value!=null) return Integer.parseInt(value); + else return -1; + } + /** Sets "ttl" parameter */ + public void setTtl(int ttl) + { setParameter(ttl_param,Integer.toString(ttl)); + } + +} diff --git a/src/org/zoolu/sip/header/WwwAuthenticateHeader.java b/src/org/zoolu/sip/header/WwwAuthenticateHeader.java new file mode 100644 index 0000000..db5be3c --- /dev/null +++ b/src/org/zoolu/sip/header/WwwAuthenticateHeader.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.header; + + +import java.util.Vector; + + +/** SIP WWW-Authenticate header */ +public class WwwAuthenticateHeader extends AuthenticationHeader +{ + /** Creates a new WwwAuthenticateHeader */ + public WwwAuthenticateHeader(String hvalue) + { super(SipHeaders.WWW_Authenticate,hvalue); + } + + /** Creates a new WwwAuthenticateHeader */ + public WwwAuthenticateHeader(Header hd) + { super(hd); + } + + /** Creates a new WwwAuthenticateHeader + * specifing the auth_scheme and the vector of authentication parameters. + *

    auth_param is a vector of String of the form parm_name "=" parm_value */ + public WwwAuthenticateHeader(String auth_scheme, Vector auth_params) + { super(SipHeaders.WWW_Authenticate,auth_scheme,auth_params); + } +} diff --git a/src/org/zoolu/sip/message/BaseMessage.java b/src/org/zoolu/sip/message/BaseMessage.java new file mode 100644 index 0000000..e9805d0 --- /dev/null +++ b/src/org/zoolu/sip/message/BaseMessage.java @@ -0,0 +1,1255 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.message; + + +import org.zoolu.sip.provider.*; +import org.zoolu.sip.header.*; +import org.zoolu.sip.address.*; +import org.zoolu.sip.message.SipMethods; +import org.zoolu.net.UdpPacket; +import java.util.*; + + +/** Class BaseMessage implements a generic SIP Message. */ +public abstract class BaseMessage +{ + /** UDP */ + public static final String PROTO_UDP="udp"; + /** TCP */ + public static final String PROTO_TCP="tcp"; + /** TLS */ + public static final String PROTO_TLS="tls"; + /** SCTP */ + public static final String PROTO_SCTP="sctp"; + + /** Maximum receiving packet size */ + protected static int MAX_PKT_SIZE=8000; + + /** The remote ip address */ + protected String remote_addr; + + /** The remote port */ + protected int remote_port; + + /** Transport protocol */ + protected String transport_proto; + + /** Connection identifier */ + protected ConnectionIdentifier connection_id; + + /** Packet length */ + //protected int packet_length; + + /** The message string */ + private String message; + + + /** Inits empty Message */ + private void init() + { //message=""; + remote_addr=null; + remote_port=0; + transport_proto=null; + connection_id=null; + } + + /** Costructs a new empty Message */ + public BaseMessage() + { init(); + message=""; + } + + /** Costructs a new Message */ + public BaseMessage(byte[] data, int offset, int len) + { init(); + message=new String(data,offset,len); + } + + /** Costructs a new Message */ + public BaseMessage(UdpPacket packet) + { init(); + message=new String(packet.getData(),packet.getOffset(),packet.getLength()); + } + + /** Costructs a new Message */ + public BaseMessage(String str) + { init(); + message=new String(str); + } + + /** Costructs a new Message */ + public BaseMessage(BaseMessage msg) + { //message=new String(msg.message); + message=msg.message; + remote_addr=msg.remote_addr; + remote_port=msg.remote_port; + transport_proto=msg.transport_proto; + connection_id=msg.connection_id; + //packet_length=msg.packet_length; + } + + /** Creates and returns a clone of the Message */ + abstract public Object clone(); + //{ return new Message(message); + //} + + /** Sets the entire message */ + public void setMessage(String message) + { this.message=message; + } + + /** Gets string representation of Message */ + public String toString() + { return message; + } + + /** Gets remote ip address */ + public String getRemoteAddress() + { return remote_addr; + } + + /** Gets remote port */ + public int getRemotePort() + { return remote_port; + } + + /** Gets transport protocol */ + public String getTransportProtocol() + { return transport_proto; + } + + /** Gets connection identifier */ + public ConnectionIdentifier getConnectionId() + { return connection_id; + } + + /** Gets message length */ + public int getLength() + { return message.length(); + } + + /** Sets remote ip address */ + public void setRemoteAddress(String addr) + { remote_addr=addr; + } + + /** Sets remote port */ + public void setRemotePort(int port) + { remote_port=port; + } + + /** Sets transport protocol */ + public void setTransport(String proto) + { transport_proto=proto; + } + + /** Sets connection identifier */ + public void setConnectionId(ConnectionIdentifier conn_id) + { connection_id=conn_id; + } + + /** Gets the inique DialogIdentifier for an INCOMING message */ + public DialogIdentifier getDialogId() + { String call_id=getCallIdHeader().getCallId(); + String local_tag, remote_tag; + if (isRequest()) { local_tag=getToHeader().getTag(); remote_tag=getFromHeader().getTag(); } + else { local_tag=getFromHeader().getTag(); remote_tag=getToHeader().getTag(); } + return new DialogIdentifier(call_id,local_tag,remote_tag); + } + + + /** Gets the inique TransactionIdentifier */ + public TransactionIdentifier getTransactionId() + { String call_id=getCallIdHeader().getCallId(); + ViaHeader top_via=getViaHeader(); + String branch=null; + if (top_via.hasBranch()) branch=top_via.getBranch(); + String sent_by=top_via.getSentBy(); + CSeqHeader cseqh=getCSeqHeader(); + long seqn=cseqh.getSequenceNumber(); + String method=cseqh.getMethod(); + return new TransactionIdentifier(call_id,seqn,method,sent_by,branch); + } + + + /** Gets the MethodIdentifier */ + public MethodIdentifier getMethodId() + { String method=getCSeqHeader().getMethod(); + return new MethodIdentifier(method); + } + + + //**************************** Requests ****************************/ + + /** Whether Message is a Request */ + public boolean isRequest() throws NullPointerException + { // Req-Line = Method ' ' SIP-URL ' ' "SIP/2.0" CRLF + if (message==null || isResponse()) return false; + String firstline=(new SipParser(message)).getLine(); + String version=(new SipParser(firstline)).skipString().skipString().getString(); + if (version==null || version.length()<4) return false; + version=version.substring(0,4); + String target="SIP/"; + //if (version.compareToIgnoreCase(target)==0) return true; + if (version.equalsIgnoreCase(target)) return true; + return false; + } + + /** Whether Message is a method request */ + public boolean isRequest(String method) + { //if (message==null) return false; + if (message.startsWith(method)) return true; else return false; + } + + /** Whether Message is a Method that creates a dialog */ + public boolean createsDialog() + { if (!isRequest()) return false; + //else + String method=getRequestLine().getMethod(); + for (int i=0; imheader at the top/bottom */ + public void addHeaders(MultipleHeader mheader, boolean top) + { addHeaders(mheader.toString(),top); + } + + /** Adds a one or more Headers at the top/bottom. + * The bottom is considered before the Content-Length and Content-Type headers */ + protected void addHeaders(String str, boolean top) + { int i,aux; + if (top) + { if (this.hasRequestLine() || this.hasStatusLine()) + { SipParser par=new SipParser(message); + par.goToNextHeader(); + i=par.getPos(); + } + else i=0; + } + else + { SipParser par=new SipParser(message); + // index the end of headers + i=par.goToEndOfLastHeader().goToNextLine().getPos(); + par=new SipParser(message); + // if Content_Length is present, jump before + aux=par.indexOfHeader(SipHeaders.Content_Length); + if (auxindex within the Message */ + protected void addHeaders(String str, int index) + { if (index>message.length()) index=message.length(); + message=message.substring(0,index)+str+message.substring(index); + } + + /** Adds Header before the first header refer_header + * .

    If there is no header of such type, it is added at top */ + public void addHeaderBefore(Header new_header, String refer_header) + { addHeadersBefore(new_header.toString(),refer_header); + } + + /** Adds MultipleHeader(s) before the first header refer_header + * .

    If there is no header of such type, they are added at top */ + public void addHeadersBefore(MultipleHeader mheader, String refer_header) + { addHeadersBefore(mheader.toString(),refer_header); + } + + /** Adds Headers before the first header refer_header + * .

    If there is no header of such type, they are added at top */ + protected void addHeadersBefore(String str, String refer_header) + { if (!hasHeader(refer_header)) addHeaders(str,true); + else + { SipParser par=new SipParser(message); + par.goTo(refer_header); + int here=par.getPos(); + message=message.substring(0,here)+str+message.substring(here); + } + } + + /** Adds Header after the first header refer_header + * .

    If there is no header of such type, it is added at bottom */ + public void addHeaderAfter(Header new_header, String refer_header) + { addHeadersAfter(new_header.toString(),refer_header); + } + + /** Adds MultipleHeader(s) after the first header refer_header + * .

    If there is no header of such type, they are added at bottom */ + public void addHeadersAfter(MultipleHeader mheader, String refer_header) + { addHeadersAfter(mheader.toString(),refer_header); + } + + /** Adds Headers after the first header refer_header + * .

    If there is no header of such type, they are added at bottom */ + protected void addHeadersAfter(String str, String refer_header) + { if (!hasHeader(refer_header)) addHeaders(str,false); + else + { SipParser par=new SipParser(message); + par.goTo(refer_header); + int here=par.indexOfNextHeader(); + message=message.substring(0,here)+str+message.substring(here); + } + } + + /** Removes first Header of specified name */ + public void removeHeader(String hname) + { removeHeader(hname,true); + } + + /** Removes first (or last) Header of specified name */ + public void removeHeader(String hname, boolean first) + { String[] target={'\n'+hname, '\r'+hname}; + SipParser par=new SipParser(message); + par.goTo(target); + if (!par.hasMore()) return; + if (!first) + while(true) + { int next=par.indexOf(target); + if (next<0) break; + par.setPos(next); + } + par.skipChar(); + String head=message.substring(0,par.getPos()); + par.goToNextHeader(); + String tail=message.substring(par.getPos()); + message=head.concat(tail); + } + + /** Sets the new Header (removing any previous headers of the same name) */ + /*public void setHeader(Header hd) + { if (hasHeader(hd.getName())) removeAllHeaders(hd.getName()); + addHeader(hd,false); + }*/ + + /** Sets the Header hd removing any previous headers of the same type */ + public void setHeader(Header hd) + { String hname=hd.getName(); + if (hasHeader(hname)) + { int index=(new SipParser(message)).indexOfHeader(hname); + removeAllHeaders(hname); + addHeaders(hd.toString(),index); + } + else + addHeader(hd,false); + } + + /** Removes all Headers of specified name */ + public void removeAllHeaders(String hname) + { String[] target={'\n'+hname, '\r'+hname}; + SipParser par=new SipParser(message); + par.goTo(target); + while (par.hasMore()) + { par.skipChar(); + String head=message.substring(0,par.getPos()); + String tail=message.substring(par.indexOfNextHeader()); + message=head.concat(tail); + par=new SipParser(message,par.getPos()-1); + par.goTo(target); + } + } + + /** Sets MultipleHeader mheader */ + /*public void setHeaders(MultipleHeader mheader) + { if (hasHeader(mheader.getName())) removeAllHeaders(mheader.getName()); + addHeaders(mheader,false); + }*/ + + /** Sets MultipleHeader mheader */ + public void setHeaders(MultipleHeader mheader) + { String hname=mheader.getName(); + if (hasHeader(hname)) + { int index=(new SipParser(message)).indexOfHeader(hname); + removeAllHeaders(hname); + addHeaders(mheader.toString(),index); + } + else + addHeaders(mheader,false); + } + + + //**************************** Specific Headers ****************************/ + + /** Whether Message has MaxForwardsHeader */ + public boolean hasMaxForwardsHeader() + { return hasHeader(SipHeaders.Max_Forwards); + } + /** Gets MaxForwardsHeader of Message */ + public MaxForwardsHeader getMaxForwardsHeader() + { Header h=getHeader(SipHeaders.Max_Forwards); + if (h==null) return null; + else return new MaxForwardsHeader(h); + } + /** Sets MaxForwardsHeader of Message */ + public void setMaxForwardsHeader(MaxForwardsHeader mfh) + { setHeader(mfh); + } + /** Removes MaxForwardsHeader from Message */ + public void removeMaxForwardsHeader() + { removeHeader(SipHeaders.Max_Forwards); + } + + + /** Whether Message has FromHeader */ + public boolean hasFromHeader() + { return hasHeader(SipHeaders.From); + } + /** Gets FromHeader of Message */ + public FromHeader getFromHeader() + { Header h=getHeader(SipHeaders.From); + if (h==null) return null; + else return new FromHeader(h); + } + /** Sets FromHeader of Message */ + public void setFromHeader(FromHeader fh) + { setHeader(fh); + } + /** Removes FromHeader from Message */ + public void removeFromHeader() + { removeHeader(SipHeaders.From); + } + + /** Whether Message has ToHeader */ + public boolean hasToHeader() + { return hasHeader(SipHeaders.To); + } + /** Gets ToHeader of Message */ + public ToHeader getToHeader() + { Header h=getHeader(SipHeaders.To); + if (h==null) return null; + else return new ToHeader(h); + } + /** Sets ToHeader of Message */ + public void setToHeader(ToHeader th) + { setHeader(th); + } + /** Removes ToHeader from Message */ + public void removeToHeader() + { removeHeader(SipHeaders.To); + } + + + /** Whether Message has ContactHeader */ + public boolean hasContactHeader() + { return hasHeader(SipHeaders.Contact); + } + /** Deprecated. Gets ContactHeader of Message. Use getContacts instead. */ + public ContactHeader getContactHeader() + { //Header h=getHeader(SipHeaders.Contact); + //if (h==null) return null; else return new ContactHeader(h); + MultipleHeader mh=getContacts(); + if (mh==null) return null; return new ContactHeader(mh.getTop()); + } + /** Adds ContactHeader */ + public void addContactHeader(ContactHeader ch, boolean top) + { addHeader(ch,top); + } + /** Sets ContactHeader */ + public void setContactHeader(ContactHeader ch) + { if (hasContactHeader()) removeContacts(); + addHeader(ch,false); + } + /** Gets a MultipleHeader of Contacts */ + public MultipleHeader getContacts() + { Vector v=getHeaders(SipHeaders.Contact); + if (v.size()>0) return new MultipleHeader(v); + else return null; + } + /** Adds Contacts */ + public void addContacts(MultipleHeader contacts, boolean top) + { addHeaders(contacts,top); + } + /** Sets Contacts */ + public void setContacts(MultipleHeader contacts) + { if (hasContactHeader()) removeContacts(); + addContacts(contacts,false); + } + /** Removes ContactHeaders from Message */ + public void removeContacts() + { removeAllHeaders(SipHeaders.Contact); + } + + + /** Whether Message has ViaHeaders */ + public boolean hasViaHeader() + { return hasHeader(SipHeaders.Via); + } + /** Adds ViaHeader at the top */ + public void addViaHeader(ViaHeader vh) + { addHeader(vh,true); + } + /** Gets the first ViaHeader */ + public ViaHeader getViaHeader() + { //Header h=getHeader(SipHeaders.Via); + //if (h==null) return null; else return new ViaHeader(h); + MultipleHeader mh=getVias(); + if (mh==null) return null; return new ViaHeader(mh.getTop()); + } + /** Removes the top ViaHeader */ + public void removeViaHeader() + { //removeHeader(SipHeaders.Via); + MultipleHeader mh=getVias(); + mh.removeTop(); + setVias(mh); + } + /** Gets all Vias */ + public MultipleHeader getVias() + { Vector v=getHeaders(SipHeaders.Via); + if (v.size()>0) return new MultipleHeader(v); + else return null; + } + /** Adds Vias */ + public void addVias(MultipleHeader vias, boolean top) + { addHeaders(vias,top); + } + /** Sets Vias */ + public void setVias(MultipleHeader vias) + { if (hasViaHeader()) removeVias(); + addContacts(vias,true); + } + /** Removes ViaHeaders from Message (if any exists) */ + public void removeVias() + { removeAllHeaders(SipHeaders.Via); + } + + + /** Whether Message has RouteHeader */ + public boolean hasRouteHeader() + { return hasHeader(SipHeaders.Route); + } + /** Adds RouteHeader at the top */ + public void addRouteHeader(RouteHeader h) + { addHeaderAfter(h,SipHeaders.Via); + } + /** Adds multiple Route headers at the top */ + public void addRoutes(MultipleHeader routes) + { addHeadersAfter(routes,SipHeaders.Via); + } + /** Gets the top RouteHeader */ + public RouteHeader getRouteHeader() + { //Header h=getHeader(SipHeaders.Route); + //if (h==null) return null; else return new RouteHeader(h); + MultipleHeader mh=getRoutes(); + if (mh==null) return null; return new RouteHeader(mh.getTop()); + } + /** Gets the whole route */ + public MultipleHeader getRoutes() + { Vector v=getHeaders(SipHeaders.Route); + if (v.size()>0) return new MultipleHeader(v); + else return null; + } + /** Removes the top RouteHeader */ + public void removeRouteHeader() + { //removeHeader(SipHeaders.Route); + MultipleHeader mh=getRoutes(); + mh.removeTop(); + setRoutes(mh); + } + /** Removes all RouteHeaders from Message (if any exists) */ + public void removeRoutes() + { removeAllHeaders(SipHeaders.Route); + } + /** Sets the whole route */ + public void setRoutes(MultipleHeader routes) + { if (hasRouteHeader()) removeRoutes(); + addRoutes(routes); + } + + + /** Whether Message has RecordRouteHeader */ + public boolean hasRecordRouteHeader() + { return hasHeader(SipHeaders.Record_Route); + } + /** Adds RecordRouteHeader at the top */ + public void addRecordRouteHeader(RecordRouteHeader rr) + { //addHeaderAfter(rr,SipHeaders.Via); + addHeaderAfter(rr,SipHeaders.CSeq); + } + /** Adds multiple RecordRoute headers at the top */ + public void addRecordRoutes(MultipleHeader routes) + { //addHeadersAfter(routes,SipHeaders.Via); + addHeadersAfter(routes,SipHeaders.CSeq); + } + /** Gets the top RecordRouteHeader */ + public RecordRouteHeader getRecordRouteHeader() + { //Header h=getHeader(SipHeaders.Record_Route); + //if (h==null) return null; else return new RecordRouteHeader(h); + MultipleHeader mh=getRecordRoutes(); + if (mh==null) return null; return new RecordRouteHeader(mh.getTop()); + } + /** Gets the whole RecordRoute headers */ + public MultipleHeader getRecordRoutes() + { Vector v=getHeaders(SipHeaders.Record_Route); + if (v.size()>0) return new MultipleHeader(v); + else return null; + } + /** Removes the top RecordRouteHeader */ + public void removeRecordRouteHeader() + { //removeHeader(SipHeaders.Record_Route); + MultipleHeader mh=getRecordRoutes(); + mh.removeTop(); + setRecordRoutes(mh); + } + /** Removes all RecordRouteHeader from Message (if any exists) */ + public void removeRecordRoutes() + { removeAllHeaders(SipHeaders.Record_Route); + } + /** Sets the whole RecordRoute headers */ + public void setRecordRoutes(MultipleHeader routes) + { if (hasRecordRouteHeader()) removeRecordRoutes(); + addRecordRoutes(routes); + } + + + /** Whether Message has CSeqHeader */ + public boolean hasCSeqHeader() + { return hasHeader(SipHeaders.CSeq); + } + /** Gets CSeqHeader of Message */ + public CSeqHeader getCSeqHeader() + { Header h=getHeader(SipHeaders.CSeq); + if (h==null) return null; + else return new CSeqHeader(h); + } + /** Sets CSeqHeader of Message */ + public void setCSeqHeader(CSeqHeader csh) + { setHeader(csh); + } + /** Removes CSeqHeader from Message */ + public void removeCSeqHeader() + { removeHeader(SipHeaders.CSeq); + } + + + /** Whether has CallIdHeader */ + public boolean hasCallIdHeader() + { return hasHeader(SipHeaders.Call_ID); + } + /** Sets CallIdHeader of Message */ + public void setCallIdHeader(CallIdHeader cih) + { setHeader(cih); + } + /** Gets CallIdHeader of Message */ + public CallIdHeader getCallIdHeader() + { Header h=getHeader(SipHeaders.Call_ID); + if (h==null) return null; + else return new CallIdHeader(h); + } + /** Removes CallIdHeader from Message */ + public void removeCallIdHeader() + { removeHeader(SipHeaders.Call_ID); + } + + + /** Whether Message has SubjectHeader */ + public boolean hasSubjectHeader() + { return hasHeader(SipHeaders.Subject); + } + /** Sets SubjectHeader of Message */ + public void setSubjectHeader(SubjectHeader sh) + { setHeader(sh); + } + /** Gets SubjectHeader of Message */ + public SubjectHeader getSubjectHeader() + { Header h=getHeader(SipHeaders.Subject); + if (h==null) return null; + else return new SubjectHeader(h); + } + /** Removes SubjectHeader from Message */ + public void removeSubjectHeader() + { removeHeader(SipHeaders.Subject); + } + + + /** Whether Message has DateHeader */ + public boolean hasDateHeader() + { return hasHeader(SipHeaders.Date); + } + /** Gets DateHeader of Message */ + public DateHeader getDateHeader() + { Header h=getHeader(SipHeaders.Date); + if (h==null) return null; + else return new DateHeader(h); + } + /** Sets DateHeader of Message */ + public void setDateHeader(DateHeader dh) + { setHeader(dh); + } + /** Removes DateHeader from Message (if it exists) */ + public void removeDateHeader() + { removeHeader(SipHeaders.Date); + } + + + /** Whether has UserAgentHeader */ + public boolean hasUserAgentHeader() + { return hasHeader(SipHeaders.User_Agent); + } + /** Sets UserAgentHeader */ + public void setUserAgentHeader(UserAgentHeader h) + { setHeader(h); + } + /** Gets UserAgentHeader */ + public UserAgentHeader getUserAgentHeader() + { Header h=getHeader(SipHeaders.User_Agent); + if (h==null) return null; + else return new UserAgentHeader(h); + } + /** Removes UserAgentHeader */ + public void removeUserAgentHeader() + { removeHeader(SipHeaders.User_Agent); + } + + + /** Whether has ServerHeader */ + public boolean hasServerHeader() + { return hasHeader(SipHeaders.Server); + } + /** Sets ServerHeader */ + public void setServerHeader(ServerHeader h) + { setHeader(h); + } + /** Gets ServerHeader */ + public ServerHeader getServerHeader() + { Header h=getHeader(SipHeaders.Server); + if (h==null) return null; + else return new ServerHeader(h); + } + /** Removes ServerHeader */ + public void removeServerHeader() + { removeHeader(SipHeaders.Server); + } + + + /** Whether has AcceptHeader */ + public boolean hasAcceptHeader() + { return hasHeader(SipHeaders.Accept); + } + /** Sets AcceptHeader */ + public void setAcceptHeader(AcceptHeader h) + { setHeader(h); + } + /** Gets AcceptHeader */ + public AcceptHeader getAcceptHeader() + { Header h=getHeader(SipHeaders.Accept); + if (h==null) return null; + else return new AcceptHeader(h); + } + /** Removes AcceptHeader */ + public void removeAcceptHeader() + { removeHeader(SipHeaders.Accept); + } + + + /** Whether has AlertInfoHeader */ + public boolean hasAlertInfoHeader() + { return hasHeader(SipHeaders.Alert_Info); + } + /** Sets AlertInfoHeader */ + public void setAlertInfoHeader(AlertInfoHeader h) + { setHeader(h); + } + /** Gets AlertInfoHeader */ + public AlertInfoHeader getAlertInfoHeader() + { Header h=getHeader(SipHeaders.Alert_Info); + if (h==null) return null; + else return new AlertInfoHeader(h); + } + /** Removes AlertInfoHeader */ + public void removeAlertInfoHeader() + { removeHeader(SipHeaders.Alert_Info); + } + + + /** Whether has AllowHeader */ + public boolean hasAllowHeader() + { return hasHeader(SipHeaders.Allow); + } + /** Sets AllowHeader */ + public void setAllowHeader(AllowHeader h) + { setHeader(h); + } + /** Gets AllowHeader */ + public AllowHeader getAllowHeader() + { Header h=getHeader(SipHeaders.Allow); + if (h==null) return null; + else return new AllowHeader(h); + } + /** Removes AllowHeader */ + public void removeAllowHeader() + { removeHeader(SipHeaders.Allow); + } + + + /** Whether Message has ExpiresHeader */ + public boolean hasExpiresHeader() + { return hasHeader(SipHeaders.Expires); + } + /** Gets ExpiresHeader of Message */ + public ExpiresHeader getExpiresHeader() + { Header h=getHeader(SipHeaders.Expires); + if (h==null) return null; + else return new ExpiresHeader(h); + } + /** Sets ExpiresHeader of Message */ + public void setExpiresHeader(ExpiresHeader eh) + { setHeader(eh); + } + /** Removes ExpiresHeader from Message (if it exists) */ + public void removeExpiresHeader() + { removeHeader(SipHeaders.Expires); + } + + + /** Whether Message has ContentTypeHeader */ + public boolean hasContentTypeHeader() + { return hasHeader(SipHeaders.Content_Type); + } + /** Gets ContentTypeHeader of Message */ + public ContentTypeHeader getContentTypeHeader() + { Header h=getHeader(SipHeaders.Content_Type); + if (h==null) return null; + else return new ContentTypeHeader(h); + } + /** Sets ContentTypeHeader of Message */ + protected void setContentTypeHeader(ContentTypeHeader cth) + { setHeader(cth); + } + /** Removes ContentTypeHeader from Message (if it exists) */ + protected void removeContentTypeHeader() + { removeHeader(SipHeaders.Content_Type); + } + + + /** Whether Message has ContentLengthHeader */ + public boolean hasContentLengthHeader() + { return hasHeader(SipHeaders.Content_Length); + } + /** Gets ContentLengthHeader of Message */ + public ContentLengthHeader getContentLengthHeader() + { Header h=getHeader(SipHeaders.Content_Length); + if (h==null) return null; + else return new ContentLengthHeader(h); + } + /** Sets ContentLengthHeader of Message */ + protected void setContentLengthHeader(ContentLengthHeader clh) + { setHeader(clh); + } + /** Removes ContentLengthHeader from Message (if it exists) */ + protected void removeContentLengthHeader() + { removeHeader(SipHeaders.Content_Length); + } + + + /** Whether Message has Body */ + public boolean hasBody() + { if (hasContentLengthHeader()) return getContentLengthHeader().getContentLength()>0; + else return hasContentTypeHeader(); + } + /** Gets body(content) type */ + public String getBodyType() + { return getContentTypeHeader().getContentType(); + } + /** Sets the message body */ + public void setBody(String content_type, String body) + { removeBody(); + if (body!=null && body.length()>0) + { setContentTypeHeader(new ContentTypeHeader(content_type)); + setContentLengthHeader(new ContentLengthHeader(body.length())); + message=message+"\r\n"+body; + } + else + { setContentLengthHeader(new ContentLengthHeader(0)); + message=message+"\r\n"; + } + } + /** Sets sdp body */ + public void setBody(String body) + { setBody("application/sdp",body); + } + /** Gets message body. The end of body is evaluated + * from the Content-Length header if present (SIP-RFC compliant), + * or from the end of message if no Content-Length header is present (non-SIP-RFC compliant) */ + public String getBody() + { //if (!hasBody()) return ""; + if (!hasBody()) return null; + int begin=(new SipParser(message)).goToBody().getPos(); + int len; + // the following 'if' is for robustness with non SIP-compliant UAs; + // copliant UAs must insert Content-Length header when body is present.. + if (this.hasContentLengthHeader()) len=getContentLengthHeader().getContentLength(); + else + { //printWarning("No Content-Length header found for the Body",3); + len=message.length()-begin; + } + int end=begin+len; + if (end>message.length()) + { //printWarning("Found a Message Body shorter than Content-Length",3); + end=message.length(); + } + return message.substring(begin,end); + } + /** Removes the message body (if it exists) and the final empty line */ + public void removeBody() + { int pos=(new SipParser(message)).goToEndOfLastHeader().goToNextLine().getPos(); + message=message.substring(0,pos); + removeContentLengthHeader(); + removeContentTypeHeader(); + } + + + //**************************** Authentication ****************************/ + + + /** Whether has AuthenticationInfoHeader */ + public boolean hasAuthenticationInfoHeader() + { return hasHeader(SipHeaders.Authentication_Info); + } + /** Sets AuthenticationInfoHeader */ + public void setAuthenticationInfoHeader(AuthenticationInfoHeader h) + { setHeader(h); + } + /** Gets AuthenticationInfoHeader */ + public AuthenticationInfoHeader getAuthenticationInfoHeader() + { Header h=getHeader(SipHeaders.Authentication_Info); + if (h==null) return null; + else return new AuthenticationInfoHeader(h); + } + /** Removes AuthenticationInfoHeader */ + public void removeAuthenticationInfoHeader() + { removeHeader(SipHeaders.Authentication_Info); + } + + + /** Whether has AuthorizationHeader */ + public boolean hasAuthorizationHeader() + { return hasHeader(SipHeaders.Authorization); + } + /** Sets AuthorizationHeader */ + public void setAuthorizationHeader(AuthorizationHeader h) + { setHeader(h); + } + /** Gets AuthorizationHeader */ + public AuthorizationHeader getAuthorizationHeader() + { Header h=getHeader(SipHeaders.Authorization); + if (h==null) return null; + else return new AuthorizationHeader(h); + } + /** Removes AuthorizationHeader */ + public void removeAuthorizationHeader() + { removeHeader(SipHeaders.Authorization); + } + + + /** Whether has WwwAuthenticateHeader */ + public boolean hasWwwAuthenticateHeader() + { return hasHeader(SipHeaders.WWW_Authenticate); + } + /** Sets WwwAuthenticateHeader */ + public void setWwwAuthenticateHeader(WwwAuthenticateHeader h) + { setHeader(h); + } + /** Gets WwwAuthenticateHeader */ + public WwwAuthenticateHeader getWwwAuthenticateHeader() + { Header h=getHeader(SipHeaders.WWW_Authenticate); + if (h==null) return null; + else return new WwwAuthenticateHeader(h); + } + /** Removes WwwAuthenticateHeader */ + public void removeWwwAuthenticateHeader() + { removeHeader(SipHeaders.WWW_Authenticate); + } + + + /** Whether has ProxyAuthenticateHeader */ + public boolean hasProxyAuthenticateHeader() + { return hasHeader(SipHeaders.Proxy_Authenticate); + } + /** Sets ProxyAuthenticateHeader */ + public void setProxyAuthenticateHeader(ProxyAuthenticateHeader h) + { setHeader(h); + } + /** Gets ProxyAuthenticateHeader */ + public ProxyAuthenticateHeader getProxyAuthenticateHeader() + { Header h=getHeader(SipHeaders.Proxy_Authenticate); + if (h==null) return null; + else return new ProxyAuthenticateHeader(h); + } + /** Removes ProxyAuthenticateHeader */ + public void removeProxyAuthenticateHeader() + { removeHeader(SipHeaders.Proxy_Authenticate); + } + + + /** Whether has ProxyAuthorizationHeader */ + public boolean hasProxyAuthorizationHeader() + { return hasHeader(SipHeaders.Proxy_Authorization); + } + /** Sets ProxyAuthorizationHeader */ + public void setProxyAuthorizationHeader(ProxyAuthorizationHeader h) + { setHeader(h); + } + /** Gets ProxyAuthorizationHeader */ + public ProxyAuthorizationHeader getProxyAuthorizationHeader() + { Header h=getHeader(SipHeaders.Proxy_Authorization); + if (h==null) return null; + else return new ProxyAuthorizationHeader(h); + } + /** Removes ProxyAuthorizationHeader */ + public void removeProxyAuthorizationHeader() + { removeHeader(SipHeaders.Proxy_Authorization); + } + + + + //**************************** RFC 2543 Legacy ****************************/ + + + /** Checks whether the next Route is formed according to RFC2543 Strict Route + * and adapts the message. */ + public void rfc2543RouteAdapt() + { if (hasRouteHeader()) + { MultipleHeader mrh=getRoutes(); + RouteHeader rh=new RouteHeader(mrh.getTop()); + if (!(new RouteHeader(mrh.getTop())).getNameAddress().getAddress().hasLr()) + { // re-format the message according to the RFC2543 Strict Route rule + SipURL next_hop=(new RouteHeader(mrh.getTop())).getNameAddress().getAddress(); + SipURL recipient=getRequestLine().getAddress(); + mrh.removeTop(); + mrh.addBottom(new RouteHeader(new NameAddress(recipient))); + setRoutes(mrh); + setRequestLine(new RequestLine(getRequestLine().getMethod(),next_hop)); + } + } + } + + + /** Changes form RFC2543 Strict Route to RFC3261 Lose Route. + *

    The Request-URI is replaced with the last + * value from the Route header, and that value is removed from the + * Route header. */ + public void rfc2543toRfc3261RouteUpdate() + { // the message is formed according with RFC2543 strict route + // the next hop is the request-uri + // the recipient of the message is the last Route value + RequestLine request_line=getRequestLine(); + SipURL next_hop=request_line.getAddress(); + MultipleHeader mrh=getRoutes(); + SipURL target=(new RouteHeader(mrh.getBottom())).getNameAddress().getAddress(); + mrh.removeBottom(); + next_hop.addLr(); + mrh.addTop(new RouteHeader(new NameAddress(next_hop))); + removeRoutes(); + addRoutes(mrh); + setRequestLine(new RequestLine(request_line.getMethod(),target)); + } + +} diff --git a/src/org/zoolu/sip/message/BaseMessageFactory.java b/src/org/zoolu/sip/message/BaseMessageFactory.java new file mode 100644 index 0000000..074dbb5 --- /dev/null +++ b/src/org/zoolu/sip/message/BaseMessageFactory.java @@ -0,0 +1,373 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.message; + + +import org.zoolu.sip.address.*; +import org.zoolu.sip.header.*; +import org.zoolu.sip.dialog.Dialog; +import org.zoolu.sip.provider.SipStack; +import org.zoolu.sip.provider.SipProvider; +import org.zoolu.sip.message.Message; +import org.zoolu.sip.message.SipMethods; +import org.zoolu.sip.message.SipResponses; + +import java.util.Vector; + + +/** BaseMessageFactory is used to create SIP messages, requests and + * responses by means of + * two static methods: createRequest(), createResponse(). + *
    A valid SIP request sent by a UAC MUST, at least, contain + * the following header fields: To, From, CSeq, Call-ID, Max-Forwards, + * and Via; all of these header fields are mandatory in all SIP + * requests. These sip header fields are the fundamental building + * blocks of a SIP message, as they jointly provide for most of the + * critical message routing services including the addressing of + * messages, the routing of responses, limiting message propagation, + * ordering of messages, and the unique identification of transactions. + * These header fields are in addition to the mandatory request line, + * which contains the method, Request-URI, and SIP version. + */ +public abstract class BaseMessageFactory +{ + + /** Creates a SIP request message. + * @param method method name + * @param request_uri request-uri + * @param to ToHeader NameAddress + * @param from FromHeader NameAddress + * @param contact Contact NameAddress (if null, no ContactHeader is added) + * @param host_addr Via address + * @param host_port Via port number + * @param call_id Call-ID value + * @param cseq CSeq value + * @param local_tag tag in FromHeader + * @param remote_tag tag in ToHeader (if null, no tag is added) + * @param branch branch value (if null, a random value is picked) + * @param body body (if null, no body is added) */ + public static Message createRequest(String method, SipURL request_uri, NameAddress to, NameAddress from, NameAddress contact, String proto, String via_addr, int host_port, boolean rport, String call_id, long cseq, String local_tag, String remote_tag, String branch, String body) + { Message req=new Message(); + //mandatory headers first (To, From, Via, Max-Forwards, Call-ID, CSeq): + req.setRequestLine(new RequestLine(method,request_uri)); + ViaHeader via=new ViaHeader(proto,via_addr,host_port); + if (rport) via.setRport(); + if (branch==null) branch=SipProvider.pickBranch(); + via.setBranch(branch); + req.addViaHeader(via); + req.setMaxForwardsHeader(new MaxForwardsHeader(70)); + if (remote_tag==null) req.setToHeader(new ToHeader(to)); + else req.setToHeader(new ToHeader(to,remote_tag)); + req.setFromHeader(new FromHeader(from,local_tag)); + req.setCallIdHeader(new CallIdHeader(call_id)); + req.setCSeqHeader(new CSeqHeader(cseq,method)); + //optional headers: + if (contact!=null) + { MultipleHeader contacts=new MultipleHeader(SipHeaders.Contact); + contacts.addBottom(new ContactHeader(contact)); + //System.out.println("DEBUG: Contact: "+contact.toString()); + req.setContacts(contacts); + } + req.setExpiresHeader(new ExpiresHeader(String.valueOf(SipStack.default_expires))); + // add User-Agent header field + if (SipStack.ua_info!=null) req.setUserAgentHeader(new UserAgentHeader(SipStack.ua_info)); + //if (body!=null) req.setBody(body); else req.setBody(""); + req.setBody(body); + //System.out.println("DEBUG: MessageFactory: request:\n"+req); + return req; + } + + + /** Creates a SIP request message. + * Where

      + *
    • via address and port are taken from SipProvider + *
    • transport protocol is taken from request-uri (if transport parameter is present) + * or the default transport for the SipProvider is used. + *
    + * @param sip_provider the SipProvider used to fill the Via field + * @see #createRequest(String,SipURL,NameAddress,NameAddress,NameAddress,String,String,int,String,long,String,String,String,String) */ + public static Message createRequest(SipProvider sip_provider, String method, SipURL request_uri, NameAddress to, NameAddress from, NameAddress contact, String call_id, long cseq, String local_tag, String remote_tag, String branch, String body) + { String via_addr=sip_provider.getViaAddress(); + int host_port=sip_provider.getPort(); + boolean rport=sip_provider.isRportSet(); + String proto; + if (request_uri.hasTransport()) proto=request_uri.getTransport(); + else proto=sip_provider.getDefaultTransport(); + + return createRequest(method,request_uri,to,from,contact,proto,via_addr,host_port,rport,call_id,cseq,local_tag,remote_tag,branch,body); + } + + + /** Creates a SIP request message. + * Where
      + *
    • request-uri equals the To sip url + *
    • via address and port are taken from SipProvider + *
    • transport protocol is taken from request-uri (if transport parameter is present) + * or the default transport for the SipProvider is used. + *
    • call_id is picked random + *
    • cseq is picked random + *
    • local_tag is picked random + *
    • branch is picked random + *
    + * @see #createRequest(String,SipURL,NameAddress,NameAddress,NameAddress,String,String,int,String,long,String,String,String,String) */ + public static Message createRequest(SipProvider sip_provider, String method, SipURL request_uri, NameAddress to, NameAddress from, NameAddress contact, String body) + { //SipURL request_uri=to.getAddress(); + String call_id=sip_provider.pickCallId(); + int cseq=SipProvider.pickInitialCSeq(); + String local_tag=SipProvider.pickTag(); + //String branch=SipStack.pickBranch(); + return createRequest(sip_provider,method,request_uri,to,from,contact,call_id,cseq,local_tag,null,null,body); + } + + + /** Creates a SIP request message. + * Where
      + *
    • request-uri equals the To sip url + *
    • via address and port are taken from SipProvider + *
    • transport protocol is taken from request-uri (if transport parameter is present) + * or the default transport for the SipProvider is used. + *
    • contact is formed by the 'From' user-name and by the address and port taken from SipProvider + *
    • call_id is picked random + *
    • cseq is picked random + *
    • local_tag is picked random + *
    • branch is picked random + *
    + * @see #createRequest(SipProvider,String,NameAddress,NameAddress,NameAddress,String) */ + public static Message createRequest(SipProvider sip_provider, String method, NameAddress to, NameAddress from, String body) + { String contact_user=from.getAddress().getUserName(); + NameAddress contact=new NameAddress(new SipURL(contact_user,sip_provider.getViaAddress(),sip_provider.getPort())); + return createRequest(sip_provider,method,to.getAddress(),to,from,contact,body); + } + + + /** Creates a SIP request message within a dialog, with a new branch via-parameter. + * @param dialog the Dialog used to compose the various Message headers + * @param method the request method + * @param body the message body */ + public static Message createRequest(Dialog dialog, String method, String body) + { NameAddress to=dialog.getRemoteName(); + NameAddress from=dialog.getLocalName(); + NameAddress target=dialog.getRemoteContact(); + if (target==null) target=to; + SipURL request_uri=target.getAddress(); + if (request_uri==null) request_uri=dialog.getRemoteName().getAddress(); + SipProvider sip_provider=dialog.getSipProvider(); + String via_addr=sip_provider.getViaAddress(); + int host_port=sip_provider.getPort(); + boolean rport=sip_provider.isRportSet(); + String proto; + if (target.getAddress().hasTransport()) proto=target.getAddress().getTransport(); + else proto=sip_provider.getDefaultTransport(); + NameAddress contact=dialog.getLocalContact(); + if (contact==null) contact=from; + // increment the CSeq, if method is not ACK nor CANCEL + if (!SipMethods.isAck(method) && !SipMethods.isCancel(method)) dialog.incLocalCSeq(); + String call_id=dialog.getCallID(); + long cseq=dialog.getLocalCSeq(); + String local_tag=dialog.getLocalTag(); + String remote_tag=dialog.getRemoteTag(); + //String branch=SipStack.pickBranch(); + Message req=createRequest(method,request_uri,to,from,contact,proto,via_addr,host_port,rport,call_id,cseq,local_tag,remote_tag,null,body); + Vector route=dialog.getRoute(); + if (route!=null && route.size()>0) + req.addRoutes(new MultipleHeader(SipHeaders.Route,route)); + req.rfc2543RouteAdapt(); + return req; + } + + + /** Creates a new INVITE request out of any pre-existing dialogs. + * @see #createRequest(String,SipURL,NameAddress,NameAddress,NameAddress,String,String,int,boolean,String,long,String,String,String,String) */ + public static Message createInviteRequest(SipProvider sip_provider, SipURL request_uri, NameAddress to, NameAddress from, NameAddress contact, String body) + { String call_id=sip_provider.pickCallId(); + int cseq=SipProvider.pickInitialCSeq(); + String local_tag=SipProvider.pickTag(); + //String branch=SipStack.pickBranch(); + if (contact==null) contact=from; + return createRequest(sip_provider,SipMethods.INVITE,request_uri,to,from,contact,call_id,cseq,local_tag,null,null,body); + } + + + /** Creates a new INVITE request within a dialog (re-invite). + * @see #createRequest(Dialog,String,String) */ + public static Message createInviteRequest(Dialog dialog, String body) + { return createRequest(dialog,SipMethods.INVITE,body); + } + + + /** Creates an ACK request for a 2xx response. + * @see #createRequest(Dialog,String,String) */ + public static Message create2xxAckRequest(Dialog dialog, String body) + { return createRequest(dialog,SipMethods.ACK,body); + } + + + /** Creates an ACK request for a non-2xx response */ + public static Message createNon2xxAckRequest(SipProvider sip_provider, Message method, Message resp) + { SipURL request_uri=method.getRequestLine().getAddress(); + FromHeader from=method.getFromHeader(); + ToHeader to=resp.getToHeader(); + String via_addr=sip_provider.getViaAddress(); + int host_port=sip_provider.getPort(); + boolean rport=sip_provider.isRportSet(); + String proto; + if (request_uri.hasTransport()) proto=request_uri.getTransport(); + else proto=sip_provider.getDefaultTransport(); + String branch=method.getViaHeader().getBranch(); + NameAddress contact=null; + Message ack=createRequest(SipMethods.ACK,request_uri,to.getNameAddress(),from.getNameAddress(),contact,proto,via_addr,host_port,rport,method.getCallIdHeader().getCallId(),method.getCSeqHeader().getSequenceNumber(),from.getParameter("tag"),to.getParameter("tag"),branch,null); + ack.removeExpiresHeader(); + if (method.hasRouteHeader()) ack.setRoutes(method.getRoutes()); + return ack; + } + + + /** Creates an ACK request for a 2xx-response. Contact value is taken from SipStack */ + /*public static Message create2xxAckRequest(Message resp, String body) + { ToHeader to=resp.getToHeader(); + FromHeader from=resp.getFromHeader(); + int code=resp.getStatusLine().getCode(); + SipURL request_uri; + request_uri=resp.getContactHeader().getNameAddress().getAddress(); + if (request_uri==null) request_uri=to.getNameAddress().getAddress(); + String branch=SipStack.pickBranch(); + NameAddress contact=null; + if (SipStack.contact_url!=null) contact=new NameAddress(SipStack.contact_url); + return createRequest(SipMethods.ACK,request_uri,to.getNameAddress(),from.getNameAddress(),contact,resp.getCallIdHeader().getCallId(),resp.getCSeqHeader().getSequenceNumber(),from.getParameter("tag"),to.getParameter("tag"),branch,body); + }*/ + + + /** Creates an ACK request for a 2xx-response within a dialog */ + /*public static Message create2xxAckRequest(Dialog dialog, NameAddress contact, String body) + { return createRequest(SipMethods.ACK,dialog,contact,body); + }*/ + + + /** Creates an ACK request for a 2xx-response within a dialog */ + /*public static Message create2xxAckRequest(Dialog dialog, String body) + { return createRequest(SipMethods.ACK,dialog,body); + }*/ + + + /** Creates a CANCEL request. */ + public static Message createCancelRequest(Message method) + { ToHeader to=method.getToHeader(); + FromHeader from=method.getFromHeader(); + SipURL request_uri=method.getRequestLine().getAddress(); + NameAddress contact=method.getContactHeader().getNameAddress(); + ViaHeader via=method.getViaHeader(); + String host_addr=via.getHost(); + int host_port=via.getPort(); + boolean rport=via.hasRport(); + String proto=via.getProtocol(); + String branch=method.getViaHeader().getBranch(); + return createRequest(SipMethods.CANCEL,request_uri,to.getNameAddress(),from.getNameAddress(),contact,proto,host_addr,host_port,rport,method.getCallIdHeader().getCallId(),method.getCSeqHeader().getSequenceNumber(),from.getParameter("tag"),to.getParameter("tag"),branch,""); + } + + + /** Creates a BYE request. */ + public static Message createByeRequest(Dialog dialog) + { Message msg=createRequest(dialog,SipMethods.BYE,null); + msg.removeExpiresHeader(); + msg.removeContacts(); + return msg; + } + + + /** Creates a new REGISTER request. + *

    If contact is null, set contact as star * (register all) */ + public static Message createRegisterRequest(SipProvider sip_provider, NameAddress to, NameAddress from, NameAddress contact) + { SipURL to_url=to.getAddress(); + SipURL registrar=new SipURL(to_url.getHost(),to_url.getPort()); + String via_addr=sip_provider.getViaAddress(); + int host_port=sip_provider.getPort(); + boolean rport=sip_provider.isRportSet(); + String proto; + if (to_url.hasTransport()) proto=to_url.getTransport(); + else proto=sip_provider.getDefaultTransport(); + String call_id=sip_provider.pickCallId(); + int cseq=SipProvider.pickInitialCSeq(); + String local_tag=SipProvider.pickTag(); + //String branch=SipStack.pickBranch(); + Message req=createRequest(SipMethods.REGISTER,registrar,to,from,contact,proto,via_addr,host_port,rport,call_id,cseq,local_tag,null,null,null); + // if no contact, deregister all + if (contact==null) + { ContactHeader star=new ContactHeader(); // contact is * + req.setContactHeader(star); + req.setExpiresHeader(new ExpiresHeader(String.valueOf(SipStack.default_expires))); + } + return req; + } + + + //################ Can be removed? ################ + /** Creates a new REGISTER request. + *

    If contact is null, set contact as star * (register all) */ + /*public static Message createRegisterRequest(SipProvider sip_provider, NameAddress to, NameAddress contact) + { return createRegisterRequest(sip_provider,to,to,contact); + }*/ + + + /** Creates a SIP response message. + * @param req the request message + * @param code the response code + * @param reason the response reason + * @param contact the contact address + * @param local_tag the local tag in the 'To' header + * @param body the message body */ + public static Message createResponse(Message req, int code, String reason, String local_tag, NameAddress contact, String content_type, String body) + { Message resp=new Message(); + resp.setStatusLine(new StatusLine(code,reason)); + resp.setVias(req.getVias()); + if (code>=180 && code<300 && req.hasRecordRouteHeader()) + resp.setRecordRoutes(req.getRecordRoutes()); + ToHeader toh=req.getToHeader(); + if (local_tag!=null) + toh.setParameter("tag",local_tag); + resp.setToHeader(toh); + resp.setFromHeader(req.getFromHeader()); + resp.setCallIdHeader(req.getCallIdHeader()); + resp.setCSeqHeader(req.getCSeqHeader()); + if (contact!=null) resp.setContactHeader(new ContactHeader(contact)); + // add Server header field + if (SipStack.server_info!=null) resp.setServerHeader(new ServerHeader(SipStack.server_info)); + //if (body!=null) resp.setBody(body); else resp.setBody(""); + if (content_type==null) resp.setBody(body); + else resp.setBody(content_type,body); + //System.out.println("DEBUG: MessageFactory: response:\n"+resp.toString()); + return resp; + } + + /** Creates a SIP response message. For 2xx responses generates the local tag by means of the SipStack.pickTag(req) method. + * @see #createResponse(Message,int,String,NameAddress,String,String body) */ + public static Message createResponse(Message req, int code, String reason, NameAddress contact) + { //String reason=SipResponses.reasonOf(code); + String localtag=null; + if (req.createsDialog() && !req.getToHeader().hasTag()) + { if (SipStack.early_dialog || (code>=200 && code<300)) localtag=SipProvider.pickTag(req); + } + return createResponse(req,code,reason,localtag,contact,null,null); + } + +} \ No newline at end of file diff --git a/src/org/zoolu/sip/message/BaseMessageOtp.java b/src/org/zoolu/sip/message/BaseMessageOtp.java new file mode 100644 index 0000000..f5b4026 --- /dev/null +++ b/src/org/zoolu/sip/message/BaseMessageOtp.java @@ -0,0 +1,416 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.message; + + +import org.zoolu.sip.provider.*; +import org.zoolu.sip.header.*; +import org.zoolu.sip.address.*; +import org.zoolu.sip.message.SipMethods; +import org.zoolu.net.UdpPacket; +import java.util.*; + + +/** Class BaseMessageOtp implements a generic SIP Message. + * It extends class BaseMessage adding one-time-parsing functionality + * (it parses the entire Message just when it is costructed). + *

    At the contrary, class BaseMessage works in a just-in-time manner + * (it parses the message each time a particular header field is requested). */ +public abstract class BaseMessageOtp extends BaseMessage +{ + + protected RequestLine request_line; + protected StatusLine status_line; + + protected Vector headers; + protected String body; + + + /** Inits empty Message */ + private void init() + { request_line=null; + status_line=null; + headers=null; + body=null; + } + + /** Costructs a new empty Message */ + public BaseMessageOtp() + { init(); + headers=new Vector(); + } + + /** Costructs a new Message */ + public BaseMessageOtp(byte[] data, int offset, int len) + { init(); + parseIt(new String(data,offset,len)); + } + + /** Costructs a new Message */ + public BaseMessageOtp(UdpPacket packet) + { init(); + parseIt(new String(packet.getData(),packet.getOffset(),packet.getLength())); + } + + /** Costructs a new Message */ + public BaseMessageOtp(String str) + { init(); + parseIt(str); + } + + /** Costructs a new Message */ + public BaseMessageOtp(BaseMessageOtp msg) + { init(); + remote_addr=msg.remote_addr; + remote_port=msg.remote_port; + transport_proto=msg.transport_proto; + connection_id=msg.connection_id; + //packet_length=msg.packet_length; + request_line=msg.request_line; + status_line=msg.status_line; + headers=new Vector(); + for (int i=0; imethod request */ + public boolean isRequest(String method) + { if (request_line!=null && request_line.getMethod().equalsIgnoreCase(method)) return true; + else return false; + } + + + /** Whether Message has Request-line */ + protected boolean hasRequestLine() + { return request_line!=null; + } + + /** Gets RequestLine in Message (Returns null if called for no request message) */ + public RequestLine getRequestLine() + { return request_line; + } + + /** Sets RequestLine of the Message */ + public void setRequestLine(RequestLine rl) + { request_line=rl; + } + + /** Removes RequestLine of the Message */ + public void removeRequestLine() + { request_line=null; + } + + + //**************************** Responses ****************************/ + + /** Whether Message is a Response */ + public boolean isResponse() throws NullPointerException + { if (status_line!=null) return true; + else return false; + } + + /** Whether Message has Status-line */ + protected boolean hasStatusLine() + { return status_line!=null; + } + + /** Gets StautsLine in Message (Returns null if called for no response message) */ + public StatusLine getStatusLine() + { return status_line; + } + + /** Sets StatusLine of the Message */ + public void setStatusLine(StatusLine sl) + { status_line=sl; + } + + /** Removes StatusLine of the Message */ + public void removeStatusLine() + { status_line=null; + } + + + //**************************** Generic Headers ****************************/ + + /** Removes Request\Status Line of the Message */ + protected void removeFirstLine() + { removeRequestLine(); + removeStatusLine(); + } + + /** Gets the position of header hname. */ + protected int indexOfHeader(String hname) + { for (int i=0; imheader at the top/bottom */ + public void addHeaders(MultipleHeader mheader, boolean top) + { if (mheader.isCommaSeparated()) addHeader(mheader.toHeader(),top); + else addHeaders(mheader.getHeaders(),top); + } + + /** Adds Header before the first header refer_hname + * .

    If there is no header of such type, it is added at top */ + public void addHeaderBefore(Header new_header, String refer_hname) + { int i=indexOfHeader(refer_hname); + if (i<0) i=0; + headers.insertElementAt(new_header,i); + } + + /** Adds MultipleHeader(s) before the first header refer_hname + * .

    If there is no header of such type, they are added at top */ + public void addHeadersBefore(MultipleHeader mheader, String refer_hname) + { if (mheader.isCommaSeparated()) addHeaderBefore(mheader.toHeader(),refer_hname); + else + { int index=indexOfHeader(refer_hname); + if (index<0) index=0; + Vector hs=mheader.getHeaders(); + for (int k=0; krefer_hname + * .

    If there is no header of such type, it is added at bottom */ + public void addHeaderAfter(Header new_header, String refer_hname) + { int i=indexOfHeader(refer_hname); + if (i>=0) i++; else i=headers.size(); + headers.insertElementAt(new_header,i); + } + + /** Adds MultipleHeader(s) after the first header refer_hname + * .

    If there is no header of such type, they are added at bottom */ + public void addHeadersAfter(MultipleHeader mheader, String refer_hname) + { if (mheader.isCommaSeparated()) addHeaderAfter(mheader.toHeader(),refer_hname); + else + { int index=indexOfHeader(refer_hname); + if (index>=0) index++; else index=headers.size(); + Vector hs=mheader.getHeaders(); + for (int k=0; k=0) headers.removeElementAt(index); + } + + /** Removes all Headers of specified name */ + public void removeAllHeaders(String hname) + { for (int i=0 ; ihd removing any previous headers of the same type. */ + public void setHeader(Header hd) + { boolean first=true; + String hname=hd.getName(); + for (int i=0 ; imheader */ + public void setHeaders(MultipleHeader mheader) + { if (mheader.isCommaSeparated()) setHeader(mheader.toHeader()); + else + { boolean first=true; + String hname=mheader.getName(); + for (int i=0 ; i0) + { setContentTypeHeader(new ContentTypeHeader(content_type)); + setContentLengthHeader(new ContentLengthHeader(body.length())); + this.body=body; + } + else + { setContentLengthHeader(new ContentLengthHeader(0)); + this.body=null; + } + } + /** Gets message body. The end of body is evaluated + * from the Content-Length header if present (SIP-RFC compliant), + * or from the end of message if no Content-Length header is present (non-SIP-RFC compliant) */ + public String getBody() + { return this.body; + } + /** Removes the message body (if it exists) and the final empty line */ + public void removeBody() + { removeContentLengthHeader(); + removeContentTypeHeader(); + this.body=null; + } + +} diff --git a/src/org/zoolu/sip/message/BaseSipMethods.java b/src/org/zoolu/sip/message/BaseSipMethods.java new file mode 100644 index 0000000..dafac57 --- /dev/null +++ b/src/org/zoolu/sip/message/BaseSipMethods.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.message; + + + + +/** Class BaseSipMethods collects all SIP method names. + */ +public abstract class BaseSipMethods +{ + /** String "INVITE" */ + public static final String INVITE="INVITE"; + /** Whether str is INVITE */ + public static boolean isInvite(String str) { return same(str,INVITE); } + /** String "ACK" */ + public static final String ACK="ACK"; + /** Whether str is ACK */ + public static boolean isAck(String str) { return same(str,ACK); } + /** String "CANCEL" */ + public static final String CANCEL="CANCEL"; + /** Whether str is CANCEL */ + public static boolean isCancel(String str) { return same(str,CANCEL); } + /** String "BYE" */ + public static final String BYE="BYE"; + /** Whether str is BYE */ + public static boolean isBye(String str) { return same(str,BYE); } + /** String "INFO" */ + public static final String INFO="INFO"; + /** Whether str is INFO */ + public static boolean isInfo(String str) { return same(str,INFO); } + /** String "OPTION" */ + public static final String OPTION="OPTION"; + /** Whether str is OPTION */ + public static boolean isOption(String str) { return same(str,OPTION); } + /** String "REGISTER" */ + public static final String REGISTER="REGISTER"; + /** Whether str is REGISTER */ + public static boolean isRegister(String str) { return same(str,REGISTER); } + /** String "UPDATE" */ + public static final String UPDATE="UPDATE"; + /** Whether str is UPDATE */ + public static boolean isUpdate(String str) { return same(str,UPDATE); } + + /** Whether s1 and s2 are case-unsensitive-equal. */ + protected static boolean same(String s1, String s2) + { //return s1.compareToIgnoreCase(s2)==0; + return s1.equalsIgnoreCase(s2); + } + + /** Array of standard methods */ + public static final String[] methods={ INVITE,ACK,CANCEL,BYE,INFO,OPTION,REGISTER,UPDATE }; + + /** Array of standards methods that creates a dialog */ + public static final String[] dialog_methods={ INVITE }; +} diff --git a/src/org/zoolu/sip/message/BaseSipResponses.java b/src/org/zoolu/sip/message/BaseSipResponses.java new file mode 100644 index 0000000..728feb1 --- /dev/null +++ b/src/org/zoolu/sip/message/BaseSipResponses.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.message; + + + + +/** Class SipResponses provides all raeson-phrases + * corrspondent to the various SIP response codes */ +public abstract class BaseSipResponses +{ + //static Hashtable reasons; + protected static String[] reasons; + + private static boolean is_init=false; + + protected static void init() + { if (is_init) return; + //else + + //reasons=new Hashtable(); + //reasons.put(new Integer(100),"Trying"); + //.. + reasons=new String[700]; + for (int i=0; i<700; i++) reasons[i]=null; + + // Not defined (included just to robustness) + reasons[0]="Internal error"; + + // Informational + reasons[100]="Trying"; + reasons[180]="Ringing"; + reasons[181]="Call Is Being Forwarded"; + reasons[182]="Queued"; + reasons[183]="Session Progress"; + + // Success + reasons[200]="OK"; + + // Redirection + reasons[300]="Multiple Choices"; + reasons[301]="Moved Permanently"; + reasons[302]="Moved Temporarily"; + reasons[305]="Use Proxy"; + reasons[380]="Alternative Service"; + + // Client-Error + reasons[400]="Bad Request"; + reasons[401]="Unauthorized"; + reasons[402]="Payment Required"; + reasons[403]="Forbidden"; + reasons[404]="Not Found"; + reasons[405]="Method Not Allowed"; + reasons[406]="Not Acceptable"; + reasons[407]="Proxy Authentication Required"; + reasons[408]="Request Timeout"; + reasons[410]="Gone"; + reasons[413]="Request Entity Too Large"; + reasons[414]="Request-URI Too Large"; + reasons[415]="Unsupported Media Type"; + reasons[416]="Unsupported URI Scheme"; + reasons[420]="Bad Extension"; + reasons[421]="Extension Required"; + reasons[423]="Interval Too Brief"; + reasons[480]="Temporarily not available"; + reasons[481]="Call Leg/Transaction Does Not Exist"; + reasons[482]="Loop Detected"; + reasons[483]="Too Many Hops"; + reasons[484]="Address Incomplete"; + reasons[485]="Ambiguous"; + reasons[486]="Busy Here"; + reasons[487]="Request Terminated"; + reasons[488]="Not Acceptable Here"; + reasons[491]="Request Pending"; + reasons[493]="Undecipherable"; + + // Server-Error + reasons[500]="Internal Server Error"; + reasons[501]="Not Implemented"; + reasons[502]="Bad Gateway"; + reasons[503]="Service Unavailable"; + reasons[504]="Server Time-out"; + reasons[505]="SIP Version not supported"; + reasons[513]="Message Too Large"; + + // Global-Failure + reasons[600]="Busy Everywhere"; + reasons[603]="Decline"; + reasons[604]="Does not exist anywhere"; + reasons[606]="Not Acceptable"; + + is_init=true; + } + + /** Gets the reason phrase of a given response code */ + public static String reasonOf(int code) + { if (!is_init) init(); + if (reasons[code]!=null) return reasons[code]; + else return reasons[((int)(code/100))*100]; + } + + /** Sets the reason phrase for a given response code */ + /*public static void setReason(int code, String reason) + { reasons[((int)(code/100))*100]=reason; + }*/ +} \ No newline at end of file diff --git a/src/org/zoolu/sip/message/Message.java b/src/org/zoolu/sip/message/Message.java new file mode 100644 index 0000000..523e766 --- /dev/null +++ b/src/org/zoolu/sip/message/Message.java @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.message; + + +import org.zoolu.sip.header.Header; +import org.zoolu.sip.header.MultipleHeader; +import org.zoolu.sip.header.ReferToHeader; +import org.zoolu.sip.header.ReferredByHeader; +import org.zoolu.sip.header.EventHeader; +import org.zoolu.sip.header.AllowEventsHeader; +import org.zoolu.sip.header.SubscriptionStateHeader; +import org.zoolu.sip.header.SipHeaders; +import org.zoolu.net.UdpPacket; + + +/** Class Message extends class sip.message.BaseMessage adding some SIP extensions. + *

    + * Class Message supports all methods and header definened in RFC3261, plus: + *

      + *
    • method MESSAGE (RFC3428) + *
    • method REFER (RFC3515) + *
    • header Refer-To + *
    • header Referred-By + *
    • header Event + *
    + */ +public class Message extends org.zoolu.sip.message.BaseMessage +{ + /** Costructs a new empty Message */ + public Message() { super(); } + + /** Costructs a new Message */ + public Message(String str) + { super(str); + } + + /** Costructs a new Message */ + public Message(byte[] buff, int offset, int len) + { super(buff,offset,len); + } + + /** Costructs a new Message */ + public Message(UdpPacket packet) + { super(packet); + } + + /** Costructs a new Message */ + public Message(Message msg) + { super(msg); + } + + /** Creates and returns a clone of the Message */ + public Object clone() + { return new Message(this); + } + + + //****************************** Extensions *******************************/ + + /** Returns boolean value to indicate if Message is a MESSAGE request (RFC3428) */ + public boolean isMessage() throws NullPointerException + { return isRequest(SipMethods.MESSAGE); + } + + /** Returns boolean value to indicate if Message is a REFER request (RFC3515) */ + public boolean isRefer() throws NullPointerException + { return isRequest(SipMethods.REFER); + } + + /** Returns boolean value to indicate if Message is a NOTIFY request (RFC3265) */ + public boolean isNotify() throws NullPointerException + { return isRequest(SipMethods.NOTIFY); + } + + /** Returns boolean value to indicate if Message is a SUBSCRIBE request (RFC3265) */ + public boolean isSubscribe() throws NullPointerException + { return isRequest(SipMethods.SUBSCRIBE); + } + + /** Returns boolean value to indicate if Message is a PUBLISH request (RFC3903) */ + public boolean isPublish() throws NullPointerException + { return isRequest(SipMethods.PUBLISH); + } + + + /** Whether the message has the Refer-To header */ + public boolean hasReferToHeader() + { return hasHeader(SipHeaders.Refer_To); + } + /** Gets ReferToHeader */ + public ReferToHeader getReferToHeader() + { Header h=getHeader(SipHeaders.Refer_To); + if (h==null) return null; + return new ReferToHeader(h); + } + /** Sets ReferToHeader */ + public void setReferToHeader(ReferToHeader h) + { setHeader(h); + } + /** Removes ReferToHeader from Message (if it exists) */ + public void removeReferToHeader() + { removeHeader(SipHeaders.Refer_To); + } + + + + /** Whether the message has the Referred-By header */ + public boolean hasReferredByHeader() + { return hasHeader(SipHeaders.Refer_To); + } + /** Gets ReferredByHeader */ + public ReferredByHeader getReferredByHeader() + { Header h=getHeader(SipHeaders.Referred_By); + if (h==null) return null; + return new ReferredByHeader(h); + } + /** Sets ReferredByHeader */ + public void setReferredByHeader(ReferredByHeader h) + { setHeader(h); + } + /** Removes ReferredByHeader from Message (if it exists) */ + public void removeReferredByHeader() + { removeHeader(SipHeaders.Referred_By); + } + + + + /** Whether the message has the EventHeader */ + public boolean hasEventHeader() + { return hasHeader(SipHeaders.Event); + } + /** Gets EventHeader */ + public EventHeader getEventHeader() + { Header h=getHeader(SipHeaders.Event); + if (h==null) return null; + return new EventHeader(h); + } + /** Sets EventHeader */ + public void setEventHeader(EventHeader h) + { setHeader(h); + } + /** Removes EventHeader from Message (if it exists) */ + public void removeEventHeader() + { removeHeader(SipHeaders.Event); + } + + + /** Whether the message has the AllowEventsHeader */ + public boolean hasAllowEventsHeader() + { return hasHeader(SipHeaders.Allow_Events); + } + /** Gets AllowEventsHeader */ + public AllowEventsHeader getAllowEventsHeader() + { Header h=getHeader(SipHeaders.Allow_Events); + if (h==null) return null; + return new AllowEventsHeader(h); + } + /** Sets AllowEventsHeader */ + public void setAllowEventsHeader(AllowEventsHeader h) + { setHeader(h); + } + /** Removes AllowEventsHeader from Message (if it exists) */ + public void removeAllowEventsHeader() + { removeHeader(SipHeaders.Allow_Events); + } + + + /** Whether the message has the Subscription-State header */ + public boolean hasSubscriptionStateHeader() + { return hasHeader(SipHeaders.Subscription_State); + } + /** Gets SubscriptionStateHeader */ + public SubscriptionStateHeader getSubscriptionStateHeader() + { Header h=getHeader(SipHeaders.Subscription_State); + if (h==null) return null; + return new SubscriptionStateHeader(h); + } + /** Sets SubscriptionStateHeader */ + public void setSubscriptionStateHeader(SubscriptionStateHeader h) + { setHeader(h); + } + /** Removes SubscriptionStateHeader from Message (if it exists) */ + public void removeSubscriptionStateHeader() + { removeHeader(SipHeaders.Subscription_State); + } + +} diff --git a/src/org/zoolu/sip/message/MessageFactory.java b/src/org/zoolu/sip/message/MessageFactory.java new file mode 100644 index 0000000..3607e67 --- /dev/null +++ b/src/org/zoolu/sip/message/MessageFactory.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.message; + + + +import org.zoolu.sip.address.*; +import org.zoolu.sip.header.ContentLengthHeader; +import org.zoolu.sip.header.ContentTypeHeader; +import org.zoolu.sip.header.SubjectHeader; +import org.zoolu.sip.header.ReferToHeader; +import org.zoolu.sip.header.ReferredByHeader; +import org.zoolu.sip.header.EventHeader; +import org.zoolu.sip.dialog.Dialog; +import org.zoolu.sip.provider.SipStack; +import org.zoolu.sip.provider.SipProvider; + + + +/** Class sipx.message.MessageFactory extends class sip.message.BaseMessageFactory. + *

    + * MessageFactory is used to create SIP messages (requests and + * responses). + *
    A valid SIP request sent by a UAC MUST, at least, contain + * the following header fields: To, From, CSeq, Call-ID, Max-Forwards, + * and Via; all of these header fields are mandatory in all SIP + * requests. These sip header fields are the fundamental building + * blocks of a SIP message, as they jointly provide for most of the + * critical message routing services including the addressing of + * messages, the routing of responses, limiting message propagation, + * ordering of messages, and the unique identification of transactions. + * These header fields are in addition to the mandatory request line, + * which contains the method, Request-URI, and SIP version. + */ +public class MessageFactory extends org.zoolu.sip.message.BaseMessageFactory +{ + + /** Creates a new MESSAGE request (RFC3428) */ + public static Message createMessageRequest(SipProvider sip_provider, NameAddress recipient, NameAddress from, String subject, String type, String body) + { SipURL request_uri=recipient.getAddress(); + String callid=sip_provider.pickCallId(); + int cseq=SipProvider.pickInitialCSeq(); + String localtag=SipProvider.pickTag(); + //String branch=SipStack.pickBranch(); + Message req=createRequest(sip_provider,SipMethods.MESSAGE,request_uri,recipient,from,null,callid,cseq,localtag,null,null,null); + if (subject!=null) req.setSubjectHeader(new SubjectHeader(subject)); + req.setBody(type,body); + return req; + } + + /** Creates a new REFER request (RFC3515) */ + public static Message createReferRequest(SipProvider sip_provider, NameAddress recipient, NameAddress from, NameAddress contact, NameAddress refer_to/*, NameAddress referred_by*/) + { SipURL request_uri=recipient.getAddress(); + String callid=sip_provider.pickCallId(); + int cseq=SipProvider.pickInitialCSeq(); + String localtag=SipProvider.pickTag(); + //String branch=SipStack.pickBranch(); + Message req=createRequest(sip_provider,SipMethods.REFER,request_uri,recipient,from,contact,callid,cseq,localtag,null,null,null); + req.setReferToHeader(new ReferToHeader(refer_to)); + //if (referred_by!=null) req.setReferredByHeader(new ReferredByHeader(referred_by)); + req.setReferredByHeader(new ReferredByHeader(from)); + return req; + } + + /** Creates a new REFER request (RFC3515) within a dialog + *

    parameters: + *
    - refer_to mandatory + *
    - referred_by optional + */ + public static Message createReferRequest(Dialog dialog, NameAddress refer_to, NameAddress referred_by) + { Message req=createRequest(dialog,SipMethods.REFER,null); + req.setReferToHeader(new ReferToHeader(refer_to)); + if (referred_by!=null) req.setReferredByHeader(new ReferredByHeader(referred_by)); + else req.setReferredByHeader(new ReferredByHeader(dialog.getLocalName())); + return req; + } + + /** Creates a new SUBSCRIBE request (RFC3265) out of any pre-existing dialogs. */ + public static Message createSubscribeRequest(SipProvider sip_provider, SipURL recipient, NameAddress to, NameAddress from, NameAddress contact, String event, String id, String content_type, String body) + { Message req=createRequest(sip_provider,SipMethods.SUBSCRIBE,recipient,to,from,contact,null); + req.setEventHeader(new EventHeader(event,id)); + req.setBody(content_type,body); + return req; + } + + + /** Creates a new SUBSCRIBE request (RFC3265) within a dialog (re-subscribe). */ + public static Message createSubscribeRequest(Dialog dialog, String event, String id, String content_type, String body) + { Message req=createRequest(dialog,SipMethods.SUBSCRIBE,null); + req.setEventHeader(new EventHeader(event,id)); + req.setBody(content_type,body); + return req; + } + + + /** Creates a new NOTIFY request (RFC3265) within a dialog */ + public static Message createNotifyRequest(Dialog dialog, String event, String id, String content_type, String body) + { Message req=createRequest(dialog,SipMethods.NOTIFY,null); + req.removeExpiresHeader(); + req.setEventHeader(new EventHeader(event,id)); + req.setBody(content_type,body); + return req; + } + + + /** Creates a new NOTIFY request (RFC3265) within a dialog */ + public static Message createNotifyRequest(Dialog dialog, String event, String id, String sipfragment) + { Message req=createRequest(dialog,SipMethods.NOTIFY,null); + req.removeExpiresHeader(); + req.setEventHeader(new EventHeader(event,id)); + req.setBody("message/sipfrag;version=2.0",sipfragment); + return req; + } + +} \ No newline at end of file diff --git a/src/org/zoolu/sip/message/SipMethods.java b/src/org/zoolu/sip/message/SipMethods.java new file mode 100644 index 0000000..da1de32 --- /dev/null +++ b/src/org/zoolu/sip/message/SipMethods.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.message; + + + + +/** Class SipMethods extends org.zoolu.sip.message.BaseSipMethods + * and collects all SIP method names. + */ +public class SipMethods extends BaseSipMethods +{ + + //****************************** Extensions *******************************/ + + /** String "SUBSCRIBE" */ + public static final String SUBSCRIBE="SUBSCRIBE"; + /** Whether str is SUBSCRIBE */ + public static boolean isSubscribe(String str) { return same(str,SUBSCRIBE); } + + /** String "NOTIFY" */ + public static final String NOTIFY="NOTIFY"; + /** Whether str is NOTIFY */ + public static boolean isNotify(String str) { return same(str,NOTIFY); } + + /** String "MESSAGE" for method MESSAGE defined in RFC3428 */ + public static final String MESSAGE="MESSAGE"; + /** Whether str is MESSAGE */ + public static boolean isMessage(String str) { return same(str,MESSAGE); } + + /** String "REFER" for method REFER defined in RFC3515 */ + public static final String REFER="REFER"; + /** Whether str is REFER */ + public static boolean isRefer(String str) { return same(str,REFER); } + + /** String "PUBLISH" for method PUBLISH defined in RFC3903 */ + public static final String PUBLISH="PUBLISH"; + /** Whether str is PUBLISH */ + public static boolean isPublish(String str) { return same(str,PUBLISH); } + + + /** Array of all methods ( standard (RFC3261) + new (RFC3428,..) ) */ + public static final String[] methods={ INVITE,ACK,CANCEL,BYE,INFO,OPTION,REGISTER,UPDATE,SUBSCRIBE,NOTIFY,MESSAGE,REFER,PUBLISH }; + + /** Array of all methods that create a dialog */ + public static final String[] dialog_methods={ INVITE,SUBSCRIBE }; +} diff --git a/src/org/zoolu/sip/message/SipResponses.java b/src/org/zoolu/sip/message/SipResponses.java new file mode 100644 index 0000000..fce49c4 --- /dev/null +++ b/src/org/zoolu/sip/message/SipResponses.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.message; + + + + +/** Class SipResponses provides all raeson-phrases + * corrspondent to the various SIP response codes */ +public class SipResponses extends BaseSipResponses +{ + private static boolean is_init=false; + + public static void init() + { if (is_init) return; + //else + + BaseSipResponses.init(); + + // New response codes + //reasons[xxx]="This Reason"; + //reasons[yyy]="A Second Reason"; + //.. + + // Success + reasons[202]="Accepted"; + + // Failure + reasons[489]="Bad Event"; + + is_init=true; + } + + + /** Gets the reason phrase of a response code */ + public static String reasonOf(int code) + { if (!is_init) init(); + return BaseSipResponses.reasonOf(code); + } + +} diff --git a/src/org/zoolu/sip/provider/ConnectedTransport.java b/src/org/zoolu/sip/provider/ConnectedTransport.java new file mode 100644 index 0000000..f0a14ec --- /dev/null +++ b/src/org/zoolu/sip/provider/ConnectedTransport.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.provider; + + +import org.zoolu.sip.message.Message; +import org.zoolu.net.IpAddress; +import java.io.IOException; + + +/** ConnectedTransport is a generic CO transport service for SIP. + */ +interface ConnectedTransport extends Transport +{ + /** Gets the remote IpAddress */ + public IpAddress getRemoteAddress(); + + /** Gets the remote port */ + public int getRemotePort(); + + /** Gets the last time the ConnectedTransport has been used (in millisconds) */ + public long getLastTimeMillis(); + + /** Sends a Message */ + public void sendMessage(Message msg) throws IOException; + +} diff --git a/src/org/zoolu/sip/provider/ConnectionIdentifier.java b/src/org/zoolu/sip/provider/ConnectionIdentifier.java new file mode 100644 index 0000000..3d771d2 --- /dev/null +++ b/src/org/zoolu/sip/provider/ConnectionIdentifier.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.provider; + + +import org.zoolu.net.IpAddress; + + +/** ConnectionIdentifier is the reference for active transport connections. + */ +public class ConnectionIdentifier extends Identifier +{ + /** Costructs a new ConnectionIdentifier. */ + public ConnectionIdentifier(String protocol, IpAddress remote_ipaddr, int remote_port) + { super(getId(protocol,remote_ipaddr,remote_port)); + } + + /** Costructs a new ConnectionIdentifier. */ + public ConnectionIdentifier(ConnectionIdentifier conn_id) + { super(conn_id); + } + + /** Costructs a new ConnectionIdentifier. */ + public ConnectionIdentifier(String id) + { super(id); + } + + /** Costructs a new ConnectionIdentifier. */ + public ConnectionIdentifier(ConnectedTransport conn) + { super(getId(conn.getProtocol(),conn.getRemoteAddress(),conn.getRemotePort())); + } + + + /** Gets the id. */ + private static String getId(String protocol, IpAddress remote_ipaddr, int remote_port) + { return protocol+":"+remote_ipaddr+":"+remote_port; + } + +} diff --git a/src/org/zoolu/sip/provider/DialogIdentifier.java b/src/org/zoolu/sip/provider/DialogIdentifier.java new file mode 100644 index 0000000..b13c488 --- /dev/null +++ b/src/org/zoolu/sip/provider/DialogIdentifier.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.provider; + + +import org.zoolu.sip.message.Message; + + +/** DialogIdentifier is used to address specific dialogs to the SipProvider. + */ +public class DialogIdentifier extends Identifier +{ + /** Costructs a new DialogIdentifier based on call-id, local and remote tags. */ + public DialogIdentifier(String call_id, String local_tag, String remote_tag) + { id=call_id+"-"+local_tag+"-"+remote_tag; + } + + /** Costructs a new DialogIdentifier. */ + public DialogIdentifier(DialogIdentifier i) + { super(i); + } +} diff --git a/src/org/zoolu/sip/provider/Identifier.java b/src/org/zoolu/sip/provider/Identifier.java new file mode 100644 index 0000000..f079586 --- /dev/null +++ b/src/org/zoolu/sip/provider/Identifier.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.provider; + + + + +/** Generic Identifier. + */ +public class Identifier +{ + /** The actual id */ + String id; + + /** Costructs a new void Identifier. */ + Identifier() + { + } + + /** Costructs a new Identifier. */ + Identifier(String id) + { this.id=id; + } + + /** Costructs a new Identifier. */ + Identifier(Identifier i) + { this.id=i.id; + } + + /** Whether the Identifier equals to obj. */ + public boolean equals(Object obj) + { try + { Identifier i=(Identifier)obj; + return id.equals(i.id); + } + catch (Exception e) { return false; } + } + + /** Gets an int hashCode for the Identifier. */ + public int hashCode() + { return id.hashCode(); + } + + /** Gets a String value for the Identifier */ + public String toString() + { return id; + } +} diff --git a/src/org/zoolu/sip/provider/MethodIdentifier.java b/src/org/zoolu/sip/provider/MethodIdentifier.java new file mode 100644 index 0000000..48dac70 --- /dev/null +++ b/src/org/zoolu/sip/provider/MethodIdentifier.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.provider; + + + + +/** MethodIdentifier is used to address specific methods to the SipProvider. + */ +public class MethodIdentifier extends Identifier +{ + /** Costructs a new MethodIdentifier. */ + public MethodIdentifier(String method) + { super(method); + } + + /** Costructs a new MethodIdentifier. */ + public MethodIdentifier(MethodIdentifier i) + { super(i); + } +} diff --git a/src/org/zoolu/sip/provider/SipInterface.java b/src/org/zoolu/sip/provider/SipInterface.java new file mode 100644 index 0000000..67c97ef --- /dev/null +++ b/src/org/zoolu/sip/provider/SipInterface.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.provider; + + +import org.zoolu.sip.message.Message; + + +/** SipInterface is actually the SIP SAP (Service Access Point) and can be used to send + * and receive SIP messages associated with a specific method, transaction, or dialog. + *

    + * SipInterface provides a simple interface to the multiplexing function provided + * by the SipProvider layer. + *
    It simply wraps the SipProvider by adding and removing the listener + * for capturing received SIP messages. + *

    + * When creating a new SipInterface the following parameters, have to be specified: + * - sip_provider is the SipProvider the SipInterface has to be bound to, + * - id is the SIP interface identifier the SipInterface has to be bound to, + * - listener is the SipInterfaceListener that received messages are passed to. + *

    + * The SIP interface id specifies the type of messages the listener is going to + * receive for. Together with the sip_provider, it represents the complete SIP + * Service Access Point (SAP) address/identifier used for demultiplexing SIP messages + * at receiving side. + *

    + * The identifier can be of one of the three following types: transaction_id, dialog_id, + * or method_id. These types of identifiers characterize respectively: + *
    - messages within a specific transaction, + *
    - messages within a specific dialog, + *
    - messages related to a specific SIP method. + * It is also possible to use the the identifier ANY to specify + *
    - all messages that are out of any transactions, dialogs, or already specified + * method types. + *

    + * When receiving a message, the underling SipProvider first tries to look for + * a SipInterface associated to the corresponding transaction, then looks for + * a SipInterface associated to the corresponding dialog, then for + * a SipInterface associated to the corresponding method type, and finally for + * a SipInterface associated to ANY messages. + * If the present SipInterface id matches, the SipInterfaceListener method + * onReceivedMessage() is fired. + */ +public class SipInterface implements SipProviderListener +{ + + /** SipProvider */ + SipProvider sip_provider; + + /** Identifier */ + Identifier id; + + /** SipInterfaceListener */ + SipInterfaceListener listener; + + + // *************************** Costructors *************************** + + /** Creates a new SipInterface. */ + public SipInterface(SipProvider sip_provider, SipInterfaceListener listener) + { this.sip_provider=sip_provider; + this.listener=listener; + id=SipProvider.ANY; + sip_provider.addSipProviderListener(id,this); + } + + + /** Creates a new SipInterface. */ + public SipInterface(SipProvider sip_provider, Identifier id, SipInterfaceListener listener) + { this.sip_provider=sip_provider; + this.listener=listener; + this.id=id; + sip_provider.addSipProviderListener(id,this); + } + + + // ************************** Public methods ************************* + + /** Close the SipInterface. */ + public void close() + { sip_provider.removeSipProviderListener(id); + } + + + /** Gets the SipProvider. */ + public SipProvider getSipProvider() + { return sip_provider; + } + + + /** Sends a Message, specifing the transport portocol, nexthop address and port. + *

    This is a low level method and + * forces the message to be routed to a specific nexthop address, port and transport, + * regardless whatever the Via, Route, or request-uri, address to. + *

    + * In case of connection-oriented transport, the connection is selected as follows: + *
    - if an existing connection is found matching the destination + * end point (socket), such connection is used, otherwise + *
    - a new connection is established + * + * @return It returns a Connection in case of connection-oriented delivery + * (e.g. TCP) or null in case of connection-less delivery (e.g. UDP) + */ + public ConnectionIdentifier sendMessage(Message msg, String proto, String dest_addr, int dest_port, int ttl) + { return sip_provider.sendMessage(msg,proto,dest_addr,dest_port,ttl); + } + + /** Sends the message msg. + *

    + * The destination for the request is computed as follows: + *
    - if outbound_addr is set, outbound_addr and + * outbound_port are used, otherwise + *
    - if message has Route header with lr option parameter (i.e. RFC3261 compliant), + * the first Route address is used, otherwise + *
    - the request's Request-URI is considered. + *

    + * The destination for the response is computed based on the sent-by parameter in + * the Via header field (RFC3261 compliant) + *

    + * As transport it is used the protocol specified in the 'via' header field + *

    + * In case of connection-oriented transport: + *
    - if an already established connection is found matching the destination + * end point (socket), such connection is used, otherwise + *
    - a new connection is established + * + * @return Returns a ConnectionIdentifier in case of connection-oriented delivery + * (e.g. TCP) or null in case of connection-less delivery (e.g. UDP) + */ + public ConnectionIdentifier sendMessage(Message msg) + { return sip_provider.sendMessage(msg); + } + + + /** Sends the message msg using the specified connection. */ + public ConnectionIdentifier sendMessage(Message msg, ConnectionIdentifier conn_id) + { return sip_provider.sendMessage(msg,conn_id); + } + + + //************************* Callback methods ************************* + + /** When a new Message is received by the SipProvider. */ + public void onReceivedMessage(SipProvider sip_provider, Message message) + { if (listener!=null) listener.onReceivedMessage(this,message); + } + +} diff --git a/src/org/zoolu/sip/provider/SipInterfaceListener.java b/src/org/zoolu/sip/provider/SipInterfaceListener.java new file mode 100644 index 0000000..7b0816b --- /dev/null +++ b/src/org/zoolu/sip/provider/SipInterfaceListener.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.provider; + + +import org.zoolu.sip.message.Message; + + +/** A SipInterfaceListener listens for SipInterface onReceivedMessage(SipInterfaceListener,Message) events. + */ +public interface SipInterfaceListener +{ + /** When a new Message is received by the SipInterface. */ + public void onReceivedMessage(SipInterface sip, Message message); +} diff --git a/src/org/zoolu/sip/provider/SipParser.java b/src/org/zoolu/sip/provider/SipParser.java new file mode 100644 index 0000000..1d310fd --- /dev/null +++ b/src/org/zoolu/sip/provider/SipParser.java @@ -0,0 +1,411 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.provider; + + +import org.zoolu.sip.address.*; +import org.zoolu.sip.header.Header; +import org.zoolu.sip.header.RequestLine; +import org.zoolu.sip.header.StatusLine; +import org.zoolu.sip.message.Message; +import org.zoolu.tools.DateFormat; +import java.util.Vector; +import java.util.Date; +//import java.text.DateFormat; +//import java.text.SimpleDateFormat; +import org.zoolu.tools.Parser; + + +/** Class SipParser extends class Parser for parsing of SIP messages. + */ +public class SipParser extends Parser +{ + /** Creates a new SipParser based on String s */ + public SipParser(String s) + { super(s); + } + + /** Creates a new SipParser based on String s and starting from position i */ + public SipParser(String s, int i) + { super(s,i); + } + + /** Creates a new SipParser based on StringBuffer sb */ + public SipParser(StringBuffer sb) + { super(sb); + } + + /** Creates a new SipParser based on StringBuffer sb and starting from position i */ + public SipParser(StringBuffer sb, int i) + { super(sb,i); + } + + /** Creates a new SipParser starting from the current position. */ + public SipParser(Parser p) + { super(p.getWholeString(),p.getPos()); + } + + /** MARK char[], composed by: '-' , '_' , '.' , '!' , '~' , '*' , '\'' , '|' */ + public static char[] MARK={'-','_','.','!','~','*','\'','|'}; + + /** SEPARATOR char[], composed by: ' ','\t','\r','\n','(',')','<','>',',',';','\\','"','/','[',']','?','=','{','}' */ + public static char[] SEPARATOR={' ','\t','\r','\n','(',')','<','>',',',';','\\','"','/','[',']','?','=','{','}'}; + + /** Checks whether a char is any MARK */ + public static boolean isMark(char c) + { //return (c=='-' || c=='_' || c=='.' || c=='!' || c=='~' || c=='*' || c=='\'' || c=='|'); + return isAnyOf(MARK,c); + } + + /** Unreserved char; that is an alphanum or a mark*/ + public static boolean isUnreserved(char c) + { return (isAlphanum(c) || isMark(c)); + } + + /** Separator; differently form RFC2543, do not include '@' and ':', while include '\r' and '\n'*/ + public static boolean isSeparator(char c) + { //return (isSpace(c) || isCRLF(c) || c=='(' || c==')' || c=='<' || c=='>' || c==',' || c==';' || c=='\\' || c=='"' || c=='/' || c=='[' || c==']' || c=='?' || c=='=' || c=='{' || c=='}'); + return isAnyOf(SEPARATOR,c); + } + + /** Returns the first occurence of a separator or the end of the string*/ + public int indexOfSeparator() + { int begin=index; + while(beginhname
    */ + public int indexOfHeader(String hname) + { if (str.startsWith(hname,index)) return index; + String[] target={'\n'+hname, '\r'+hname}; + SipParser par=new SipParser(str,index); + //par.goTo(target); + par.goToIgnoreCase(target); + if (par.hasMore()) par.skipChar(); + return par.getPos(); + } + + /** Goes to the begin of next header */ + public SipParser goToNextHeader() + { index=indexOfEOH(); + goToNextLine(); + return this; + } + + /** Go to the end of the last header. + * The final empty line delimiter is not considered as header */ + public SipParser goToEndOfLastHeader() + { String[] delimiters={"\r\n\r\n","\n\n"}; // double newline + goTo(delimiters); + if (!hasMore()) // no double newline found + { if (str.startsWith("\r\n",str.length()-2)) index=str.length()-2; + else if (str.charAt(str.length()-1)=='\n') index=str.length()-1; + else index=str.length(); + } + return this; + } + + /** Go to the begin (first char of) Message Body */ + public SipParser goToBody() + { goToEndOfLastHeader(); + goTo('\n').skipChar(); + goTo('\n').skipChar(); + return this; + } + + /** Returns the first header and goes to the next line. */ + public Header getHeader() + { if (!hasMore()) return null; + int begin=getPos(); + int end=indexOfEOH(); + String header_str=getString(end-begin); + goToNextLine(); + int colon=header_str.indexOf(':'); + if (colon<0) return null; + String hname=header_str.substring(0,colon).trim(); + String hvalue=header_str.substring(++colon).trim(); + return new Header(hname,hvalue); + } + + /** Returns the first occurence of Header hname. */ + public Header getHeader(String hname) + { SipParser par=new SipParser(str,indexOfHeader(hname)); + if (!par.hasMore()) return null; + par.skipN(hname.length()); + int begin=par.indexOf(':')+1; + int end=par.indexOfEOH(); + if (begin>end) return null; + String hvalue=str.substring(begin,end).trim(); + index=end; + return new Header(hname,hvalue); + } + + + //************************ first-line ************************ + + /** Returns the request-line. */ + public RequestLine getRequestLine() + { String method=getString(); + skipWSP(); + int begin=getPos(); + int end=indexOfEOH(); + String request_uri=getString(end-begin); + goToNextLine(); + return new RequestLine(method,(new SipParser(request_uri)).getSipURL()); + } + + /** Returns the status-line or null (if it doesn't start with "SIP/"). */ + public StatusLine getStatusLine() + { String version=getString(4); + if (!version.equalsIgnoreCase("SIP/")) { index=str.length(); return null; } + skipString().skipWSP(); // "SIP/2.0 " + int code=getInt(); + int begin=getPos(); + int end=indexOfEOH(); + String reason=getString(end-begin).trim(); + goToNextLine(); + return new StatusLine(code,reason); + } + + + //*************************** URIs *************************** + + public static char[] uri_separators={' ','>','\n','\r'}; + + /** Returns the first URL. + * If no URL is found, it returns null */ + public SipURL getSipURL() + { goTo("sip:"); + if (!hasMore()) return null; + int begin=getPos(); + int end=indexOf(uri_separators); + if (end<0) end=str.length(); + String url=getString(end-begin); + if (hasMore()) skipChar(); + return new SipURL(url); + } +/* + public static SipURL parseSipURL(String s) + { SipParser par=new SipParser(s); + return par.parseSipURL(); + } +*/ + /** Returns the first NameAddress in the string str. + * If no NameAddress is found, it returns null. + * A NameAddress is a string of the form of: + *

       "user's name" <sip url> 
    */ + public NameAddress getNameAddress() + { String text=null; + SipURL url=null; + int begin=getPos(); + int begin_url=indexOf("str; if no NameAddress is found, it returns null*/ +/*public String parseQuotedString() + { int begin=str.indexOf('\"',index); + if (begin<0) return null; + begin++; + int end=str.indexOf('\"',begin); + String quotedtext=str.substring(begin,end); + index=end; + return quotedtext; + } +*/ + + //*************************** DATE *************************** + + + /** Returns a Date object according with the SIP standard date format */ + public Date getDate() + { + //DateFormat df=new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'",Locale.US); + try + { //Date d=df.parse(str,new ParsePosition(index)); + Date d=DateFormat.parseEEEddMMM(str,index); + index=str.indexOf("GMT",index)+3; + return d; + } + catch (Exception e) { e.printStackTrace(); index=str.length(); return null; } + } + + + //*************************** PARAMETERS *************************** + + public static char[] param_separators={ ' ', '=', ';', ',', '\n', '\r' }; + + /** Gets the value of specified parameter. + * @returns the parameter value or null if parameter does not exist or doesn't have a value (i.e. in case of flag parameter). */ + public String getParameter(String name) + { while (hasMore()) + { if (getWord(param_separators).equals(name)) + { skipWSP(); + if (nextChar()=='=') + { skipChar(); + return getWordSkippingQuoted(param_separators); + } + else return null; + } + goToSkippingQuoted(';'); + if (hasMore()) skipChar(); // skip ';' + } + return null; + } + + /** Gets a String Vector of parameter names. + *
    Returns null if no parameter is present */ + public Vector getParameters() + { String name; + Vector params=new Vector(); + while (hasMore()) + { name=getWord(param_separators); + if (name.length()>0) params.addElement(new String(name)); + goToSkippingQuoted(';'); + if (hasMore()) skipChar(); // skip ';' + } + return params; + } + + /** Whether there is the specified parameter */ + public boolean hasParameter(String name) + { while (hasMore()) + { if (getWord(param_separators).equals(name)) return true; + goToSkippingQuoted(';'); + if (hasMore()) skipChar(); // skip ';' + } + return false; + } + + + //************************ MULTIPLE HEADERS ************************ + + /** Finds the first comma-separator. Return -1 if no comma is found. */ + public int indexOfCommaHeaderSeparator() + { boolean inside_quoted_string=false; + for (int i=index; iThe message begins from the first non-CRLF char. */ + public Message getSipMessage() + { // skip any CRLF sequence + skipCRLF(); + // Get content length; if no Content-Length header found return null + String text; + if (getPos()==0) text=str; else text=getRemainingString(); + Message msg=new Message(text); + if (!msg.hasContentLengthHeader()) return null; + int body_len=msg.getContentLengthHeader().getContentLength(); + + // gets the message (and go ahead), or returns null + int begin=getPos(); + goToEndOfLastHeader(); + if (!hasMore()) return null; + goTo('\n'); + if (!hasMore()) return null; + skipChar().goTo('\n'); // skip the LF of last header and go the the new line + if (!hasMore()) return null; + int body_pos=skipChar().getPos(); // skip the LF of the empty line and go the the body + + int end=body_pos+body_len; + if (end<=str.length()) + { index=end; + return new Message(str.substring(begin,end)); + } + else return null; + } +} diff --git a/src/org/zoolu/sip/provider/SipPromisqueInterface.java b/src/org/zoolu/sip/provider/SipPromisqueInterface.java new file mode 100644 index 0000000..f315202 --- /dev/null +++ b/src/org/zoolu/sip/provider/SipPromisqueInterface.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.provider; + + +import org.zoolu.sip.message.Message; + + +/** SipPromisqueInterface is the SipInterface for capturing + * all SIP messages in PROMISQUE mode. + * All incoming messages are passed to the listener associated to the SipPromisqueInterface + * regardless of any other opened SipInterface. + *

    + * More than one SipPromisqueInterface can be open concurrently. + */ +public class SipPromisqueInterface extends SipInterface +{ + /** Creates a new SipPromisqueInterface. */ + public SipPromisqueInterface(SipProvider sip_provider, SipInterfaceListener listener) + { super(sip_provider,SipProvider.PROMISQUE,listener); + } +} diff --git a/src/org/zoolu/sip/provider/SipProvider.java b/src/org/zoolu/sip/provider/SipProvider.java new file mode 100644 index 0000000..28eead0 --- /dev/null +++ b/src/org/zoolu/sip/provider/SipProvider.java @@ -0,0 +1,1229 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.provider; + + +import org.zoolu.net.*; +import org.zoolu.sip.header.*; +import org.zoolu.sip.message.Message; +import org.zoolu.sip.address.*; +import org.zoolu.sip.transaction.Transaction; +import org.zoolu.tools.Configure; +import org.zoolu.tools.Configurable; +import org.zoolu.tools.Parser; +import org.zoolu.tools.Random; +import org.zoolu.tools.Log; +import org.zoolu.tools.LogLevel; +import org.zoolu.tools.RotatingLog; +//import org.zoolu.tools.MD5; +import org.zoolu.tools.SimpleDigest; +import org.zoolu.tools.DateFormat; + +import java.util.Hashtable; +import java.io.IOException; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +//PersonalJava +//import java.util.HashSet; +//import java.util.Iterator; +import org.zoolu.tools.HashSet; +import org.zoolu.tools.Iterator; +import java.util.Enumeration; +import java.util.Vector; +import java.util.Date; + + +/** SipProvider implements the SIP transport layer, that is the layer responsable for + * sending and receiving SIP messages. Messages are received by the callback function + * defined in the interface SipProviderListener. + *

    + * SipProvider implements also multiplexing/demultiplexing service through the use of + * SIP interface identifiers and onReceivedMessage() callback function + * of specific SipProviderListener. + *

    + * A SipProviderListener can be added to a SipProvider through the + * addSipProviderListener(id,listener) method, where: + * - id is the SIP interface identifier the listener has to be bound to, + * - listener is the SipProviderListener that received messages are passed to. + *

    + * The SIP interface identifier specifies the type of messages the listener is going to + * receive for. Together with the specific SipProvider, it represents the complete SIP + * Service Access Point (SAP) address/identifier used for demultiplexing SIP messages + * at receiving side. + *

    + * The identifier can be of one of the three following types: transaction_id, dialog_id, + * or method_id. These types of identifiers characterize respectively: + *
    - messages within a specific transaction, + *
    - messages within a specific dialog, + *
    - messages related to a specific SIP method. + * It is also possible to use the the identifier ANY to specify + *
    - all messages that are out of any transactions, dialogs, or already specified + * method types. + *

    + * When receiving a message, the SipProvider first tries to look for a matching + * transaction, then looks for a matching dialog, then for a matching method type, + * and finally for a default listener (i.e. that with identifier ANY). + * For the matched SipProviderListener, the method onReceivedMessage() is fired. + *

    + * Note: no 482 (Loop Detected) responses are generated for requests that does not + * properly match any ongoing transactions, dialogs, nor method types. + */ +public class SipProvider implements Configurable, TransportListener, TcpServerListener +{ + + // **************************** Constants **************************** + + /** UDP protocol type */ + public static final String PROTO_UDP="udp"; + /** TCP protocol type */ + public static final String PROTO_TCP="tcp"; + /** TLS protocol type */ + public static final String PROTO_TLS="tls"; + /** SCTP protocol type */ + public static final String PROTO_SCTP="sctp"; + + /** String value "auto-configuration" used for auto configuration of the host address. */ + public static final String AUTO_CONFIGURATION="AUTO-CONFIGURATION"; + + /** String value "auto-configuration" used for auto configuration of the host address. */ + public static final String ALL_INTERFACES="ALL-INTERFACES"; + + /** String value "NO-OUTBOUND" used for setting no outbound proxy. */ + //public static final String NO_OUTBOUND="NO-OUTBOUND"; + + /** Identifier used as listener id for capturing ANY incoming messages + * that does not match any active method_id, transaction_id, nor dialog_id. + *
    In this context, "active" means that there is a active listener + * for that specific method, transaction, or dialog. */ + public static final Identifier ANY=new Identifier("ANY"); + + /** Identifier used as listener id for capturing any incoming messages in PROMISQUE mode, + * that means that messages are passed to the present listener regardless of + * any other active SipProviderListeners for specific messages. + *

    + * More than one SipProviderListener can be added and be active concurrently + * for capturing messages in PROMISQUE mode. */ + public static final Identifier PROMISQUE=new Identifier("PROMISQUE"); + + + /** Minimum length for a valid SIP message. */ + private static final int MIN_MESSAGE_LENGTH=12; + + // ***************** Readable/configurable attributes ***************** + + /** Via address/name. + * Use 'auto-configuration' for auto detection, or let it undefined. */ + String via_addr=null; + + /** Local SIP port */ + int host_port=0; + + /** Network interface (IP address) used by SIP. + * Use 'ALL-INTERFACES' for binding SIP to all interfaces (or let it undefined). */ + String host_ifaddr=null; + + /** Transport protocols (the first protocol is used as default) */ + String[] transport_protocols=null; + + /** Max number of (contemporary) open connections */ + int nmax_connections=0; + + /** Outbound proxy (host_addr[:host_port]). + * Use 'NONE' for not using an outbound proxy (or let it undefined). */ + SocketAddress outbound_proxy=null; + + /** Whether logging all packets (including non-SIP keepalive tokens). */ + boolean log_all_packets=false; + + + // for backward compatibility: + + /** Outbound proxy addr (for backward compatibility). */ + private String outbound_addr=null; + /** Outbound proxy port (for backward compatibility). */ + private int outbound_port=-1; + + + // ********************* Non-readable attributes ********************* + + /** Event Loger */ + protected Log event_log=null; + + /** Message Loger */ + protected Log message_log=null; + + /** Network interface (IP address) used by SIP. */ + IpAddress host_ipaddr=null; + + /** Default transport */ + String default_transport=null; + + /** Whether using UDP as transport protocol */ + boolean transport_udp=false; + /** Whether using TCP as transport protocol */ + boolean transport_tcp=false; + /** Whether using TLS as transport protocol */ + boolean transport_tls=false; + /** Whether using SCTP as transport protocol */ + boolean transport_sctp=false; + + /** Whether adding 'rport' parameter on outgoing requests. */ + boolean rport=true; + + /** Whether forcing 'rport' parameter on incoming requests ('force-rport' mode). */ + boolean force_rport=false; + + /** List of provider listeners */ + Hashtable listeners=null; + + /** List of exception listeners */ + HashSet exception_listeners=null; + + /** UDP transport */ + UdpTransport udp=null; + + /** Tcp server */ + TcpServer tcp_server=null; + + /** Connections */ + Hashtable connections=null; + + + // *************************** Costructors *************************** + + /** Creates a void SipProvider. */ + /*protected SipProvider() + { + }*/ + + /** Creates a new SipProvider. */ + public SipProvider(String via_addr, int port) + { init(via_addr,port,null,null); + initlog(); + startTrasport(); + } + + + /** Creates a new SipProvider. + * Costructs the SipProvider, initializing the SipProviderListeners, the transport protocols, and other attributes. */ + public SipProvider(String via_addr, int port, String[] protocols, String ifaddr) + { init(via_addr,port,protocols,ifaddr); + initlog(); + startTrasport(); + } + + + /** Creates a new SipProvider. + * The SipProvider attributres are read from file. */ + public SipProvider(String file) + { if (!SipStack.isInit()) SipStack.init(file); + new Configure(this,file); + init(via_addr,host_port,transport_protocols,host_ifaddr); + initlog(); + startTrasport(); + } + + + /** Inits the SipProvider, initializing the SipProviderListeners, the transport protocols, the outbound proxy, and other attributes. */ + private void init(String viaddr, int port, String[] protocols, String ifaddr) + { if (!SipStack.isInit()) SipStack.init(); + via_addr=viaddr; + if (via_addr==null || via_addr.equalsIgnoreCase(AUTO_CONFIGURATION)) via_addr=IpAddress.getLocalHostAddress().toString(); + host_port=port; + if (host_port<=0) host_port=SipStack.default_port; + host_ipaddr=null; + if (ifaddr!=null && !ifaddr.equalsIgnoreCase(ALL_INTERFACES)) + { try { host_ipaddr=IpAddress.getByName(ifaddr); } catch (IOException e) { e.printStackTrace(); host_ipaddr=null; } + } + transport_protocols=protocols; + if (transport_protocols==null) transport_protocols=SipStack.default_transport_protocols; + default_transport=transport_protocols[0]; + for (int i=0; i0) + { String filename=SipStack.log_path+"//"+via_addr+"."+host_port; + event_log=new RotatingLog(filename+"_events.log",null,SipStack.debug_level,SipStack.max_logsize*1024,SipStack.log_rotations,SipStack.rotation_scale,SipStack.rotation_time); + message_log=new RotatingLog(filename+"_messages.log",null,SipStack.debug_level,SipStack.max_logsize*1024,SipStack.log_rotations,SipStack.rotation_scale,SipStack.rotation_time); + } + printLog("Date: "+DateFormat.formatHHMMSS(new Date()),LogLevel.HIGH); + printLog("SipStack: "+SipStack.release,LogLevel.HIGH); + printLog("new SipProvider(): "+toString(),LogLevel.HIGH); + } + + + /** Starts the transport services. */ + private void startTrasport() + { + // start udp + if (transport_udp) + { try + { if (host_ipaddr==null) udp=new UdpTransport(host_port,this); + else udp=new UdpTransport(host_port,host_ipaddr,this); + printLog("udp is up",LogLevel.MEDIUM); + } + catch (Exception e) + { printException(e,LogLevel.HIGH); + } + } + // start tcp + if (transport_tcp) + { try + { if (host_ipaddr==null) tcp_server=new TcpServer(host_port,this); + else tcp_server=new TcpServer(host_port,host_ipaddr,this); + printLog("tcp is up",LogLevel.MEDIUM); + } + catch (Exception e) + { printException(e,LogLevel.HIGH); + } + } + //printLog("transport is up",LogLevel.MEDIUM); + } + + + /** Stops the transport services. */ + private void stopTrasport() + { + // stop udp + if (udp!=null) + { printLog("udp is going down",LogLevel.LOWER); + udp.halt(); + udp=null; + } + // stop tcp + if (tcp_server!=null) + { printLog("tcp is going down",LogLevel.LOWER); + tcp_server.halt(); + tcp_server=null; + } + if (connections!=null) + { printLog("connections are going down",LogLevel.LOWER); + for (Enumeration e=connections.elements(); e.hasMoreElements(); ) + { ConnectedTransport c=(ConnectedTransport)e.nextElement(); + c.halt(); + } + connections=null; + } + } + + /** Stops the SipProviders. */ + public void halt() + { printLog("halt: SipProvider is going down",LogLevel.MEDIUM); + stopTrasport(); + listeners=new Hashtable(); + exception_listeners=new HashSet(); + } + + + /** Parses a single line (loaded from the config file) */ + public void parseLine(String line) + { String attribute; + Parser par; + int index=line.indexOf("="); + if (index>0) { attribute=line.substring(0,index).trim(); par=new Parser(line,index+1); } + else { attribute=line; par=new Parser(""); } + char[] delim={' ',','}; + + if (attribute.equals("via_addr")) { via_addr=par.getString(); return; } + if (attribute.equals("host_port")) { host_port=par.getInt(); return; } + if (attribute.equals("host_ifaddr")) { host_ifaddr=par.getString(); return; } + if (attribute.equals("transport_protocols")) { transport_protocols=par.getWordArray(delim); return; } + if (attribute.equals("nmax_connections")) { nmax_connections=par.getInt(); return; } + if (attribute.equals("outbound_proxy")) + { String soaddr=par.getString(); + if (soaddr==null || soaddr.length()==0 || soaddr.equalsIgnoreCase(Configure.NONE) || soaddr.equalsIgnoreCase("NO-OUTBOUND")) outbound_proxy=null; + else outbound_proxy=new SocketAddress(soaddr); + return; + } + if (attribute.equals("log_all_packets")) { log_all_packets=(par.getString().toLowerCase().startsWith("y")); return; } + + // old parameters + if (attribute.equals("host_addr")) System.err.println("WARNING: parameter 'host_addr' is no more supported; use 'via_addr' instead."); + if (attribute.equals("all_interfaces")) System.err.println("WARNING: parameter 'all_interfaces' is no more supported; use 'host_iaddr' for setting a specific interface or let it undefined."); + if (attribute.equals("use_outbound")) System.err.println("WARNING: parameter 'use_outbound' is no more supported; use 'outbound_proxy' for setting an outbound proxy or let it undefined."); + if (attribute.equals("outbound_addr")) + { System.err.println("WARNING: parameter 'outbound_addr' has been deprecated; use 'outbound_proxy=[:]' instead."); + outbound_addr=par.getString(); + return; + } + if (attribute.equals("outbound_port")) + { System.err.println("WARNING: parameter 'outbound_port' has been deprecated; use 'outbound_proxy=[:]' instead."); + outbound_port=par.getInt(); + return; + } + } + + + /** Converts the entire object into lines (to be saved into the config file) */ + protected String toLines() + { // currently not implemented.. + return toString(); + } + + + /** Gets a String with the list of transport protocols. */ + private String transportProtocolsToString() + { String list=transport_protocols[0]; + for (int i=1; i + * When capturing messages in promisque mode all messages are passed to the SipProviderListener + * before passing them to the specific listener (if present). + *
    Note that more that one SipProviderListener can be active in promisque mode + * at the same time;in that case the same message is passed to all PROMISQUE + * SipProviderListeners. + * @param listener is the SipProviderListener. + * @return It returns true if the SipProviderListener is added, + * false if the listener_ID is already in use. */ + public boolean addSipProviderPromisqueListener(SipProviderListener listener) + { return addSipProviderListener(PROMISQUE,listener); + } + + /** Adds a new listener to the SipProvider for caputering ANY message. + * It is the same as using method addSipProviderListener(SipProvider.ANY,listener). + * @param listener is the SipProviderListener. + * @return It returns true if the SipProviderListener is added, + * false if the listener_ID is already in use. */ + public boolean addSipProviderListener(SipProviderListener listener) + { return addSipProviderListener(ANY,listener); + } + + /** Adds a new listener to the SipProvider. + * @param id is the unique identifier for the messages which the listener + * as to be associated to. It is used as key. + * It can identify a method, a transaction, or a dialog. + * Use SipProvider.ANY to capture all messages. + * Use SipProvider.PROMISQUE if you want to capture all message in promisque mode + * (letting other listeners to capture the same received messages). + * @param listener is the SipProviderListener for this message id. + * @return It returns true if the SipProviderListener is added, + * false if the listener_ID is already in use. */ + public boolean addSipProviderListener(Identifier id, SipProviderListener listener) + { printLog("adding SipProviderListener: "+id,LogLevel.MEDIUM); + boolean ret; + Identifier key=id; + if (listeners.containsKey(key)) + { printWarning("trying to add a SipProviderListener with a id that is already in use.",LogLevel.HIGH); + ret=false; + } + else + { listeners.put(key,listener); + ret=true; + } + + if (listeners!=null) + { String list=""; + for (Enumeration e=listeners.keys(); e.hasMoreElements();) list+=e.nextElement()+", "; + printLog(listeners.size()+" listeners: "+list,LogLevel.LOW); + } + return ret; + } + + + /** Removes a SipProviderListener. + * @param id is the unique identifier used to select the listened messages. + * @return It returns true if the SipProviderListener is removed, + * false if the identifier is missed. */ + public boolean removeSipProviderListener(Identifier id) + { printLog("removing SipProviderListener: "+id,LogLevel.MEDIUM); + boolean ret; + Identifier key=id; + if (!listeners.containsKey(key)) + { printWarning("trying to remove a missed SipProviderListener.",LogLevel.HIGH); + ret=false; + } + else + { listeners.remove(key); + ret=true; + } + + if (listeners!=null) + { String list=""; + for (Enumeration e=listeners.keys(); e.hasMoreElements();) list+=e.nextElement()+", "; + printLog(listeners.size()+" listeners: "+list,LogLevel.LOW); + } + return ret; + } + + + /** Sets the SipProviderExceptionListener. + * The SipProviderExceptionListener is the listener for all exceptions + * thrown by the SipProviders. + * @param e_listener is the SipProviderExceptionListener. + * @return It returns true if the SipProviderListener has been correctly set, + * false if the SipProviderListener was already set. */ + public boolean addSipProviderExceptionListener(SipProviderExceptionListener e_listener) + { printLog("adding SipProviderExceptionListener",LogLevel.MEDIUM); + if (exception_listeners.contains(e_listener)) + { printWarning("trying to add an already present SipProviderExceptionListener.",LogLevel.HIGH); + return false; + } + else + { exception_listeners.add(e_listener); + return true; + } + } + + + /** Removes a SipProviderExceptionListener. + * @param e_listener is the SipProviderExceptionListener. + * @return It returns true if the SipProviderExceptionListener has been correctly removed, + * false if the SipProviderExceptionListener is missed. */ + public boolean removeSipProviderExceptionListener(SipProviderExceptionListener e_listener) + { printLog("removing SipProviderExceptionListener",LogLevel.MEDIUM); + if (!exception_listeners.contains(e_listener)) + { printWarning("trying to remove a missed SipProviderExceptionListener.",LogLevel.HIGH); + return false; + } + else + { exception_listeners.remove(e_listener); + return true; + } + } + + + /** Sends a Message, specifing the transport portocol, nexthop address and port. + *

    This is a low level method and + * forces the message to be routed to a specific nexthop address, port and transport, + * regardless whatever the Via, Route, or request-uri, address to. + *

    + * In case of connection-oriented transport, the connection is selected as follows: + *
    - if an existing connection is found matching the destination + * end point (socket), such connection is used, otherwise + *
    - a new connection is established + * + * @return It returns a Connection in case of connection-oriented delivery + * (e.g. TCP) or null in case of connection-less delivery (e.g. UDP) + */ + public ConnectionIdentifier sendMessage(Message msg, String proto, String dest_addr, int dest_port, int ttl) + { if (log_all_packets || msg.getLength()>MIN_MESSAGE_LENGTH) printLog("Resolving host address '"+dest_addr+"'",LogLevel.MEDIUM); + try + { IpAddress dest_ipaddr=IpAddress.getByName(dest_addr); + return sendMessage(msg,proto,dest_ipaddr,dest_port,ttl); + } + catch (Exception e) + { printException(e,LogLevel.HIGH); + return null; + } + } + + /** Sends a Message, specifing the transport portocol, nexthop address and port. */ + private ConnectionIdentifier sendMessage(Message msg, String proto, IpAddress dest_ipaddr, int dest_port, int ttl) + { ConnectionIdentifier conn_id=new ConnectionIdentifier(proto,dest_ipaddr,dest_port); + if (log_all_packets || msg.getLength()>MIN_MESSAGE_LENGTH) printLog("Sending message to "+conn_id,LogLevel.MEDIUM); + + if (transport_udp && proto.equals(PROTO_UDP)) + { // UDP + //printLog("using UDP",LogLevel.LOW); + conn_id=null; + try + { // if (ttl>0 && multicast_address) do something? + udp.sendMessage(msg,dest_ipaddr,dest_port); + } + catch (IOException e) + { printException(e,LogLevel.HIGH); + return null; + } + } + else + if (transport_tcp && proto.equals(PROTO_TCP)) + { // TCP + //printLog("using TCP",LogLevel.LOW); + if (!connections.containsKey(conn_id)) + { printLog("no active connection found matching "+conn_id,LogLevel.MEDIUM); + printLog("open "+proto+" connection to "+dest_ipaddr+":"+dest_port,LogLevel.MEDIUM); + TcpTransport conn=null; + try + { conn=new TcpTransport(dest_ipaddr,dest_port,this); + } + catch (Exception e) + { printLog("connection setup FAILED",LogLevel.HIGH); + return null; + } + printLog("connection "+conn+" opened",LogLevel.HIGH); + addConnection(conn); + } + else + { printLog("active connection found matching "+conn_id,LogLevel.MEDIUM); + } + ConnectedTransport conn=(ConnectedTransport)connections.get(conn_id); + if (conn!=null) + { printLog("sending data through conn "+conn,LogLevel.MEDIUM); + try + { conn.sendMessage(msg); + conn_id=new ConnectionIdentifier(conn); + } + catch (IOException e) + { printException(e,LogLevel.HIGH); + return null; + } + } + else + { // this point has not to be reached + printLog("ERROR: conn "+conn_id+" not found: abort.",LogLevel.MEDIUM); + return null; + } + } + else + { // otherwise + printWarning("Unsupported protocol ("+proto+"): Message discarded",LogLevel.HIGH); + return null; + } + // logs + String dest_addr=dest_ipaddr.toString(); + printMessageLog(proto,dest_addr,dest_port,msg.getLength(),msg,"sent"); + return conn_id; + } + + + /** Sends the message msg. + *

    + * The destination for the request is computed as follows: + *
    - if outbound_addr is set, outbound_addr and + * outbound_port are used, otherwise + *
    - if message has Route header with lr option parameter (i.e. RFC3261 compliant), + * the first Route address is used, otherwise + *
    - the request's Request-URI is considered. + *

    + * The destination for the response is computed based on the sent-by parameter in + * the Via header field (RFC3261 compliant) + *

    + * As transport it is used the protocol specified in the 'via' header field + *

    + * In case of connection-oriented transport: + *
    - if an already established connection is found matching the destination + * end point (socket), such connection is used, otherwise + *
    - a new connection is established + * + * @return Returns a ConnectionIdentifier in case of connection-oriented delivery + * (e.g. TCP) or null in case of connection-less delivery (e.g. UDP) + */ + public ConnectionIdentifier sendMessage(Message msg) + { printLog("Sending message:\r\n"+msg.toString(),LogLevel.LOWER); + + // select the transport protocol + ViaHeader via=msg.getViaHeader(); + String proto=via.getProtocol().toLowerCase(); + printLog("using transport "+proto,LogLevel.MEDIUM); + + // select the destination address and port + String dest_addr=null; + int dest_port=0; + int ttl=0; + + if (msg.isRequest()) + { // REQUESTS + if (outbound_proxy!=null) + { dest_addr=outbound_proxy.getAddress().toString(); + dest_port=outbound_proxy.getPort(); + } + else + { if (msg.hasRouteHeader() && msg.getRouteHeader().getNameAddress().getAddress().hasLr()) + { + SipURL url=msg.getRouteHeader().getNameAddress().getAddress(); + dest_addr=url.getHost(); + dest_port=url.getPort(); + } + else + { SipURL url=msg.getRequestLine().getAddress(); + dest_addr=url.getHost(); + dest_port=url.getPort(); + if (url.hasMaddr()) + { dest_addr=url.getMaddr(); + if (url.hasTtl()) ttl=url.getTtl(); + // update the via header by adding maddr and ttl params + via.setMaddr(dest_addr); + if (ttl>0) via.setTtl(ttl); + msg.removeViaHeader(); + msg.addViaHeader(via); + } + } + } + } + else + { // RESPONSES + SipURL url=via.getSipURL(); + if (via.hasReceived()) dest_addr=via.getReceived(); else dest_addr=url.getHost(); + if (via.hasRport()) dest_port=via.getRport(); + if (dest_port<=0) dest_port=url.getPort(); + } + + if (dest_port<=0) dest_port=SipStack.default_port; + + return sendMessage(msg,proto,dest_addr,dest_port,ttl); + } + + + /** Sends the message msg using the specified connection. */ + public ConnectionIdentifier sendMessage(Message msg, ConnectionIdentifier conn_id) + { if (log_all_packets || msg.getLength()>MIN_MESSAGE_LENGTH) printLog("Sending message through conn "+conn_id,LogLevel.HIGH); + printLog("message:\r\n"+msg.toString(),LogLevel.LOWER); + + if (conn_id!=null && connections.containsKey(conn_id)) + { // connection exists + printLog("active connection found matching "+conn_id,LogLevel.MEDIUM); + ConnectedTransport conn=(ConnectedTransport)connections.get(conn_id); + try + { conn.sendMessage(msg); + // logs + //String proto=conn.getProtocol(); + String proto=conn.getProtocol(); + String dest_addr=conn.getRemoteAddress().toString(); + int dest_port=conn.getRemotePort(); + printMessageLog(proto,dest_addr,dest_port,msg.getLength(),msg,"sent"); + return conn_id; + } + catch (Exception e) + { printException(e,LogLevel.HIGH); + } + } + //else + printLog("no active connection found matching "+conn_id,LogLevel.MEDIUM); + return sendMessage(msg); + } + + + /** Processes the message received. + * It is called each time a new message is received by the transport layer, and + * it performs the actual message processing. */ + protected void processReceivedMessage(Message msg) + { try + { // logs + printMessageLog(msg.getTransportProtocol(),msg.getRemoteAddress(),msg.getRemotePort(),msg.getLength(),msg,"received"); + + // discard too short messages + if (msg.getLength()<=2) + { if (log_all_packets) printLog("message too short: discarded\r\n",LogLevel.LOW); + return; + } + // discard non-SIP messages + String first_line=msg.getFirstLine(); + if (first_line==null || first_line.toUpperCase().indexOf("SIP/2.0")<0) + { if (log_all_packets) printLog("NOT a SIP message: discarded\r\n",LogLevel.LOW); + return; + } + printLog("received new SIP message",LogLevel.HIGH); + printLog("message:\r\n"+msg.toString(),LogLevel.LOWER); + + // if a request, handle "received" and "rport" parameters + if (msg.isRequest()) + { ViaHeader vh=msg.getViaHeader(); + boolean via_changed=false; + String src_addr=msg.getRemoteAddress(); + int src_port=msg.getRemotePort(); + String via_addr=vh.getHost(); + int via_port=vh.getPort(); + if (via_port<=0) via_port=SipStack.default_port; + + if (!via_addr.equals(src_addr)) + { vh.setReceived(src_addr); + via_changed=true; + } + + if (vh.hasRport()) + { vh.setRport(src_port); + via_changed=true; + } + else + { if (force_rport && via_port!=src_port) + { vh.setRport(src_port); + via_changed=true; + } + } + + if (via_changed) + { msg.removeViaHeader(); + msg.addViaHeader(vh); + } + } + + // is there any listeners? + if (listeners==null || listeners.size()==0) + { printLog("no listener found: meesage discarded.",LogLevel.HIGH); + return; + } + + // try to look for a UA in promisque mode + if (listeners.containsKey(PROMISQUE)) + { printLog("message passed to uas: "+PROMISQUE,LogLevel.MEDIUM); + ((SipProviderListener)listeners.get(PROMISQUE)).onReceivedMessage(this,msg); + } + + // after the callback check if the message is still valid + if (!msg.isRequest() && !msg.isResponse()) + { printLog("No valid SIP message: message discarded.",LogLevel.HIGH); + return; + } + + // this was the promisque listener; now keep on looking for a tighter listener.. + + // try to look for a transaction + Identifier key=msg.getTransactionId(); + printLog("DEBUG: transaction-id: "+key,LogLevel.MEDIUM); + if (listeners.containsKey(key)) + { printLog("message passed to transaction: "+key,LogLevel.MEDIUM); + ((SipProviderListener)listeners.get(key)).onReceivedMessage(this,msg); + return; + } + // try to look for a dialog + key=msg.getDialogId(); + printLog("DEBUG: dialog-id: "+key,LogLevel.MEDIUM); + if (listeners.containsKey(key)) + { printLog("message passed to dialog: "+key,LogLevel.MEDIUM); + ((SipProviderListener)listeners.get(key)).onReceivedMessage(this,msg); + return; + } + // try to look for a UAS + key=msg.getMethodId(); + if (listeners.containsKey(key)) + { printLog("message passed to uas: "+key,LogLevel.MEDIUM); + ((SipProviderListener)listeners.get(key)).onReceivedMessage(this,msg); + return; + } + // try to look for a default UA + if (listeners.containsKey(ANY)) + { printLog("message passed to uas: "+ANY,LogLevel.MEDIUM); + ((SipProviderListener)listeners.get(ANY)).onReceivedMessage(this,msg); + return; + } + + // if we are here, no listener_ID matched.. + printLog("No SipListener found matching that message: message DISCARDED",LogLevel.HIGH); + //printLog("Pending SipProviderListeners= "+getListeners().size(),3); + printLog("Pending SipProviderListeners= "+listeners.size(),LogLevel.MEDIUM); + } + catch (Exception e) + { printWarning("Error handling a new incoming message",LogLevel.HIGH); + printException(e,LogLevel.MEDIUM); + if (exception_listeners==null || exception_listeners.size()==0) + { System.err.println("Error handling a new incoming message"); + e.printStackTrace(); + } + else + { for (Iterator i=exception_listeners.iterator(); i.hasNext(); ) + try + { ((SipProviderExceptionListener)i.next()).onMessageException(msg,e); + } + catch (Exception e2) + { printWarning("Error handling handling the Exception",LogLevel.HIGH); + printException(e2,LogLevel.MEDIUM); + } + } + } + } + + + /** Adds a new Connection */ + private void addConnection(ConnectedTransport conn) + { ConnectionIdentifier conn_id=new ConnectionIdentifier(conn); + if (connections.containsKey(conn_id)) + { // remove the previous connection + printLog("trying to add the already established connection "+conn_id,LogLevel.HIGH); + printLog("connection "+conn_id+" will be replaced",LogLevel.HIGH); + ConnectedTransport old_conn=(ConnectedTransport)connections.get(conn_id); + old_conn.halt(); + connections.remove(conn_id); + } + else + if (connections.size()>=nmax_connections) + { // remove the older unused connection + printLog("reached the maximum number of connection: removing the older unused connection",LogLevel.HIGH); + long older_time=System.currentTimeMillis(); + ConnectionIdentifier older_id=null; + for (Enumeration e=connections.elements(); e.hasMoreElements(); ) + { ConnectedTransport co=(ConnectedTransport)e.nextElement(); + if (co.getLastTimeMillis()req
    . + * This tag can be generated for responses in a stateless + * manner - in a manner that will generate the same tag for the + * same request consistently. + */ + public static String pickTag(Message req) + { //return String.valueOf(tag_generator++); + //return (new MD5(request.toString())).asHex().substring(0,8); + return (new SimpleDigest(8,req.toString())).asHex(); + } + + + /** Picks a new call-id. + * The call-id is a globally unique + * identifier over space and time. It is implemented in the + * form "localid@host". Call-id must be considered case-sensitive and is + * compared byte-by-byte. */ + public String pickCallId() + { //String str=Long.toString(Math.abs(Random.nextLong()),16); + //if (str.length()<12) str+="000000000000"; + //return str.substring(0,12)+"@"+getViaAddress(); + return Random.nextNumString(12)+"@"+getViaAddress(); + } + + + /** picks an initial CSeq */ + public static int pickInitialCSeq() + { return 1; + } + + + /** (Deprecated) Constructs a NameAddress based on an input string. + * The input string can be a: + *
    - user name, + *
    - user@address url, + *
    - "Name" <sip:user@address> address, + *

    + * In the former case, + * a SIP URL is costructed using the outbound proxy as host address if present, + * otherwise the local via address is used. */ + public NameAddress completeNameAddress(String str) + { if (str.indexOf("=0) return new NameAddress(str); + else + { SipURL url=completeSipURL(str); + return new NameAddress(url); + } + } + /** Constructs a SipURL based on an input string. */ + private SipURL completeSipURL(String str) + { // in case it is passed only the 'user' field, add '@'[':'] + if (!str.startsWith("sip:") && str.indexOf("@")<0 && str.indexOf(".")<0 && str.indexOf(":")<0) + { // may be it is just the user name.. + String url="sip:"+str+"@"; + if (outbound_proxy!=null) + { url+=outbound_proxy.getAddress().toString(); + int port=outbound_proxy.getPort(); + if (port>0 && port!=SipStack.default_port) url+=":"+port; + } + else + { url+=via_addr; + if (host_port>0 && host_port!=SipStack.default_port) url+=":"+host_port; + } + return new SipURL(url); + } + else return new SipURL(str); + } + + /** Constructs a SipURL for the given username on the local SIP UA. + * If username is null, only host address and port are used. */ + /*public SipURL getSipURL(String user_name) + { return new SipURL(user_name,via_addr,(host_port!=SipStack.default_port)?host_port:-1); + }*/ + + + //******************************* Logs ******************************* + + /** Gets a String value for this object */ + public String toString() + { if (host_ipaddr==null) return host_port+"/"+transportProtocolsToString(); + else return host_ipaddr.toString()+":"+host_port+"/"+transportProtocolsToString(); + } + + /** Adds a new string to the default Log */ + private final void printLog(String str, int level) + { if (event_log!=null) + { String provider_id=(host_ipaddr==null)? Integer.toString(host_port) : host_ipaddr.toString()+":"+host_port; + event_log.println("SipProvider-"+provider_id+": "+str,level+SipStack.LOG_LEVEL_TRANSPORT); + } + } + + /** Adds a WARNING to the default Log */ + private final void printWarning(String str, int level) + { printLog("WARNING: "+str,level); + } + + /** Adds the Exception message to the default Log */ + private final void printException(Exception e, int level) + { if (event_log!=null) event_log.printException(e,level+SipStack.LOG_LEVEL_TRANSPORT); + } + + /** Adds the SIP message to the messageslog */ + private final void printMessageLog(String proto, String addr, int port, int len, Message msg, String str) + { if (log_all_packets || len>=MIN_MESSAGE_LENGTH) + { if (message_log!=null) + { message_log.printPacketTimestamp(proto,addr,port,len,str+"\r\n"+msg.toString()+"-----End-of-message-----\r\n",1); + } + if (event_log!=null) + { String first_line=msg.getFirstLine(); + if (first_line!=null) first_line=first_line.trim(); else first_line="NOT a SIP message"; + event_log.print("\r\n"); + event_log.printPacketTimestamp(proto,addr,port,len,first_line+", "+str,1); + event_log.print("\r\n"); + } + } + } + +} diff --git a/src/org/zoolu/sip/provider/SipProviderExceptionListener.java b/src/org/zoolu/sip/provider/SipProviderExceptionListener.java new file mode 100644 index 0000000..8f78860 --- /dev/null +++ b/src/org/zoolu/sip/provider/SipProviderExceptionListener.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.provider; + + +import org.zoolu.sip.message.Message; + + +/** A SipProviderExceptionListener listens for SipProvider onMessageException(Message,Exception) events. + */ +public interface SipProviderExceptionListener +{ + public void onMessageException(Message msg, Exception e); +} diff --git a/src/org/zoolu/sip/provider/SipProviderListener.java b/src/org/zoolu/sip/provider/SipProviderListener.java new file mode 100644 index 0000000..2031afd --- /dev/null +++ b/src/org/zoolu/sip/provider/SipProviderListener.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.provider; + + +import org.zoolu.sip.message.Message; + + +/** A SipProviderListener listens for SipProvider onReceivedMessage(SipProvider,Message) events. + */ +public interface SipProviderListener +{ + /** When a new Message is received by the SipProvider. */ + public void onReceivedMessage(SipProvider sip_provider, Message message); +} diff --git a/src/org/zoolu/sip/provider/SipStack.java b/src/org/zoolu/sip/provider/SipStack.java new file mode 100644 index 0000000..ce4d6ba --- /dev/null +++ b/src/org/zoolu/sip/provider/SipStack.java @@ -0,0 +1,258 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.provider; + + +import org.zoolu.tools.Configure; +import org.zoolu.tools.Parser; +import org.zoolu.tools.Log; +import org.zoolu.tools.RotatingLog; +import org.zoolu.tools.Timer; + +import java.io.*; +import java.util.Vector; + + +/** SipStack includes all static attributes used by the sip stack. + *

    + * Static attributes includes the logging configuration, + * default SIP port, deafult supported transport protocols, timeouts, etc. + */ +public class SipStack extends Configure +{ + // ********************** private attributes ********************** + + /** Whether SipStack configuration has been already loaded */ + private static boolean is_init=false; + + /** The default SipProvider */ + //private static SipProvider provider=null; + + + // *********************** software release *********************** + + /** Release */ + public static final String release="mjsip stack 1.6"; + /** Authors */ + public static final String authors="Luca Veltri - University of Parma (Italy)"; + + + // ********************** static attributes *********************** + + /** String value "no-ua-info" used for setting no 'User-Agent' header filed. */ + //public static final String NO_UA_INFO="NO-UA-INFO"; + + /** String value "no-server-info" used for setting no 'Server' header filed. */ + //public static final String NO_SERVER_INFO="NO-SERVER-INFO"; + + + // ************* default sip provider configurations ************** + + /** Default SIP port. + * Note that this is not the port used by the running stack, but simply the standard default SIP port. + *
    Normally it sould be set to 5060 as defined by RFC 3261. Using a different value may cause + * some problems when interacting with other unaware SIP UAs. */ + public static int default_port=5060; + /** Default supported transport protocols. */ + public static String[] default_transport_protocols={ SipProvider.PROTO_UDP, SipProvider.PROTO_TCP }; + /** Default max number of contemporary open transport connections. */ + public static int default_nmax_connections=32; + /** Whether adding 'rport' parameter on via header fields of outgoing requests. */ + public static boolean use_rport=true; + /** Whether adding (forcing) 'rport' parameter on via header fields of incoming requests. */ + public static boolean force_rport=false; + + + // ******************** general configurations ******************** + + /** default max-forwards value (RFC3261 recommends value 70) */ + public static int max_forwards=70; + /** starting retransmission timeout (milliseconds); called T1 in RFC2361; they suggest T1=500ms */ + public static long retransmission_timeout=500; + /** maximum retransmission timeout (milliseconds); called T2 in RFC2361; they suggest T2=4sec */ + public static long max_retransmission_timeout=4000; + /** transaction timeout (milliseconds); RFC2361 suggests 64*T1=32000ms */ + public static long transaction_timeout=32000; + /** clearing timeout (milliseconds); T4 in RFC2361; they suggest T4=5sec */ + public static long clearing_timeout=5000; + + /** Whether using only one thread for all timer instances. */ + public static boolean single_timer=false; + + /** Whether 1xx responses create an "early dialog" for methods that create dialog. */ + public static boolean early_dialog=false; + + /** Default 'expires' value in seconds. RFC2361 suggests 3600s as default value. */ + public static int default_expires=3600; + + /** UA info included in request messages in the 'User-Agent' header field. + * Use "NONE" if the 'User-Agent' header filed must not be added. */ + public static String ua_info=release; + /** Server info included in response messages in the 'Server' header field + * Use "NONE" if the 'Server' header filed must not be added. */ + public static String server_info=release; + + + // ************************ debug and logs ************************ + + /** Base level (offset) for logging Transport events */ + public static int LOG_LEVEL_TRANSPORT=1; + /** Base level (offset) for logging Transaction events */ + public static int LOG_LEVEL_TRANSACTION=2; + /** Base level (offset) for logging Dialog events */ + public static int LOG_LEVEL_DIALOG=2; + /** Base level (offset) for logging Call events */ + public static int LOG_LEVEL_CALL=1; + /** Base level (offset) for logging UA events */ + public static int LOG_LEVEL_UA=0; + + /** Log level. Only logs with a level less or equal to this are written. */ + public static int debug_level=1; + /** Path for the log folder where log files are written. + * By default, it is used the "./log" folder. + * Use ".", to store logs in the current root folder. */ + public static String log_path="log"; + /** The size limit of the log file [kB] */ + public static int max_logsize=2048; // 2MB + /** The number of rotations of log files. Use '0' for NO rotation, '1' for rotating a single file */ + public static int log_rotations=0; // no rotation + /** The rotation period, in MONTHs or DAYs or HOURs or MINUTEs + * examples: log_rotation_time=3 MONTHS, log_rotations=90 DAYS + * Default value: log_rotation_time=2 MONTHS */ + private static String log_rotation_time=null; + /** The rotation time scale */ + public static int rotation_scale=RotatingLog.MONTH; + /** The rotation time value */ + public static int rotation_time=2; + + + // ************************** costructor ************************** + + + /** Parses a single text line (read from the config file) */ + protected void parseLine(String line) + { String attribute; + Parser par; + int index=line.indexOf("="); + if (index>0) { attribute=line.substring(0,index).trim(); par=new Parser(line,index+1); } + else { attribute=line; par=new Parser(""); } + char[] delim={' ',','}; + + // general configurations + if (attribute.equals("default_port")) { default_port=par.getInt(); return; } + if (attribute.equals("default_transport_protocols")) { default_transport_protocols=par.getWordArray(delim); return; } + if (attribute.equals("default_nmax_connections")) { default_nmax_connections=par.getInt(); return; } + if (attribute.equals("use_rport")) { use_rport=(par.getString().toLowerCase().startsWith("y")); return; } + if (attribute.equals("force_rport")) { force_rport=(par.getString().toLowerCase().startsWith("y")); return; } + if (attribute.equals("max_forwards")) { max_forwards=par.getInt(); return; } + if (attribute.equals("retransmission_timeout")) { retransmission_timeout=par.getInt(); return; } + if (attribute.equals("max_retransmission_timeout")) { max_retransmission_timeout=par.getInt(); return; } + if (attribute.equals("transaction_timeout")) { transaction_timeout=par.getInt(); return; } + if (attribute.equals("clearing_timeout")) { clearing_timeout=par.getInt(); return; } + if (attribute.equals("single_timer")) { single_timer=(par.getString().toLowerCase().startsWith("y")); return; } + if (attribute.equals("early_dialog")) { early_dialog=(par.getString().toLowerCase().startsWith("y")); return; } + if (attribute.equals("default_expires")){ default_expires=par.getInt(); return; } + if (attribute.equals("ua_info")) { ua_info=par.getRemainingString().trim(); return; } + if (attribute.equals("server_info")) { server_info=par.getRemainingString().trim(); return; } + + // debug and logs + if (attribute.equals("debug_level")) { debug_level=par.getInt(); return; } + if (attribute.equals("log_path")) { log_path=par.getString(); return; } + if (attribute.equals("max_logsize")) { max_logsize=par.getInt(); return; } + if (attribute.equals("log_rotations")) { log_rotations=par.getInt(); return; } + if (attribute.equals("log_rotation_time")) { log_rotation_time=par.getRemainingString(); return; } + + // old parameters + if (attribute.equals("host_addr")) printLog("WARNING: parameter 'host_addr' is no more supported; use 'via_addr' instead."); + if (attribute.equals("all_interfaces")) printLog("WARNING: parameter 'all_interfaces' is no more supported; use 'host_iaddr' for setting a specific interface or let it undefined."); + if (attribute.equals("use_outbound")) printLog("WARNING: parameter 'use_outbound' is no more supported; use 'outbound_addr' for setting an outbound proxy or let it undefined."); + if (attribute.equals("log_file")) printLog("WARNING: parameter 'log_file' is no more supported."); + } + + /** Converts the entire object into lines (to be saved into the config file) */ + protected String toLines() + { // currently not implemented.. + return "SipStack/"+release; + } + + /** Costructs a non-static SipStack */ + private SipStack() + { + } + + /** Inits SipStack */ + public static void init() + { init(null); + } + + /** Inits SipStack from the specified file */ + public static void init(String file) + { + (new SipStack()).loadFile(file); + + // user-agent info + if (ua_info!=null && (ua_info.length()==0 || ua_info.equalsIgnoreCase(Configure.NONE) || ua_info.equalsIgnoreCase("NO-UA-INFO"))) ua_info=null; + + // server info + if (server_info!=null && (server_info.length()==0 || server_info.equalsIgnoreCase(Configure.NONE) || server_info.equalsIgnoreCase("NO-SERVER-INFO"))) server_info=null; + + // timers + Timer.SINGLE_THREAD=single_timer; + + // logs + if (debug_level>0) + { if (log_rotation_time!=null) + { SipParser par=new SipParser(log_rotation_time); + rotation_time=par.getInt(); + String scale=par.getString(); + if (scale==null) scale="null"; + if (scale.toUpperCase().startsWith("MONTH")) rotation_scale=RotatingLog.MONTH; + else if (scale.toUpperCase().startsWith("DAY")) rotation_scale=RotatingLog.DAY; + else if (scale.toUpperCase().startsWith("HOUR")) rotation_scale=RotatingLog.HOUR; + else if (scale.toUpperCase().startsWith("MINUTE")) rotation_scale=RotatingLog.MINUTE; + else + { rotation_time=7; + rotation_scale=RotatingLog.DAY; + printLog("Error with the log rotation time. Logs will rotate every week."); + } + } + } + + is_init=true; + //if (file!=null) printLog("SipStack loaded",1); + } + + /** Whether SipStack has been already initialized */ + public static boolean isInit() + { return is_init; + } + + + // ************************ private methods *********************** + + /** Logs a string message. */ + private static void printLog(String str) + { System.out.println("SipStack: "+str); + } +} diff --git a/src/org/zoolu/sip/provider/TcpTransport.java b/src/org/zoolu/sip/provider/TcpTransport.java new file mode 100644 index 0000000..55cd24d --- /dev/null +++ b/src/org/zoolu/sip/provider/TcpTransport.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.provider; + + +import org.zoolu.net.*; +import org.zoolu.sip.message.Message; +import java.io.IOException; + + +/** TcpTransport provides a TCP trasport service for SIP. + */ +class TcpTransport implements ConnectedTransport, TcpConnectionListener +{ + /** TCP protocol type */ + public static final String PROTO_TCP="tcp"; + + /** TCP connection */ + TcpConnection tcp_conn; + + /** TCP connection */ + ConnectionIdentifier connection_id; + + /** The last time that has been used (in milliseconds) */ + long last_time; + + /** the current received text. */ + String text; + + /** Transport listener */ + TransportListener listener; + + + /** Creates a new TcpTransport */ + public TcpTransport(IpAddress remote_ipaddr, int remote_port, TransportListener listener) throws IOException + { this.listener=listener; + TcpSocket socket=new TcpSocket(remote_ipaddr,remote_port); + tcp_conn=new TcpConnection(socket,this); + connection_id=new ConnectionIdentifier(this); + last_time=System.currentTimeMillis(); + text=""; + } + + + /** Costructs a new TcpTransport */ + public TcpTransport(TcpSocket socket, TransportListener listener) + { this.listener=listener; + tcp_conn=new TcpConnection(socket,this); + connection_id=null; + last_time=System.currentTimeMillis(); + text=""; + } + + + /** Gets protocol type */ + public String getProtocol() + { return PROTO_TCP; + } + + + /** Gets the remote IpAddress */ + public IpAddress getRemoteAddress() + { if (tcp_conn!=null) return tcp_conn.getRemoteAddress(); + else return null; + } + + + /** Gets the remote port */ + public int getRemotePort() + { if (tcp_conn!=null) return tcp_conn.getRemotePort(); + else return 0; + } + + + /** Gets the last time the Connection has been used (in millisconds) */ + public long getLastTimeMillis() + { return last_time; + } + + + /** Sends a Message through the connection. Parameters dest_addr/dest_addr + * are not used, and the message is addressed to the connection remote peer. + *

    Better use sendMessage(Message msg) method instead. */ + public void sendMessage(Message msg, IpAddress dest_ipaddr, int dest_port) throws IOException + { sendMessage(msg); + } + + + /** Sends a Message */ + public void sendMessage(Message msg) throws IOException + { if (tcp_conn!=null) + { last_time=System.currentTimeMillis(); + byte[] data=msg.toString().getBytes(); + tcp_conn.send(data); + } + } + + + /** Stops running */ + public void halt() + { if (tcp_conn!=null) tcp_conn.halt(); + } + + + /** Gets a String representation of the Object */ + public String toString() + { if (tcp_conn!=null) return tcp_conn.toString(); + else return null; + } + + + //************************* Callback methods ************************* + + /** When new data is received through the TcpConnection. */ + public void onReceivedData(TcpConnection tcp_conn, byte[] data, int len) + { last_time=System.currentTimeMillis(); + + text+=new String(data,0,len); + SipParser par=new SipParser(text); + Message msg=par.getSipMessage(); + while (msg!=null) + { //System.out.println("DEBUG: message len: "+msg.getLength()); + msg.setRemoteAddress(tcp_conn.getRemoteAddress().toString()); + msg.setRemotePort(tcp_conn.getRemotePort()); + msg.setTransport(PROTO_TCP); + msg.setConnectionId(connection_id); + if (listener!=null) listener.onReceivedMessage(this,msg); + + text=par.getRemainingString(); + //System.out.println("DEBUG: text left: "+text.length()); + par=new SipParser(text); + msg=par.getSipMessage(); + } + } + + + /** When TcpConnection terminates. */ + public void onConnectionTerminated(TcpConnection tcp_conn, Exception error) + { if (listener!=null) listener.onTransportTerminated(this,error); + TcpSocket socket=tcp_conn.getSocket(); + if (socket!=null) try { socket.close(); } catch (Exception e) {} + this.tcp_conn=null; + this.listener=null; + } + +} diff --git a/src/org/zoolu/sip/provider/TransactionIdentifier.java b/src/org/zoolu/sip/provider/TransactionIdentifier.java new file mode 100644 index 0000000..917f455 --- /dev/null +++ b/src/org/zoolu/sip/provider/TransactionIdentifier.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.provider; + + +import org.zoolu.sip.transaction.Transaction; +import org.zoolu.sip.message.Message; +import org.zoolu.sip.message.SipMethods; +import org.zoolu.sip.header.ViaHeader; +import org.zoolu.sip.header.CSeqHeader; + + +/** TransactionIdentifier is used to address specific transaction to the SipProvider. + */ +public class TransactionIdentifier extends Identifier +{ + /** Costructs a new TransactionIdentifier. */ + public TransactionIdentifier(TransactionIdentifier i) + { super(i); + } + + /** Costructs a new TransactionIdentifier based only on method name. */ + public TransactionIdentifier(String method) + { id=method; + } + + /** Costructs a new TransactionIdentifier based on call-id, seqn, method, sent-by, and branch. */ + public TransactionIdentifier(String call_id, long seqn, String method, String sent_by, String branch) + { if (branch==null) branch=""; + if (method.equals(SipMethods.ACK)) method=SipMethods.INVITE; + id=call_id+"-"+seqn+"-"+method+"-"+sent_by+"-"+branch; + } +} diff --git a/src/org/zoolu/sip/provider/Transport.java b/src/org/zoolu/sip/provider/Transport.java new file mode 100644 index 0000000..8ef3a05 --- /dev/null +++ b/src/org/zoolu/sip/provider/Transport.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.provider; + + +import org.zoolu.sip.message.Message; +import org.zoolu.net.IpAddress; +import java.io.IOException; + + +/** Transport is a generic transport service for SIP. + */ +interface Transport +{ + /** Gets protocol type */ + public String getProtocol(); + + /** Stops running */ + public void halt(); + + /** Sends a Message to a destination address and port */ + public void sendMessage(Message msg, IpAddress dest_ipaddr, int dest_port) throws IOException; + + /** Gets a String representation of the Object */ + public String toString(); +} diff --git a/src/org/zoolu/sip/provider/TransportListener.java b/src/org/zoolu/sip/provider/TransportListener.java new file mode 100644 index 0000000..9513151 --- /dev/null +++ b/src/org/zoolu/sip/provider/TransportListener.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.provider; + + +import org.zoolu.sip.message.Message; + + +/** Listener for Transport events. + */ +interface TransportListener +{ + /** When a new SIP message is received. */ + public void onReceivedMessage(Transport transport, Message msg); + + /** When Transport terminates. */ + public void onTransportTerminated(Transport transport, Exception error); +} diff --git a/src/org/zoolu/sip/provider/UdpTransport.java b/src/org/zoolu/sip/provider/UdpTransport.java new file mode 100644 index 0000000..5cf09bf --- /dev/null +++ b/src/org/zoolu/sip/provider/UdpTransport.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.provider; + + +import org.zoolu.net.*; +import org.zoolu.sip.message.Message; +import java.io.IOException; + + +/** UdpTransport provides an UDP transport service for SIP. + */ +class UdpTransport implements Transport, UdpProviderListener +{ + /** UDP protocol type */ + public static final String PROTO_UDP="udp"; + + + /** UDP provider */ + UdpProvider udp_provider; + + + /** The protocol type */ + String proto; + + /** Transport listener */ + TransportListener listener; + + + /** Creates a new UdpTransport */ + public UdpTransport(int port, TransportListener listener) throws IOException + { this.listener=listener; + UdpSocket socket=new UdpSocket(port); + udp_provider=new UdpProvider(socket,this); + } + + /** Creates a new UdpTransport */ + public UdpTransport(int port, IpAddress ipaddr, TransportListener listener) throws IOException + { this.listener=listener; + UdpSocket socket=new UdpSocket(port,ipaddr); + udp_provider=new UdpProvider(socket,this); + } + + /** Creates a new UdpTransport */ + public UdpTransport(UdpSocket socket, TransportListener listener) + { this.listener=listener; + udp_provider=new UdpProvider(socket,this); + } + + + /** Gets protocol type */ + public String getProtocol() + { return PROTO_UDP; + } + + + /** Sends a Message to a destination address and port */ + public void sendMessage(Message msg, IpAddress dest_ipaddr, int dest_port) throws IOException + { if (udp_provider!=null) + { byte[] data=msg.toString().getBytes(); + UdpPacket packet=new UdpPacket(data,data.length); + packet.setIpAddress(dest_ipaddr); + packet.setPort(dest_port); + udp_provider.send(packet); + } + } + + + /** Stops running */ + public void halt() + { if (udp_provider!=null) udp_provider.halt(); + } + + + /** Gets a String representation of the Object */ + public String toString() + { if (udp_provider!=null) return udp_provider.toString(); + else return null; + } + + + //************************* Callback methods ************************* + + /** When a new UDP datagram is received. */ + public void onReceivedPacket(UdpProvider udp, UdpPacket packet) + { Message msg=new Message(packet); + msg.setRemoteAddress(packet.getIpAddress().toString()); + msg.setRemotePort(packet.getPort()); + msg.setTransport(PROTO_UDP); + if (listener!=null) listener.onReceivedMessage(this,msg); + } + + + /** When DatagramService stops receiving UDP datagrams. */ + public void onServiceTerminated(UdpProvider udp, Exception error) + { if (listener!=null) listener.onTransportTerminated(this,error); + UdpSocket socket=udp.getUdpSocket(); + if (socket!=null) try { socket.close(); } catch (Exception e) {} + this.udp_provider=null; + this.listener=null; + } + +} diff --git a/src/org/zoolu/sip/transaction/AckTransactionClient.java b/src/org/zoolu/sip/transaction/AckTransactionClient.java new file mode 100644 index 0000000..582f192 --- /dev/null +++ b/src/org/zoolu/sip/transaction/AckTransactionClient.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.transaction; + + +import org.zoolu.sip.address.SipURL; +import org.zoolu.sip.provider.*; +import org.zoolu.sip.message.*; +import org.zoolu.tools.Timer; +import org.zoolu.tools.TimerListener; +import org.zoolu.tools.LogLevel; + + + +/** ACK client transaction should follow an INVITE client transaction within an INVITE Dialog in a SIP UAC. + * The AckTransactionClient simply sends an ACK request message and terminates. + */ +public class AckTransactionClient extends Transaction +{ + + /** the TransactionClientListener that captures the events fired by the AckTransactionClient */ + TransactionClientListener transaction_listener; + + + /** Creates a new AckTransactionClient. */ + public AckTransactionClient(SipProvider sip_provider, Message ack, TransactionClientListener listener) + { super(sip_provider); + request=new Message(ack); + transaction_listener=listener; + transaction_id=request.getTransactionId(); + printLog("id: "+String.valueOf(transaction_id),LogLevel.HIGH); + printLog("created",LogLevel.HIGH); + } + + /** Starts the AckTransactionClient and sends the ACK request. */ + public void request() + { printLog("start",LogLevel.LOW); + sip_provider.sendMessage(request); + changeStatus(STATE_TERMINATED); + //if (transaction_listener!=null) transaction_listener.onAckCltTerminated(this); + // (CHANGE-040421) free the link to transaction_listener + transaction_listener=null; + } + + /** Method used to drop an active transaction. */ + public void terminate() + { changeStatus(STATE_TERMINATED); + // (CHANGE-040421) free the link to transaction_listener + transaction_listener=null; + } + + + //**************************** Logs ****************************/ + + /** Adds a new string to the default Log */ + protected void printLog(String str, int level) + { if (log!=null) log.println("AckTransactionClient#"+transaction_sqn+": "+str,level+SipStack.LOG_LEVEL_TRANSACTION); + } + +} diff --git a/src/org/zoolu/sip/transaction/AckTransactionServer.java b/src/org/zoolu/sip/transaction/AckTransactionServer.java new file mode 100644 index 0000000..3af91be --- /dev/null +++ b/src/org/zoolu/sip/transaction/AckTransactionServer.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.transaction; + + +import org.zoolu.sip.address.SipURL; +import org.zoolu.sip.provider.*; +import org.zoolu.sip.message.*; +import org.zoolu.tools.Timer; +import org.zoolu.tools.TimerListener; +import org.zoolu.tools.LogLevel; + + +/** ACK server transaction should follow an INVITE server transaction within an INVITE Dialog in a SIP UAC. + * The AckTransactionServer sends the final response message and retransmits it + * several times until the method terminate() is called or the trasaction timeout fires. + */ +public class AckTransactionServer extends Transaction +{ + /** the TransactionServerListener that captures the events fired by the AckTransactionServer */ + AckTransactionServerListener transaction_listener; + + /** last response message */ + Message response; + + /** retransmission timeout */ + Timer retransmission_to; + /** transaction timeout */ + Timer transaction_to; + + + /** Initializes timeouts and state */ + /** Creates a new AckTransactionServer. + * The AckTransactionServer starts sending a the resp message. + * It retransmits the resp several times if no ACK request is received. */ + public AckTransactionServer(SipProvider sip_provider, Message resp, AckTransactionServerListener listener) + { super(sip_provider); + response=resp; + init(listener,new TransactionIdentifier(SipMethods.ACK),null); + } + + /** Creates a new AckTransactionServer. + * The AckTransactionServer starts sending the response message resp + * through the connection conn_id. */ + public AckTransactionServer(SipProvider sip_provider, ConnectionIdentifier connection_id, Message resp, AckTransactionServerListener listener) + { super(sip_provider); + response=resp; + init(listener,new TransactionIdentifier(SipMethods.ACK),connection_id); + } + + /** Initializes timeouts and listener. */ + void init(AckTransactionServerListener listener, TransactionIdentifier transaction_id, ConnectionIdentifier connaction_id) + { this.transaction_listener=listener; + this.transaction_id=transaction_id; + this.connection_id=connection_id; + transaction_to=new Timer(SipStack.transaction_timeout,"Transaction",this); + retransmission_to=new Timer(SipStack.retransmission_timeout,"Retransmission",this); + // (CHANGE-040905) now timeouts started in listen() + //transaction_to.start(); + //if (connection_id==null) retransmission_to.start(); + printLog("id: "+String.valueOf(transaction_id),LogLevel.HIGH); + printLog("created",LogLevel.HIGH); + } + + /** Starts the AckTransactionServer. */ + public void respond() + { printLog("start",LogLevel.LOW); + changeStatus(STATE_PROCEEDING); + //transaction_id=null; // it is not required since no SipProviderListener is implemented + // (CHANGE-040905) now timeouts started in listen() + transaction_to.start(); + if (connection_id==null) retransmission_to.start(); + + sip_provider.sendMessage(response,connection_id); + } + + + /** Method derived from interface TimerListener. + * It's fired from an active Timer. + */ + public void onTimeout(Timer to) + { try + { if (to.equals(retransmission_to) && statusIs(STATE_PROCEEDING)) + { printLog("Retransmission timeout expired",LogLevel.HIGH); + long timeout=2*retransmission_to.getTime(); + if (timeout>SipStack.max_retransmission_timeout) timeout=SipStack.max_retransmission_timeout; + retransmission_to=new Timer(timeout,retransmission_to.getLabel(),this); + retransmission_to.start(); + sip_provider.sendMessage(response,connection_id); + } + if (to.equals(transaction_to) && statusIs(STATE_PROCEEDING)) + { printLog("Transaction timeout expired",LogLevel.HIGH); + changeStatus(STATE_TERMINATED); + if (transaction_listener!=null) transaction_listener.onTransAckTimeout(this); + // (CHANGE-040421) now it can free links to transaction_listener and timers + transaction_listener=null; + //retransmission_to=null; + //transaction_to=null; + } + } + catch (Exception e) + { printException(e,LogLevel.HIGH); + } + } + + /** Method used to drop an active transaction. */ + public void terminate() + { retransmission_to.halt(); + transaction_to.halt(); + changeStatus(STATE_TERMINATED); + //if (transaction_listener!=null) transaction_listener.onAckSrvTerminated(this); + // (CHANGE-040421) now it can free links to transaction_listener and timers + transaction_listener=null; + //retransmission_to=null; + //transaction_to=null; + } + + + //**************************** Logs ****************************/ + + /** Adds a new string to the default Log */ + protected void printLog(String str, int level) + { if (log!=null) log.println("AckTransactionServer#"+transaction_sqn+": "+str,level+SipStack.LOG_LEVEL_TRANSACTION); + } + +} diff --git a/src/org/zoolu/sip/transaction/AckTransactionServerListener.java b/src/org/zoolu/sip/transaction/AckTransactionServerListener.java new file mode 100644 index 0000000..c9816e2 --- /dev/null +++ b/src/org/zoolu/sip/transaction/AckTransactionServerListener.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.transaction; + + +import org.zoolu.sip.message.Message; + + +/** A AckTransactionServerListener listens for AckTransactionServer onTransAckTimeout(AckTransactionServer) events. + */ +public interface AckTransactionServerListener +{ + /** When the AckTransactionServer goes into the "Terminated" state, caused by transaction timeout */ + public void onTransAckTimeout(AckTransactionServer transaction); +} diff --git a/src/org/zoolu/sip/transaction/InviteTransactionClient.java b/src/org/zoolu/sip/transaction/InviteTransactionClient.java new file mode 100644 index 0000000..9f7afb0 --- /dev/null +++ b/src/org/zoolu/sip/transaction/InviteTransactionClient.java @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.transaction; + + +import org.zoolu.sip.address.SipURL; +import org.zoolu.sip.provider.*; +import org.zoolu.sip.message.*; +import org.zoolu.tools.Timer; +import org.zoolu.tools.TimerListener; +import org.zoolu.tools.LogLevel; + + +/** INVITE client transaction as defined in RFC 3261 (Section 17.2.1). + *
    An InviteTransactionClient is responsable to create a new SIP invite + * transaction, starting with a invite message sent through the SipProvider + * and ending with a final response. + *
    The changes of the internal status and the received messages are fired + * to the TransactionListener passed to the InviteTransactionClient object. + */ +public class InviteTransactionClient extends TransactionClient +{ + /** the TransactionClientListener that captures the events fired by the InviteTransactionClient */ + TransactionClientListener transaction_listener; + + /** ack message */ + Message ack; + + /** retransmission timeout ("Timer A" in RFC 3261) */ + //Timer retransmission_to; + /** transaction timeout ("Timer B" in RFC 3261) */ + //Timer transaction_to; + /** end timeout for invite transactions ("Timer D" in RFC 3261)*/ + Timer end_to; + + + /** Creates a new ClientTransaction */ + public InviteTransactionClient(SipProvider sip_provider, Message req, TransactionClientListener listener) + { super(sip_provider); + request=new Message(req); + init(listener,request.getTransactionId()); + } + + /** Initializes timeouts and listener. */ + void init(TransactionClientListener listener, TransactionIdentifier transaction_id) + { this.transaction_listener=listener; + this.transaction_id=transaction_id; + this.ack=null; + retransmission_to=new Timer(SipStack.retransmission_timeout,"Retransmission",this); + transaction_to=new Timer(SipStack.transaction_timeout,"Transaction",this); + end_to=new Timer(SipStack.transaction_timeout,"End",this); + printLog("id: "+String.valueOf(transaction_id),LogLevel.HIGH); + printLog("created",LogLevel.HIGH); + } + + /** Starts the InviteTransactionClient and sends the invite request. */ + public void request() + { printLog("start",LogLevel.LOW); + changeStatus(STATE_TRYING); + retransmission_to.start(); + transaction_to.start(); + + sip_provider.addSipProviderListener(transaction_id,this); + connection_id=sip_provider.sendMessage(request); + } + + /** Method derived from interface SipListener. + * It's fired from the SipProvider when a new message is catch for to the present ServerTransaction. + */ + public void onReceivedMessage(SipProvider provider, Message msg) + { if (msg.isResponse()) + { int code=msg.getStatusLine().getCode(); + if (code>=100 && code<200 && (statusIs(STATE_TRYING) || statusIs(STATE_PROCEEDING))) + { if (statusIs(STATE_TRYING)) + { retransmission_to.halt(); + transaction_to.halt(); + changeStatus(STATE_PROCEEDING); + } + if (transaction_listener!=null) transaction_listener.onTransProvisionalResponse(this,msg); + return; + } + if (code>=300 && code<700 && (statusIs(STATE_TRYING) || statusIs(STATE_PROCEEDING) || statusIs(STATE_COMPLETED))) + { if (statusIs(STATE_TRYING) || statusIs(STATE_PROCEEDING)) + { retransmission_to.halt(); + transaction_to.halt(); + ack=MessageFactory.createNon2xxAckRequest(sip_provider,request,msg); + changeStatus(STATE_COMPLETED); + if (transaction_listener!=null) transaction_listener.onTransFailureResponse(this,msg); + transaction_listener=null; + connection_id=sip_provider.sendMessage(ack); + if (connection_id==null) end_to.start(); + else + { printLog("end_to=0 for reliable transport",LogLevel.LOW); + onTimeout(end_to); + } + } + else + { // retransmit ACK only in case of unreliable transport + if (connection_id==null) sip_provider.sendMessage(ack); + } + return; + } + if (code>=200 && code<300 && (statusIs(STATE_TRYING) || statusIs(STATE_PROCEEDING))) + { retransmission_to.halt(); + transaction_to.halt(); + end_to.halt(); + changeStatus(STATE_TERMINATED); + sip_provider.removeSipProviderListener(transaction_id); + if (transaction_listener!=null) transaction_listener.onTransSuccessResponse(this,msg); + transaction_listener=null; + return; + } + } + } + + /** Method derived from interface TimerListener. + * It's fired from an active Timer. */ + public void onTimeout(Timer to) + { try + { if (to.equals(retransmission_to) && statusIs(STATE_TRYING)) + { printLog("Retransmission timeout expired",LogLevel.HIGH); + // retransmission only in case of unreliable transport + if (connection_id==null) + { sip_provider.sendMessage(request); + long timeout=2*retransmission_to.getTime(); + retransmission_to=new Timer(timeout,retransmission_to.getLabel(),this); + retransmission_to.start(); + } + else printLog("No retransmissions for reliable transport ("+connection_id+")",LogLevel.LOW); + } + if (to.equals(transaction_to)) + { printLog("Transaction timeout expired",LogLevel.HIGH); + retransmission_to.halt(); + end_to.halt(); + sip_provider.removeSipProviderListener(transaction_id); + changeStatus(STATE_TERMINATED); + if (transaction_listener!=null) transaction_listener.onTransTimeout(this); + transaction_listener=null; + } + if (to.equals(end_to)) + { printLog("End timeout expired",LogLevel.HIGH); + retransmission_to.halt(); + transaction_to.halt(); + sip_provider.removeSipProviderListener(transaction_id); + changeStatus(STATE_TERMINATED); + transaction_listener=null; // already null.. + } + } + catch (Exception e) + { printException(e,LogLevel.HIGH); + } + } + + /** Terminates the transaction. */ + public void terminate() + { if (!statusIs(STATE_TERMINATED)) + { retransmission_to.halt(); + transaction_to.halt(); + end_to.halt(); + sip_provider.removeSipProviderListener(transaction_id); + changeStatus(STATE_TERMINATED); + transaction_listener=null; + } + } + +} diff --git a/src/org/zoolu/sip/transaction/InviteTransactionServer.java b/src/org/zoolu/sip/transaction/InviteTransactionServer.java new file mode 100644 index 0000000..505fe2f --- /dev/null +++ b/src/org/zoolu/sip/transaction/InviteTransactionServer.java @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.transaction; + + +import org.zoolu.sip.address.SipURL; +import org.zoolu.sip.provider.*; +import org.zoolu.sip.message.*; +import org.zoolu.tools.Timer; +import org.zoolu.tools.TimerListener; +import org.zoolu.tools.LogLevel; + + +/** INVITE server transaction as defined in RFC 3261 (Section 17.2.1). + *
    An InviteTransactionServer is responsable to create a new SIP invite + * transaction that starts with a INVITE message received by the SipProvider + * and ends sending a final response. + *
    The changes of the internal status and the received messages are fired + * to the TransactionListener passed to the InviteTransactionServer object. + *
    This implementation of InviteTransactionServer automatically generates + * a "100 Trying" response when the INVITE message is received + * (as suggested by RFC3261) + */ +public class InviteTransactionServer extends TransactionServer +{ + /** Default behavior for automatically sending 100 Trying on INVITE. */ + public static boolean AUTO_TRYING=true; + + + /** the TransactionServerListener that captures the events fired by the InviteTransactionServer */ + InviteTransactionServerListener transaction_listener; + + /** last response message */ + //Message response=null; + + /** retransmission timeout ("Timer G" in RFC 3261) */ + Timer retransmission_to; + /** end timeout ("Timer H" in RFC 3261) */ + Timer end_to; + /** clearing timeout ("Timer I" in RFC 3261) */ + //Timer clearing_to; + + /** Whether automatically sending 100 Trying on INVITE. */ + boolean auto_trying; + + + /** Creates a new InviteTransactionServer. */ + public InviteTransactionServer(SipProvider sip_provider, InviteTransactionServerListener listener) + { super(sip_provider); + init(listener,new TransactionIdentifier(SipMethods.INVITE),null); + } + + /** Creates a new InviteTransactionServer for the already received INVITE request invite. */ + public InviteTransactionServer(SipProvider sip_provider, Message invite, InviteTransactionServerListener listener) + { super(sip_provider); + request=new Message(invite); + init(listener,request.getTransactionId(),request.getConnectionId()); + + changeStatus(STATE_TRYING); + sip_provider.addSipProviderListener(transaction_id,this); + // automatically send "100 Tryng" response and go to STATE_PROCEEDING + if (auto_trying) + { Message trying100=MessageFactory.createResponse(request,100,SipResponses.reasonOf(100),null); + respondWith(trying100); // this method makes it going automatically to STATE_PROCEEDING + } + } + + /** Creates a new InviteTransactionServer for the already received INVITE request invite. */ + public InviteTransactionServer(SipProvider sip_provider, Message invite, boolean auto_trying, InviteTransactionServerListener listener) + { super(sip_provider); + request=new Message(invite); + init(listener,request.getTransactionId(),request.getConnectionId()); + this.auto_trying=auto_trying; + + changeStatus(STATE_TRYING); + sip_provider.addSipProviderListener(transaction_id,this); + // automatically send "100 Tryng" response and go to STATE_PROCEEDING + if (auto_trying) + { Message trying100=MessageFactory.createResponse(request,100,SipResponses.reasonOf(100),null); + respondWith(trying100); // this method makes it going automatically to STATE_PROCEEDING + } + } + + /** Initializes timeouts and listener. */ + void init(InviteTransactionServerListener listener, TransactionIdentifier transaction_id, ConnectionIdentifier connaction_id) + { this.transaction_listener=listener; + this.transaction_id=transaction_id; + this.connection_id=connection_id; + auto_trying=AUTO_TRYING; + retransmission_to=new Timer(SipStack.retransmission_timeout,"Retransmission",this); + end_to=new Timer(SipStack.transaction_timeout,"End",this); + clearing_to=new Timer(SipStack.clearing_timeout,"Clearing",this); + printLog("id: "+String.valueOf(transaction_id),LogLevel.HIGH); + printLog("created",LogLevel.HIGH); + } + + /** Whether automatically sending 100 Trying on INVITE. */ + public void setAutoTrying(boolean auto_trying) + { this.auto_trying=auto_trying; + } + + + /** Starts the InviteTransactionServer. */ + public void listen() + { printLog("start",LogLevel.LOW); + if (statusIs(STATE_IDLE)) + { changeStatus(STATE_WAITING); + sip_provider.addSipProviderListener(new TransactionIdentifier(SipMethods.INVITE),this); + } + } + + /** Sends a response message */ + public void respondWith(Message resp) + { response=resp; + int code=response.getStatusLine().getCode(); + if (statusIs(STATE_TRYING) || statusIs(STATE_PROCEEDING)) sip_provider.sendMessage(response,connection_id); + if (code>=100 && code<200 && statusIs(STATE_TRYING)) + { changeStatus(STATE_PROCEEDING); + return; + } + if (code>=200 && code<300 && (statusIs(STATE_TRYING) || statusIs(STATE_PROCEEDING))) + { sip_provider.removeSipProviderListener(transaction_id); + changeStatus(STATE_TERMINATED); + transaction_listener=null; + return; + } + if (code>=300 && code<700 && (statusIs(STATE_TRYING) || statusIs(STATE_PROCEEDING))) + { changeStatus(STATE_COMPLETED); + // retransmission only in case of unreliable transport + if (connection_id==null) + { retransmission_to.start(); + end_to.start(); + } + else + { printLog("No retransmissions for reliable transport ("+connection_id+")",LogLevel.LOW); + onTimeout(end_to); + } + } + } + + /** Method derived from interface SipListener. + * It's fired from the SipProvider when a new message is catch for to the present ServerTransaction. */ + public void onReceivedMessage(SipProvider provider, Message msg) + { if (msg.isRequest()) + { String req_method=msg.getRequestLine().getMethod(); + + // invite received + if (req_method.equals(SipMethods.INVITE)) + { + if (statusIs(STATE_WAITING)) + { request=new Message(msg); + connection_id=request.getConnectionId(); + transaction_id=request.getTransactionId(); + sip_provider.addSipProviderListener(transaction_id,this); + sip_provider.removeSipProviderListener(new TransactionIdentifier(SipMethods.INVITE)); + changeStatus(STATE_TRYING); + // automatically send "100 Tryng" response and go to STATE_PROCEEDING + if (auto_trying) + { Message trying100=MessageFactory.createResponse(request,100,SipResponses.reasonOf(100),null); + respondWith(trying100); // this method makes it going automatically to STATE_PROCEEDING + } + if (transaction_listener!=null) transaction_listener.onTransRequest(this,msg); + return; + } + if (statusIs(STATE_PROCEEDING) || statusIs(STATE_COMPLETED)) + { // retransmission of the last response + sip_provider.sendMessage(response,connection_id); + return; + } + } + // ack received + if (req_method.equals(SipMethods.ACK) && statusIs(STATE_COMPLETED)) + { retransmission_to.halt(); + end_to.halt(); + changeStatus(STATE_CONFIRMED); + if (transaction_listener!=null) transaction_listener.onTransFailureAck(this,msg); + clearing_to.start(); + return; + } + } + } + + /** Method derived from interface TimerListener. + * It's fired from an active Timer. */ + public void onTimeout(Timer to) + { try + { if (to.equals(retransmission_to) && statusIs(STATE_COMPLETED)) + { printLog("Retransmission timeout expired",LogLevel.HIGH); + long timeout=2*retransmission_to.getTime(); + if (timeout>SipStack.max_retransmission_timeout) timeout=SipStack.max_retransmission_timeout; + retransmission_to=new Timer(timeout,retransmission_to.getLabel(),this); + retransmission_to.start(); + sip_provider.sendMessage(response,connection_id); + } + if (to.equals(end_to) && statusIs(STATE_COMPLETED)) + { printLog("End timeout expired",LogLevel.HIGH); + retransmission_to.halt(); + sip_provider.removeSipProviderListener(transaction_id); + changeStatus(STATE_TERMINATED); + transaction_listener=null; + } + if (to.equals(clearing_to) && statusIs(STATE_CONFIRMED)) + { printLog("Clearing timeout expired",LogLevel.HIGH); + sip_provider.removeSipProviderListener(transaction_id); + changeStatus(STATE_TERMINATED); + transaction_listener=null; + } + } + catch (Exception e) + { printException(e,LogLevel.HIGH); + } + } + + /** Method used to drop an active transaction */ + public void terminate() + { retransmission_to.halt(); + clearing_to.halt(); + end_to.halt(); + if (statusIs(STATE_TRYING)) sip_provider.removeSipProviderListener(new TransactionIdentifier(SipMethods.INVITE)); + else sip_provider.removeSipProviderListener(transaction_id); + changeStatus(STATE_TERMINATED); + transaction_listener=null; + } + +} diff --git a/src/org/zoolu/sip/transaction/InviteTransactionServerListener.java b/src/org/zoolu/sip/transaction/InviteTransactionServerListener.java new file mode 100644 index 0000000..2a2d887 --- /dev/null +++ b/src/org/zoolu/sip/transaction/InviteTransactionServerListener.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.transaction; + + +import org.zoolu.sip.message.Message; + + +/** A TransactionServerListener listens for InviteTransactionServer events. + * It extends TransactionServerListener by adding the onTransFailureAck(InviteTransactionServer,Message) method. + */ +public interface InviteTransactionServerListener extends TransactionServerListener +{ + /** When an InviteTransactionServer goes into the "Confirmed" state receining an ACK for NON-2xx response */ + public void onTransFailureAck(InviteTransactionServer ts, Message ack); + +} diff --git a/src/org/zoolu/sip/transaction/Transaction.java b/src/org/zoolu/sip/transaction/Transaction.java new file mode 100644 index 0000000..f576a17 --- /dev/null +++ b/src/org/zoolu/sip/transaction/Transaction.java @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.transaction; + + +import org.zoolu.sip.address.SipURL; +import org.zoolu.sip.provider.*; +import org.zoolu.sip.header.*; +import org.zoolu.sip.message.*; +import org.zoolu.tools.Timer; +import org.zoolu.tools.TimerListener; +import org.zoolu.tools.Log; +import org.zoolu.tools.LogLevel; + + +/** Abstract class Transaction is hinerited by classes + * ClientTransaction, ServerTransaction, InviteClientTransaction and InviteServerTransaction. + * An Object Transaction is responsable to handle a new SIP transaction.
    + * The changes of the internal status and the received messages are fired to the + * TransactionListener passed to the Transaction Objects.
    + */ +public abstract class Transaction implements SipProviderListener, TimerListener +{ + /** Transactions counter */ + protected static int transaction_counter=0; + + // all transaction states: + /** State Waiting, used only by server transactions */ + static final int STATE_IDLE=0; + /** State Waiting, used only by server transactions */ + static final int STATE_WAITING=1; + /** State Trying */ + static final int STATE_TRYING=2; + /** State Proceeding */ + static final int STATE_PROCEEDING=3; + /** State Completed */ + static final int STATE_COMPLETED=4; + /** State Confirmed, used only by invite server transactions */ + static final int STATE_CONFIRMED=5; + /** State Waiting. */ + static final int STATE_TERMINATED=7; + + /** Gets the transaction state. */ + static String getStatus(int st) + { switch (st) + { case STATE_IDLE : return "T_Idle"; + case STATE_WAITING : return "T_Waiting"; + case STATE_TRYING : return "T_Trying"; + case STATE_PROCEEDING : return "T_Proceeding"; + case STATE_COMPLETED : return "T_Completed"; + case STATE_CONFIRMED : return "T_Confirmed"; + case STATE_TERMINATED : return "T_Terminated"; + default : return null; + } + } + + + /** Transaction sequence number */ + int transaction_sqn; + + /** Event logger. */ + Log log; + + /** Lower layer dispatcher that sends and receive messages. + * The messages received by the SipProvider are fired to the Transaction + * by means of the onReceivedMessage() method. */ + SipProvider sip_provider; + + /** Internal state-machine status */ + int status; + + /** transaction request message/method */ + Message request; + + /** the Transaction ID */ + TransactionIdentifier transaction_id; + + /** Transaction connection id */ + ConnectionIdentifier connection_id; + + + /** Costructs a new Transaction */ + protected Transaction(SipProvider sip_provider) + { this.sip_provider=sip_provider; + log=sip_provider.getLog(); + this.transaction_id=null; + this.request=null; + this.connection_id=null; + this.transaction_sqn=transaction_counter++; + this.status=STATE_IDLE; + } + + /** Changes the internal status */ + void changeStatus(int newstatus) + { status=newstatus; + //transaction_listener.onChangedTransactionStatus(status); + printLog("changed transaction state: "+getStatus(),LogLevel.MEDIUM); + } + + /** Whether the internal status is equal to st */ + boolean statusIs(int st) + { return status==st; + } + + /** Gets the current transaction state. */ + String getStatus() + { return getStatus(status); + } + + /** Gets the SipProvider of this Transaction. */ + public SipProvider getSipProvider() + { return sip_provider; + } + + /** Gets the Transaction request message */ + public Message getRequestMessage() + { return request; + } + + /** Gets the Transaction method */ + public String getTransactionMethod() + { return request.getTransactionMethod(); + } + + /** Gets the transaction-ID */ + public TransactionIdentifier getTransactionId() + { return transaction_id; + } + + /** Gets the transaction connection id */ + public ConnectionIdentifier getConnectionId() + { return connection_id; + } + + /** Method derived from interface SipListener. + * It's fired from the SipProvider when a new message is catch for to the present ServerTransaction. + */ + public void onReceivedMessage(SipProvider provider, Message msg) + { //do nothing + } + + /** Method derived from interface TimerListener. + * It's fired from an active Timer. + */ + public void onTimeout(Timer to) + { //do nothing + } + + /** Terminates the transaction. */ + public abstract void terminate(); + + + //**************************** Logs ****************************/ + + /** Adds a new string to the default Log */ + protected void printLog(String str, int level) + { if (log!=null) log.println("Transaction#"+transaction_sqn+": "+str,level+SipStack.LOG_LEVEL_TRANSACTION); + } + + /** Adds a WARNING to the default Log */ + protected void printWarning(String str, int level) + { printLog("WARNING: "+str,level); + } + + /** Adds the Exception to the log file */ + protected void printException(Exception e, int level) + { if (log!=null) log.printException(e,level+SipStack.LOG_LEVEL_TRANSACTION); + } + +} diff --git a/src/org/zoolu/sip/transaction/TransactionClient.java b/src/org/zoolu/sip/transaction/TransactionClient.java new file mode 100644 index 0000000..dabb0e8 --- /dev/null +++ b/src/org/zoolu/sip/transaction/TransactionClient.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.transaction; + + +import org.zoolu.sip.address.SipURL; +import org.zoolu.sip.provider.*; +import org.zoolu.sip.message.*; +import org.zoolu.tools.Timer; +import org.zoolu.tools.TimerListener; +import org.zoolu.tools.LogLevel; + + +/** Generic client transaction as defined in RFC 3261 (Section 17.1.2). + * A TransactionClient is responsable to create a new SIP transaction, starting with a request message sent through the SipProvider and ending with a final response.
    + * The changes of the internal status and the received messages are fired to the TransactionListener passed to the TransactionClient object.
    + */ + +public class TransactionClient extends Transaction +{ + /** the TransactionClientListener that captures the events fired by the TransactionClient */ + TransactionClientListener transaction_listener; + + /** retransmission timeout ("Timer E" in RFC 3261) */ + Timer retransmission_to; + /** transaction timeout ("Timer F" in RFC 3261) */ + Timer transaction_to; + /** clearing timeout ("Timer K" in RFC 3261)*/ + Timer clearing_to; + + + /** Costructs a new TransactionClient. */ + protected TransactionClient(SipProvider sip_provider) + { super(sip_provider); + transaction_listener=null; + } + + /** Creates a new TransactionClient */ + public TransactionClient(SipProvider sip_provider, Message req, TransactionClientListener listener) + { super(sip_provider); + request=new Message(req); + init(listener,request.getTransactionId()); + } + + /** Initializes timeouts and listener. */ + void init(TransactionClientListener listener, TransactionIdentifier transaction_id) + { this.transaction_listener=listener; + this.transaction_id=transaction_id; + retransmission_to=new Timer(SipStack.retransmission_timeout,"Retransmission",this); + transaction_to=new Timer(SipStack.transaction_timeout,"Transaction",this); + clearing_to=new Timer(SipStack.clearing_timeout,"Clearing",this); + printLog("id: "+String.valueOf(transaction_id),LogLevel.HIGH); + printLog("created",LogLevel.HIGH); + } + + /** Starts the TransactionClient and sends the transaction request. */ + public void request() + { printLog("start",LogLevel.LOW); + changeStatus(STATE_TRYING); + retransmission_to.start(); + transaction_to.start(); + + sip_provider.addSipProviderListener(transaction_id,this); + connection_id=sip_provider.sendMessage(request); + } + + /** Method derived from interface SipListener. + * It's fired from the SipProvider when a new message is received for to the present TransactionClient. */ + public void onReceivedMessage(SipProvider provider, Message msg) + { if (msg.isResponse()) + { int code=msg.getStatusLine().getCode(); + if (code>=100 && code<200 && (statusIs(STATE_TRYING) || statusIs(STATE_PROCEEDING))) + { if (statusIs(STATE_TRYING)) changeStatus(STATE_PROCEEDING); + if (transaction_listener!=null) transaction_listener.onTransProvisionalResponse(this,msg); + return; + } + if (code>=200 && code<700 && (statusIs(STATE_TRYING) || statusIs(STATE_PROCEEDING))) + { retransmission_to.halt(); + transaction_to.halt(); + changeStatus(STATE_COMPLETED); + if (code<300) + { if (transaction_listener!=null) transaction_listener.onTransSuccessResponse(this,msg); + } + else + { if (transaction_listener!=null) transaction_listener.onTransFailureResponse(this,msg); + } + transaction_listener=null; + if (connection_id==null) clearing_to.start(); + else + { printLog("clearing_to=0 for reliable transport",LogLevel.LOW); + onTimeout(clearing_to); + } + return; + } + } + } + + /** Method derived from interface TimerListener. + * It's fired from an active Timer. */ + public void onTimeout(Timer to) + { try + { if (to.equals(retransmission_to) && (statusIs(STATE_TRYING) || statusIs(STATE_PROCEEDING))) + { printLog("Retransmission timeout expired",LogLevel.HIGH); + // retransmission only for unreliable transport + if (connection_id==null) + { sip_provider.sendMessage(request); + long timeout=2*retransmission_to.getTime(); + if (timeout>SipStack.max_retransmission_timeout || statusIs(STATE_PROCEEDING)) timeout=SipStack.max_retransmission_timeout; + retransmission_to=new Timer(timeout,retransmission_to.getLabel(),this); + retransmission_to.start(); + } + else printLog("No retransmissions for reliable transport ("+connection_id+")",LogLevel.LOW); + } + if (to.equals(transaction_to)) + { printLog("Transaction timeout expired",LogLevel.HIGH); + retransmission_to.halt(); + clearing_to.halt(); + sip_provider.removeSipProviderListener(transaction_id); + changeStatus(STATE_TERMINATED); + if (transaction_listener!=null) transaction_listener.onTransTimeout(this); + transaction_listener=null; + } + if (to.equals(clearing_to)) + { printLog("Clearing timeout expired",LogLevel.HIGH); + retransmission_to.halt(); + transaction_to.halt(); + sip_provider.removeSipProviderListener(transaction_id); + changeStatus(STATE_TERMINATED); + } + } + catch (Exception e) + { printException(e,LogLevel.HIGH); + } + } + + /** Terminates the transaction. */ + public void terminate() + { if (!statusIs(STATE_TERMINATED)) + { retransmission_to.halt(); + transaction_to.halt(); + clearing_to.halt(); + sip_provider.removeSipProviderListener(transaction_id); + changeStatus(STATE_TERMINATED); + transaction_listener=null; + } + } + + + //**************************** Logs ****************************/ + + /** Adds a new string to the default Log */ + protected void printLog(String str, int level) + { if (log!=null) log.println("TransactionClient#"+transaction_sqn+": "+str,level+SipStack.LOG_LEVEL_TRANSACTION); + } + +} diff --git a/src/org/zoolu/sip/transaction/TransactionClientListener.java b/src/org/zoolu/sip/transaction/TransactionClientListener.java new file mode 100644 index 0000000..6cd03ef --- /dev/null +++ b/src/org/zoolu/sip/transaction/TransactionClientListener.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.transaction; + + +import org.zoolu.sip.message.Message; + + +/** A TransactionClientListener listens for TransactionClient events. + * It collects all TransactionClient callback functions. + */ +public interface TransactionClientListener +{ + /** When the TransactionClient is (or goes) in "Proceeding" state and receives a new 1xx provisional response */ + public void onTransProvisionalResponse(TransactionClient tc, Message resp); + + /** When the TransactionClient goes into the "Completed" state receiving a 2xx response */ + public void onTransSuccessResponse(TransactionClient tc, Message resp); + + /** When the TransactionClient goes into the "Completed" state receiving a 300-699 response */ + public void onTransFailureResponse(TransactionClient tc, Message resp); + + /** When the TransactionClient goes into the "Terminated" state, caused by transaction timeout */ + public void onTransTimeout(TransactionClient tc); +} diff --git a/src/org/zoolu/sip/transaction/TransactionServer.java b/src/org/zoolu/sip/transaction/TransactionServer.java new file mode 100644 index 0000000..7f8d8fe --- /dev/null +++ b/src/org/zoolu/sip/transaction/TransactionServer.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.transaction; + + +import org.zoolu.tools.Timer; +import org.zoolu.tools.TimerListener; +import org.zoolu.sip.address.SipURL; +import org.zoolu.sip.provider.*; +import org.zoolu.sip.message.*; +import org.zoolu.tools.LogLevel; + + +/** Generic server transaction as defined in RFC 3261 (Section 17.2.2). + * A TransactionServer is responsable to create a new SIP transaction that starts with a request message received by the SipProvider and ends sending a final response.
    + * The changes of the internal status and the received messages are fired to the TransactionListener passed to the TransactionServer object.
    + * When costructing a new TransactionServer, the transaction type is passed as String parameter to the costructor (e.g. "CANCEL", "BYE", etc..) + */ + +public class TransactionServer extends Transaction +{ + /** the TransactionServerListener that captures the events fired by the TransactionServer */ + TransactionServerListener transaction_listener; + + /** last response message */ + Message response; + + /** clearing timeout ("Timer J" in RFC 3261) */ + Timer clearing_to; + + + /** Costructs a new TransactionServer. */ + protected TransactionServer(SipProvider sip_provider) + { super(sip_provider); + transaction_listener=null; + response=null; + } + + /** Creates a new TransactionServer of type method. */ + public TransactionServer(SipProvider sip_provider, String method, TransactionServerListener listener) + { super(sip_provider); + init(listener,new TransactionIdentifier(method),null); + } + + /** Creates a new TransactionServer for the already received request req. */ + public TransactionServer(SipProvider provider, Message req, TransactionServerListener listener) + { super(provider); + request=new Message(req); + init(listener,request.getTransactionId(),request.getConnectionId()); + + printLog("start",LogLevel.LOW); + changeStatus(STATE_TRYING); + sip_provider.addSipProviderListener(transaction_id,this); + } + + /** Initializes timeouts and listener. */ + void init(TransactionServerListener listener, TransactionIdentifier transaction_id, ConnectionIdentifier connaction_id) + { this.transaction_listener=listener; + this.transaction_id=transaction_id; + this.connection_id=connection_id; + this.response=null; + clearing_to=new Timer(SipStack.transaction_timeout,"Clearing",this); + printLog("id: "+String.valueOf(transaction_id),LogLevel.HIGH); + printLog("created",LogLevel.HIGH); + } + + /** Starts the TransactionServer. */ + public void listen() + { if (statusIs(STATE_IDLE)) + { printLog("start",LogLevel.LOW); + changeStatus(STATE_WAITING); + sip_provider.addSipProviderListener(transaction_id,this); + } + } + + /** Sends a response message */ + public void respondWith(Message resp) + { response=resp; + if (statusIs(STATE_TRYING) || statusIs(STATE_PROCEEDING)) + { sip_provider.sendMessage(response,connection_id); + int code=response.getStatusLine().getCode(); + if (code>=100 && code<200 && statusIs(STATE_TRYING)) + { changeStatus(STATE_PROCEEDING); + } + if (code>=200 && code<700) + { changeStatus(STATE_COMPLETED); + if (connection_id==null) clearing_to.start(); + else + { printLog("clearing_to=0 for reliable transport",LogLevel.LOW); + onTimeout(clearing_to); + } + } + } + } + + /** Method derived from interface SipListener. + * It's fired from the SipProvider when a new message is received for to the present TransactionServer. */ + public void onReceivedMessage(SipProvider provider, Message msg) + { if (msg.isRequest()) + { if (statusIs(STATE_WAITING)) + { request=new Message(msg); + connection_id=msg.getConnectionId(); + sip_provider.removeSipProviderListener(transaction_id); + transaction_id=request.getTransactionId(); + sip_provider.addSipProviderListener(transaction_id,this); + changeStatus(STATE_TRYING); + if (transaction_listener!=null) transaction_listener.onTransRequest(this,msg); + return; + } + if (statusIs(STATE_PROCEEDING) || statusIs(STATE_COMPLETED)) + { // retransmission of the last response + printLog("response retransmission",LogLevel.LOW); + sip_provider.sendMessage(response,connection_id); + return; + } + } + } + + /** Method derived from interface TimerListener. + * It's fired from an active Timer. */ + public void onTimeout(Timer to) + { try + { if (to.equals(clearing_to)) + { printLog("Clearing timeout expired",LogLevel.HIGH); + sip_provider.removeSipProviderListener(transaction_id); + changeStatus(STATE_TERMINATED); + transaction_listener=null; + //clearing_to=null; + } + } + catch (Exception e) + { printException(e,LogLevel.HIGH); + } + } + + /** Terminates the transaction. */ + public void terminate() + { if (!statusIs(STATE_TERMINATED)) + { clearing_to.halt(); + sip_provider.removeSipProviderListener(transaction_id); + changeStatus(STATE_TERMINATED); + transaction_listener=null; + //clearing_to=null; + } + } + + + //**************************** Logs ****************************/ + + /** Adds a new string to the default Log */ + protected void printLog(String str, int level) + { if (log!=null) log.println("TransactionServer#"+transaction_sqn+": "+str,level+SipStack.LOG_LEVEL_TRANSACTION); + } + +} + diff --git a/src/org/zoolu/sip/transaction/TransactionServerListener.java b/src/org/zoolu/sip/transaction/TransactionServerListener.java new file mode 100644 index 0000000..38d3c3c --- /dev/null +++ b/src/org/zoolu/sip/transaction/TransactionServerListener.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.sip.transaction; + + +import org.zoolu.sip.message.Message; + + +/** A TransactionServerListener listens for TransactionServer events. + * It collects all TransactionServer callback functions. + */ +public interface TransactionServerListener +{ + /** When the TransactionServer goes into the "Trying" state receiving a request */ + public void onTransRequest(TransactionServer ts, Message req); + +} diff --git a/src/org/zoolu/tools/Archive.java b/src/org/zoolu/tools/Archive.java new file mode 100644 index 0000000..a8e07ae --- /dev/null +++ b/src/org/zoolu/tools/Archive.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.tools; + + +import java.io.*; +import java.net.URL; +//import java.net.URI; +import java.awt.Image; +import java.awt.Toolkit; +import javax.swing.ImageIcon; +import javax.sound.sampled.AudioInputStream; +import javax.sound.sampled.AudioSystem; + + +/** Collection of static methods for handling files and jar archives. +*/ +public class Archive +{ + /** The base path */ + public static String BASE_PATH=(new File("")).getAbsolutePath(); + //**** PersonalJava **** + //public static String BASE_PATH="\\My Documents\\Lavoro\\"; + + + /** Gets the complete url of a file included within a jar archive. */ + public static URL getJarURL(String jar_archive, String file_name) + { if (jar_archive==null || file_name==null) return null; + // else + String url="jar:file:"+BASE_PATH+"/"+jar_archive+"!/"+file_name; + try + { return new URL(url); + } + catch (java.net.MalformedURLException e) + { System.err.println("ERROR: malformed url "+url); + return null; + } + } + + + /** Gets the complete url of a file. */ + public static URL getFileURL(String file_name) + { if (file_name==null) return null; + // else + String url="file:"+BASE_PATH+"/"+file_name; + try + { return new URL("file:"+BASE_PATH+"/"+file_name); + } + catch (java.net.MalformedURLException e) + { System.err.println("ERROR: malformed url "+url); + return null; + } + } + + + /** Gets an Image from file */ + public static Image getImage(String file_name) + { if (file_name==null) return null; + //else + Toolkit toolkit=Toolkit.getDefaultToolkit(); + Image image=null; + file_name=BASE_PATH+"/"+file_name; + if ((new File(file_name)).canRead()) + { image=toolkit.getImage(file_name); + // wait for image loading (4 attempts with 80ms of delay) + for (int i=0; i<4 && image.getWidth(null)<0; i++) try { Thread.sleep(80); } catch (Exception e) {} + } + return image; + } + + + /** Gets an Image from a URL. */ + public static Image getImage(URL url) + { if (url==null) return null; + //else + Toolkit toolkit=Toolkit.getDefaultToolkit(); + Image image=null; + try + { url.openConnection().connect(); + image=toolkit.getImage(url); + // wait for image loading (4 attempts with 80ms of delay) + for (int i=0; i<4 && image.getWidth(null)<0; i++) try { Thread.sleep(80); } catch (Exception e) {} + } + catch (java.io.IOException e) + { System.err.println("ERROR: can't read the file "+url.toString()); + } + return image; + } + + + /** Gets an ImageIcon from file */ + public static ImageIcon getImageIcon(String file_name) + { file_name=BASE_PATH+"/"+file_name; + return new ImageIcon(file_name); + } + + + /** Gets an ImageIcon from an URL */ + public static ImageIcon getImageIcon(URL url) + { if (url==null) return null; + //else + ImageIcon icon=null; + try + { url.openConnection().connect(); + icon=new ImageIcon(url); + } + catch (java.io.IOException e) + { System.err.println("ERROR: can't read the file "+url.toString()); + } + return icon; + } + + + /** Gets an InputStream from an URL */ + public static InputStream getInputStream(URL url) + { if (url==null) return null; + //else + InputStream in=null; + try + { in=(url).openStream(); + } + catch (java.io.IOException e) + { System.err.println("ERROR: can't read the file "+url.toString()); + } + return in; + } + + + /** Gets an AudioInputStream from an URL */ + public static AudioInputStream getAudioInputStream(URL url) throws javax.sound.sampled.UnsupportedAudioFileException + { if (url==null) return null; + //else + AudioInputStream in=null; + try + { in=AudioSystem.getAudioInputStream(url); + } + catch (java.io.IOException e) + { System.err.println("ERROR: can't read the file "+url.toString()); + } + return in; + } + +} diff --git a/src/org/zoolu/tools/Assert.java b/src/org/zoolu/tools/Assert.java new file mode 100644 index 0000000..17ff17a --- /dev/null +++ b/src/org/zoolu/tools/Assert.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.tools; + + + + +/** Class Assert provides some static methods to check some inline conditions. + * When an assertion fails an AssertionException is throws. + *

    Such a tool could be helpful for debugging. + */ +public class Assert +{ + /** Check that exp is true, otherwise an AssertionException is thrown. */ + public final static void isTrue(boolean exp) + { if(!exp) onError("Assertion failed"); + } + + /** Check that exp is false, otherwise an AssertionException is thrown. */ + public final static void isFalse(boolean exp) + { if(exp) onError("Assertion failed"); + } + + /** Check that exp is true, otherwise an AssertionException is thrown. */ + public final static void isTrue(boolean exp, String msg) + { if(!exp) onError("Assertion failed: "+msg); + } + + /** Check that exp is false, otherwise an AssertionException is thrown. */ + public final static void isFalse(boolean exp, String msg) + { if(exp) onError("Assertion failed: "+msg); + } + + private static void onError(String msg) + { throw new AssertException(msg); + } + +} diff --git a/src/org/zoolu/tools/AssertException.java b/src/org/zoolu/tools/AssertException.java new file mode 100644 index 0000000..29ba0e1 --- /dev/null +++ b/src/org/zoolu/tools/AssertException.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.tools; + + + + +/** AssertException is used to indicate that a condition is not met. + *
    It is thrown by methods isTrue() and isFalse() of class Assert. + */ +public class AssertException extends java.lang.RuntimeException +{ + /** Costructs a new AssertException. */ + public AssertException(String msg) + { super(msg); + } + + /** Costructs a new AssertException. */ + public AssertException() + { super(); + } +} \ No newline at end of file diff --git a/src/org/zoolu/tools/Base64.java b/src/org/zoolu/tools/Base64.java new file mode 100644 index 0000000..b2a15d8 --- /dev/null +++ b/src/org/zoolu/tools/Base64.java @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.tools; + + + +/** Class Base64 can be used for base64-encoding a byte array + * and/or for base64-decoding a base64-string. + * + * @author Camilla Ferramola, Univeristy of Parma, Italy - 2004 + */ +public class Base64 +{ + private static final String base64codes ="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + private static int[] aux = new int[4]; + + + /** Base64 encoder */ + public static String encode(byte[] input) + { + + String stringacod = ""; + byte[] bin = new byte[3]; + + int iter = (input.length)/3; + int nzero = (input.length)%3; + int i = 0; + + for (i=0; i>> 2)&63); + aux[1] = ((bin[0] & 3) << 4) + ((bin[1] >>> 4)&15); + aux[2] = ((bin[1] & 15) << 2) + ((bin[2] >>> 6)&3); + aux[3] = (bin[2] & 63); + + // uso gli interi memorizzati in aux[] come indice della stringa base64codes + //System.out.println("aux[0]="+aux[0]+" aux[1]="+aux[1]); + stringacod = stringacod + base64codes.charAt(aux[0])+ base64codes.charAt(aux[1])+ + base64codes.charAt(aux[2])+base64codes.charAt(aux[3]); + } + + if( i==iter ) + { + if ( nzero==0 ) + { + } + else + if ( nzero==1 ) + { // l'ultimo pacchetto da analizzare ha 8 bit + // quindi ottengo due caratteri in base64 e due caratteri padding "=" + aux[0] = ((input[iter*3] >>> 2) & 63); + aux[1] = (input[iter*3] & 3) << 4; + + stringacod = stringacod + base64codes.charAt(aux[0])+base64codes.charAt(aux[1])+"=="; + } + else + if (nzero==2) + { // l'ultimo pacchetto da analizzare ha 16 bit quindi ottengo + // tre caratteri in base 64 e uno di padding "=" + aux[0] = ((input[iter*3] >>> 2) & 63) ; + aux[1] = ((input[iter*3] & 3) << 4) + ((input[iter*3+1] >>> 4) & 15); + aux[2] = (input[iter*3+1 ] & 15) << 2; + + stringacod = stringacod + base64codes.charAt(aux[0])+ base64codes.charAt(aux[1])+ + base64codes.charAt(aux[2])+"="; + } + } + + return stringacod; + + } + + + /** Base64 decoder */ + public static byte[] decode (String stringacod) + { + // tolgo gli eventuali "=" alla fine della stringa + int uguale = stringacod.indexOf("="); + if ( uguale != -1) stringacod = stringacod.substring(0,uguale); + + int[] bin = new int[3]; + int iter = (stringacod.length())/4; + int resto = (stringacod.length())%4; + + int nzero = 0; + if (resto!=0) nzero = 1; + byte[] output = new byte[iter*3 + nzero*(resto-1)]; + + int i = 0; + for (i=0; i>>4); + bin[1] = (aux[1]%16 <<4) + (aux[2]>>>2); + bin[2] = (aux[2]%4 <<6) + aux[3]; + + output[i*3] = (byte)bin [0]; + output[i*3+1] = (byte)bin [1]; + output[i*3+2] = (byte)bin [2]; + } + + if (i==iter) + { + // per come è costruita la codifica a base64 + // togliendo gli eventuali caratteri "=" di padding + // il resto può essere 0, 2 o 3 + if (resto==0) + { + } + if (resto==2) + { + aux[0] = base64codes.indexOf(stringacod.charAt(i*4)); + aux[1] = base64codes.indexOf(stringacod.charAt(i*4+1)); + + bin[0] = (aux[0]<<2) + (aux[1]>>>4); + + output[i*3] = (byte)bin[0]; + } + + if (resto==3) + { + aux[0] = base64codes.indexOf(stringacod.charAt(i*4)); + aux[1] = base64codes.indexOf(stringacod.charAt(i*4+1)); + aux[2] = base64codes.indexOf(stringacod.charAt(i*4+2)); + + bin[0] = (aux[0]<<2) + (aux[1]>>>4); + bin[1] = (aux[1]%16 <<4) + (aux[2]>>>2); + + output[i*3] = (byte)bin [0]; + output[i*3+1] = (byte)bin [1]; + } + } + return output; + } + + + // ******************************* MAIN ******************************* + + public static void main (String[] args) + { + String messaggio = args[0]; + byte[] bmess = messaggio.getBytes(); + String mess64 = encode(bmess); + System.out.println("messaggio codificato: "+mess64); + byte[] decodificato = decode(mess64); + String strdecodificato = ""; + try { + strdecodificato = new String (decodificato,"ISO-8859-1"); + } + catch (Exception e) { e.printStackTrace();} + System.out.println("messaggio decodificato è: "+strdecodificato); + } +} diff --git a/src/org/zoolu/tools/Configurable.java b/src/org/zoolu/tools/Configurable.java new file mode 100644 index 0000000..389d0c7 --- /dev/null +++ b/src/org/zoolu/tools/Configurable.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.tools; + + + +/** Configurable is the base interface for classes that can be configurated by a text file. + */ +public interface Configurable +{ + /** Parses a single text line. */ + public void parseLine(String line); +} diff --git a/src/org/zoolu/tools/Configure.java b/src/org/zoolu/tools/Configure.java new file mode 100644 index 0000000..c82b338 --- /dev/null +++ b/src/org/zoolu/tools/Configure.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.tools; + + +import java.io.*; + + +/** Configure helps the loading and saving of configuration data. + */ +public class Configure +{ + + /** String 'NONE' used as undefined value (i.e. null). */ + public static String NONE="NONE"; + + /** The object that should be configured */ + Configurable configurable; + + + /** Parses a single text line (read from the config file) */ + protected void parseLine(String line) + { // parse the text line.. + } + + /** Converts the entire object into lines (to be saved into the config file) */ + protected String toLines() + { // convert the object into to one or more text line.. + return ""; + } + + /** Costructs a Configure container */ + protected Configure() + { this.configurable=null; + } + + /** Costructs a Configure container */ + public Configure(Configurable configurable, String file) + { this.configurable=configurable; + loadFile(file); + } + + + /** Loads Configure attributes from the specified file */ + protected void loadFile(String file) + { //System.out.println("DEBUG: loading Configuration"); + if (file==null) + { //System.out.println("DEBUG: no Configuration file"); + return; + } + //else + BufferedReader in=null; + try + { in=new BufferedReader(new FileReader(file)); + + while (true) + { String line=null; + try { line=in.readLine(); } catch (Exception e) { e.printStackTrace(); System.exit(0); } + if (line==null) break; + + if (!line.startsWith("#")) + { if (configurable==null) parseLine(line); else configurable.parseLine(line); + } + } + in.close(); + } + catch (Exception e) + { System.err.println("WARNING: error reading file \""+file+"\""); + //System.exit(0); + return; + } + //System.out.println("DEBUG: loading Configuration: done."); + } + + + /** Saves Configure attributes on the specified file */ + protected void saveFile(String file) + { if (file==null) return; + //else + try + { BufferedWriter out=new BufferedWriter(new FileWriter(file)); + out.write(toLines()); + out.close(); + } + catch (IOException e) + { System.err.println("ERROR writing on file \""+file+"\""); + } + } + +} diff --git a/src/org/zoolu/tools/DateFormat.java b/src/org/zoolu/tools/DateFormat.java new file mode 100644 index 0000000..6a13901 --- /dev/null +++ b/src/org/zoolu/tools/DateFormat.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.tools; + + +import java.util.Date; +import java.util.Calendar; + + +/** Class DateFormat replaces the format method of java.text.DateFormat. + */ +public class DateFormat +{ + + /** Months */ + private static final String[] MONTHS={ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; + + + /** Days of the week */ + private static final String[] WEEKDAYS={ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }; + + + /** Gets a "HH:mm:ss.SSS EEE dd MMM yyyy" representation of a Date */ + public static String formatHHMMSS(Date date) + { //DateFormat df=new SimpleDateFormat("HH:mm:ss.SSS EEE dd MMM yyyy",Locale.US); + //return df.format(date); + /* + String str=date.toString(); // dow mon dd hh:mm:ss zzz yyyy + int len=str.length(); + String weekday=str.substring(0,3); + String month=str.substring(4,7); + String day=str.substring(8,10); + String time=str.substring(11,19); + String millisec=Integer.toString((int)(date.getTime()%1000)); + if (millisec.length()==1) millisec="00"+millisec; + else if (millisec.length()==2) millisec="0"+millisec; + String year=str.substring(len-4,len); + + return time+"."+millisec+" "+weekday+" "+day+" "+month+" "+year; + */ + Calendar cal=Calendar.getInstance(); + cal.setTime(date); + String weekday=WEEKDAYS[cal.get(Calendar.DAY_OF_WEEK)-1]; + String month=MONTHS[cal.get(Calendar.MONTH)]; + String year=Integer.toString(cal.get(Calendar.YEAR)); + String day=Integer.toString(cal.get(Calendar.DAY_OF_MONTH)); + String hour=Integer.toString(cal.get(Calendar.HOUR_OF_DAY)); + String min=Integer.toString(cal.get(Calendar.MINUTE)); + String sec=Integer.toString(cal.get(Calendar.SECOND)); + String millisec=Integer.toString(cal.get(Calendar.MILLISECOND)); + if (day.length()==1) day="0"+day; + if (hour.length()==1) hour="0"+hour; + if (min.length()==1) min="0"+min; + if (sec.length()==1) sec="0"+sec; + if (millisec.length()==1) millisec="00"+millisec; + else if (millisec.length()==2) millisec="0"+millisec; + + return hour+":"+min+":"+sec+"."+millisec+" "+weekday+" "+day+" "+month+" "+year; + } + + + /** Gets a "yyyy MMM dd, HH:mm:ss.SSS" representation of a Date */ + public static String formatYYYYMMDD(Date date) + { Calendar cal=Calendar.getInstance(); + cal.setTime(date); + String weekday=WEEKDAYS[cal.get(Calendar.DAY_OF_WEEK)-1]; + //String month=MONTHS[cal.get(Calendar.MONTH)]; + String year=Integer.toString(cal.get(Calendar.YEAR)); + String day=Integer.toString(cal.get(Calendar.DAY_OF_MONTH)); + String hour=Integer.toString(cal.get(Calendar.HOUR_OF_DAY)); + String min=Integer.toString(cal.get(Calendar.MINUTE)); + String sec=Integer.toString(cal.get(Calendar.SECOND)); + String millisec=Integer.toString(cal.get(Calendar.MILLISECOND)); + if (day.length()==1) day="0"+day; + if (hour.length()==1) hour="0"+hour; + if (min.length()==1) min="0"+min; + if (sec.length()==1) sec="0"+sec; + if (millisec.length()==1) millisec="00"+millisec; + else if (millisec.length()==2) millisec="0"+millisec; + + String month=Integer.toString(cal.get(Calendar.MONTH)+1); + if (month.length()==1) month="0"+month; + + return year+"-"+month+"-"+day+" "+hour+":"+min+":"+sec+"."+millisec; + } + + + /** Gets a "EEE, dd MMM yyyy hh:mm:ss 'GMT'" representation of a Date */ + public static String formatEEEddMMM(Date date) + { //DateFormat df=new SimpleDateFormat("EEE, dd MMM yyyy hh:mm:ss 'GMT'",Locale.US); + //return df.format(date); + /* + String str=date.toString(); // dow mon dd hh:mm:ss zzz yyyy + int len=str.length(); + String weekday=str.substring(0,3); + String month=str.substring(4,7); + String day=str.substring(8,10); + String time=str.substring(11,19); + String year=str.substring(len-4,len); + return weekday+", "+day+" "+month+" "+year+" "+time+" GMT"; + */ + Calendar cal=Calendar.getInstance(); + cal.setTime(date); + String weekday=WEEKDAYS[cal.get(Calendar.DAY_OF_WEEK)-1]; + String month=MONTHS[cal.get(Calendar.MONTH)]; + String year=Integer.toString(cal.get(Calendar.YEAR)); + String day=Integer.toString(cal.get(Calendar.DAY_OF_MONTH)); + String hour=Integer.toString(cal.get(Calendar.HOUR_OF_DAY)); + String min=Integer.toString(cal.get(Calendar.MINUTE)); + String sec=Integer.toString(cal.get(Calendar.SECOND)); + if (day.length()==1) day="0"+day; + if (hour.length()==1) hour="0"+hour; + if (min.length()==1) min="0"+min; + if (sec.length()==1) sec="0"+sec; + + return weekday+", "+day+" "+month+" "+year+" "+hour+":"+min+":"+sec+" GMT"; + } + + + /** Parses a String for a "EEE, dd MMM yyyy hh:mm:ss 'GMT'" formatted Date */ + public static Date parseEEEddMMM(String str, int index) + { //DateFormat df=new SimpleDateFormat("EEE, dd MMM yyyy hh:mm:ss 'GMT'",Locale.US); + //return df.format(date); + Calendar cal=Calendar.getInstance(); + char[] delim={ ' ', ',', ':' }; + Parser par=new Parser(str,index); + String EEE=par.getString(); // day of the week + int day=par.getInt(); // day of the month + String MMM=par.getString(); // month + int month=0; + for (; month<12; month++) if (MMM.equalsIgnoreCase(MONTHS[month])) break; + if (month==12) return null; // ERROR.. + // else + int year=par.getInt(); + int hour=Integer.parseInt(par.getWord(delim)); + int min=Integer.parseInt(par.getWord(delim)); + int sec=Integer.parseInt(par.getWord(delim)); + + cal.set(Calendar.YEAR,year); + cal.set(Calendar.MONTH,month); + cal.set(Calendar.DAY_OF_MONTH,day); + cal.set(Calendar.HOUR_OF_DAY,hour); + cal.set(Calendar.MINUTE,min); + cal.set(Calendar.SECOND,sec); + + return cal.getTime(); + } +} diff --git a/src/org/zoolu/tools/ExceptionPrinter.java b/src/org/zoolu/tools/ExceptionPrinter.java new file mode 100644 index 0000000..9c1311f --- /dev/null +++ b/src/org/zoolu/tools/ExceptionPrinter.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.tools; + + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + + +/** Class ExceptionPrinter just contains method getStackTrace(Exception e) + * for getting a String reporting the stack-trace and description of Exception e. + */ +public class ExceptionPrinter +{ + /** Gets the stack-trace and description of Exception e. + * @return It returns a String with the stack-trace and description of the Exception. */ + public static String getStackTraceOf(Exception e) + { //return e.toString(); + ByteArrayOutputStream err=new ByteArrayOutputStream(); + e.printStackTrace(new PrintStream(err)); + return err.toString(); + } +} + + diff --git a/src/org/zoolu/tools/HashSet.java b/src/org/zoolu/tools/HashSet.java new file mode 100644 index 0000000..7d50a4c --- /dev/null +++ b/src/org/zoolu/tools/HashSet.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.tools; + + +import java.util.Vector; + + +/** HashSet + */ +public class HashSet +{ + Vector set; + + public HashSet() + { set=new Vector(); + } + + public int size() + { return set.size(); + } + + public boolean isEmpty() + { return set.isEmpty(); + } + + public boolean add(Object o) + { set.addElement(o); + return true; + } + + public boolean remove(Object o) + { return set.removeElement(o); + } + + public boolean contains(Object o) + { return set.contains(o); + } + + public Iterator iterator() + { return new Iterator(set); + } +} + diff --git a/src/org/zoolu/tools/InnerTimer.java b/src/org/zoolu/tools/InnerTimer.java new file mode 100644 index 0000000..0d1cd0c --- /dev/null +++ b/src/org/zoolu/tools/InnerTimer.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.tools; + + +/** Class InnerTimer implements a separated-thread timer */ +class InnerTimer extends Thread +{ + long timeout; + InnerTimerListener listener; + + public InnerTimer(long timeout, InnerTimerListener listener) + { this.timeout=timeout; + this.listener=listener; + start(); + } + + public void run() + { if (listener!=null) + { try + { Thread.sleep(timeout); + listener.onInnerTimeout(); + } + catch (Exception e) { e.printStackTrace(); } + listener=null; + } + } +} diff --git a/src/org/zoolu/tools/InnerTimerListener.java b/src/org/zoolu/tools/InnerTimerListener.java new file mode 100644 index 0000000..2fddd1f --- /dev/null +++ b/src/org/zoolu/tools/InnerTimerListener.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.tools; + + +/** Listen for an InnerTimer */ +interface InnerTimerListener +{ + /** When the Timeout fires */ + void onInnerTimeout(); +} + diff --git a/src/org/zoolu/tools/InnerTimerST.java b/src/org/zoolu/tools/InnerTimerST.java new file mode 100644 index 0000000..13fe96d --- /dev/null +++ b/src/org/zoolu/tools/InnerTimerST.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.tools; + + +/** Class InnerTimerST implements a single-thread timer. + * The same thread is used for all instances of class InnerTimerST. */ +class InnerTimerST extends java.util.TimerTask +{ + static java.util.Timer single_timer=new java.util.Timer(true); + + //long timeout; + InnerTimerListener listener; + + public InnerTimerST(long timeout, InnerTimerListener listener) + { //this.timeout=timeout; + this.listener=listener; + single_timer.schedule(this,timeout); + } + + public void run() + { if (listener!=null) + { listener.onInnerTimeout(); + listener=null; + } + } +} + diff --git a/src/org/zoolu/tools/Iterator.java b/src/org/zoolu/tools/Iterator.java new file mode 100644 index 0000000..dad149d --- /dev/null +++ b/src/org/zoolu/tools/Iterator.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.tools; + + +import java.util.Vector; + + +/** Iterator + */ +public class Iterator +{ + Vector v; + int i; + + public Iterator(Vector vector) + { v=vector; + i=-1; + } + + public boolean hasNext() + { return i<(v.size()-1); + } + + public Object next() + { if (++i + * Every Log has a verboselevel associated with it; + * any log request with loglevel less or equal + * to the verbose-level is logged. + *
    Verbose level 0 indicates no log. The log levels should be greater than 0. + *

    + * Parameter logname, if non-null, is used as log header + * (i.e. written at the begin of each log row). + */ +public class Log +{ + + /******************************* Attributes *******************************/ + + /** (static) Default maximum log file size (4MB) */ + //public static final long MAX_SIZE=4096*1024; // 4MB + public static final long MAX_SIZE=1024*1024; // 1MB + + /** The log output stream */ + PrintStream out_stream; + + /** The log tag at the beginning of each log row */ + String log_tag; + + /** The log_level. + *

    Only messages with a level less or equal this log_level + * are effectively logged */ + int verbose_level; + + /** The maximum size of the log stream/file [bytes] + * Value -1 indicates no maximum size */ + long max_size; + + /** The size of the log tag (e.g. "MyLog: " has tag_size=7) */ + int tag_size; + + /** Whether messages are logged */ + boolean do_log; + + /** The char counter of the already logged data */ + long counter; + + + /****************************** Constructors ******************************/ + + /** Associates a new Log to the PrintStream outstream. + * Log size has no bound */ + public Log(PrintStream out_stream, String log_tag, int verbose_level) + { init(out_stream,log_tag,verbose_level,-1); + } + + /** Associates a new Log to the file filename. + * Log size is limited to the MAX_SIZE */ + public Log(String file_name, String log_tag, int verbose_level) + { PrintStream os=null; + if (verbose_level>0) + { try { os=new PrintStream(new FileOutputStream(file_name)); } catch (IOException e) { e.printStackTrace(); } + init(os,log_tag,verbose_level,MAX_SIZE); + } + } + + /** Associates a new Log to the file filename. + * Log size is limited to logsize [bytes] */ + public Log(String file_name, String log_tag, int verbose_level, long max_size) + { PrintStream os=null; + if (verbose_level>0) + { try { os=new PrintStream(new FileOutputStream(file_name)); } catch (IOException e) { e.printStackTrace(); } + init(os,log_tag,verbose_level,max_size); + } + else + { init(null,log_tag,0,0); + do_log=false; + } + } + + /** Associates a new Log to the file filename. + * Log size is limited to logsize [bytes] */ + public Log(String file_name, String log_tag, int verbose_level, long max_size, boolean append) + { PrintStream os=null; + if (verbose_level>0) + { try { os=new PrintStream(new FileOutputStream(file_name,append)); } catch (IOException e) { e.printStackTrace(); } + init(os,log_tag,verbose_level,max_size); + } + else + { init(null,log_tag,0,0); + do_log=false; + } + } + + + /**************************** Protected methods ****************************/ + + /** Initializes the log */ + protected void init(PrintStream out_stream, String log_tag, int verbose_level, long max_size) + { this.out_stream=out_stream; + this.log_tag=log_tag; + this.verbose_level=verbose_level; + this.max_size=max_size; + + if (log_tag!=null) tag_size=log_tag.length()+2; else tag_size=0; + do_log=true; + counter=0; + } + + /** Flushes */ + protected Log flush() + { if (verbose_level>0) out_stream.flush(); + return this; + } + + + /***************************** Public methods *****************************/ + + /** Closes the log */ + public void close() + { do_log=false; + out_stream.close(); + } + + /** Logs the Exception */ + public Log printException(Exception e, int level) + { //ByteArrayOutputStream err=new ByteArrayOutputStream(); + //e.printStackTrace(new PrintStream(err)); + //return println("Exception: "+err.toString(),level); + return println("Exception: "+ExceptionPrinter.getStackTraceOf(e),level); + } + + /** Logs the Exception.toString() and Exception.printStackTrace() */ + public Log printException(Exception e) + { return printException(e,1); + } + + /** Logs the packet timestamp */ + public Log printPacketTimestamp(String proto, String remote_addr, int remote_port, int len, String message, int level) + { String str=remote_addr+":"+remote_port+"/"+proto+" ("+len+" bytes)"; + if (message!=null) str+=": "+message; + println(DateFormat.formatHHMMSS(new Date())+", "+str,level); + return this; + } + + /** Prints the log if level isn't greater than the Log verbose_level */ + public Log println(String message, int level) + { return print(message+"\r\n",level).flush(); + } + + /** Prints the log if the Log verbose_level is greater than 0 */ + public Log println(String message) + { return println(message,1); + } + + /** Prints the log if the Log verbose_level is greater than 0 */ + public Log print(String message) + { return print(message,1); + } + + /** Prints the log if level isn't greater than the Log verbose_level */ + public Log print(String message, int level) + { if (do_log && level<=verbose_level) + { + if (log_tag!=null) out_stream.print(log_tag+": "+message); + else out_stream.print(message); + + if (max_size>=0) + { counter+=tag_size+message.length(); + if (counter>max_size) + { out_stream.println("\r\n----MAXIMUM LOG SIZE----\r\nSuccessive logs are lost."); + do_log=false; + } + } + } + return this; + } + +} diff --git a/src/org/zoolu/tools/LogLevel.java b/src/org/zoolu/tools/LogLevel.java new file mode 100644 index 0000000..89037b9 --- /dev/null +++ b/src/org/zoolu/tools/LogLevel.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.tools; + + +/** LogLevel provides a set of static log levels that can be used as default. + */ +public class LogLevel +{ + /** Default level for hight priority logs. */ + public static final int HIGH=1; + /** Default level for medium priority logs. */ + public static final int MEDIUM=3; + /** Default level for low priority logs. */ + public static final int LOW=5; + /** Default level for very low priority logs. */ + public static final int LOWER=9; +} diff --git a/src/org/zoolu/tools/MD5.java b/src/org/zoolu/tools/MD5.java new file mode 100644 index 0000000..5900f9d --- /dev/null +++ b/src/org/zoolu/tools/MD5.java @@ -0,0 +1,670 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.tools; + + + +/** MD5 hash algorithm. + *

    Implements the RSA Data Security, Inc. MD5 Message-Digest Algorithm. + * This is almoust straight implementation of the reference implementation + * given in RFC1321 by RSA. + */ +public class MD5 extends MessageDigest +{ + + // ********************** Mangle functions ********************** + + /** Rotates w, shifting n bits left. */ + //private static int rotateLeft(int w, int n) { return (w << n) | (w >>> (32-n)); } + + /** Rotates w, shifting n bits right. */ + //private static int rotateRight(int w, int n) { return (w >>> n) | (w << (32-n)); } + + /** Rotates an array of words, shifting 1 word left. */ + /*private static int[] rotateLeft(int[] w) + { int len=w.length; + int w1=w[len-1]; + for (int i=len-1; i>0; i--) w[i]=w[i-1]; + w[0]=w1; + return w; + }*/ + + /** Rotates an array of words, shifting 1 word right. */ + /*private static int[] rotateRight(int[] w) + { int len=w.length; + int w0=w[0]; + for (int i=1; isrc into array dst with offset offset */ + //public static void copyBytes(byte[] src, byte[] dst, int offset) { for (int k=0; klen bytes of array src into array dst with offset offset */ + //public static void copyBytes(byte[] src, byte[] dst, int offset, int len) { for (int k=0; k>24); + b[2]=(byte)((n>>16)%256); + b[1]=(byte)((n>>8)%256); + b[0]=(byte)(n%256); + return b; + }*/ + + + // ************************* Attributes ************************* + + /** The digest */ + byte[] message_digest; + + /** byte counter mod 2^64 */ + long count; + + /** 128bit state (A,B,C, and D words) */ + int state[]; + + /** 64B (512 bits) chunk of the input message */ + byte block[]; + + /** Number of bytes remained into the chunk */ + int block_offset; + + /** Padding */ + static byte zeropadding[]= { (byte)0x80, 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, 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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + + + /* Constants for MD5 transformation */ + /* + static final int S11=7; + static final int S12=12; + static final int S13=17; + static final int S14=22; + static final int S21=5; + static final int S22=9; + static final int S23=14; + static final int S24=20; + static final int S31=4; + static final int S32=11; + static final int S33=16; + static final int S34=23; + static final int S41=6; + static final int S42=10; + static final int S43=15; + static final int S44=21; + */ + + + // *********************** Public methods *********************** + + /** Constructor */ + public MD5() + { init(); + } + + + /** Constructor */ + public MD5(byte[] buffer) + { init(); + update(buffer); + } + + + /** Constructor */ + public MD5(byte[] buffer, int offset, int len) + { init(); + update(buffer,offset,len); + } + + + /** Constructor */ + public MD5(String str) + { init(); + update(str); + } + + + /** Inits the MD5 */ + private void init() + { count=0; + // init the first block + block=new byte[64]; + block_offset=0; + // load magic initialization constants + state=new int[4]; + state[0]=0x67452301; + state[1]=0xefcdab89; + state[2]=0x98badcfe; + state[3]=0x10325476; + + message_digest=null; + } + + + /** MessageDigest block update operation. + * Continues a message-digest operation, + * processing another message block, and updating the context. */ + /*public MD5 update(String str) + { byte[] buf=str.getBytes(); + return update(buf,0,buf.length); + }*/ + + + /** MessageDigest block update operation. + * Continues a message-digest operation, + * processing another message block, and updating the context. */ + /*public MD5 update(byte[] buffer) + { return update(buffer,0,buffer.length); + }*/ + + + /** MessageDigest block update operation. + * Continues a message-digest operation, + * processing another message block, and updating the context. */ + public MessageDigest update(byte[] buffer, int offset, int len) + { + if (message_digest!=null) return this; + //else + + count+=len; + + // num of remaining bytes to be processed + int size=block.length-block_offset; + + while(len>=size) + { // fill a block + for (int i=0; i>=8; } + + // process the last chunk, i.e. zero-pading(1-64B) + length-field(8B) (1 or 2 blocks) + update(zeropadding,0,npad); + update(len_field,0,8); + + message_digest=new byte[16]; + // convert 4 words to 16 bytes + //for (int i=0; i<4; i++) { copyBytes(wordToBytes(state[i]),message_digest,i*4); } + int k=0; + for (int i=0; i<4; i++) + { message_digest[k++]=(byte)((state[i]) & 0xff); + message_digest[k++]=(byte)((state[i]>>>8) & 0xff); + message_digest[k++]=(byte)((state[i]>>>16) & 0xff); + message_digest[k++]=(byte)((state[i]>>>24) & 0xff); + } + return message_digest; + } + + + /** MD5 basic transformation. Transforms state based on block. */ + private static void transform(int[] state, byte[] block) + { + int a=state[0]; + int b=state[1]; + int c=state[2]; + int d=state[3]; + + int[] x=new int[16]; + x[0]=((int) (block[0] & 0xff)) | + (((int) (block[1] & 0xff)) << 8) | + (((int) (block[2] & 0xff)) << 16) | + (((int) (block[3])) << 24); + x[1]=((int) (block[4] & 0xff)) | + (((int) (block[5] & 0xff)) << 8) | + (((int) (block[6] & 0xff)) << 16) | + (((int) (block[7])) << 24); + x[2]=((int) (block[8] & 0xff)) | + (((int) (block[9] & 0xff)) << 8) | + (((int) (block[10] & 0xff)) << 16) | + (((int) (block[11])) << 24); + x[3]=((int) (block[12] & 0xff)) | + (((int) (block[13] & 0xff)) << 8) | + (((int) (block[14] & 0xff)) << 16) | + (((int) (block[15])) << 24); + x[4]=((int) (block[16] & 0xff)) | + (((int) (block[17] & 0xff)) << 8) | + (((int) (block[18] & 0xff)) << 16) | + (((int) (block[19])) << 24); + x[5]=((int) (block[20] & 0xff)) | + (((int) (block[21] & 0xff)) << 8) | + (((int) (block[22] & 0xff)) << 16) | + (((int) (block[23])) << 24); + x[6]=((int) (block[24] & 0xff)) | + (((int) (block[25] & 0xff)) << 8) | + (((int) (block[26] & 0xff)) << 16) | + (((int) (block[27])) << 24); + x[7]=((int) (block[28] & 0xff)) | + (((int) (block[29] & 0xff)) << 8) | + (((int) (block[30] & 0xff)) << 16) | + (((int) (block[31])) << 24); + x[8]=((int) (block[32] & 0xff)) | + (((int) (block[33] & 0xff)) << 8) | + (((int) (block[34] & 0xff)) << 16) | + (((int) (block[35])) << 24); + x[9]=((int) (block[36] & 0xff)) | + (((int) (block[37] & 0xff)) << 8) | + (((int) (block[38] & 0xff)) << 16) | + (((int) (block[39])) << 24); + x[10]=((int)(block[40] & 0xff)) | + (((int) (block[41] & 0xff)) << 8) | + (((int) (block[42] & 0xff)) << 16) | + (((int) (block[43])) << 24); + x[11]=((int)(block[44] & 0xff)) | + (((int) (block[45] & 0xff)) << 8) | + (((int) (block[46] & 0xff)) << 16) | + (((int) (block[47])) << 24); + x[12]=((int)(block[48] & 0xff)) | + (((int) (block[49] & 0xff)) << 8) | + (((int) (block[50] & 0xff)) << 16) | + (((int) (block[51])) << 24); + x[13]=((int)(block[52] & 0xff)) | + (((int) (block[53] & 0xff)) << 8) | + (((int) (block[54] & 0xff)) << 16) | + (((int) (block[55])) << 24); + x[14]=((int)(block[56] & 0xff)) | + (((int) (block[57] & 0xff)) << 8) | + (((int) (block[58] & 0xff)) << 16) | + (((int) (block[59])) << 24); + x[15]=((int)(block[60] & 0xff)) | + (((int) (block[61] & 0xff)) << 8) | + (((int) (block[62] & 0xff)) << 16) | + (((int) (block[63])) << 24); + + /* Round 1 */ + a+= ((b & c) | (~b & d)) + x[ 0] + 0xd76aa478; + a = ((a << 7) | (a >>> 25)) + b; + d+= ((a & b) | (~a & c)) + x[ 1] + 0xe8c7b756; + d = ((d << 12) | (d >>> 20)) + a; + c+= ((d & a) | (~d & b)) + x[ 2] + 0x242070db; + c = ((c << 17) | (c >>> 15)) + d; + b+= ((c & d) | (~c & a)) + x[ 3] + 0xc1bdceee; + b = ((b << 22) | (b >>> 10)) + c; + + a+= ((b & c) | (~b & d)) + x[ 4] + 0xf57c0faf; + a = ((a << 7) | (a >>> 25)) + b; + d+= ((a & b) | (~a & c)) + x[ 5] + 0x4787c62a; + d = ((d << 12) | (d >>> 20)) + a; + c+= ((d & a) | (~d & b)) + x[ 6] + 0xa8304613; + c = ((c << 17) | (c >>> 15)) + d; + b+= ((c & d) | (~c & a)) + x[ 7] + 0xfd469501; + b = ((b << 22) | (b >>> 10)) + c; + + a+= ((b & c) | (~b & d)) + x[ 8] + 0x698098d8; + a = ((a << 7) | (a >>> 25)) + b; + d+= ((a & b) | (~a & c)) + x[ 9] + 0x8b44f7af; + d = ((d << 12) | (d >>> 20)) + a; + c+= ((d & a) | (~d & b)) + x[10] + 0xffff5bb1; + c = ((c << 17) | (c >>> 15)) + d; + b+= ((c & d) | (~c & a)) + x[11] + 0x895cd7be; + b = ((b << 22) | (b >>> 10)) + c; + + a+= ((b & c) | (~b & d)) + x[12] + 0x6b901122; + a = ((a << 7) | (a >>> 25)) + b; + d+= ((a & b) | (~a & c)) + x[13] + 0xfd987193; + d = ((d << 12) | (d >>> 20)) + a; + c+= ((d & a) | (~d & b)) + x[14] + 0xa679438e; + c = ((c << 17) | (c >>> 15)) + d; + b+= ((c & d) | (~c & a)) + x[15] + 0x49b40821; + b = ((b << 22) | (b >>> 10)) + c; + + + /* Round 2 */ + a+= ((b & d) | (c & ~d)) + x[ 1] + 0xf61e2562; + a = ((a << 5) | (a >>> 27)) + b; + d+= ((a & c) | (b & ~c)) + x[ 6] + 0xc040b340; + d = ((d << 9) | (d >>> 23)) + a; + c+= ((d & b) | (a & ~b)) + x[11] + 0x265e5a51; + c = ((c << 14) | (c >>> 18)) + d; + b+= ((c & a) | (d & ~a)) + x[ 0] + 0xe9b6c7aa; + b = ((b << 20) | (b >>> 12)) + c; + + a+= ((b & d) | (c & ~d)) + x[ 5] + 0xd62f105d; + a = ((a << 5) | (a >>> 27)) + b; + d+= ((a & c) | (b & ~c)) + x[10] + 0x02441453; + d = ((d << 9) | (d >>> 23)) + a; + c+= ((d & b) | (a & ~b)) + x[15] + 0xd8a1e681; + c = ((c << 14) | (c >>> 18)) + d; + b+= ((c & a) | (d & ~a)) + x[ 4] + 0xe7d3fbc8; + b = ((b << 20) | (b >>> 12)) + c; + + a+= ((b & d) | (c & ~d)) + x[ 9] + 0x21e1cde6; + a = ((a << 5) | (a >>> 27)) + b; + d+= ((a & c) | (b & ~c)) + x[14] + 0xc33707d6; + d = ((d << 9) | (d >>> 23)) + a; + c+= ((d & b) | (a & ~b)) + x[ 3] + 0xf4d50d87; + c = ((c << 14) | (c >>> 18)) + d; + b+= ((c & a) | (d & ~a)) + x[ 8] + 0x455a14ed; + b = ((b << 20) | (b >>> 12)) + c; + + a+= ((b & d) | (c & ~d)) + x[13] + 0xa9e3e905; + a = ((a << 5) | (a >>> 27)) + b; + d+= ((a & c) | (b & ~c)) + x[ 2] + 0xfcefa3f8; + d = ((d << 9) | (d >>> 23)) + a; + c+= ((d & b) | (a & ~b)) + x[ 7] + 0x676f02d9; + c = ((c << 14) | (c >>> 18)) + d; + b+= ((c & a) | (d & ~a)) + x[12] + 0x8d2a4c8a; + b = ((b << 20) | (b >>> 12)) + c; + + + /* Round 3 */ + a+= (b ^ c ^ d) + x[ 5] + 0xfffa3942; + a = ((a << 4) | (a >>> 28)) + b; + d+= (a ^ b ^ c) + x[ 8] + 0x8771f681; + d = ((d << 11) | (d >>> 21)) + a; + c+= (d ^ a ^ b) + x[11] + 0x6d9d6122; + c = ((c << 16) | (c >>> 16)) + d; + b+= (c ^ d ^ a) + x[14] + 0xfde5380c; + b = ((b << 23) | (b >>> 9)) + c; + + a+= (b ^ c ^ d) + x[ 1] + 0xa4beea44; + a = ((a << 4) | (a >>> 28)) + b; + d+= (a ^ b ^ c) + x[ 4] + 0x4bdecfa9; + d = ((d << 11) | (d >>> 21)) + a; + c+= (d ^ a ^ b) + x[ 7] + 0xf6bb4b60; + c = ((c << 16) | (c >>> 16)) + d; + b+= (c ^ d ^ a) + x[10] + 0xbebfbc70; + b = ((b << 23) | (b >>> 9)) + c; + + a+= (b ^ c ^ d) + x[13] + 0x289b7ec6; + a = ((a << 4) | (a >>> 28)) + b; + d+= (a ^ b ^ c) + x[ 0] + 0xeaa127fa; + d = ((d << 11) | (d >>> 21)) + a; + c+= (d ^ a ^ b) + x[ 3] + 0xd4ef3085; + c = ((c << 16) | (c >>> 16)) + d; + b+= (c ^ d ^ a) + x[ 6] + 0x04881d05; + b = ((b << 23) | (b >>> 9)) + c; + + a+= (b ^ c ^ d) + x[ 9] + 0xd9d4d039; + a = ((a << 4) | (a >>> 28)) + b; + d+= (a ^ b ^ c) + x[12] + 0xe6db99e5; + d = ((d << 11) | (d >>> 21)) + a; + c+= (d ^ a ^ b) + x[15] + 0x1fa27cf8; + c = ((c << 16) | (c >>> 16)) + d; + b+= (c ^ d ^ a) + x[ 2] + 0xc4ac5665; + b = ((b << 23) | (b >>> 9)) + c; + + + /* Round 4 */ + a+= (c ^ (b | ~d)) + x[ 0] + 0xf4292244; + a = ((a << 6) | (a >>> 26)) + b; + d+= (b ^ (a | ~c)) + x[ 7] + 0x432aff97; + d = ((d << 10) | (d >>> 22)) + a; + c+= (a ^ (d | ~b)) + x[14] + 0xab9423a7; + c = ((c << 15) | (c >>> 17)) + d; + b+= (d ^ (c | ~a)) + x[ 5] + 0xfc93a039; + b = ((b << 21) | (b >>> 11)) + c; + + a+= (c ^ (b | ~d)) + x[12] + 0x655b59c3; + a = ((a << 6) | (a >>> 26)) + b; + d+= (b ^ (a | ~c)) + x[ 3] + 0x8f0ccc92; + d = ((d << 10) | (d >>> 22)) + a; + c+= (a ^ (d | ~b)) + x[10] + 0xffeff47d; + c = ((c << 15) | (c >>> 17)) + d; + b+= (d ^ (c | ~a)) + x[ 1] + 0x85845dd1; + b = ((b << 21) | (b >>> 11)) + c; + + a+= (c ^ (b | ~d)) + x[ 8] + 0x6fa87e4f; + a = ((a << 6) | (a >>> 26)) + b; + d+= (b ^ (a | ~c)) + x[15] + 0xfe2ce6e0; + d = ((d << 10) | (d >>> 22)) + a; + c+= (a ^ (d | ~b)) + x[ 6] + 0xa3014314; + c = ((c << 15) | (c >>> 17)) + d; + b+= (d ^ (c | ~a)) + x[13] + 0x4e0811a1; + b = ((b << 21) | (b >>> 11)) + c; + + a+= (c ^ (b | ~d)) + x[ 4] + 0xf7537e82; + a = ((a << 6) | (a >>> 26)) + b; + d+= (b ^ (a | ~c)) + x[11] + 0xbd3af235; + d = ((d << 10) | (d >>> 22)) + a; + c+= (a ^ (d | ~b)) + x[ 2] + 0x2ad7d2bb; + c = ((c << 15) | (c >>> 17)) + d; + b+= (d ^ (c | ~a)) + x[ 9] + 0xeb86d391; + b = ((b << 21) | (b >>> 11)) + c; + + state[0]+= a; + state[1]+= b; + state[2]+= c; + state[3]+= d; + } + + + /** F MD5 function. */ + //private static int F(int x, int y, int z) { return (x & y) | ((~x) & z); } + /** G MD5 function. */ + //private static int G(int x, int y, int z) { return (x & z) | (y & (~z)); } + /** H MD5 function. */ + //private static int H(int x, int y, int z) { return (x ^ y ^ z); } + /** I MD5 function. */ + //private static int I(int x, int y, int z) { return y ^ (x | (~z)); } + + + /** FF transformation for round 1. + Rotation is separate from addition to prevent recomputation. */ + /*private static int[] FF(int[] w, int x, int s, int t) + { w[0] += F(w[1],w[2],w[3]) + x + t; + w[0] = rotateLeft(w[0],s) + w[1]; + return rotateLeft(w); + }*/ + /** GG transformation for round 2. + Rotation is separate from addition to prevent recomputation. */ + /*private static int[] GG(int[] w, int x, int s, int t) + { w[0] += G(w[1],w[2],w[3]) + x + t; + w[0] = rotateLeft(w[0],s) + w[1]; + return rotateLeft(w); + }* + /** HH transformation for round 3. + Rotation is separate from addition to prevent recomputation. */ + /*private static int[] HH(int[] w, int x, int s, int t) + { w[0] += H(w[1],w[2],w[3]) + x + t; + w[0] = rotateLeft(w[0],s) + w[1]; + return rotateLeft(w); + }* + /** II transformation for round 4. + Rotation is separate from addition to prevent recomputation. */ + /*private static int[] II(int[] w, int x, int s, int t) + { w[0] += I(w[1],w[2],w[3]) + x + t; + w[0] = rotateLeft(w[0],s) + w[1]; + return rotateLeft(w); + }*/ + + + /** MD5 basic transformation. Transforms state based on block. */ + /*private static void transform(int[] state, byte[] block) + { + int[] x=new int[16]; + for (int i=0; i<16; i++) x[i]=(int)bytesToWord(block,(i*4)); + + // make a copy of the state + int[] state_cp=new int[4]; + for (int i=0; i<4; i++) state_cp[i]=state[i]; + + // Round 1 + FF (state, x[ 0], S11, 0xd76aa478); + FF (state, x[ 1], S12, 0xe8c7b756); + FF (state, x[ 2], S13, 0x242070db); + FF (state, x[ 3], S14, 0xc1bdceee); + FF (state, x[ 4], S11, 0xf57c0faf); + FF (state, x[ 5], S12, 0x4787c62a); + FF (state, x[ 6], S13, 0xa8304613); + FF (state, x[ 7], S14, 0xfd469501); + FF (state, x[ 8], S11, 0x698098d8); + FF (state, x[ 9], S12, 0x8b44f7af); + FF (state, x[10], S13, 0xffff5bb1); + FF (state, x[11], S14, 0x895cd7be); + FF (state, x[12], S11, 0x6b901122); + FF (state, x[13], S12, 0xfd987193); + FF (state, x[14], S13, 0xa679438e); + FF (state, x[15], S14, 0x49b40821); + + // Round 2 + GG (state, x[ 1], S21, 0xf61e2562); + GG (state, x[ 6], S22, 0xc040b340); + GG (state, x[11], S23, 0x265e5a51); + GG (state, x[ 0], S24, 0xe9b6c7aa); + GG (state, x[ 5], S21, 0xd62f105d); + GG (state, x[10], S22, 0x2441453); + GG (state, x[15], S23, 0xd8a1e681); + GG (state, x[ 4], S24, 0xe7d3fbc8); + GG (state, x[ 9], S21, 0x21e1cde6); + GG (state, x[14], S22, 0xc33707d6); + GG (state, x[ 3], S23, 0xf4d50d87); + GG (state, x[ 8], S24, 0x455a14ed); + GG (state, x[13], S21, 0xa9e3e905); + GG (state, x[ 2], S22, 0xfcefa3f8); + GG (state, x[ 7], S23, 0x676f02d9); + GG (state, x[12], S24, 0x8d2a4c8a); + + // Round 3 + HH (state, x[ 5], S31, 0xfffa3942); + HH (state, x[ 8], S32, 0x8771f681); + HH (state, x[11], S33, 0x6d9d6122); + HH (state, x[14], S34, 0xfde5380c); + HH (state, x[ 1], S31, 0xa4beea44); + HH (state, x[ 4], S32, 0x4bdecfa9); + HH (state, x[ 7], S33, 0xf6bb4b60); + HH (state, x[10], S34, 0xbebfbc70); + HH (state, x[13], S31, 0x289b7ec6); + HH (state, x[ 0], S32, 0xeaa127fa); + HH (state, x[ 3], S33, 0xd4ef3085); + HH (state, x[ 6], S34, 0x4881d05); + HH (state, x[ 9], S31, 0xd9d4d039); + HH (state, x[12], S32, 0xe6db99e5); + HH (state, x[15], S33, 0x1fa27cf8); + HH (state, x[ 2], S34, 0xc4ac5665); + + // Round 4 + II (state, x[ 0], S41, 0xf4292244); + II (state, x[ 7], S42, 0x432aff97); + II (state, x[14], S43, 0xab9423a7); + II (state, x[ 5], S44, 0xfc93a039); + II (state, x[12], S41, 0x655b59c3); + II (state, x[ 3], S42, 0x8f0ccc92); + II (state, x[10], S43, 0xffeff47d); + II (state, x[ 1], S44, 0x85845dd1); + II (state, x[ 8], S41, 0x6fa87e4f); + II (state, x[15], S42, 0xfe2ce6e0); + II (state, x[ 6], S43, 0xa3014314); + II (state, x[13], S44, 0x4e0811a1); + II (state, x[ 4], S41, 0xf7537e82); + II (state, x[11], S42, 0xbd3af235); + II (state, x[ 2], S43, 0x2ad7d2bb); + II (state, x[ 9], S44, 0xeb86d391); + + for (int i=0; i<4; i++) state[i]+=state_cp[i]; + }*/ + + + + /** Calculates the MD5. */ + public static byte[] digest(byte[] buffer, int offset, int len) + { MD5 md5=new MD5(buffer,offset,len); + return md5.doFinal(); + } + + + /** Calculates the MD5. */ + public static byte[] digest(byte[] buffer) + { return digest(buffer,0,buffer.length); + } + + + /** Calculates the MD5. */ + public static byte[] digest(String str) + { MD5 md5=new MD5(str); + return md5.doFinal(); + } + + + /** Gets the message-digest as string of hex values */ + /*public String asHex() + { return asHex(doFinal()); + }*/ + + + /** Transforms an array of bytes into a string of hex values */ + /*public static String asHex(byte[] buf) + { String str=new String(); + for (int i=0; i>>4)&0x0F); + str+=Integer.toHexString(buf[i]&0x0F); + } + return str; + }*/ + +} diff --git a/src/org/zoolu/tools/MD5.java.saved b/src/org/zoolu/tools/MD5.java.saved new file mode 100644 index 0000000..b68fac1 --- /dev/null +++ b/src/org/zoolu/tools/MD5.java.saved @@ -0,0 +1,670 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.tools; + + + +/** MD5 hash algorithm. + *

    Implements the RSA Data Security, Inc. MD5 Message-Digest Algorithm. + * This is almoust straight implementation of the reference implementation + * given in RFC1321 by RSA. + */ +public class MD5 +{ + + // ********************** Mangle functions ********************** + + /** Rotates w, shifting n bits left. */ + //private static int rotateLeft(int w, int n) { return (w << n) | (w >>> (32-n)); } + + /** Rotates w, shifting n bits right. */ + //private static int rotateRight(int w, int n) { return (w >>> n) | (w << (32-n)); } + + /** Rotates an array of words, shifting 1 word left. */ + /*private static int[] rotateLeft(int[] w) + { int len=w.length; + int w1=w[len-1]; + for (int i=len-1; i>0; i--) w[i]=w[i-1]; + w[0]=w1; + return w; + }*/ + + /** Rotates an array of words, shifting 1 word right. */ + /*private static int[] rotateRight(int[] w) + { int len=w.length; + int w0=w[0]; + for (int i=1; isrc into array dst with offset offset */ + //public static void copyBytes(byte[] src, byte[] dst, int offset) { for (int k=0; klen bytes of array src into array dst with offset offset */ + //public static void copyBytes(byte[] src, byte[] dst, int offset, int len) { for (int k=0; k>24); + b[2]=(byte)((n>>16)%256); + b[1]=(byte)((n>>8)%256); + b[0]=(byte)(n%256); + return b; + }*/ + + + // ************************* Attributes ************************* + + /** The digest */ + byte[] message_digest; + + /** byte counter mod 2^64 */ + long count; + + /** 128bit state (A,B,C, and D words) */ + int state[]; + + /** 64B (512 bits) chunk of the input message */ + byte block[]; + + /** Number of bytes remained into the chunk */ + int block_offset; + + /** Padding */ + static byte zeropadding[]= { (byte)0x80, 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, 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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + + + /* Constants for MD5 transformation */ + /* + static final int S11=7; + static final int S12=12; + static final int S13=17; + static final int S14=22; + static final int S21=5; + static final int S22=9; + static final int S23=14; + static final int S24=20; + static final int S31=4; + static final int S32=11; + static final int S33=16; + static final int S34=23; + static final int S41=6; + static final int S42=10; + static final int S43=15; + static final int S44=21; + */ + + + // *********************** Public methods *********************** + + /** Constructor */ + public MD5() + { init(); + } + + + /** Constructor */ + public MD5(byte[] buffer) + { init(); + update(buffer); + } + + + /** Constructor */ + public MD5(byte[] buffer, int offset, int len) + { init(); + update(buffer,offset,len); + } + + + /** Constructor */ + public MD5(String str) + { init(); + update(str); + } + + + /** Inits the MD5 */ + private void init() + { count=0; + // init the first block + block=new byte[64]; + block_offset=0; + // load magic initialization constants + state=new int[4]; + state[0]=0x67452301; + state[1]=0xefcdab89; + state[2]=0x98badcfe; + state[3]=0x10325476; + + message_digest=null; + } + + + /** MD5 block update operation. + * Continues a MD5 message-digest operation, + * processing another message block, and updating the context. */ + public MD5 update(String str) + { byte[] buf=str.getBytes(); + return update(buf,0,buf.length); + } + + + /** MD5 block update operation. + * Continues a MD5 message-digest operation, + * processing another message block, and updating the context. */ + public MD5 update(byte[] buffer) + { return update(buffer,0,buffer.length); + } + + + /** MD5 block update operation. + * Continues a MD5 message-digest operation, + * processing another message block, and updating the context. */ + public MD5 update(byte[] buffer, int offset, int len) + { + if (message_digest!=null) return this; + //else + + count+=len; + + // num of remaining bytes to be processed + int size=block.length-block_offset; + + while(len>=size) + { // fill a block + for (int i=0; i>=8; } + + // process the last chunk, i.e. zero-pading(1-64B) + length-field(8B) (1 or 2 blocks) + update(zeropadding,0,npad); + update(len_field,0,8); + + message_digest=new byte[16]; + // convert 4 words to 16 bytes + //for (int i=0; i<4; i++) { copyBytes(wordToBytes(state[i]),message_digest,i*4); } + int k=0; + for (int i=0; i<4; i++) + { message_digest[k++]=(byte)((state[i]) & 0xff); + message_digest[k++]=(byte)((state[i]>>>8) & 0xff); + message_digest[k++]=(byte)((state[i]>>>16) & 0xff); + message_digest[k++]=(byte)((state[i]>>>24) & 0xff); + } + return message_digest; + } + + + /** MD5 basic transformation. Transforms state based on block. */ + private static void transform(int[] state, byte[] block) + { + int a=state[0]; + int b=state[1]; + int c=state[2]; + int d=state[3]; + + int[] x=new int[16]; + x[0]=((int) (block[0] & 0xff)) | + (((int) (block[1] & 0xff)) << 8) | + (((int) (block[2] & 0xff)) << 16) | + (((int) (block[3])) << 24); + x[1]=((int) (block[4] & 0xff)) | + (((int) (block[5] & 0xff)) << 8) | + (((int) (block[6] & 0xff)) << 16) | + (((int) (block[7])) << 24); + x[2]=((int) (block[8] & 0xff)) | + (((int) (block[9] & 0xff)) << 8) | + (((int) (block[10] & 0xff)) << 16) | + (((int) (block[11])) << 24); + x[3]=((int) (block[12] & 0xff)) | + (((int) (block[13] & 0xff)) << 8) | + (((int) (block[14] & 0xff)) << 16) | + (((int) (block[15])) << 24); + x[4]=((int) (block[16] & 0xff)) | + (((int) (block[17] & 0xff)) << 8) | + (((int) (block[18] & 0xff)) << 16) | + (((int) (block[19])) << 24); + x[5]=((int) (block[20] & 0xff)) | + (((int) (block[21] & 0xff)) << 8) | + (((int) (block[22] & 0xff)) << 16) | + (((int) (block[23])) << 24); + x[6]=((int) (block[24] & 0xff)) | + (((int) (block[25] & 0xff)) << 8) | + (((int) (block[26] & 0xff)) << 16) | + (((int) (block[27])) << 24); + x[7]=((int) (block[28] & 0xff)) | + (((int) (block[29] & 0xff)) << 8) | + (((int) (block[30] & 0xff)) << 16) | + (((int) (block[31])) << 24); + x[8]=((int) (block[32] & 0xff)) | + (((int) (block[33] & 0xff)) << 8) | + (((int) (block[34] & 0xff)) << 16) | + (((int) (block[35])) << 24); + x[9]=((int) (block[36] & 0xff)) | + (((int) (block[37] & 0xff)) << 8) | + (((int) (block[38] & 0xff)) << 16) | + (((int) (block[39])) << 24); + x[10]=((int)(block[40] & 0xff)) | + (((int) (block[41] & 0xff)) << 8) | + (((int) (block[42] & 0xff)) << 16) | + (((int) (block[43])) << 24); + x[11]=((int)(block[44] & 0xff)) | + (((int) (block[45] & 0xff)) << 8) | + (((int) (block[46] & 0xff)) << 16) | + (((int) (block[47])) << 24); + x[12]=((int)(block[48] & 0xff)) | + (((int) (block[49] & 0xff)) << 8) | + (((int) (block[50] & 0xff)) << 16) | + (((int) (block[51])) << 24); + x[13]=((int)(block[52] & 0xff)) | + (((int) (block[53] & 0xff)) << 8) | + (((int) (block[54] & 0xff)) << 16) | + (((int) (block[55])) << 24); + x[14]=((int)(block[56] & 0xff)) | + (((int) (block[57] & 0xff)) << 8) | + (((int) (block[58] & 0xff)) << 16) | + (((int) (block[59])) << 24); + x[15]=((int)(block[60] & 0xff)) | + (((int) (block[61] & 0xff)) << 8) | + (((int) (block[62] & 0xff)) << 16) | + (((int) (block[63])) << 24); + + /* Round 1 */ + a+= ((b & c) | (~b & d)) + x[ 0] + 0xd76aa478; + a = ((a << 7) | (a >>> 25)) + b; + d+= ((a & b) | (~a & c)) + x[ 1] + 0xe8c7b756; + d = ((d << 12) | (d >>> 20)) + a; + c+= ((d & a) | (~d & b)) + x[ 2] + 0x242070db; + c = ((c << 17) | (c >>> 15)) + d; + b+= ((c & d) | (~c & a)) + x[ 3] + 0xc1bdceee; + b = ((b << 22) | (b >>> 10)) + c; + + a+= ((b & c) | (~b & d)) + x[ 4] + 0xf57c0faf; + a = ((a << 7) | (a >>> 25)) + b; + d+= ((a & b) | (~a & c)) + x[ 5] + 0x4787c62a; + d = ((d << 12) | (d >>> 20)) + a; + c+= ((d & a) | (~d & b)) + x[ 6] + 0xa8304613; + c = ((c << 17) | (c >>> 15)) + d; + b+= ((c & d) | (~c & a)) + x[ 7] + 0xfd469501; + b = ((b << 22) | (b >>> 10)) + c; + + a+= ((b & c) | (~b & d)) + x[ 8] + 0x698098d8; + a = ((a << 7) | (a >>> 25)) + b; + d+= ((a & b) | (~a & c)) + x[ 9] + 0x8b44f7af; + d = ((d << 12) | (d >>> 20)) + a; + c+= ((d & a) | (~d & b)) + x[10] + 0xffff5bb1; + c = ((c << 17) | (c >>> 15)) + d; + b+= ((c & d) | (~c & a)) + x[11] + 0x895cd7be; + b = ((b << 22) | (b >>> 10)) + c; + + a+= ((b & c) | (~b & d)) + x[12] + 0x6b901122; + a = ((a << 7) | (a >>> 25)) + b; + d+= ((a & b) | (~a & c)) + x[13] + 0xfd987193; + d = ((d << 12) | (d >>> 20)) + a; + c+= ((d & a) | (~d & b)) + x[14] + 0xa679438e; + c = ((c << 17) | (c >>> 15)) + d; + b+= ((c & d) | (~c & a)) + x[15] + 0x49b40821; + b = ((b << 22) | (b >>> 10)) + c; + + + /* Round 2 */ + a+= ((b & d) | (c & ~d)) + x[ 1] + 0xf61e2562; + a = ((a << 5) | (a >>> 27)) + b; + d+= ((a & c) | (b & ~c)) + x[ 6] + 0xc040b340; + d = ((d << 9) | (d >>> 23)) + a; + c+= ((d & b) | (a & ~b)) + x[11] + 0x265e5a51; + c = ((c << 14) | (c >>> 18)) + d; + b+= ((c & a) | (d & ~a)) + x[ 0] + 0xe9b6c7aa; + b = ((b << 20) | (b >>> 12)) + c; + + a+= ((b & d) | (c & ~d)) + x[ 5] + 0xd62f105d; + a = ((a << 5) | (a >>> 27)) + b; + d+= ((a & c) | (b & ~c)) + x[10] + 0x02441453; + d = ((d << 9) | (d >>> 23)) + a; + c+= ((d & b) | (a & ~b)) + x[15] + 0xd8a1e681; + c = ((c << 14) | (c >>> 18)) + d; + b+= ((c & a) | (d & ~a)) + x[ 4] + 0xe7d3fbc8; + b = ((b << 20) | (b >>> 12)) + c; + + a+= ((b & d) | (c & ~d)) + x[ 9] + 0x21e1cde6; + a = ((a << 5) | (a >>> 27)) + b; + d+= ((a & c) | (b & ~c)) + x[14] + 0xc33707d6; + d = ((d << 9) | (d >>> 23)) + a; + c+= ((d & b) | (a & ~b)) + x[ 3] + 0xf4d50d87; + c = ((c << 14) | (c >>> 18)) + d; + b+= ((c & a) | (d & ~a)) + x[ 8] + 0x455a14ed; + b = ((b << 20) | (b >>> 12)) + c; + + a+= ((b & d) | (c & ~d)) + x[13] + 0xa9e3e905; + a = ((a << 5) | (a >>> 27)) + b; + d+= ((a & c) | (b & ~c)) + x[ 2] + 0xfcefa3f8; + d = ((d << 9) | (d >>> 23)) + a; + c+= ((d & b) | (a & ~b)) + x[ 7] + 0x676f02d9; + c = ((c << 14) | (c >>> 18)) + d; + b+= ((c & a) | (d & ~a)) + x[12] + 0x8d2a4c8a; + b = ((b << 20) | (b >>> 12)) + c; + + + /* Round 3 */ + a+= (b ^ c ^ d) + x[ 5] + 0xfffa3942; + a = ((a << 4) | (a >>> 28)) + b; + d+= (a ^ b ^ c) + x[ 8] + 0x8771f681; + d = ((d << 11) | (d >>> 21)) + a; + c+= (d ^ a ^ b) + x[11] + 0x6d9d6122; + c = ((c << 16) | (c >>> 16)) + d; + b+= (c ^ d ^ a) + x[14] + 0xfde5380c; + b = ((b << 23) | (b >>> 9)) + c; + + a+= (b ^ c ^ d) + x[ 1] + 0xa4beea44; + a = ((a << 4) | (a >>> 28)) + b; + d+= (a ^ b ^ c) + x[ 4] + 0x4bdecfa9; + d = ((d << 11) | (d >>> 21)) + a; + c+= (d ^ a ^ b) + x[ 7] + 0xf6bb4b60; + c = ((c << 16) | (c >>> 16)) + d; + b+= (c ^ d ^ a) + x[10] + 0xbebfbc70; + b = ((b << 23) | (b >>> 9)) + c; + + a+= (b ^ c ^ d) + x[13] + 0x289b7ec6; + a = ((a << 4) | (a >>> 28)) + b; + d+= (a ^ b ^ c) + x[ 0] + 0xeaa127fa; + d = ((d << 11) | (d >>> 21)) + a; + c+= (d ^ a ^ b) + x[ 3] + 0xd4ef3085; + c = ((c << 16) | (c >>> 16)) + d; + b+= (c ^ d ^ a) + x[ 6] + 0x04881d05; + b = ((b << 23) | (b >>> 9)) + c; + + a+= (b ^ c ^ d) + x[ 9] + 0xd9d4d039; + a = ((a << 4) | (a >>> 28)) + b; + d+= (a ^ b ^ c) + x[12] + 0xe6db99e5; + d = ((d << 11) | (d >>> 21)) + a; + c+= (d ^ a ^ b) + x[15] + 0x1fa27cf8; + c = ((c << 16) | (c >>> 16)) + d; + b+= (c ^ d ^ a) + x[ 2] + 0xc4ac5665; + b = ((b << 23) | (b >>> 9)) + c; + + + /* Round 4 */ + a+= (c ^ (b | ~d)) + x[ 0] + 0xf4292244; + a = ((a << 6) | (a >>> 26)) + b; + d+= (b ^ (a | ~c)) + x[ 7] + 0x432aff97; + d = ((d << 10) | (d >>> 22)) + a; + c+= (a ^ (d | ~b)) + x[14] + 0xab9423a7; + c = ((c << 15) | (c >>> 17)) + d; + b+= (d ^ (c | ~a)) + x[ 5] + 0xfc93a039; + b = ((b << 21) | (b >>> 11)) + c; + + a+= (c ^ (b | ~d)) + x[12] + 0x655b59c3; + a = ((a << 6) | (a >>> 26)) + b; + d+= (b ^ (a | ~c)) + x[ 3] + 0x8f0ccc92; + d = ((d << 10) | (d >>> 22)) + a; + c+= (a ^ (d | ~b)) + x[10] + 0xffeff47d; + c = ((c << 15) | (c >>> 17)) + d; + b+= (d ^ (c | ~a)) + x[ 1] + 0x85845dd1; + b = ((b << 21) | (b >>> 11)) + c; + + a+= (c ^ (b | ~d)) + x[ 8] + 0x6fa87e4f; + a = ((a << 6) | (a >>> 26)) + b; + d+= (b ^ (a | ~c)) + x[15] + 0xfe2ce6e0; + d = ((d << 10) | (d >>> 22)) + a; + c+= (a ^ (d | ~b)) + x[ 6] + 0xa3014314; + c = ((c << 15) | (c >>> 17)) + d; + b+= (d ^ (c | ~a)) + x[13] + 0x4e0811a1; + b = ((b << 21) | (b >>> 11)) + c; + + a+= (c ^ (b | ~d)) + x[ 4] + 0xf7537e82; + a = ((a << 6) | (a >>> 26)) + b; + d+= (b ^ (a | ~c)) + x[11] + 0xbd3af235; + d = ((d << 10) | (d >>> 22)) + a; + c+= (a ^ (d | ~b)) + x[ 2] + 0x2ad7d2bb; + c = ((c << 15) | (c >>> 17)) + d; + b+= (d ^ (c | ~a)) + x[ 9] + 0xeb86d391; + b = ((b << 21) | (b >>> 11)) + c; + + state[0]+= a; + state[1]+= b; + state[2]+= c; + state[3]+= d; + } + + + /** F MD5 function. */ + //private static int F(int x, int y, int z) { return (x & y) | ((~x) & z); } + /** G MD5 function. */ + //private static int G(int x, int y, int z) { return (x & z) | (y & (~z)); } + /** H MD5 function. */ + //private static int H(int x, int y, int z) { return (x ^ y ^ z); } + /** I MD5 function. */ + //private static int I(int x, int y, int z) { return y ^ (x | (~z)); } + + + /** FF transformation for round 1. + Rotation is separate from addition to prevent recomputation. */ + /*private static int[] FF(int[] w, int x, int s, int t) + { w[0] += F(w[1],w[2],w[3]) + x + t; + w[0] = rotateLeft(w[0],s) + w[1]; + return rotateLeft(w); + }*/ + /** GG transformation for round 2. + Rotation is separate from addition to prevent recomputation. */ + /*private static int[] GG(int[] w, int x, int s, int t) + { w[0] += G(w[1],w[2],w[3]) + x + t; + w[0] = rotateLeft(w[0],s) + w[1]; + return rotateLeft(w); + }* + /** HH transformation for round 3. + Rotation is separate from addition to prevent recomputation. */ + /*private static int[] HH(int[] w, int x, int s, int t) + { w[0] += H(w[1],w[2],w[3]) + x + t; + w[0] = rotateLeft(w[0],s) + w[1]; + return rotateLeft(w); + }* + /** II transformation for round 4. + Rotation is separate from addition to prevent recomputation. */ + /*private static int[] II(int[] w, int x, int s, int t) + { w[0] += I(w[1],w[2],w[3]) + x + t; + w[0] = rotateLeft(w[0],s) + w[1]; + return rotateLeft(w); + }*/ + + + /** MD5 basic transformation. Transforms state based on block. */ + /*private static void transform(int[] state, byte[] block) + { + int[] x=new int[16]; + for (int i=0; i<16; i++) x[i]=(int)bytesToWord(block,(i*4)); + + // make a copy of the state + int[] state_cp=new int[4]; + for (int i=0; i<4; i++) state_cp[i]=state[i]; + + // Round 1 + FF (state, x[ 0], S11, 0xd76aa478); + FF (state, x[ 1], S12, 0xe8c7b756); + FF (state, x[ 2], S13, 0x242070db); + FF (state, x[ 3], S14, 0xc1bdceee); + FF (state, x[ 4], S11, 0xf57c0faf); + FF (state, x[ 5], S12, 0x4787c62a); + FF (state, x[ 6], S13, 0xa8304613); + FF (state, x[ 7], S14, 0xfd469501); + FF (state, x[ 8], S11, 0x698098d8); + FF (state, x[ 9], S12, 0x8b44f7af); + FF (state, x[10], S13, 0xffff5bb1); + FF (state, x[11], S14, 0x895cd7be); + FF (state, x[12], S11, 0x6b901122); + FF (state, x[13], S12, 0xfd987193); + FF (state, x[14], S13, 0xa679438e); + FF (state, x[15], S14, 0x49b40821); + + // Round 2 + GG (state, x[ 1], S21, 0xf61e2562); + GG (state, x[ 6], S22, 0xc040b340); + GG (state, x[11], S23, 0x265e5a51); + GG (state, x[ 0], S24, 0xe9b6c7aa); + GG (state, x[ 5], S21, 0xd62f105d); + GG (state, x[10], S22, 0x2441453); + GG (state, x[15], S23, 0xd8a1e681); + GG (state, x[ 4], S24, 0xe7d3fbc8); + GG (state, x[ 9], S21, 0x21e1cde6); + GG (state, x[14], S22, 0xc33707d6); + GG (state, x[ 3], S23, 0xf4d50d87); + GG (state, x[ 8], S24, 0x455a14ed); + GG (state, x[13], S21, 0xa9e3e905); + GG (state, x[ 2], S22, 0xfcefa3f8); + GG (state, x[ 7], S23, 0x676f02d9); + GG (state, x[12], S24, 0x8d2a4c8a); + + // Round 3 + HH (state, x[ 5], S31, 0xfffa3942); + HH (state, x[ 8], S32, 0x8771f681); + HH (state, x[11], S33, 0x6d9d6122); + HH (state, x[14], S34, 0xfde5380c); + HH (state, x[ 1], S31, 0xa4beea44); + HH (state, x[ 4], S32, 0x4bdecfa9); + HH (state, x[ 7], S33, 0xf6bb4b60); + HH (state, x[10], S34, 0xbebfbc70); + HH (state, x[13], S31, 0x289b7ec6); + HH (state, x[ 0], S32, 0xeaa127fa); + HH (state, x[ 3], S33, 0xd4ef3085); + HH (state, x[ 6], S34, 0x4881d05); + HH (state, x[ 9], S31, 0xd9d4d039); + HH (state, x[12], S32, 0xe6db99e5); + HH (state, x[15], S33, 0x1fa27cf8); + HH (state, x[ 2], S34, 0xc4ac5665); + + // Round 4 + II (state, x[ 0], S41, 0xf4292244); + II (state, x[ 7], S42, 0x432aff97); + II (state, x[14], S43, 0xab9423a7); + II (state, x[ 5], S44, 0xfc93a039); + II (state, x[12], S41, 0x655b59c3); + II (state, x[ 3], S42, 0x8f0ccc92); + II (state, x[10], S43, 0xffeff47d); + II (state, x[ 1], S44, 0x85845dd1); + II (state, x[ 8], S41, 0x6fa87e4f); + II (state, x[15], S42, 0xfe2ce6e0); + II (state, x[ 6], S43, 0xa3014314); + II (state, x[13], S44, 0x4e0811a1); + II (state, x[ 4], S41, 0xf7537e82); + II (state, x[11], S42, 0xbd3af235); + II (state, x[ 2], S43, 0x2ad7d2bb); + II (state, x[ 9], S44, 0xeb86d391); + + for (int i=0; i<4; i++) state[i]+=state_cp[i]; + }*/ + + + + /** Calculates the MD5. */ + public static byte[] digest(byte[] buffer, int offset, int len) + { MD5 md5=new MD5(buffer,offset,len); + return md5.doFinal(); + } + + + /** Calculates the MD5. */ + public static byte[] digest(byte[] buffer) + { return digest(buffer,0,buffer.length); + } + + + /** Calculates the MD5. */ + public static byte[] digest(String str) + { MD5 md5=new MD5(str); + return md5.doFinal(); + } + + + /** Gets the message-digest as string of hex values */ + public String asHex() + { return asHex(doFinal()); + } + + + /** Transforms an array of bytes into a string of hex values */ + public static String asHex(byte[] buf) + { String str=new String(); + for (int i=0; i>>4)&0x0F); + str+=Integer.toHexString(buf[i]&0x0F); + } + return str; + } + +} \ No newline at end of file diff --git a/src/org/zoolu/tools/MD5OTP.java b/src/org/zoolu/tools/MD5OTP.java new file mode 100644 index 0000000..3e41650 --- /dev/null +++ b/src/org/zoolu/tools/MD5OTP.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.tools; + + +import java.io.*; +import java.util.Random; + + +/** OTP (One Time Pad) encryption algorithm based on MD5 hash function. + * It uses a PRG (Pseudo-Random-Generator) function in OFB (Output Feadback) + * to genarate a byte-stream (the OTP) that is XORed with the plaintext. + *
    The PRG is based on MD5. + * + *

    The OTP is calculated starting from a key and an IV, as follows: + *
    h_0=hash(skey|iv) + *
    h_i=hash(skey|h_i-1) + * + *

    where: + *
    hash(.)==MD5(.) + *
    skey==key + * + *

    while the ciphertext is calculated as follows: + *
    c_0=iv + *
    c_i=m_i XOR h_i with i=1,2,.. + * + *

    Note that optionally it could modified initializing the skey as follows: + *
    skey==hash(key|iv) + *
    in order to not keep in memory the secret key for long time + */ +public class MD5OTP +{ + /** Block size in bytes */ + static int size; + /** the OTP stream key */ + byte[] skey; + /** pseudorandom-stream (OTP) block */ + byte[] h; + /** index within a single block */ + int index; + + /** Creates a new MD5OTP */ + /*public MD5OTP(int bsize, byte[] key, byte[] iv) + { init(bsize,key,iv); + }*/ + + /** Creates a new MD5OTP */ + public MD5OTP(byte[] skey, byte[] iv) + { init(16,skey,iv); + } + + /** Creates a new MD5OTP with IV=0 */ + public MD5OTP(byte[] skey) + { init(16,skey,null); + } + + /** Inits the MD5OTP algorithm */ + private void init(int size, byte[] skey, byte[] iv) + { this.size=size; + if (iv==null) { iv=new byte[size]; for (int i=0; i0) + out.write(update(sub(buff,0,len))); + } + catch (IOException e) { e.printStackTrace(); } + } + + /** Encrypts an array of bytes. An IV is chosen and saved at top. */ + public static byte[] encrypt(byte[] m, byte[] key) + { // choose a random IV + byte[] iv=MD5.digest(Long.toString((new Random()).nextLong())); + // do encryption + byte[] c=(new MD5OTP(key,iv)).update(m); + return cat(iv,c); + } + + + /** Decrypts an array of bytes with the IV at top. */ + public static byte[] decrypt(byte[] c, byte[] key) + { // read the IV + byte[] iv=sub(c,0,16); + byte[] buf=sub(c,16,c.length); + return (new MD5OTP(key,iv)).update(buf); + } + + + /** Returns an hex representation of the byte array */ + private static String asHex(byte[] bb) + { StringBuffer buf=new StringBuffer(bb.length*2); + for (int i=0; i []"); + System.exit(0); + } + + byte[] msg=args[0].getBytes(); + byte[] key=args[1].getBytes(); + byte[] iv=null; + if (args.length>2) iv=args[2].getBytes(); + + System.out.println("m= "+asHex(msg)+" ("+new String(msg)+")"); + byte[] cip=(new MD5OTP(key,iv)).update(msg); + System.out.println("c= "+asHex(cip)); + cip=(new MD5OTP(key,iv)).update(cip); + System.out.println("m= "+asHex(cip)+" ("+new String(cip)+")"); + } + +} diff --git a/src/org/zoolu/tools/Mangle.java b/src/org/zoolu/tools/Mangle.java new file mode 100644 index 0000000..660178c --- /dev/null +++ b/src/org/zoolu/tools/Mangle.java @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.tools; + + +import java.io.*; +import java.util.Vector; +import java.util.Random; + + +/** Mangle collects some static methods for mangling binary-data structures + */ +public class Mangle +{ + + /** Compares two arrays of bytes */ + public static boolean compare(byte[] a, byte[] b) + { if (a.length!=b.length) return false; + for (int i=0; ivalue */ + public static byte[] initBytes(byte[] b, int value) + { for (int i=0; i>> (32-n)); + } + + /** Rotates w right n bits. */ + private static int rotateRight(int w, int n) + { return (w >>> n) | (w << (32-n)); + } + + /** Rotates an array of int (words), shifting 1 word left. */ + private static int[] rotateLeft(int[] w) + { int len=w.length; + int w1=w[len-1]; + for (int i=len-1; i>1; i--) w[i]=w[i-1]; + w[0]=w1; + return w; + } + + /** Rotates an array of int (words), shifting 1 word right. */ + private static int[] rotateRight(int[] w) + { int len=w.length; + int w0=w[0]; + for (int i=1; i1; i--) b[i]=b[i-1]; + b[0]=b1; + return b; + } + + /** Rotates an array of bytes, shifting 1 byte right. */ + private static byte[] rotateRight(byte[] b) + { int len=b.length; + byte b0=b[0]; + for (int i=1; ib */ + public static byte[] clone(byte[] b) { return getBytes(b,0,b.length); } + + /** Returns a len-byte array from array b with offset offset */ + public static byte[] getBytes(byte[] b, int offset, int len) { byte[] bb=new byte[len]; for (int k=0; kb with offset offset */ + public static byte[] twoBytes(byte[] b, int offset) { return getBytes(b,offset,2); } + + /** Returns a 4-byte array from array b with offset offset */ + public static byte[] fourBytes(byte[] b, int offset) { return getBytes(b,offset,4); } + + /** Copies all bytes of array src into array dst with offset offset */ + public static void copyBytes(byte[] src, byte[] dst, int offset) { for (int k=0; klen bytes of array src into array dst with offset offset */ + public static void copyBytes(byte[] src, byte[] dst, int offset, int len) { for (int k=0; ksrc into array dst with offset offset */ + public static void copyTwoBytes(byte[] src, byte[] dst, int offset) { copyBytes(src,dst,offset,2); } + + /** Copies a the first 4 bytes of array src into array dst with offset index */ + public static void copyFourBytes(byte[] src, byte[] dst, int offset) { copyBytes(src,dst,offset,4); } + + + /** Transforms the first len bytes of an array into a string of hex values */ + public static String bytesToHexString(byte[] b, int len) + { String s=new String(); + for (int i=0; ilen. + * The string may include ':' chars. + * If len is set to -1, all string is converted. */ + public static byte[] hexStringToBytes(String str, int len) + { // if the string is of the form xx:yy:zz:ww.., remove all ':' first + if (str.indexOf(":")>=0) + { String aux=""; + char c; + for (int i=0; i>24); + b[1]=(byte)((n>>16)%256); + b[2]=(byte)((n>>8)%256); + b[3]=(byte)(n%256); + return b; + } + + /** Transforms a 4-bytes array into a 32-bit word (with the more significative byte at left) */ + public static long bytesToWord(byte[] b, int offset) + { return ((((((long)uByte(b[offset+3])<<8)+uByte(b[offset+2]))<<8)+uByte(b[offset+1]))<<8)+uByte(b[offset+0]); + } + + /** Transforms a 4-bytes array into a 32-bit word (with the more significative byte at left) */ + public static long bytesToWord(byte[] b) + { return ((((((long)uByte(b[3])<<8)+uByte(b[2]))<<8)+uByte(b[1]))<<8)+uByte(b[0]); + } + + /** Transforms a 32-bit word (with the more significative byte at left) into a 4-bytes array */ + public static byte[] wordToBytes(long n) + { byte[] b=new byte[4]; + b[3]=(byte)(n>>24); + b[2]=(byte)((n>>16)%256); + b[1]=(byte)((n>>8)%256); + b[0]=(byte)(n%256); + return b; + } + + + + + private static void print(String str) + { System.out.println(str); + } + + + // *************************** MAIN **************************** + + + private static void decode(byte buffer[], int[] out) + { int offset=0; + int len=64; + for (int i = 0; offset < len; i++, offset += 4) + { out[i] = ((int) (buffer[offset] & 0xff)) | + (((int) (buffer[offset + 1] & 0xff)) << 8) | + (((int) (buffer[offset + 2] & 0xff)) << 16) | + (((int) buffer[offset + 3]) << 24); + } + } + + + public static void main(String[] args) + { + byte[] buff=new byte[64]; + for (int i=0; i<64; i++) buff[i]=(byte)i; + + int[] x=new int[16]; + for (int i=0; i<16; i++) x[i]=(int)bytesToWord(buff,(i*4)); + + for (int i=0; i<16; i++) print("x["+i+"]: "+bytesToHexString(wordToBytes(x[i]))); + decode(buff,x); + for (int i=0; i<16; i++) print("x["+i+"]: "+bytesToHexString(wordToBytes(x[i]))); + } +} diff --git a/src/org/zoolu/tools/MessageDigest.java b/src/org/zoolu/tools/MessageDigest.java new file mode 100644 index 0000000..5ad405a --- /dev/null +++ b/src/org/zoolu/tools/MessageDigest.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.tools; + + + +/** Generic hash/message-digest algorithm. + */ +public abstract class MessageDigest +{ + /** MessageDigest block update operation. + * Continues a message-digest operation, + * processing another message block, and updating the context. */ + abstract public MessageDigest update(byte[] buffer, int offset, int len); + + + /** MessageDigest block update operation. + * Continues a message-digest operation, + * processing another message block, and updating the context. */ + public MessageDigest update(String str) + { byte[] buf=str.getBytes(); + return update(buf,0,buf.length); + } + + + /** MessageDigest block update operation. + * Continues a message-digest operation, + * processing another message block, and updating the context. */ + public MessageDigest update(byte[] buffer) + { return update(buffer,0,buffer.length); + } + + + /** MessageDigest finalization. Ends a message-digest operation, writing the + * the message digest and zeroizing the context. */ + abstract public byte[] doFinal(); + + + /** Gets the MessageDigest. The same as doFinal(). */ + public byte[] getDigest() + { return doFinal(); + } + + + /** Gets the Message Digest as string of hex values. */ + public String asHex() + { return asHex(doFinal()); + } + + + /** Transforms an array of bytes into a string of hex values. */ + public static String asHex(byte[] buf) + { String str=new String(); + for (int i=0; i>>4)&0x0F); + str+=Integer.toHexString(buf[i]&0x0F); + } + return str; + } + +} diff --git a/src/org/zoolu/tools/Parser.java b/src/org/zoolu/tools/Parser.java new file mode 100644 index 0000000..72ddeb3 --- /dev/null +++ b/src/org/zoolu/tools/Parser.java @@ -0,0 +1,468 @@ +/* + * Copyright (C) 2005 Luca Veltri - University of Parma - Italy + * + * This file is part of MjSip (http://www.mjsip.org) + * + * MjSip is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MjSip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MjSip; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author(s): + * Luca Veltri (luca.veltri@unipr.it) + */ + +package org.zoolu.tools; + + +import java.util.*; + + +/** Class Parser allows the parsing of String objects. + *
    An object Parser is costructed from a String object and provides various methods for parsing the String in a stream oriented manner. + * The class Parser also collects different static methods for parsing non-pre-associated strings.
    + * Parser uses the following definitions:

    + *   
    string = any string of chars included between ' ' and '~' + *
    word = any string of chars without separators + *
    separators = a vector of chars; e.g. ( ) < > @ , ; : \ " / | [ ] ? = { } HT SP + *
    alpha = a-z, A-Z + *
    digit = 0-9 + *
    integer = any digit word parsed by {@link java.lang.Integer Integer.parseInt(String)} + *
    + */ +public class Parser +{ + + /** The string that is being parsed. */ + protected String str; + /** The the current pointer to the next char within the string. */ + protected int index; + + /** Creates the Parser from the String s and point to the beginning of the string.*/ + public Parser(String s) + { if (s==null) throw (new RuntimeException("Tried to costruct a new Parser with a null String")); + str=s; + index=0; + } + /** Creates the Parser from the String s and point to the position i. */ + public Parser(String s, int i) + { if (s==null) throw (new RuntimeException("Tried to costruct a new Parser with a null String")); + str=s; + index=i; + } + /** Creates the Parser from the StringBuffer sb and point to the beginning of the string.*/ + public Parser(StringBuffer sb) + { if (sb==null) throw (new RuntimeException("Tried to costruct a new Parser with a null StringBuffer")); + str=sb.toString(); + index=0; + } + /** Creates the Parser from the StringBuffer sb and point to the position i. */ + public Parser(StringBuffer sb, int i) + { if (sb==null) throw (new RuntimeException("Tried to costruct a new Parser with a null StringBuffer")); + str=sb.toString(); + index=i; + } + + /** Gets the current index position. */ + public int getPos() { return index; } + + /** Gets the entire string */ + public String getWholeString() { return str; } + + /** Gets the rest of the (unparsed) string. */ + public String getRemainingString() { return str.substring(index); } + + /** Returns a new the Parser of len chars statirng from the current position. */ + public Parser subParser(int len) { return new Parser(str.substring(index,index+len)); } + + /** Length of unparsed string. */ + public int length() { return (str.length()-index); } + + /** Whether there are more chars to parse. */ + public boolean hasMore() { return length()>0; } + + /** Gets the next char and go over */ + public char getChar() { return str.charAt(index++); } + + /** Gets the char at distance n WITHOUT going over */ + public char charAt(int n) { return str.charAt(index+n); } + + /** Gets the next char WITHOUT going over */ + public char nextChar() { return charAt(0); } + + /** Goes to position i */ + public Parser setPos(int i) { index= i; return this; } + + /** Goes to the next occurence of char c */ + public Parser goTo(char c) { index=str.indexOf(c,index); if (index<0) index=str.length(); return this; } + + /** Goes to the next occurence of any char of array cc */ + public Parser goTo(char[] cc) { index=indexOf(cc); if (index<0) index=str.length(); return this; } + + /** Goes to the next occurence of String s */ + public Parser goTo(String s) { index=str.indexOf(s,index); if (index<0) index=str.length(); return this; } + + /** Goes to the next occurence of any string of array ss */ + public Parser goTo(String[] ss) { index=indexOf(ss); if (index<0) index=str.length(); return this; } + + /** Goes to the next occurence of String s */ + public Parser goToIgnoreCase(String s) { index=indexOfIgnoreCase(s); if (index<0) index=str.length(); return this; } + + /** Goes to the next occurence of any string of array ss */ + public Parser goToIgnoreCase(String[] ss) { index=indexOfIgnoreCase(ss); if (index<0) index=str.length(); return this; } + + /** Goes to the begin of the new line */ + public Parser goToNextLine() + { while (indexch is any char of array ca */ + public static boolean isAnyOf(char[] ca, char ch) + { boolean found=false; + for (int i=0; i='A' && c<='Z'); } + /** Low alpha */ + public static boolean isLowAlpha(char c) { return (c>='a' && c<='z'); } + /** Alpha */ + public static boolean isAlpha(char c) { return (isUpAlpha(c) || isLowAlpha(c)); } + /** Alphanum */ + public static boolean isAlphanum(char c){ return (isAlpha(c) || isDigit(c)); } + /** Digit */ + public static boolean isDigit(char c) { return (c>='0' && c<='9'); } + /** Valid ASCII char */ + public static boolean isChar(char c) { return (c>' ' && c<='~'); } + /** CR */ + public static boolean isCR(char c) { return (c=='\r'); } + /** LF */ + public static boolean isLF(char c) { return (c=='\n'); } + /** CR or LF */ + public static boolean isCRLF(char c) { return isAnyOf(CRLF,c); } + /** HT */ + public static boolean isHT(char c) { return (c=='\t'); } + /** SP */ + public static boolean isSP(char c) { return (c==' '); } + /** SP or tab */ + public static boolean isWSP(char c){ return isAnyOf(WSP,c); } + /** SP, tab, CR, or LF */ + public static boolean isWSPCRLF(char c){ return isAnyOf(WSPCRLF,c); } + + + /** Compares two chars ignoring case */ + public static int compareIgnoreCase(char c1, char c2) + { if (isUpAlpha(c1)) c1+=32; + if (isUpAlpha(c2)) c2+=32; + return c1-c2; + } + +/* + private boolean isUpAlpha(int i) { return isUpAlpha(str.charAt(i)); } + private boolean isLowAlpha(int i) { return isLowAlpha(str.charAt(i)); } + private boolean isAlpha(int i) { return isAlpha(str.charAt(i)); } + private boolean isDigit(int i) { return isDigit(str.charAt(i)); } + private boolean isChar(int i) { return isChar(str.charAt(i)); } + private boolean isCR(int i) { return isCR(str.charAt(i)); } + private boolean isLF(int i) { return isLF(str.charAt(i)); } + private boolean isHT(int i) { return isHT(str.charAt(i)); } + private boolean isSP(int i) { return isSP(str.charAt(i)); } + private boolean isCRLF(int i) { return (isCR(str.charAt(i)) && isLF(str.charAt(i+1))); } + private boolean isSeparator(int i) { return isSeparator(str.charAt(i)); } +*/ + + // ************************ Indexes ************************ + + /** Gets the index of the first occurence of char c */ + public int indexOf(char c) + { return str.indexOf(c,index); + } + /** Gets the index of the first occurence of any char of array cc within string str starting form begin; return -1 if no occurence is found*/ + public int indexOf(char[] cc) + { boolean found=false; + int begin=index; + while (begins */ + public int indexOf(String s) + { return str.indexOf(s,index); + } + /** Gets the index of the first occurence of any string of array ss within string str; return -1 if no occurence is found. */ + public int indexOf(String[] ss) + { boolean found=false; + int begin=index; + while (begins ignoring case. */ + public int indexOfIgnoreCase(String s) + { Parser par=new Parser(str,index); + while (par.hasMore()) + { if (par.startsWithIgnoreCase(s)) return par.getPos(); + else par.skipChar(); + } + return -1; + } + /** Gets the index of the first occurence of any string of array ss ignoring case. */ + public int indexOfIgnoreCase(String[] ss) + { Parser par=new Parser(str,index); + while (par.hasMore()) + { if (par.startsWithIgnoreCase(ss)) return par.getPos(); + else par.skipChar(); + } + return -1; + } + + /** Gets the begin of next line */ + public int indexOfNextLine() + { Parser par=new Parser(str,index); + par.goToNextLine(); + int i=par.getPos(); + return (is. */ + public boolean startsWith(String s) + { return str.startsWith(s,index); + } + /** Whether next chars equal to any string of array ss. */ + public boolean startsWith(String[] ss) + { for (int i=0; is ignoring case. */ + public boolean startsWithIgnoreCase(String s) + { for (int k=0; kss ignoring case. */ + public boolean startsWithIgnoreCase(String[] ss) + { for (int i=0; istr.length()) index=str.length(); + return this; + } + /** Skips all spaces */ + public Parser skipWSP() + { while (indexlen and move over. */ + public String getString(int len) + { int start=index; + index=start+len; + return str.substring(start,index); + } + /** Gets a string of chars separated by any of chars of separators */ + public String getWord(char[] separators) + { int begin=index; + while (beginseparators */ + public Vector getWordVector(char[] separators) + { Vector list=new Vector(); + do { list.addElement(getWord(separators)); } while (hasMore()); + return list; + } + /** Gets all string of chars separated by any char belonging to separators */ + public String[] getWordArray(char[] separators) + { Vector list=getWordVector(separators); + String[] array=new String[list.size()]; + for (int i=0; iseparators + * , skipping any separator inside possible quoted texts. */ + public String getWordSkippingQuoted(char[] separators) + { int begin=index; + while (beginIn the latter case, quotes are dropped. */ + public String getStringUnquoted() + { // jump possible "non-chars" + while (index0) + { // is quoted text + String qtext=str.substring(index+1,next_qmark); + index=next_qmark+1; + return qtext; + } + else + { // is not a quoted text + return getString(); + } + } + /** Points to the next occurence of char c not in quotes. */ + public Parser goToSkippingQuoted(char c) + { boolean inside_quotes=false; + try + { while (index + + + MjSIP + + + +

    +MjSip is a compact and powerful SIP library for easily building SIP applications and services. +It provides in the same time the SIP APIs and SIP stack implementation bound together in MjSip packages. +

    +MjSip includes all classes and methods for creating SIP-based application. It implements the complete layered stack architecture as defined in RFC 3261, and is fully compliant with the standard. Moreover it includes higher level interfaces for Call Control. +

    +Specific information on IETF standardization process can be found on the official IETF SIP Working Group site. + + +

    +SIP (Session Initiation Protocol) is the IETF (Internet Engineering Task Force) signaling standard for managing multimedia session initiation; it is currently defined in RFC 3261. +
    SIP can be used to initiate voice, video and multimedia sessions, for both interactive applications (e.g. an IP phone call or a videoconference) and not interactive ones (e.g. a Video Streaming), and it is the more promising candidate as call setup signaling for the present day and future IP based telephony services. SIP has been also proposed for session initiation related uses, such as for messaging, gaming, etc. +