Added support for tcp/tls sip transport. The sip module can now use (bind on) more then one address for udp.
git-svn-id: http://voip.null.ro/svn/yate@4493 acf43c95-373e-0410-b603-e72c3f656dc1
This commit is contained in:
parent
0bc6a8eb4f
commit
f9f3241df5
|
@ -1,18 +1,37 @@
|
|||
; This file configures the SIP channel
|
||||
;
|
||||
; NOTES on UDP listeners
|
||||
; - Address/port can be changed and reloaded
|
||||
; - If address/port is changed for an enabled listener this will be destroyed and recreated
|
||||
; - When an UDP listener is destroyed all channels using it will be dropped and
|
||||
; all lines using it will be unregistered
|
||||
; - If the only configured listener is 'general' this one will be the default one
|
||||
; - After initializing the module will find for a default transport:
|
||||
; 1: First search for a default listener whose name is not 'general'
|
||||
; 2: Use 'general' if no other listener is set to be the default
|
||||
|
||||
|
||||
[general]
|
||||
; This section sets global variables of the implementation
|
||||
|
||||
; port: int: SIP UDP port
|
||||
;port=5060
|
||||
|
||||
; addr: ipaddress: IP address to bind to
|
||||
;addr=0.0.0.0
|
||||
|
||||
; maxpkt: int: Maximum received packet size, 524 to 65528, default 1500
|
||||
; maxpkt: int: Maximum received UDP packet size, 524 to 65528, default 1500
|
||||
; This parameter is applied on reload and can be overridden in UDP listener sections
|
||||
;maxpkt=1500
|
||||
|
||||
; buffer: int: Requested size of socket's receive buffer, 0 to use default
|
||||
; buffer: int: Requested size of UDP socket's receive buffer, 0 to use default
|
||||
; This can be overridden in UDP listener sections
|
||||
;buffer=0
|
||||
|
||||
; tcp_maxpkt: int: Maximum received TCP packet size, 524 to 65528, default 4096
|
||||
; This parameter is applied on reload and can be overridden in TCP/TLS listener sections
|
||||
; The parameter is not applied on reload for already created listeners or connections
|
||||
;tcp_maxpkt=4096
|
||||
|
||||
; tcp_out_rtp_localip: ipaddress: IP address to bind local RTP to for outgoing
|
||||
; TCP connections, empty to guess best
|
||||
; This parameter is applied on reload for new connections only
|
||||
;tcp_out_rtp_localip=
|
||||
|
||||
; thread: keyword: Default priority of the SIP handling threads
|
||||
; Can be one of: lowest, low, normal, high, highest
|
||||
; High priorities need superuser privileges on POSIX operating systems
|
||||
|
@ -85,9 +104,6 @@
|
|||
; forward_sdp: bool: Include the raw SDP body to be used as-is for forwarding RTP
|
||||
;forward_sdp=disable
|
||||
|
||||
; rtp_localip: ipaddress: IP address to bind local RTP to, empty to guess best
|
||||
;rtp_localip=
|
||||
|
||||
; rtp_start: bool: Start RTP when sending 200 on incoming instead of receiving ACK
|
||||
;rtp_start=disable
|
||||
|
||||
|
@ -101,6 +117,11 @@
|
|||
; See SIPMessage::Flags and SIPMessage::complete() in the source for gory details
|
||||
;flags=0
|
||||
|
||||
; printmsg: boolean: Print SIP messages to output
|
||||
; This parameter is applied on reload
|
||||
; Defaults to yes
|
||||
;printmsg=yes
|
||||
|
||||
|
||||
[registrar]
|
||||
; Controls the behaviour when acting as registrar
|
||||
|
@ -215,3 +236,100 @@
|
|||
; ignore_sdp_port: bool: Ignore SDP changes if only the port is different
|
||||
; This allows preserving the local RTP session and port
|
||||
;ignore_sdp_port=no
|
||||
|
||||
|
||||
[listener general]
|
||||
; This section has the following purposes:
|
||||
; - Maintain compatibility with old configuration
|
||||
; - Setup an UDP listener named 'general'
|
||||
; This section will be processed before any other listener sections
|
||||
; The following parameters can be overridden from 'general' section: maxpkt, buffer
|
||||
|
||||
; enable: boolean: Enable or disable the UDP listener
|
||||
; This parameter is applied on reload and defaults to yes
|
||||
;enable=yes
|
||||
|
||||
; default: boolean: Specifiy if this is the default transport to use when none specified
|
||||
; Defaults to yes (unlike the other listeners)
|
||||
;default=yes
|
||||
|
||||
; udp_force_bind: boolean: Try to use a random port if failed to bind on configured one
|
||||
; Defaults to yes
|
||||
;udp_force_bind=yes
|
||||
|
||||
; addr: ipaddress: IP address to bind to
|
||||
; Leave it empty to listen on all available interfaces
|
||||
;addr=
|
||||
|
||||
; port: integer: Port to bind to
|
||||
; Defaults to 5060
|
||||
;port=5060
|
||||
|
||||
; rtp_localip: ipaddress: IP address to bind local RTP to, empty to guess best
|
||||
; This parameter is applied on reload
|
||||
; RTP local IP address will default to bound IP address if not binding on all interfaces
|
||||
; Explicitly set it to empty string to avoid using bound IP address
|
||||
;rtp_localip=
|
||||
|
||||
; thread: keyword: Listener thread priority
|
||||
; Can be one of: lowest, low, normal, high, highest
|
||||
; High priorities need superuser privileges on POSIX operating systems
|
||||
; Low priorities are not recommended except for debugging
|
||||
;thread=normal
|
||||
|
||||
|
||||
;[listener name]
|
||||
; This section configures a listener named 'name' ('general' is reserved and will be ignored)
|
||||
; The following parameters can be overridden from 'general' section:
|
||||
; UDP: maxpkt, buffer
|
||||
; TCP/TLS: tcp_maxpkt
|
||||
|
||||
; type: keyword: Listener type
|
||||
; Allowed values:
|
||||
; udp: Build an UDP listener. Ignored if multihomed is disabled
|
||||
; tcp: Build a TCP listener
|
||||
; tls: Build a TLS listener (encrypted TCP)
|
||||
;type=
|
||||
|
||||
; enable: boolean: Enable or disable this listener
|
||||
; This parameter is applied on reload and defaults to yes
|
||||
;enable=yes
|
||||
|
||||
; default: boolean: UDP only: specifiy if this is the default transport to use when none specified
|
||||
; Defaults to no
|
||||
;default=no
|
||||
|
||||
; udp_force_bind: boolean: UDP only: try to use a random port if failed to bind on configured one
|
||||
; Defaults to yes
|
||||
;udp_force_bind=yes
|
||||
|
||||
; addr: ipaddress: IP address to bind to
|
||||
; Leave it empty to listen on all available interfaces
|
||||
;addr=
|
||||
|
||||
; port: integer: Port to bind to
|
||||
; Defaults to 5060 for UDP and TCP, 5061 for TLS listeners
|
||||
;port=
|
||||
|
||||
; rtp_localip: ipaddress: IP address to bind local RTP to
|
||||
; This parameter is applied on reload
|
||||
; TCP/TLS: this parameter is applied on reload for new connections only
|
||||
; RTP local IP address will default to bound IP address if not binding on all interfaces
|
||||
; Explicitly set it to empty string to avoid using bound IP address
|
||||
;rtp_localip=
|
||||
|
||||
; backlog: integer: Maximum length of the queue of pending connections
|
||||
; This parameter is ignored for UDP listeners
|
||||
; Set it to 0 for system maximum
|
||||
; Defaults to 5 if missing or invalid
|
||||
;backlog=5
|
||||
|
||||
; context: string: SSL context if this is an encrypted connection
|
||||
; Ignored for non TLS listeners, required for TLS listeners
|
||||
;context=
|
||||
|
||||
; thread: keyword: Listener thread priority
|
||||
; Can be one of: lowest, low, normal, high, highest
|
||||
; High priorities need superuser privileges on POSIX operating systems
|
||||
; Low priorities are not recommended except for debugging
|
||||
;thread=normal
|
||||
|
|
|
@ -70,10 +70,12 @@ static TokenDict sip_responses[] = {
|
|||
{ "Interval Too Brief", 423 },
|
||||
{ "Use Identity Header", 428 },
|
||||
{ "Provide Referrer Identity", 429 },
|
||||
{ "Flow Failed", 430 }, // RFC5626
|
||||
{ "Anonymity Disallowed", 433 },
|
||||
{ "Bad Identity-Info", 436 },
|
||||
{ "Unsupported Certificate", 437 },
|
||||
{ "Invalid Identity Header", 438 },
|
||||
{ "First Hop Lacks Outbound Support", 439 }, // RFC5626
|
||||
{ "Consent Needed", 470 },
|
||||
{ "Temporarily Unavailable", 480 },
|
||||
{ "Call/Transaction Does Not Exist", 481 },
|
||||
|
@ -108,14 +110,14 @@ static TokenDict sip_responses[] = {
|
|||
|
||||
TokenDict* TelEngine::SIPResponses = sip_responses;
|
||||
|
||||
SIPParty::SIPParty()
|
||||
: m_reliable(false)
|
||||
SIPParty::SIPParty(Mutex* mutex)
|
||||
: m_mutex(mutex), m_reliable(false), m_localPort(0), m_partyPort(0)
|
||||
{
|
||||
DDebug(DebugAll,"SIPParty::SIPParty() [%p]",this);
|
||||
}
|
||||
|
||||
SIPParty::SIPParty(bool reliable)
|
||||
: m_reliable(reliable)
|
||||
SIPParty::SIPParty(bool reliable, Mutex* mutex)
|
||||
: m_mutex(mutex), m_reliable(reliable), m_localPort(0), m_partyPort(0)
|
||||
{
|
||||
DDebug(DebugAll,"SIPParty::SIPParty(%d) [%p]",reliable,this);
|
||||
}
|
||||
|
@ -125,6 +127,24 @@ SIPParty::~SIPParty()
|
|||
DDebug(DebugAll,"SIPParty::~SIPParty() [%p]",this);
|
||||
}
|
||||
|
||||
void SIPParty::setAddr(const String& addr, int port, bool local)
|
||||
{
|
||||
Lock lock(m_mutex);
|
||||
String& a = local ? m_local : m_party;
|
||||
int& p = local ? m_localPort : m_partyPort;
|
||||
a = addr;
|
||||
p = port;
|
||||
DDebug(DebugAll,"SIPParty updated %s address '%s:%d' [%p]",
|
||||
local ? "local" : "remote",a.c_str(),p,this);
|
||||
}
|
||||
|
||||
void SIPParty::getAddr(String& addr, int& port, bool local)
|
||||
{
|
||||
Lock lock(m_mutex);
|
||||
addr = local ? m_local : m_party;
|
||||
port = local ? m_localPort : m_partyPort;
|
||||
}
|
||||
|
||||
|
||||
SIPEvent::SIPEvent(SIPMessage* message, SIPTransaction* transaction)
|
||||
: m_message(message), m_transaction(transaction),
|
||||
|
@ -290,7 +310,6 @@ void SIPEngine::processEvent(SIPEvent *event)
|
|||
{
|
||||
if (!event)
|
||||
return;
|
||||
Lock lock(this);
|
||||
const char* type = "unknown";
|
||||
if (event->isOutgoing())
|
||||
type = "outgoing";
|
||||
|
|
|
@ -74,7 +74,7 @@ SIPMessage::SIPMessage(const char* _method, const char* _uri, const char* _versi
|
|||
_method,_uri,_version,this);
|
||||
}
|
||||
|
||||
SIPMessage::SIPMessage(SIPParty* ep, const char* buf, int len)
|
||||
SIPMessage::SIPMessage(SIPParty* ep, const char* buf, int len, unsigned int* bodyLen)
|
||||
: code(0), body(0), m_ep(ep), m_valid(false),
|
||||
m_answer(false), m_outgoing(false), m_ack(false), m_cseq(-1), m_flags(-1)
|
||||
{
|
||||
|
@ -88,7 +88,7 @@ SIPMessage::SIPMessage(SIPParty* ep, const char* buf, int len)
|
|||
}
|
||||
if (len < 0)
|
||||
len = ::strlen(buf);
|
||||
m_valid = parse(buf,len);
|
||||
m_valid = parse(buf,len,bodyLen);
|
||||
}
|
||||
|
||||
SIPMessage::SIPMessage(const SIPMessage* message, int _code, const char* _reason)
|
||||
|
@ -138,7 +138,10 @@ SIPMessage::SIPMessage(const SIPMessage* original, const SIPMessage* answer)
|
|||
if (!hl) {
|
||||
String tmp;
|
||||
tmp << version << "/" << getParty()->getProtoName();
|
||||
if (getParty()) {
|
||||
Lock lock(getParty()->mutex());
|
||||
tmp << " " << getParty()->getLocalAddr() << ":" << getParty()->getLocalPort();
|
||||
}
|
||||
hl = new MimeHeaderLine("Via",tmp);
|
||||
header.append(hl);
|
||||
}
|
||||
|
@ -210,6 +213,9 @@ void SIPMessage::complete(SIPEngine* engine, const char* user, const char* domai
|
|||
return;
|
||||
}
|
||||
}
|
||||
String partyLAddr;
|
||||
int partyLPort = 0;
|
||||
getParty()->getAddr(partyLAddr,partyLPort,true);
|
||||
|
||||
// only set the dialog tag on ACK
|
||||
if (isACK()) {
|
||||
|
@ -220,14 +226,16 @@ void SIPMessage::complete(SIPEngine* engine, const char* user, const char* domai
|
|||
}
|
||||
|
||||
if (!domain)
|
||||
domain = getParty()->getLocalAddr();
|
||||
domain = partyLAddr;
|
||||
|
||||
MimeHeaderLine* hl = const_cast<MimeHeaderLine*>(getHeader("Via"));
|
||||
if (!hl) {
|
||||
String tmp;
|
||||
tmp << version << "/" << getParty()->getProtoName();
|
||||
tmp << " " << getParty()->getLocalAddr() << ":" << getParty()->getLocalPort();
|
||||
tmp << " " << partyLAddr << ":" << partyLPort;
|
||||
hl = new MimeHeaderLine("Via",tmp);
|
||||
if (isReliable() && 0 == (flags & NoConnReuse))
|
||||
hl->setParam("alias");
|
||||
if (!((flags & (NotReqRport|RportAfterBranch)) || isAnswer() || isACK()))
|
||||
hl->setParam("rport");
|
||||
header.append(hl);
|
||||
|
@ -238,8 +246,10 @@ void SIPMessage::complete(SIPEngine* engine, const char* user, const char* domai
|
|||
hl->setParam("branch",tmp);
|
||||
}
|
||||
if (isAnswer()) {
|
||||
if (!(flags & NotSetReceived))
|
||||
if (!(flags & NotSetReceived)) {
|
||||
Lock lock(getParty()->mutex());
|
||||
hl->setParam("received",getParty()->getPartyAddr());
|
||||
}
|
||||
const String* rport = hl->getParam("rport");
|
||||
if (rport && rport->null() && !(flags & NotSetRport))
|
||||
const_cast<String&>(*rport) = getParty()->getPartyPort();
|
||||
|
@ -309,8 +319,8 @@ void SIPMessage::complete(SIPEngine* engine, const char* user, const char* domai
|
|||
if (tmp)
|
||||
tmp = tmp.uriEscape('@',"+?&") + "@";
|
||||
tmp = "<sip:" + tmp;
|
||||
tmp << getParty()->getLocalAddr() << ":";
|
||||
tmp << getParty()->getLocalPort() << ">";
|
||||
tmp << partyLAddr << ":";
|
||||
tmp << partyLPort << ">";
|
||||
addHeader("Contact",tmp);
|
||||
}
|
||||
|
||||
|
@ -380,7 +390,7 @@ bool SIPMessage::parseFirst(String& line)
|
|||
return true;
|
||||
}
|
||||
|
||||
bool SIPMessage::parse(const char* buf, int len)
|
||||
bool SIPMessage::parse(const char* buf, int len, unsigned int* bodyLen)
|
||||
{
|
||||
DDebug(DebugAll,"SIPMessage::parse(%p,%d) [%p]",buf,len,this);
|
||||
String* line = 0;
|
||||
|
@ -442,6 +452,7 @@ bool SIPMessage::parse(const char* buf, int len)
|
|||
}
|
||||
line->destruct();
|
||||
}
|
||||
if (!bodyLen) {
|
||||
if (clen >= 0) {
|
||||
if (clen > len)
|
||||
Debug("SIPMessage",DebugMild,"Content length is %d but only %d in buffer",clen,len);
|
||||
|
@ -450,6 +461,33 @@ bool SIPMessage::parse(const char* buf, int len)
|
|||
len = clen;
|
||||
}
|
||||
}
|
||||
buildBody(buf,len);
|
||||
}
|
||||
else
|
||||
*bodyLen = (clen >= 0) ? clen : 0;
|
||||
DDebug(DebugAll,"SIPMessage::parse %d header lines, body %p",
|
||||
header.count(),body);
|
||||
return true;
|
||||
}
|
||||
|
||||
SIPMessage* SIPMessage::fromParsing(SIPParty* ep, const char* buf, int len, unsigned int* bodyLen)
|
||||
{
|
||||
SIPMessage* msg = new SIPMessage(ep,buf,len,bodyLen);
|
||||
if (msg->isValid())
|
||||
return msg;
|
||||
DDebug("SIPMessage",DebugInfo,"Invalid message");
|
||||
msg->destruct();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Build message's body. Reset it before
|
||||
void SIPMessage::buildBody(const char* buf, int len)
|
||||
{
|
||||
TelEngine::destruct(body);
|
||||
if (!buf)
|
||||
return;
|
||||
if (len < 0)
|
||||
len = ::strlen(buf);
|
||||
const MimeHeaderLine* cType = getHeader("Content-Type");
|
||||
if (cType)
|
||||
body = MimeBody::build(buf,len,*cType);
|
||||
|
@ -467,19 +505,8 @@ bool SIPMessage::parse(const char* buf, int len)
|
|||
body->appendHdr(line);
|
||||
}
|
||||
}
|
||||
DDebug(DebugAll,"SIPMessage::parse %d header lines, body %p",
|
||||
DDebug(DebugAll,"SIPMessage::buildBody %d header lines, body %p",
|
||||
header.count(),body);
|
||||
return true;
|
||||
}
|
||||
|
||||
SIPMessage* SIPMessage::fromParsing(SIPParty* ep, const char* buf, int len)
|
||||
{
|
||||
SIPMessage* msg = new SIPMessage(ep,buf,len);
|
||||
if (msg->isValid())
|
||||
return msg;
|
||||
DDebug("SIPMessage",DebugInfo,"Invalid message");
|
||||
msg->destruct();
|
||||
return 0;
|
||||
}
|
||||
|
||||
const MimeHeaderLine* SIPMessage::getHeader(const char* name) const
|
||||
|
@ -612,11 +639,12 @@ void SIPMessage::setParty(SIPParty* ep)
|
|||
{
|
||||
if (ep == m_ep)
|
||||
return;
|
||||
if (m_ep)
|
||||
m_ep->deref();
|
||||
if (ep && !ep->ref())
|
||||
ep = 0;
|
||||
XDebug(DebugAll,"SIPMessage::setParty(%p) current=%p [%p]",ep,m_ep,this);
|
||||
SIPParty* tmp = m_ep;
|
||||
m_ep = ep;
|
||||
if (m_ep)
|
||||
m_ep->ref();
|
||||
TelEngine::destruct(tmp);
|
||||
}
|
||||
|
||||
MimeAuthLine* SIPMessage::buildAuth(const String& username, const String& password,
|
||||
|
|
|
@ -577,8 +577,13 @@ SIPEvent* SIPTransaction::getClientEvent(int state, int timeout)
|
|||
switch (state) {
|
||||
case Initial:
|
||||
e = new SIPEvent(m_firstMessage,this);
|
||||
if (changeState(Trying))
|
||||
if (changeState(Trying)) {
|
||||
bool reliable = e->getParty() && e->getParty()->isReliable();
|
||||
if (!reliable)
|
||||
setTimeout(m_engine->getTimer(isInvite() ? 'A' : 'E'),5);
|
||||
else
|
||||
setTimeout(m_engine->getTimer(isInvite() ? 'B' : 'F',true),1);
|
||||
}
|
||||
break;
|
||||
case Trying:
|
||||
if (timeout < 0)
|
||||
|
|
|
@ -59,12 +59,17 @@ class SIPEvent;
|
|||
class YSIP_API SIPParty : public RefObject
|
||||
{
|
||||
public:
|
||||
SIPParty();
|
||||
SIPParty(bool reliable);
|
||||
SIPParty(Mutex* mutex = 0);
|
||||
SIPParty(bool reliable, Mutex* mutex = 0);
|
||||
virtual ~SIPParty();
|
||||
virtual void transmit(SIPEvent* event) = 0;
|
||||
virtual const char* getProtoName() const = 0;
|
||||
virtual bool setParty(const URI& uri) = 0;
|
||||
virtual void* getTransport() = 0;
|
||||
void setAddr(const String& addr, int port, bool local);
|
||||
void getAddr(String& addr, int& port, bool local);
|
||||
inline Mutex* mutex()
|
||||
{ return m_mutex; }
|
||||
inline const String& getLocalAddr() const
|
||||
{ return m_local; }
|
||||
inline const String& getPartyAddr() const
|
||||
|
@ -76,8 +81,8 @@ public:
|
|||
inline bool isReliable() const
|
||||
{ return m_reliable; }
|
||||
protected:
|
||||
Mutex* m_mutex;
|
||||
bool m_reliable;
|
||||
bool m_init;
|
||||
String m_local;
|
||||
String m_party;
|
||||
int m_localPort;
|
||||
|
@ -104,6 +109,7 @@ public:
|
|||
RportAfterBranch = 0x0008,
|
||||
NotSetRport = 0x0010,
|
||||
NotSetReceived = 0x0020,
|
||||
NoConnReuse = 0x0040, // Don't add 'alias' parameter to Via header (reliable only)
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -118,8 +124,14 @@ public:
|
|||
|
||||
/**
|
||||
* Creates a new SIPMessage from parsing a text buffer.
|
||||
* @param ep Party to set in message
|
||||
* @param buf Buffer to parse
|
||||
* @param len Optional buffer length
|
||||
* @param bodyLen Pointer to body length to be set if the message was received
|
||||
* on a stream transport. If not 0 the buffer must contain the message
|
||||
* without its body
|
||||
*/
|
||||
SIPMessage(SIPParty* ep, const char* buf, int len = -1);
|
||||
SIPMessage(SIPParty* ep, const char* buf, int len = -1, unsigned int* bodyLen = 0);
|
||||
|
||||
/**
|
||||
* Creates a new SIPMessage as answer to another message.
|
||||
|
@ -138,9 +150,24 @@ public:
|
|||
|
||||
/**
|
||||
* Construct a new SIP message by parsing a text buffer
|
||||
* @param ep Party to set in message
|
||||
* @param buf Buffer to parse
|
||||
* @param len Optional buffer length
|
||||
* @param bodyLen Pointer to body length to be set if the message was received
|
||||
* on a stream transport. If not 0 the buffer must contain the message
|
||||
* without its body
|
||||
* @return A pointer to a valid new message or NULL
|
||||
*/
|
||||
static SIPMessage* fromParsing(SIPParty* ep, const char* buf, int len = -1);
|
||||
static SIPMessage* fromParsing(SIPParty* ep, const char* buf, int len = -1,
|
||||
unsigned int* bodyLen = 0);
|
||||
|
||||
/**
|
||||
* Build message's body. Reset it before.
|
||||
* This method should be called after parsing a partial message (headers only)
|
||||
* @param buf Buffer to parse
|
||||
* @param len Optional buffer length
|
||||
*/
|
||||
void buildBody(const char* buf, int len = -1);
|
||||
|
||||
/**
|
||||
* Complete missing fields with defaults taken from a SIP engine
|
||||
|
@ -411,7 +438,7 @@ public:
|
|||
MimeBody* body;
|
||||
|
||||
protected:
|
||||
bool parse(const char* buf, int len);
|
||||
bool parse(const char* buf, int len, unsigned int* bodyLen);
|
||||
bool parseFirst(String& line);
|
||||
SIPParty* m_ep;
|
||||
bool m_valid;
|
||||
|
@ -759,6 +786,12 @@ public:
|
|||
inline void setTransmit()
|
||||
{ m_transmit = true; }
|
||||
|
||||
/**
|
||||
* Change transaction status to Cleared
|
||||
* This method is not thread safe
|
||||
*/
|
||||
inline void setCleared()
|
||||
{ changeState(Cleared); }
|
||||
|
||||
/**
|
||||
* Send back an authentication required response
|
||||
|
|
3688
modules/ysipchan.cpp
3688
modules/ysipchan.cpp
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue