Commit 693fea25 authored by John Selbie's avatar John Selbie

Version 1.0!

parent 7384df4b
This diff is collapsed.
.PHONY: all everything copybin debug clean
all: everything copybin
everything:
$(MAKE) $(T) --directory=common
$(MAKE) $(T) --directory=stuncore
$(MAKE) $(T) --directory=networkutils
$(MAKE) $(T) --directory=testcode
$(MAKE) $(T) --directory=client
$(MAKE) $(T) --directory=server
copybin:
rm -f ./stunserver ./stunclient ./stuntestcode
cp server/stunserver .
cp client/stunclient .
cp testcode/stuntestcode .
debug: T := debug
debug: all
clean: T := clean
clean: everything
rm -f stunserver stunclient stuntestcode
StunServer version 1.0.0
September 7, 2011
---------------------------------------------------------
The world's best stun server - coming soon!
Features:
Compliant with the latest RFCs including 5389, 5769, and 5780. Also includes
backwards compatibility for RFC 3489.
IPv4 and IPv6 support
Client test app provided
Stun server can operate in "full" mode as well as "basic" mode. Basic mode
configures the server to listen on one port and respond to STUN binding
requests. Full mode configures the service to listen on two different IP
address interfaces (if available) and provide NAT behavior and filtering
detection support for clients
Open source Apache license. See LICENSE file fore more details.
---------------------------------------------------------
Known issues:
UDP only. Command line options for working in TCP or TLS modes have yet to
be implemented.
Server does not honor the stun padding attribute. If someone really wants
this support, let me know and I will consider adding it.
By default, the stun server operates in an open mode without performing
authentication. All the code for authentication, challenge-response, message
hashing, and message integrity attributes are fully coded. HMAC/SHA1/MD5
hashing code for generating and validating the message integrity attribute
has been implemented and tested. However, the code for validating a username
or looking up a password is outside the scope of this release. Instead,
hooks are provided for implementors to write their own code to validate a
username, fetch a password, and allow/deny a request. Details of writing
your own authentication provider code are described in the file
"server/sampleauthprovider.h"
Dependency checking is not implemented in the Makefile. So if you need to
recompile, I recommend "make clean" from the root to preceed any subsequent
"make" call.
If you run an instance of stunserver locally, you may observe that
"stunclient localhost" may not successfully work. This is because the server
is not listening on the loopback adapter when running in full mode. The
workaround is to specify the actual IP address that the server is listening
on. Type "ifconfig" to discover your IP address (e.g. 10.11.12.13) followed
by "stunclient 10.11.12.13"
---------------------------------------------------------
Testing:
Fedora 15 with gcc/g++ 4.6.0
Ubuntu 11 with gcc/g++ 4.5.2
Amazon AWS with gcc/g++ 4.4
MacOS Snow Leopard (will not compile on earlier versions without updating to
a newer version of gcc/g++)
Parsing code has been fuzz tested with zzuf. http://caca.zoy.org/wiki/zzuf
---------------------------------------------------------
Prerequisites before compiling and running.
Boost header files. (Actual boost runtime not required) www.boost.org (sudo
yum install boost-devel)
OpenSSL development files and runtime. www.boost.org (sudo yum install
openssl-devel)
/usr/bin/xxd (this is a tool for converting the help text into resources. It
is usually universally installed. If not, then "sudo yum install
vim-common")
pthreads header and libs (I haven't seen a distribution where this wasn't
already installed)
---------------------------------------------------------
Compiling and running
Got Boost and OpenSSL taken care of as described above? Good. Just type
"make". There will be three resulting binaries in the root of the source
code package produced.
stuntestcode - This is the unit test code. I highly recommend you run this
program first. When run, you'll see a series of lines being printed in
regards to different code paths being tested. If you see any line that ends
in "FAIL", we likely have a bug. Please contact me immediately if you see
this.
stunserver - this is the server binary. Run "./stunserver --help" for
details on running this program. Running this program without any command
line arguments defaults to listening on port 3478 on all adapters.
stunclient - this is the client test binary. Run "./stunclient --help" for
details on running this program. Example: "./stunclient stun.selbie.com"
---------------------------------------------------------
Firewall
Don't forget to configure your firewall to allow traffic for the local ports
the stunserver will be listening on!
---------------------------------------------------------
Feature roadmap (the features I want to implement in a subsequent release)
TCP and TLS support
Finish Windows port and able to run as a Windows service
Scale across more than one CPU (for multi-core and multi-proc machines). The
threading code has already been written, just needs some finish work.
Host a full server across two separate machines (such that two ip addresses
on a single machine will not be required for full mode).
Cleanup Makefile and add "configure" and autotools support
---------------------------------------------------------
Contact the author
John Selbie
john@selbie.com
Interested? Follow up with jselbie@gmail.com
include ../common.inc
PROJECT_TARGET := stunclient
PROJECT_OBJS := clientmain.o
PROJECT_INTERMEDIATES := usage.txtcode usagelite.txtcode
INCLUDES := $(BOOST_INCLUDE) -I../common -I../stuncore -I../networkutils
LIB_PATH := -L../common -L../stuncore -L../networkutils
LIBS := -lnetworkutils -lstuncore -lcommon -lcrypto
all: $(PROJECT_TARGET)
clean:
rm -f $(PROJECT_OBJS) $(PROJECT_TARGET) $(PROJECT_INTERMEDIATES)
$(PROJECT_TARGET): $(PROJECT_OBJS)
$(LINK.cpp) -o $@ $^ $(LIB_PATH) $(LIBS)
clientmain.cpp: usage.txtcode usagelite.txtcode
%.txtcode: %.txt
sh ../server/makecodefile.sh $< $@ $(*)_text
This diff is collapsed.
Usage: stunclient [OPTIONS] server [port]
Perform a binding test with a remote STUN server and optionally perform a full behavior test
Parameters:
"server" is the IP address or FQDN of the remote server to befrom the binding tests with. It is the only required paramter
"port" is an optional paramter that can follow the server paramter. The default is 3478 for UDP and TCP. And 5349 for TLS.
Available options:
--localaddr = INTERFACE OR IPADDRESS
The value for this option may the name of an interface (such as "eth0" or "lo"). Or it may be one of the available IP addresses assigned to a network interface present on the host (such as "128.23.45.67"). The interface chosen will be the preferred address for sending and receiving responses with the remote server. The default is to let the system decide which address to send on and to listen on all addresses (INADDR_ANY).
--localport=PORTNUM
PORTNUM is a value between 1 to 65535. This is the UDP or TCP port that the primary and alternate interfaces listen on as the primary port for binding requests. If not specified, a randomly avaialbe port chosed by the system is used.
--mode=MODE
Where MODE is either "basic" or "full". "basic" mode is the default and indicates that the client should perform a STUN binding test only. "full" mode indicates that the client should attempt to diagnose NAT behavior and filtering methodologies if the server supports this mode.
--family=IPVERSION
IPVERSION is either "4" or "6" to specify the usage of IPV4 or IPV6. If not specified, the default value is "4".
--protocol=PROTO
PROTO is either "udp", "tcp", or "tls". Where "udp" is the default. "tcp" and "tls" modes are only available when the --mode option is "basic".
--verbosity=LOGLEVEL
Sets the verbosity of the logging level. 0 is the default (minimal output and logging). 1 shows slightly more. 2 and higher shows even more.
--help
Prints this help page
Examples:
stunclient stunserver.org 3478
Performs a simple binding test request with the server listening at "stunserver.org"
stunclient --mode full --localport 9999 12.34.56.78
Performs a full set of UDP NAT behavior tests from local port 9999 to the server listening at IP Address 12.34.56.78 (port 3478)
Usage: stunclient [OPTIONS] server [port]
Try 'stunclient --help' for a complete set of options and description of paramters
BOOST_INCLUDE := -I/home/jselbie/lib/boost_1_46_1
# OPENSSL_INCLUDE := /home/jselbie/lib/openssl
DEFINES := -DNDEBUG
STANDARD_FLAGS := -Wall -Wuninitialized
RELEASE_FLAGS := -O2
DEBUG_FLAGS := -g
FLAVOR_FLAGS = $(RELEASE_FLAGS)
.PHONY: all clean debug
%.h.gch: %.h
echo Building precompiled header: $@
$(COMPILE.cpp) $(INCLUDES) $(DEFINES) $(STANDARD_FLAGS) $(FLAVOR_FLAGS) $^
%.o: %.cpp
$(COMPILE.cpp) $(INCLUDES) $(DEFINES) $(STANDARD_FLAGS) $(FLAVOR_FLAGS) $^
# put "all" target first so that it is the default
all:
debug: FLAVOR_FLAGS = $(DEBUG_FLAGS)
debug: DEFINES = -DDEBUG
debug: all
include ../common.inc
PROJECT_TARGET := libcommon.a
PROJECT_SRCS := cmdlineparser.cpp common.cpp getconsolewidth.cpp getmillisecondcounter.cpp logger.cpp prettyprint.cpp refcountobject.cpp stringhelper.cpp
PROJECT_OBJS := $(subst .cpp,.o,$(PROJECT_SRCS))
INCLUDES := $(BOOST_INCLUDE)
PRECOMP_H_GCH := commonincludes.h.gch
all: $(PRECOMP_H_GCH) $(PROJECT_TARGET)
clean:
rm -f $(PROJECT_OBJS) $(PROJECT_TARGET) $(PRECOMP_H_GCH)
$(PROJECT_TARGET): $(PROJECT_OBJS)
rm -f $@
$(AR) rv $@ $^
/*
Copyright 2011 John Selbie
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#ifndef CHKMACROS_H
#define CHKMACROS_H
#include "hresult.h"
template <typename T>
inline HRESULT CheckCore(const T t)
{
t.What_You_Passed_To_Chk_Is_Not_HRESULT();
return E_FAIL;
}
template <typename T>
inline bool CheckIfCore(const T t)
{
t.What_You_Passed_To_ChkIf_Is_Not_bool();
return false;
}
template <>
inline HRESULT CheckCore<HRESULT>(const HRESULT t)
{
return t;
}
template <>
inline bool CheckIfCore<bool>(const bool t)
{
return t;
}
#define Chk(expr) \
{ \
((void)hr); \
hr = CheckCore((expr)); \
if (FAILED(hr)) \
{ \
goto Cleanup; \
} \
}
#define ChkIf(expr, hrerror) \
{ \
((void)hr); \
if (CheckIfCore((expr))) \
{ \
hr = (hrerror); \
goto Cleanup; \
} \
}
#define ChkA(expr) \
{ \
((void)hr); \
hr = CheckCore((expr)); \
if (FAILED(hr)) \
{ \
ASSERT(false); \
goto Cleanup; \
} \
}
#define ChkIfA(expr, hrerror) \
{ \
((void)hr); \
if (CheckIfCore((expr))) \
{ \
ASSERT(false); \
hr = (hrerror); \
goto Cleanup; \
} \
}
#endif
/*
Copyright 2011 John Selbie
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "commonincludes.h"
#include <getopt.h>
#include "cmdlineparser.h"
const option* CCmdLineParser::GenerateOptions()
{
_options.clear();
size_t len = _listOptionDetails.size();
for (size_t index = 0; index < len; index++)
{
option opt = {};
opt.has_arg = _listOptionDetails[index].has_arg;
opt.name = _listOptionDetails[index].strName.c_str();
_options.push_back(opt);
}
option zero = {0,0,0,0};
_options.push_back(zero);
return _options.data();
}
HRESULT CCmdLineParser::AddOption(const char* pszName, int has_arg, std::string* pStrResult)
{
HRESULT hr = S_OK;
OptionDetail od;
ChkIfA(pszName==NULL, E_INVALIDARG);
ChkIfA(has_arg < 0, E_INVALIDARG);
ChkIfA(has_arg > 2, E_INVALIDARG);
ChkIfA(pStrResult==NULL, E_INVALIDARG);
od.has_arg = has_arg;
od.pStrResult = pStrResult;
od.strName = pszName;
_listOptionDetails.push_back(od);
Cleanup:
return hr;
}
HRESULT CCmdLineParser::AddNonOption(std::string* pStrResult)
{
_namelessArgs.push_back(pStrResult);
return S_OK;
}
HRESULT CCmdLineParser::ParseCommandLine(int argc, char** argv, int startindex, bool* pErrorFlag)
{
int oldopterr = ::opterr;
size_t offset = 0;
::opterr = 0;
::optind = startindex;
const option* longopts = GenerateOptions();
if (pErrorFlag)
{
*pErrorFlag = false;
}
while (true)
{
int index = 0;
int ret;
ret = ::getopt_long_only(argc, argv, "", longopts, &index);
if (ret < 0)
{
break;
}
if ((ret == '?') || (ret == ':'))
{
if (pErrorFlag)
{
*pErrorFlag = true;
}
continue;
}
if ((longopts[index].has_arg != 0) && (optarg != NULL))
{
_listOptionDetails[index].pStrResult->assign(optarg);
}
else
{
_listOptionDetails[index].pStrResult->assign("1");
}
}
offset = 0;
for (int j = ::optind; j < argc; j++)
{
if (strcmp("--", argv[j]) == 0)
{
continue;
}
if (_namelessArgs.size() <= offset)
{
// should we set the error flag if we don't have a binding for this arg?
break;
}
_namelessArgs[offset]->assign(argv[j]);
offset++;
}
::opterr = oldopterr;
return S_OK;
}
/*
Copyright 2011 John Selbie
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#ifndef CMDLINEPARSER_H
#define CMDLINEPARSER_H
#include <getopt.h>
class CCmdLineParser
{
const option* GenerateOptions();
std::vector<option> _options;
std::vector<std::string*> _namelessArgs;
struct OptionDetail
{
std::string strName;
int has_arg;
std::string* pStrResult;
};
std::vector<OptionDetail> _listOptionDetails;
public:
HRESULT AddOption(const char* pszName, int has_arg, std::string* pStrResult);
HRESULT AddNonOption(std::string* pStrResult);
HRESULT ParseCommandLine(int argc, char** argv, int startindex, bool* fParseError);
};
#endif /* CMDLINEPARSER_H */
/*
Copyright 2011 John Selbie
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "commonincludes.h"
namespace boost
{
void assertion_failed_msg(char const * expr, char const* msg, char const * function, char const * file, long line)
{
printf("ASSERTION FAILED: %s\n"
" message: %s\n"
" function: %s\n"
" file: %s\n"
" line: %ld\n", expr?expr:"(null)", msg?msg:"(null)", function?function:"(null)", file, line);
}
void assertion_failed(char const * expr, char const * function, char const * file, long line)
{
assertion_failed_msg(expr, expr, function, file, line);
}
}
/*
Copyright 2011 John Selbie
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#ifndef STUNSERVER_COMMON_COMMONINCLUDES_H
#define STUNSERVER_COMMON_COMMONINCLUDES_H
// standard system includes
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/time.h>
#include <stdlib.h>
#include <memory.h>
#include <ifaddrs.h>
#include <net/if.h>
#include <stdarg.h>
#include <boost/shared_ptr.hpp>
#include <boost/scoped_array.hpp>
#include <boost/scoped_ptr.hpp>
#include <map>
#include <vector>
#include <list>
#include <string>
#include <pthread.h>
#if !defined(DEBUG) && !defined(NDEBUG)
You_Didnt_Define_DEBUG_Or_NDEBUG g_compilererror[-1];
#endif
#if defined(DEBUG)
#ifndef BOOST_ENABLE_ASSERT_HANDLER
#define BOOST_ENABLE_ASSERT_HANDLER
#endif
#else
#ifndef BOOST_DISABLE_ASSERTS
#define BOOST_DISABLE_ASSERTS
#endif
#endif
#include <boost/assert.hpp>
#ifdef ASSERT
#undef ASSERT
#endif
#ifdef VERIFY
#undef VERIFY
#endif
#ifdef ASSERT_MSG
#undef ASSERT_MSG
#endif
#define ASSERT(expr) BOOST_ASSERT(expr)
#define VERIFY(expr) BOOST_VERIFY(expr)
#define ASSERTMSG(expr, msg) BOOST_ASSERT_MSG (expr, msg)
#define ARRAYSIZE(arr) (sizeof(arr)/sizeof(*arr))
inline void cta_noop(const char* psz)
{
;
}
#define COMPILE_TIME_ASSERT(x) {char name$$[(x)?1:-1]; cta_noop(name$$);}
#ifndef UNREFERENCED_VARIABLE
#define UNREFERENCED_VARIABLE(unrefparam) ((void)unrefparam)
#endif
// --------------------------------------------
#include "hresult.h"
#include "chkmacros.h"
// ---------------------------------------------
// Unless there's good reason, put additional header files after hresult.h and chkmacros.h
#include "refcountobject.h"
#include "objectfactory.h"
#include "logger.h"
#endif
/*
Copyright 2011 John Selbie
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "commonincludes.h"
#include "oshelper.h"
#ifndef _WIN32
#include <sys/ioctl.h>
#endif
const size_t DEFAULT_CONSOLE_WIDTH = 80;
static size_t GetConsoleWidthUnix()
{
// this ioctl call on file id 0 (stdin) works on MacOSX and Linux. So it's probably universal
struct winsize ws = {};
int ret;
size_t retvalue = DEFAULT_CONSOLE_WIDTH;
ret = ioctl(0, TIOCGWINSZ, &ws);
if ((ret != -1) && (ws.ws_col > 0))
{
retvalue = ws.ws_col;
}
return retvalue;
}
size_t GetConsoleWidth()
{
#ifdef _WIN32
return DEFAULT_CONSOLE_WIDTH; // todo - call appropriate console apis when we port to windows
#else
return GetConsoleWidthUnix();
#endif
}
/*
Copyright 2011 John Selbie
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "commonincludes.h"
#include "oshelper.h"
static uint32_t GetMillisecondCounterUnix()
{
uint64_t milliseconds = 0;
uint32_t retvalue;
timeval tv = {};
gettimeofday(&tv, NULL);
milliseconds = (tv.tv_sec * (unsigned long long)1000) + (tv.tv_usec / 1000);
retvalue = (uint32_t)(milliseconds & (unsigned long long)0xffffffff);
return retvalue;
}
uint32_t GetMillisecondCounter()
{
#ifdef _WIN32
return GetTickCount();
#else
return GetMillisecondCounterUnix();
#endif
}
/*
Copyright 2011 John Selbie
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#ifndef HRESULT_H_
#define HRESULT_H_
// HRESULT
typedef int32_t HRESULT;
#define SEVERITY_SUCCESS 0
#define SEVERITY_ERROR 1
#define SUCCEEDED(hr) (((HRESULT)(hr)) >= 0)
#define FAILED(hr) (((HRESULT)(hr)) < 0)
#define SYSCALL_SUCCEEDED(x) (x!=-1)
#define SYSCALL_FAILED(x) (x == -1)
#define HRESULT_CODE(hr) ((hr) & 0xFFFF)
#define HRESULT_FACILITY(hr) (((hr) >> 16) & 0x1fff)
#define HRESULT_SEVERITY(hr) (((hr) >> 31) & 0x1)
#define MAKE_HRESULT(sev,fac,code) \
((HRESULT) (((unsigned long)(sev)<<31) | ((unsigned long)(fac)<<16) | ((unsigned long)(code))) )
#define FACILITY_ERRNO 0x800
#define ERRNO_TO_HRESULT(err) MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ERRNO, err)
#define ERRNOHR ERRNO_TO_HRESULT(ERRNO_TO_HRESULT(errno))
#define S_OK ((HRESULT)0)
#define S_FALSE ((HRESULT)1L)
#define E_UNEXPECTED ((HRESULT)(0x8000FFFFL))
#define E_NOTIMPL ((HRESULT)(0x80004001L))
#define E_OUTOFMEMORY ((HRESULT)(0x8007000EL))
#define E_INVALIDARG ((HRESULT)(0x80070057L))
#define E_NOINTERFACE ((HRESULT)(0x80004002L))
#define E_POINTER ((HRESULT)(0x80004003L))
#define E_HANDLE ((HRESULT)(0x80070006L))
#define E_ABORT ((HRESULT)(0x80004004L))
#define E_FAIL ((HRESULT)(0x80004005L))
#define E_ACCESSDENIED ((HRESULT)(0x80070005L))
#define E_PENDING ((HRESULT)(0x8000000AL))
#endif /* HRESULT_H_ */
/*
Copyright 2011 John Selbie
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "commonincludes.h"
#include "logger.h"
namespace Logging
{
static uint32_t s_loglevel = LL_ALWAYS; // error and usage messages only
void VPrintMsg(const char* pszFormat, va_list& args)
{
::vprintf(pszFormat, args);
::printf("\n");
}
uint32_t GetLogLevel()
{
return s_loglevel;
}
void SetLogLevel(uint32_t level)
{
s_loglevel = level;
}
void LogMsg(uint32_t level, const char* pszFormat, ...)
{
va_list args;
va_start(args, pszFormat);
if (level <= s_loglevel)
{
VPrintMsg(pszFormat, args);
}
va_end(args);
}
}
/*
Copyright 2011 John Selbie
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#ifndef LOGGING_H
#define LOGGING_H
const uint32_t LL_ALWAYS = 0; // only help messages, output the user expects to see, and critical error messages
const uint32_t LL_DEBUG = 1; // messages helpful for debugging
const uint32_t LL_VERBOSE = 2; // every packet
const uint32_t LL_VERBOSE_EXTREME = 3; // every packet and all the details
namespace Logging
{
uint32_t GetLogLevel();
void SetLogLevel(uint32_t level);
void LogMsg(uint32_t level, const char* pszFormat, ...);
}
#endif
/*
Copyright 2011 John Selbie
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#ifndef _OBJECTFACTORY_H
#define _OBJECTFACTORY_H
template <typename T>
class CObjectFactory
{
public:
template <typename X>
static HRESULT CreateInstanceNoInit(X** ppInstance)
{
T* pT = NULL;
if (ppInstance == NULL)
{
return E_INVALIDARG;
}
pT = new T();
if (pT == NULL)
{
return E_OUTOFMEMORY;
}
pT->AddRef();
*ppInstance = pT;
return S_OK;
}
template <typename I>
static HRESULT CreateInstance(I** ppI)
{
T* pInstance = NULL;
HRESULT hr = S_OK;
ChkIf(ppI == NULL, E_NOINTERFACE);
Chk(CreateInstanceNoInit(&pInstance));
Chk(pInstance->Initialize());
*ppI = pInstance;
pInstance = NULL;
Cleanup:
// Cleanup
if (pInstance)
{
pInstance->Release();
pInstance = NULL;
}
return hr;
}
template <typename A, typename I>
static HRESULT CreateInstance(A paramA, I** ppI)
{
T* pInstance = NULL;
HRESULT hr = S_OK;
ChkIf(ppI == NULL, E_NOINTERFACE);
Chk(CreateInstanceNoInit(&pInstance));
Chk(pInstance->Initialize(paramA));
*ppI = pInstance;
pInstance = NULL;
Cleanup:
// Cleanup
if (pInstance)
{
pInstance->Release();
pInstance = NULL;
}
return hr;
}
template <typename A, typename B, typename I>
static HRESULT CreateInstance(A paramA, B paramB, I** ppI)
{
T* pInstance = NULL;
HRESULT hr = S_OK;
ChkIf(ppI == NULL, E_NOINTERFACE);
Chk(CreateInstanceNoInit(&pInstance));
Chk(pInstance->Initialize(paramA, paramB));
*ppI = pInstance;
pInstance = NULL;
Cleanup:
// Cleanup
if (pInstance)
{
pInstance->Release();
pInstance = NULL;
}
return hr;
}
};
#endif /* _OBJECTFACTORY_H */
/*
Copyright 2011 John Selbie
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#ifndef OSHELPER_H
#define OSHELPER_H
uint32_t GetMillisecondCounter();
size_t GetConsoleWidth();
#endif /* OSHELPER_H */
/*
Copyright 2011 John Selbie
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "commonincludes.h"
static bool IsWhitespace(char ch)
{
return ((ch == ' ') || (ch == '\t') || (ch == '\v') || (ch == '\r') || (ch == '\n'));
}
static void SplitParagraphIntoWords(const char* pszLine, std::vector<std::string>& listWords)
{
std::string strWord;
if (pszLine == NULL)
{
return;
}
while (*pszLine)
{
while ((*pszLine) && IsWhitespace(*pszLine))
{
pszLine++;
}
if (*pszLine == 0)
{
return;
}
while ((*pszLine) && IsWhitespace(*pszLine)==false)
{
strWord += *pszLine;
pszLine++;
}
listWords.push_back(strWord);
strWord.clear();
}
}
static void SplitInputIntoParagraphs(const char* pszInput, std::vector<std::string>& listParagraphs)
{
// blindly scan to the next \r or \n
std::string strParagraph;
if (pszInput == NULL)
{
return;
}
while (*pszInput)
{
while ((*pszInput != '\0') && (*pszInput != '\r') && (*pszInput != '\n'))
{
strParagraph += *pszInput;
pszInput++;
}
listParagraphs.push_back(strParagraph);
strParagraph.clear();
if (*pszInput == '\r')
{
pszInput++;
if (*pszInput == '\n')
{
pszInput++;
}
}
else if (*pszInput == '\n')
{
pszInput++;
}
}
}
static void PrintParagraph(const char* psz, size_t width)
{
size_t indent = 0;
const char *pszProbe = psz;
std::vector<std::string> listWords;
bool fLineStart = true;
std::string strLine;
std::string strIndent;
size_t wordcount = 0;
size_t wordindex = 0;
if (width <= 0)
{
return;
}
if (psz==NULL)
{
return;
}
while ((*pszProbe) && IsWhitespace(*pszProbe))
{
indent++;
pszProbe++;
}
SplitParagraphIntoWords(psz, listWords);
wordcount = listWords.size();
if (indent >= width)
{
indent = width-1;
}
for (size_t x = 0; x < indent; x++)
{
strIndent += ' ';
}
while (wordindex < wordcount)
{
if (fLineStart)
{
fLineStart = false;
strLine = strIndent;
// we always consume a word and put it at the start of a line regardless of the space we have left
// one day we can consider doing hyphenation...
strLine += listWords[wordindex];
wordindex++;
}
else
{
// do we have enough room to fit including the space?
size_t newsize = strLine.size() + 1 + listWords[wordindex].size();
if (newsize <= width)
{
strLine += ' ';
strLine += listWords[wordindex];
wordindex++;
}
else
{
// not a fit, we need to start a new line
// flush whatever we have now
printf("%s\n", strLine.c_str());
// flag that we got a new line starting
fLineStart = true;
}
}
}
if ((strLine.size() > 0) || (wordcount == 0))
{
printf("%s\n", strLine.c_str());
}
}
void PrettyPrint(const char* pszInput, size_t width)
{
std::vector<std::string> listParagraphs;
size_t len;
SplitInputIntoParagraphs(pszInput, listParagraphs);
len = listParagraphs.size();
for (size_t x = 0; x < len; x++)
{
PrintParagraph(listParagraphs[x].c_str(), width);
}
}
/*
Copyright 2011 John Selbie
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#ifndef PRETTY_PRINT_H
#define PRETTY_PRINT_H
void PrettyPrint(const char* pszInput, size_t width);
#endif
/*
Copyright 2011 John Selbie
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "commonincludes.h"
#include "refcountobject.h"
int AtomicIncrement(int* pInt)
{
// InterlockedIncrement
return __sync_add_and_fetch(pInt, 1);
}
int AtomicDecrement(int* pInt)
{
// InterlockedDecrement
return __sync_sub_and_fetch(pInt, 1);
}
CBasicRefCount::CBasicRefCount()
{
m_nRefs = 0;
}
CBasicRefCount::~CBasicRefCount()
{
;
}
int CBasicRefCount::InternalAddRef()
{
return AtomicIncrement(&m_nRefs);
}
int CBasicRefCount::InternalRelease()
{
int refcount = AtomicDecrement(&m_nRefs);
if (refcount == 0)
{
OnFinalRelease();
}
return refcount;
}
void CBasicRefCount::OnFinalRelease()
{
delete this;
}
/*
Copyright 2011 John Selbie
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#ifndef _REFCOUNTOBJECT_H
#define _REFCOUNTOBJECT_H
int AtomicIncrement(int* pInt);
int AtomicDecrement(int* pInt);
class IRefCounted
{
public:
virtual int AddRef() = 0;
virtual int Release() = 0;
};
class CBasicRefCount
{
protected:
int m_nRefs;
public:
CBasicRefCount();
virtual ~CBasicRefCount();
int InternalAddRef();
int InternalRelease();
virtual void OnFinalRelease();
};
#define ADDREF_AND_RELEASE_IMPL() \
inline int AddRef() {return InternalAddRef();} \
inline int Release() {return InternalRelease();}
template <class T>
class CRefCountedPtr
{
protected:
T* m_ptr;
public:
CRefCountedPtr() : m_ptr(NULL)
{
;
}
CRefCountedPtr(T* ptr) : m_ptr(ptr)
{
if (m_ptr)
{
m_ptr->AddRef();
}
}
CRefCountedPtr(const CRefCountedPtr<T>& sp) : m_ptr(sp.m_ptr)
{
if (m_ptr)
{
m_ptr->AddRef();
}
}
~CRefCountedPtr()
{
if (m_ptr)
{
m_ptr->Release();
m_ptr = NULL;
}
}
T* GetPointer()
{
return m_ptr;
}
operator T*() const
{
return m_ptr;
}
// std::vector chokes because it references &element to infer behavior
// Use GetPointerPointer instead
//T** operator&()
//{
// return &m_ptr;
//}
T** GetPointerPointer()
{
return &m_ptr;
}
T* operator->() const
{
return m_ptr;
}
T* operator = (T* ptr)
{
if (ptr)
{
ptr->AddRef();
}
if (m_ptr)
{
m_ptr->Release();
}
m_ptr = ptr;
return m_ptr;
}
T* operator = (const CRefCountedPtr<T>& sp)
{
if (sp.m_ptr)
{
sp.m_ptr->AddRef();
}
if (m_ptr)
{
m_ptr->Release();
}
m_ptr = sp.m_ptr;
return m_ptr;
}
bool operator ! () const
{
return (m_ptr == NULL);
}
bool operator != (T* ptr) const
{
return (ptr != m_ptr);
}
bool operator == (T* ptr) const
{
return (ptr == m_ptr);
}
// manual release
void ReleaseAndClear()
{
if (m_ptr)
{
m_ptr->Release();
m_ptr = NULL;
}
}
void Attach(T* ptr)
{
if (m_ptr)
{
m_ptr->Release();
}
m_ptr = ptr;
}
T* Detach()
{
T* ptr = m_ptr;
m_ptr = NULL;
return ptr;
}
HRESULT CopyTo(T** ppT)
{
if (ppT == NULL)
{
return E_POINTER;
}
*ppT = m_ptr;
if (m_ptr)
{
m_ptr->AddRef();
}
return S_OK;
}
};
#endif /* _REFCOUNTOBJECT_H */
/*
Copyright 2011 John Selbie
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "commonincludes.h"
#include <stdlib.h>
#include <vector>
#include <string>
#include "stringhelper.h"
#define ISWHITESPACE(ch) (((ch >= 9)&&(ch<=0xd))||(ch== ' '))
namespace StringHelper
{
bool IsNullOrEmpty(const char* psz)
{
return ((psz == NULL) || (psz[0] == '\0'));
}
void ToLower(std::string& str)
{
const char* psz = str.c_str();
size_t length = str.length();
std::string str2;
const int diff = ('a' - 'A');
if ((psz == NULL) || (length == 0))
{
return;
}
str2.reserve(length);
for (size_t index = 0; index < length; index++)
{
char ch = str[index];
if ((ch >= 'A') && (ch <= 'Z'))
{
ch = ch + diff;
}
str2.push_back(ch);
}
str = str2;
}
void Trim(std::string& str)
{
const char* psz = str.c_str();
if (psz == NULL)
{
return;
}
int length = str.length();
int start = -1;
int end = -1;
char ch;
for (int index = 0; index < length; index++)
{
ch = psz[index];
if (ISWHITESPACE(ch))
{
continue;
}
else if (start == -1)
{
start = index;
end = index;
}
else
{
end = index;
}
}
if (start != -1)
{
str = str.substr(start, end-start+1);
}
}
int ValidateNumberString(const char* psz, int nMinValue, int nMaxValue, int* pnResult)
{
int nVal = 0;
if (IsNullOrEmpty(psz) || (pnResult==NULL))
{
return -1;
}
nVal = atoi(psz);
if(nVal < nMinValue) return -1;
if(nVal > nMaxValue) return -1;
*pnResult = nVal;
return 0;
}
}
/*
Copyright 2011 John Selbie
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#ifndef STRINGHELPER_H
#define STRINGHELPER_H
#include <string>
namespace StringHelper
{
bool IsNullOrEmpty(const char* psz);
void ToLower(std::string& str);
void Trim(std::string& str);
int ValidateNumberString(const char* psz, int nMinValue, int nMaxValue, int* pnResult);
}
#endif /* STRINGHELPER_H */
include ../common.inc
PROJECT_TARGET := libnetworkutils.a
PROJECT_OBJS := adapters.o recvfromex.o resolvehostname.o stunsocket.o
INCLUDES := $(BOOST_INCLUDE) -I../common -I../stuncore
.PHONY: all
.PHONY: clean
all: $(PROJECT_TARGET)
clean:
rm -f $(PROJECT_OBJS) $(PROJECT_TARGET)
$(PROJECT_TARGET): $(PROJECT_OBJS)
rm -f $@
$(AR) rv $@ $^
/*
Copyright 2011 John Selbie
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "commonincludes.h"
// TODO - FIX THIS SUCH THAT CSocketAddress is in it's own "base" library independent of networkutils and stuncore
#include "socketaddress.h"
void GetDefaultAdapters(int family, ifaddrs* pList, ifaddrs** ppAddrPrimary, ifaddrs** ppAddrAlternate)
{
ifaddrs* pAdapter = NULL;
*ppAddrPrimary = NULL;
*ppAddrAlternate = NULL;
pAdapter = pList;
while (pAdapter)
{
if ( (pAdapter->ifa_addr->sa_family == family) && (pAdapter->ifa_flags & IFF_UP) && !(pAdapter->ifa_flags & IFF_LOOPBACK))
{
if (*ppAddrPrimary == NULL)
{
*ppAddrPrimary = pAdapter;
}
else
{
*ppAddrAlternate = pAdapter;
break;
}
}
pAdapter = pAdapter->ifa_next;
}
}
/**
* Returns true if there are two or more host interfaces(adapters) for the specified family of IP addresses that are both "up" and not loopback adapters
* @param family either AF_INET or AF_INET6
*/
bool HasAtLeastTwoAdapters(int family)
{
HRESULT hr = S_OK;
ifaddrs* pList = NULL;
ifaddrs* pAdapter1 = NULL;
ifaddrs* pAdapter2 = NULL;
bool fRet = false;
ChkIf(getifaddrs(&pList) < 0, ERRNOHR);
GetDefaultAdapters(family, pList, &pAdapter1, &pAdapter2);
fRet = (pAdapter1 && pAdapter2);
Cleanup:
freeifaddrs(pList);
return fRet;
}
/**
* Suggests a default adapter for a given stun server socket
* @param fPrimary - true if the returned adapter is to be used for the primary socket in a stun server. Typically passing "true" means "return the first adapter enumerated", otherwise return the second adapter enumerated"
* @param family - Either AF_INET or AF_INET6
* @param pSocketAddr - OUT param. On success, contains the address to bind to
* @return S_OK on success. Error code otherwise.
*/
HRESULT GetBestAddressForSocketBind(bool fPrimary, int family, uint16_t port, CSocketAddress* pSocketAddr)
{
HRESULT hr = S_OK;
ifaddrs* pList = NULL;
ifaddrs* pAdapter = NULL;
ifaddrs* pAdapter1 = NULL;
ifaddrs* pAdapter2 = NULL;
ChkIfA(pSocketAddr == NULL, E_INVALIDARG);
ChkIf(getifaddrs(&pList) < 0, ERRNOHR);
GetDefaultAdapters(family, pList, &pAdapter1, &pAdapter2);
pAdapter = fPrimary ? pAdapter1 : pAdapter2;
ChkIf(pAdapter==NULL, E_FAIL);
*pSocketAddr = CSocketAddress(*pAdapter->ifa_addr);
pSocketAddr->SetPort(port);
Cleanup:
freeifaddrs(pList);
return hr;
}
HRESULT GetSocketAddressForAdapter(int family, const char* pszAdapterName, uint16_t port, CSocketAddress* pSocketAddr)
{
HRESULT hr = S_OK;
ifaddrs* pList = NULL;
ifaddrs* pAdapter = NULL;
ifaddrs* pAdapterFound = NULL;
ChkIfA(pszAdapterName == NULL, E_INVALIDARG);
ChkIfA(pszAdapterName[0] == '\0', E_INVALIDARG);
ChkIfA(pSocketAddr == NULL, E_INVALIDARG);
// what if the socket address is available, but not "up". Well, just let this call succeed. If the server errors out, it will get cleaned up then
ChkIf(getifaddrs(&pList) < 0, ERRNOHR);
pAdapter = pList;
while (pAdapter)
{
if (family == pAdapter->ifa_addr->sa_family)
{
if (strcmp(pAdapter->ifa_name, pszAdapterName) == 0)
{
pAdapterFound = pAdapter;
break;
}
}
pAdapter = pAdapter->ifa_next;
}
// If pszAdapterName is an IP address, convert it into a sockaddr and compare the address field with that of the adapter
// Note: an alternative approach would be to convert pAdapter->ifa_addr to a string and then do a string compare.
// But then it would be difficult to match "::1" with "0:0:0:0:0:0:0:1" and other formats of IPV6 strings
if ((pAdapterFound == NULL) && ((family == AF_INET) || (family == AF_INET6)) )
{
uint8_t addrbytes[sizeof(in6_addr)] = {};
int comparesize = (family == AF_INET) ? sizeof(in_addr) : sizeof(in6_addr);
void* pCmp = NULL;
if (inet_pton(family, pszAdapterName, addrbytes) == 1)
{
pAdapter = pList;
while (pAdapter)
{
if (family == pAdapter->ifa_addr->sa_family)
{
// offsetof(sockaddr_in, sin_addr) != offsetof(sockaddr_in6, sin6_addr)
// so you really can't do too many casting tricks like you can with sockaddr and sockaddr_in
if (family == AF_INET)
{
sockaddr_in *pAddr4 = (sockaddr_in*)(pAdapter->ifa_addr);
pCmp = &(pAddr4->sin_addr);
}
else
{
sockaddr_in6 *pAddr6 = (sockaddr_in6*)(pAdapter->ifa_addr);
pCmp = &(pAddr6->sin6_addr);
}
if (memcmp(pCmp, addrbytes, comparesize) == 0)
{
// match on ip address string found
pAdapterFound = pAdapter;
break;
}
}
pAdapter = pAdapter->ifa_next;
}
}
}
ChkIf(pAdapterFound == NULL, E_FAIL);
{
*pSocketAddr = CSocketAddress(*(pAdapterFound->ifa_addr));
pSocketAddr->SetPort(port);
}
Cleanup:
freeifaddrs(pList);
return hr;
}
/*
Copyright 2011 John Selbie
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#ifndef ADAPTERS_H
#define ADAPTERS_H
#include "socketaddress.h"
bool HasAtLeastTwoAdapters(int family);
HRESULT GetBestAddressForSocketBind(bool fPrimary, int family, uint16_t port, CSocketAddress* pSocketAddr);
HRESULT GetSocketAddressForAdapter(int family, const char* pszAdapterName, uint16_t port, CSocketAddress* pSocketAddr);
#endif /* ADAPTERS_H */
/*
Copyright 2011 John Selbie
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "commonincludes.h"
#include "socketaddress.h"
static void GetLocalPortNumberFromSocket(int sockfd, CSocketAddress* pAddr)
{
sockaddr_storage addr = {};
socklen_t len = sizeof(addr);
int ret;
ret = ::getsockname(sockfd, (sockaddr*)&addr, &len);
if (ret != -1)
{
uint16_t port=0;
if (addr.ss_family == AF_INET)
{
port = ntohs(((sockaddr_in*)&addr)->sin_port);
}
else if (addr.ss_family == AF_INET6)
{
port = ntohs(((sockaddr_in6*)&addr)->sin6_port);
}
pAddr->SetPort(port);
}
}
static void InitSocketAddress(int family, CSocketAddress* pAddr)
{
if (family == AF_INET)
{
sockaddr_in addr = {};
addr.sin_family = AF_INET;
*pAddr = CSocketAddress(addr);
}
else if (family == AF_INET6)
{
sockaddr_in6 addr = {};
addr.sin6_family = AF_INET6;
*pAddr = CSocketAddress(addr);
}
else
{
ASSERT(false);
}
}
ssize_t recvfromex(int sockfd, void* buf, size_t len, int flags, CSocketAddress* pSrcAddr, CSocketAddress* pDstAddr)
{
struct iovec vec;
ssize_t ret;
char controldata[1000];
struct msghdr hdr = {};
sockaddr_storage addrRemote = {};
vec.iov_base = buf;
vec.iov_len = len;
hdr.msg_name = &addrRemote;
hdr.msg_namelen = sizeof(addrRemote);
hdr.msg_iov = &vec;
hdr.msg_iovlen = 1;
hdr.msg_control = controldata;
hdr.msg_controllen = ARRAYSIZE(controldata);
ret = ::recvmsg(sockfd, &hdr, flags);
if (ret > 0)
{
if (pSrcAddr)
{
*pSrcAddr = CSocketAddress(*(sockaddr*)&addrRemote);
}
if (pDstAddr)
{
struct cmsghdr* pCmsg = NULL;
InitSocketAddress(addrRemote.ss_family, pDstAddr);
for (pCmsg = CMSG_FIRSTHDR(&hdr); pCmsg != NULL; pCmsg = CMSG_NXTHDR(&hdr, pCmsg))
{
// IPV6 address ----------------------------------------------------------
if ((pCmsg->cmsg_level == IPPROTO_IPV6) && (pCmsg->cmsg_type == IPV6_PKTINFO) && CMSG_DATA(pCmsg))
{
struct in6_pktinfo* pInfo = (in6_pktinfo*)CMSG_DATA(pCmsg);
sockaddr_in6 addr = {};
addr.sin6_family = AF_INET6;
addr.sin6_addr = pInfo->ipi6_addr;
*pDstAddr = CSocketAddress(addr);
GetLocalPortNumberFromSocket(sockfd, pDstAddr);
break;
}
// IPV4 address ----------------------------------------------------------
// if you change the ifdef's below, make sure you it's matched with the same logic in stunsocket.cpp
// Might be worthwhile to just use IP_RECVORIGDSTADDR and IP_ORIGDSTADDR so we can merge with the bsd code
#ifdef IP_PKTINFO
if ((pCmsg->cmsg_level == IPPROTO_IP) && (pCmsg->cmsg_type==IP_PKTINFO) && CMSG_DATA(pCmsg))
{
struct in_pktinfo* pInfo = (in_pktinfo*)CMSG_DATA(pCmsg);
sockaddr_in addr = {};
addr.sin_family = AF_INET;
addr.sin_addr = pInfo->ipi_addr;
*pDstAddr = CSocketAddress(addr);
GetLocalPortNumberFromSocket(sockfd, pDstAddr);
break;
}
#elif defined(IP_RECVDSTADDR)
// This code path for MacOSX and likely BSD as well
if ((pCmsg->cmsg_level == IPPROTO_IP) && (pCmsg->cmsg_type==IP_RECVDSTADDR) && CMSG_DATA(pCmsg))
{
sockaddr_in addr = {};
addr.sin_family = AF_INET;
addr.sin_addr = *(in_addr*)CMSG_DATA(pCmsg);
*pDstAddr = CSocketAddress(addr);
GetLocalPortNumberFromSocket(sockfd, pDstAddr);
break;
}
#else
{
int fail[-1]; // set a compile time assert if there's no option
}
#endif
}
}
}
return ret;
}
/*
Copyright 2011 John Selbie
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#ifndef RECVFROMEX_H
#define RECVFROMEX_H
ssize_t recvfromex(int sockfd, void* buf, size_t len, int flags, CSocketAddress* pSrcAddr, CSocketAddress* pDstAddr);
#endif /* RECVFROMEX_H */
/*
Copyright 2011 John Selbie
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "commonincludes.h"
#include "socketaddress.h"
#include "stringhelper.h"
HRESULT ResolveHostName(const char* pszHostName, int family, bool fNumericOnly, CSocketAddress* pAddr)
{
int ret;
HRESULT hr = S_OK;
addrinfo* pResultList = NULL;
addrinfo hints = {};
std::string strHostName(pszHostName);
StringHelper::Trim(strHostName);
ChkIf(strHostName.length() == 0, E_INVALIDARG);
ChkIf(pAddr==NULL, E_INVALIDARG);
hints.ai_family = family;
if (fNumericOnly)
{
hints.ai_flags = AI_NUMERICHOST;
}
// without a socktype hint, getaddrinfo will return 3x the number of addresses (SOCK_STREAM, SOCK_DGRAM, and SOCK_RAW)
hints.ai_socktype = SOCK_STREAM;
ret = getaddrinfo(strHostName.c_str(), NULL, &hints, &pResultList);
ChkIf(ret != 0, ERRNO_TO_HRESULT(ret));
ChkIf(pResultList==NULL, E_FAIL)
// just pick the first one found
*pAddr = CSocketAddress(*(pResultList->ai_addr));
Cleanup:
::freeaddrinfo(pResultList);
return hr;
}
/*
Copyright 2011 John Selbie
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#ifndef RESOLVEHOSTNAME_H
#define RESOLVEHOSTNAME_H
#include "socketaddress.h"
/**
* Converts a string contains a numeric address or FQDN into a socket address structure
* @param pszHostName is the string of the remote host to resolve
* @param family either AF_INET or AF_INET6
* @param fNumericOnly Prevents DNS and just resolves numeric IP addresses (e.g. "192.168.1.2"
* @param pAddr - OUT param. On success is filled with a valid socket addresss information.
* @return S_OK on success. Error code otherwise
*/
HRESULT ResolveHostName(const char* pszHostName, int family, bool fNumericOnly, CSocketAddress* pAddr);
#endif /* RESOLVEHOSTNAME_H */
/*
Copyright 2011 John Selbie
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "commonincludes.h"
#include "stuncore.h"
#include "stunsocket.h"
CStunSocket::~CStunSocket()
{
Close();
}
void CStunSocket::Close()
{
if (_sock != -1)
{
close(_sock);
_addrlocal = CSocketAddress(0,0);
}
}
int CStunSocket::GetSocketHandle() const
{
return _sock;
}
const CSocketAddress& CStunSocket::GetLocalAddress() const
{
return _addrlocal;
}
SocketRole CStunSocket::GetRole() const
{
ASSERT(_sock != -1);
return _role;
}
HRESULT CStunSocket::EnablePktInfoOption(bool fEnable)
{
int enable = fEnable?1:0;
int ret;
int family = _addrlocal.GetFamily();
int level = (family==AF_INET) ? IPPROTO_IP : IPPROTO_IPV6;
// if you change the ifdef's below, make sure you it's matched with the same logic in recvfromex.cpp
#ifdef IP_PKTINFO
int option = (family==AF_INET) ? IP_PKTINFO : IPV6_RECVPKTINFO;
#elif defined(IP_RECVDSTADDR)
int option = (family==AF_INET) ? IP_RECVDSTADDR : IPV6_PKTINFO;
#else
int fail[-1]; // set a compile time assert
#endif
ret = ::setsockopt(_sock, level, option, &enable, sizeof(enable));
// Linux documentation (man ipv6) says you are supposed to set IPV6_PKTINFO as the option
// Yet, it's really IPV6_RECVPKTINFO. Other operating systems might expect IPV6_PKTINFO.
// We'll cross that bridge, when we get to it.
// todo - we should write a unit test that tests the packet info behavior
ASSERT(ret == 0);
return (ret == 0) ? S_OK : ERRNOHR;
}
//static
HRESULT CStunSocket::Create(const CSocketAddress& addrlocal, SocketRole role, boost::shared_ptr<CStunSocket>* pStunSocketShared)
{
int sock = -1;
int ret;
CStunSocket* pSocket = NULL;
sockaddr_storage addrBind = {};
socklen_t sizeaddrBind;
HRESULT hr = S_OK;
ChkIfA(pStunSocketShared == NULL, E_INVALIDARG);
sock = socket(addrlocal.GetFamily(), SOCK_DGRAM, 0);
ChkIf(sock < 0, ERRNOHR);
ret = bind(sock, addrlocal.GetSockAddr(), addrlocal.GetSockAddrLength());
ChkIf(ret < 0, ERRNOHR);
// call get sockname to find out what port we just binded to. (Useful for when addrLocal.port is 0)
sizeaddrBind = sizeof(addrBind);
ret = ::getsockname(sock, (sockaddr*)&addrBind, &sizeaddrBind);
ChkIf(ret < 0, ERRNOHR);
pSocket = new CStunSocket();
pSocket->_sock = sock;
pSocket->_addrlocal = CSocketAddress(*(sockaddr*)&addrBind);
pSocket->_role = role;
sock = -1;
{
boost::shared_ptr<CStunSocket> spTmp(pSocket);
pStunSocketShared->swap(spTmp);
}
Cleanup:
if (sock != -1)
{
close(sock);
sock = -1;
}
return hr;
}
/*
Copyright 2011 John Selbie
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#ifndef STUNSOCKET_H
#define STUNSOCKET_H
class CStunSocket
{
private:
int _sock;
CSocketAddress _addrlocal;
SocketRole _role;
CStunSocket() {;}
CStunSocket(const CStunSocket&) {;}
void operator=(const CStunSocket&) {;}
public:
~CStunSocket();
void Close();
int GetSocketHandle() const;
const CSocketAddress& GetLocalAddress() const;
SocketRole GetRole() const;
HRESULT EnablePktInfoOption(bool fEnable);
static HRESULT Create(const CSocketAddress& local, SocketRole role, boost::shared_ptr<CStunSocket>* pStunSocketShared);
};
typedef boost::shared_ptr<CStunSocket> CRefCountedStunSocket;
#endif /* STUNSOCKET_H */
include ../common.inc
PROJECT_TARGET := stunserver
PROJECT_OBJS := main.o server.o stunsocketthread.o
PROJECT_INTERMEDIATES := usage.txtcode usagelite.txtcode
INCLUDES := $(BOOST_INCLUDE) -I../common -I../stuncore -I../networkutils
LIB_PATH := -L../common -L../stuncore -L../networkutils
LIBS := -lnetworkutils -lstuncore -lcommon -lpthread -lcrypto
all: $(PROJECT_TARGET)
clean:
rm -f $(PROJECT_OBJS) $(PROJECT_TARGET) $(PROJECT_INTERMEDIATES)
$(PROJECT_TARGET): $(PROJECT_OBJS)
$(LINK.cpp) -o $@ $^ $(LIB_PATH) $(LIBS)
main.cpp: usage.txtcode usagelite.txtcode
%.txtcode: %.txt
sh makecodefile.sh $< $@ $(*)_text
This diff is collapsed.
#
# Copyright 2011 John Selbie
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
echo BUILDING $1 INTO $2
echo const char $3[] = { > $2
xxd -i < $1 >> $2
echo ",0x00};" >> $2
echo "" >> $2
/*
Copyright 2011 John Selbie
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "commonincludes.h"
#include <openssl/hmac.h>
#include "stuncore.h"
#include "stunsocket.h"
#include "stunsocketthread.h"
#include "server.h"
#include "sampleauthprovider.h"
static const char* c_szPrivateKey = "Change this string if you are going to use this code";
static const char* c_szRealm = "YourRealmNameHere";
static HRESULT LookupPassword(bool fWithRealm, const char* pszUserName, const char* pszRealm, char* pszPassword)
{
const char* users[] = {"bruce", "steve", "nicko", "dave", "adrian"};
const char* passwords[] = {"AcesHigh", "2MinToMid", "fearofthedark!", "#ofthebeast", "Run2TheHills" };
if (fWithRealm)
{
if ((pszRealm == NULL) || (strcmp(pszRealm, c_szRealm)))
{
return E_FAIL;
}
}
if (pszUserName == NULL)
{
return E_FAIL;
}
for (size_t index = 0; index < ARRAYSIZE(users); index++)
{
if (strcmp(pszUserName, users[index]))
{
continue;
}
strcpy(pszPassword, passwords[index]);
return S_OK;
}
return E_FAIL;
}
HRESULT CShortTermAuth::DoAuthCheck(AuthAttributes* pAuthAttributes, AuthResponse* pResponse)
{
// if you want to authenticate in "short term credential mode", then this function needs to return
// a password for the passed in user name
// short-term example
// indicate to the server we are returning a short-term credential so it knows to use
// only the pResponse->szPassword field for computing the message integrity
pResponse->authCredMech = AuthCredShortTerm;
if (pAuthAttributes->fMessageIntegrityPresent == false)
{
// RFC 5389 indicates to send back a "400" if there is no message integrity. That's
// what "Reject" will signal to the server to respond with
pResponse->responseType = Reject;
return S_OK;
}
if (SUCCEEDED(LookupPassword(false, pAuthAttributes->szUser, NULL, pResponse->szPassword)))
{
// Returning "AllowConditional" indicates that the request can be accepted if and only if the
// message integrity attribute can be validated with the value placed into pResponse->szPassword
pResponse->responseType = AllowConditional;
return S_OK;
}
// If it's not a valid user (or no password could be found), just return Unauthorized.
// This will result in a 401 getting sent back
pResponse->responseType = Unauthorized;
return S_OK;
}
HRESULT CLongTermAuth::DoAuthCheck(AuthAttributes* pAuthAttributes, AuthResponse* pResponse)
{
HRESULT hr = S_OK;
pResponse->authCredMech = AuthCredLongTerm;
// Go ahead and generate a new nonce and set the realm.
// The realm and nonce attributes will only get sent back to the client when there is an auth error
CreateNonce(pResponse->szNonce);
strcpy(pResponse->szRealm, c_szRealm);
// if we're missing any authentication attributes, then just return back a 401.
// This will trigger the server to send back the nonce and realm attributes to the client within the 401 resposne
if ((pAuthAttributes->fMessageIntegrityPresent == false) || (pAuthAttributes->szNonce[0] == 0) || (pAuthAttributes->szUser[0] == 0))
{
pResponse->responseType = Unauthorized;
return S_OK;
}
// copy the user's password into szPassword
hr = LookupPassword(true, pAuthAttributes->szUser, pAuthAttributes->szNonce, pResponse->szPassword);
if (FAILED(hr))
{
// if not a valid user, same as before. Just send back a 401
pResponse->responseType = Unauthorized;
return S_OK;
}
// validate the nonce
if (FAILED(ValidateNonce(pAuthAttributes->szNonce)))
{
pResponse->responseType = StaleNonce;
return S_OK;
}
// returning "AllowConditional" indicates that the request can be accepted if and only if the
// message integrity attribute can be validated with the value placed into pResponse->szPassword
pResponse->responseType = AllowConditional;
return S_OK;
}
void CLongTermAuth::HmacToString(uint8_t* hmacresult, char* pszResult)
{
sprintf(pszResult, "%.2x%.2x%.2x%.2x%.2x%.2x%.2x%.2x%.2x%.2x%.2x%.2x%.2x%.2x%.2x%.2x%.2x%.2x%.2x%.2x",
hmacresult[0], hmacresult[1], hmacresult[2], hmacresult[3], hmacresult[4],
hmacresult[5], hmacresult[6], hmacresult[7], hmacresult[8], hmacresult[9],
hmacresult[10], hmacresult[11], hmacresult[12], hmacresult[13], hmacresult[14],
hmacresult[15], hmacresult[16], hmacresult[17], hmacresult[18], hmacresult[19]);
}
HRESULT CLongTermAuth::CreateNonce(char *pszNonce)
{
// This is a sample "nonce provider". Our implementation of "nonce" is just a a string
// indicating a timestamp followed by an HMAC hash of the timestamp.
// Validation of a nonce is to just to make sure the timestamp isn't more than a couple
// of minutes old and that the hmac hash matches
// If you use this code, make sure you change the value of c_szPrivateKey!
time_t thetime = time(NULL);
uint8_t hmacresult[20] = {};
char szHMAC[20*2+1];
char szTime[sizeof(time_t)*4];
unsigned int len = ARRAYSIZE(hmacresult);
sprintf(szTime, "%u:", (unsigned int)thetime);
HMAC(::EVP_sha1(), (unsigned char*)c_szPrivateKey, strlen(c_szPrivateKey), (unsigned char*)szTime, strlen(szTime), hmacresult, &len);
HmacToString(hmacresult, szHMAC);
strcpy(pszNonce, szTime);
strcat(pszNonce, szHMAC);
return S_OK;
}
HRESULT CLongTermAuth::ValidateNonce(char* pszNonce)
{
time_t thecurrenttime = time(NULL);
time_t thetime;
uint8_t hmacresult[20] = {};
char szHMAC[20*2+1];
char szNonce[100];
char *pRightHalf = NULL;
time_t diff;
unsigned int len = ARRAYSIZE(hmacresult);
strncpy(szNonce, pszNonce, ARRAYSIZE(szNonce));
szNonce[ARRAYSIZE(szNonce)-1] = 0;
pRightHalf = strstr(szNonce, ":");
if (pRightHalf == NULL)
{
return E_FAIL;
}
*pRightHalf++ = 0;
thetime = atoi(szNonce);
diff = thecurrenttime - thetime;
if (((thecurrenttime - thetime) > 120) || (diff < 0))
{
// nonce is more than 2 minutes old - reject
return E_FAIL;
}
// nonce timestamp is valid, but was it signed by this server?
HMAC(::EVP_sha1(), (unsigned char*)c_szPrivateKey, strlen(c_szPrivateKey), (unsigned char*)szNonce, strlen(szNonce), hmacresult, &len);
HmacToString(hmacresult, szHMAC);
if (strcmp(szHMAC, pRightHalf))
{
return E_FAIL;
}
return S_OK;
}
/*
Copyright 2011 John Selbie
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#ifndef SAMPLE_STUN_AUTH_PROVIDER
#define SAMPLE_STUN_AUTH_PROVIDER
#if 0
sampleauthprovider.h and sampleauthprovider.cpp
The stun library code knows how to generate and validate message integrity attributes
inside stun attributes. But it relies on an implemented "auth provider" to actually
confirm a username and supply the password key.
It is out of the scope of this release to implement an authentication provider that
will work for everyone. One deployment may store usernames and passwords in a database.
Other deployments may rely on a ticketing service library.
RFC 5389 specifies two authentication mechanisms for STUN. It is recommended
that you read it before deciding how timplement
"Short term credentials" is simply the case where a stun binding request
sent from client to server contains a message integrity attribute. The message
integrity attribute is simply an attribute that contins an HMAC-SHA1 hash of
the stun msesage itself. The stun message should also contain a username attribute.
The 'key' field of the HMAC operation is a private password associated with the
username in the binding request.
"Long term credentials" is similar to short term credentials but involves a challenge
response phase to prevent replay attacks. This is the recommended mechanism for
STUN authentication.
We can also support "ticket based credentials". This is not a mechanism specified
by the STUN RFC, but has been known to work in various scenarios. The username
and/or realm fields are overloaded to represent a "ticket" signed by an external
entity. The auth provider knows how to validate the ticket.
We can also implement "legacy password" authentication. This is simply the password
(with or without a username and realm) embedded in the clear in the stun binding
request. Not recommended unless the transport type is TLS.
Implementing authentication is simply implementing a class that implements
IStunAuth. IStunAuth has only one method called "DoAuthCheck". DoAuthCheck
is called for each incoming Stun Message. It takes one input parameter pointer
to a struct instance of type AuthAttributes) and one "out" param which is a struct
for handing back authentication tokens back to the server.
HRESULT DoAuthCheck(/*in*/ AuthAttributes* pAuthAttributes, /*out*/ AuthResponse* pResponse);
The AuthAttributes struct representes various attribute values received in a STUN
binding request. They are outlined as follows:
char szUser[MAX_STUN_AUTH_STRING_SIZE]; // the user name attribute in the request (if available)
char szRealm[MAX_STUN_AUTH_STRING_SIZE]; // the realm attribute in the request (if available)
char szNonce[MAX_STUN_AUTH_STRING_SIZE]; // the nonce attribute in the request (if available)
char szLegacyPassword[MAX_STUN_AUTH_STRING_SIZE]; // this is not the password used in the message integrity, this is if the request provided a password in the clear (ala rfc 3478). Not recommended, but auth providers can use it if they want.
bool fMessageIntegrityPresent; // true if there was a message integrity field
The implementation of DoAuthCheck needs to decide how to handle the AuthAttributes
and then pass back a series of results and codes through the provided AuthResponse
paramter.
The AuthResponse parameter is for indicating to the server how to authenticate a
message integrity attribute. The implementation needs to set the following fieds
as appropriate:
AuthResponseType responseType;
responseType must be set to one of the following values
// Allow - Indicates to the server to send back a response and that no
additional validation on the message integrity field is needed
// AllowConditional - Indicates to the server that a response can be sent
as lone as the message integrity attribute in the stun request
is valid with respect to szPassword (and szUserName and szNonce if in long term cred mode)
If the message integrity attribute is deemed invalid, then
a 401 is sent back instead of a binding response.
// Reject - Indicates that the server should send back a 400 without any
additional attributes
// Unauthorized - Indicates that the server should send back a 401. In long term
cred mode, this will also send back the szNonce and szRealm fields
as attributes.
// StaleNonce - Indicates that the request was likely valid, but the nonce
attribute valid has expired
AuthCredentialMechanism authCredMech;
Is either set to AuthShortTerm or AuthLongTerm to indicate to the server
how to generate and validate message integrity fields
szPassword
Ignored if _responseType is anything other than AllowConditional.
server will not send this back as an attribute. Instead it is used
for validating and generating message integrity attributes in the
stun messages.
szRealm
Ignored if using short term credentials. Otherwise, it should be
the realm field used for generating and validating message integrity fields.
It will almost always need to be sent back in error responses to
the client.
szNonce
A new nonce for subsequent requests in the event this request can not
DoAuthCheck should return S_OK unless a fatal error occurs. If DoAuthCheck returns
a failure code, then
To have the server host an instance of an IStunAuth implementation, modify
CStunServer::Initialize to create an instance of your class and initialize
_spAuth as appropriate.
#endif
class CShortTermAuth :
public CBasicRefCount,
public CObjectFactory<CShortTermAuth>,
public IStunAuth
{
public:
virtual HRESULT DoAuthCheck(AuthAttributes* pAuthAttributes, AuthResponse* pResponse);
};
class CLongTermAuth :
public CBasicRefCount,
public CObjectFactory<CLongTermAuth>,
public IStunAuth
{
private:
void HmacToString(uint8_t* hmacvalue, char* pszResult);
HRESULT CreateNonce(char* pszNonce);
HRESULT ValidateNonce(char* pszNonce);
public:
virtual HRESULT DoAuthCheck(AuthAttributes* pAuthAttributes, AuthResponse* pResponse);
};
#endif
/*
Copyright 2011 John Selbie
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "commonincludes.h"
#include <openssl/hmac.h>
#include "stuncore.h"
#include "stunsocket.h"
#include "stunsocketthread.h"
#include "server.h"
CStunServerConfig::CStunServerConfig() :
fHasPP(false),
fHasPA(false),
fHasAP(false),
fHasAA(false),
fMultiThreadedMode(false)
{
;
}
CStunServer::CStunServer()
{
;
}
CStunServer::~CStunServer()
{
Shutdown();
}
HRESULT CStunServer::Initialize(const CStunServerConfig& config)
{
HRESULT hr = S_OK;
int socketcount = 0;
CRefCountedPtr<IStunAuth> _spAuth;
// cleanup any thing that's going on now
Shutdown();
// optional code: create an authentication provider and initialize it here (if you want authentication)
// set the _spAuth member to reference it
// Chk(CYourAuthProvider::CreateInstanceNoInit(&_spAuth));
// Create the sockets
if (config.fHasPP)
{
Chk(CStunSocket::Create(config.addrPP, RolePP, &_arrSockets[RolePP]));
_arrSockets[RolePP]->EnablePktInfoOption(true);
socketcount++;
}
if (config.fHasPA)
{
Chk(CStunSocket::Create(config.addrPA, RolePA, &_arrSockets[RolePA]));
_arrSockets[RolePA]->EnablePktInfoOption(true);
socketcount++;
}
if (config.fHasAP)
{
Chk(CStunSocket::Create(config.addrAP, RoleAP, &_arrSockets[RoleAP]));
_arrSockets[RoleAP]->EnablePktInfoOption(true);
socketcount++;
}
if (config.fHasAA)
{
Chk(CStunSocket::Create(config.addrAA, RoleAA, &_arrSockets[RoleAA]));
_arrSockets[RoleAA]->EnablePktInfoOption(true);
socketcount++;
}
ChkIf(socketcount == 0, E_INVALIDARG);
if (config.fMultiThreadedMode == false)
{
Logging::LogMsg(LL_DEBUG, "Configuring single threaded mode\n");
std::vector<CRefCountedStunSocket> listsockets;
for (size_t index = 0; index < ARRAYSIZE(_arrSockets); index++)
{
if (_arrSockets[index] != NULL)
{
listsockets.push_back(_arrSockets[index]);
}
}
// create one thread for all the sockets
CStunSocketThread* pThread = new CStunSocketThread();
ChkIf(pThread==NULL, E_OUTOFMEMORY);
_threads.push_back(pThread);
Chk(pThread->Init(listsockets, this, _spAuth));
}
else
{
Logging::LogMsg(LL_DEBUG, "Configuring multi-threaded mode\n");
// one thread for every socket
CStunSocketThread* pThread = NULL;
for (size_t index = 0; index < ARRAYSIZE(_arrSockets); index++)
{
if (_arrSockets[index] != NULL)
{
std::vector<CRefCountedStunSocket> listsockets;
listsockets.push_back(_arrSockets[index]);
pThread = new CStunSocketThread();
ChkIf(pThread==NULL, E_OUTOFMEMORY);
_threads.push_back(pThread);
Chk(pThread->Init(listsockets, this, _spAuth));
}
}
}
Cleanup:
if (FAILED(hr))
{
Shutdown();
}
return hr;
}
HRESULT CStunServer::Shutdown()
{
size_t len;
Stop();
// release the sockets and the thread
for (size_t index = 0; index < ARRAYSIZE(_arrSockets); index++)
{
_arrSockets[index].reset();
}
len = _threads.size();
for (size_t index = 0; index < len; index++)
{
CStunSocketThread* pThread = _threads[index];
delete pThread;
_threads[index] = NULL;
}
_threads.clear();
_spAuth.ReleaseAndClear();
return S_OK;
}
HRESULT CStunServer::Start()
{
HRESULT hr = S_OK;
size_t len = _threads.size();
ChkIfA(len == 0, E_UNEXPECTED);
for (size_t index = 0; index < len; index++)
{
CStunSocketThread* pThread = _threads[index];
if (pThread != NULL)
{
// set the "exit flag" that each thread looks at when it wakes up from waiting
ChkA(pThread->Start());
}
}
Cleanup:
if (FAILED(hr))
{
Stop();
}
return hr;
}
HRESULT CStunServer::Stop()
{
size_t len = _threads.size();
for (size_t index = 0; index < len; index++)
{
CStunSocketThread* pThread = _threads[index];
if (pThread != NULL)
{
// set the "exit flag" that each thread looks at when it wakes up from waiting
pThread->SignalForStop(false);
}
}
for (size_t index = 0; index < len; index++)
{
CStunSocketThread* pThread = _threads[index];
// Post a bunch of empty buffers to get the threads unblocked from whatever socket call they are on
// In multi-threaded mode, this may wake up a different thread. But that's ok, since all threads start and stop together
if (pThread != NULL)
{
pThread->SignalForStop(true);
}
}
for (size_t index = 0; index < len; index++)
{
CStunSocketThread* pThread = _threads[index];
if (pThread != NULL)
{
pThread->WaitForStopAndClose();
}
}
return S_OK;
}
bool CStunServer::HasAddress(SocketRole role)
{
return (::IsValidSocketRole(role) && (_arrSockets[role].get() != NULL));
}
HRESULT CStunServer::GetSocketAddressForRole(SocketRole role, CSocketAddress* pAddr)
{
HRESULT hr = S_OK;
ChkIf(pAddr == NULL, E_INVALIDARG);
ChkIf(false == HasAddress(role), E_FAIL);
*pAddr = _arrSockets[role]->GetLocalAddress();
Cleanup:
return S_OK;
}
HRESULT CStunServer::SendResponse(SocketRole roleOutput, const CSocketAddress& addr, CRefCountedBuffer& spResponse)
{
HRESULT hr = S_OK;
int sockhandle = -1;
int ret;
ChkIf(false == HasAddress(roleOutput), E_FAIL);
sockhandle = _arrSockets[roleOutput]->GetSocketHandle();
ret = ::sendto(sockhandle, spResponse->GetData(), spResponse->GetSize(), 0, addr.GetSockAddr(), addr.GetSockAddrLength());
ChkIf(ret < 0, ERRNOHR);
Cleanup:
return hr;
}
/*
Copyright 2011 John Selbie
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#ifndef STUN_SERVER_H
#define STUN_SERVER_H
#include "stunsocket.h"
#include "stunsocketthread.h"
#include "stunauth.h"
class CStunServerConfig
{
public:
bool fHasPP; // PP: Primary ip, Primary port
bool fHasPA; // PA: Primary ip, Alternate port
bool fHasAP; // AP: Alternate ip, Primary port
bool fHasAA; // AA: Alternate ip, Alternate port
bool fMultiThreadedMode; // if true, one thread for each socket
CSocketAddress addrPP; // address for PP
CSocketAddress addrPA; // address for PA
CSocketAddress addrAP; // address for AP
CSocketAddress addrAA; // address for AA
CStunServerConfig();
};
class CStunServer :
public CBasicRefCount,
public CObjectFactory<CStunServer>,
public IStunResponder
{
private:
CRefCountedStunSocket _arrSockets[4];
// when we support multithreaded servers, this will change to a list
std::vector<CStunSocketThread*> _threads;
CStunServer();
~CStunServer();
friend class CObjectFactory<CStunServer>;
CRefCountedPtr<IStunAuth> _spAuth;
public:
HRESULT Initialize(const CStunServerConfig& config);
HRESULT Shutdown();
HRESULT Start();
HRESULT Stop();
// IStunResponder
virtual HRESULT SendResponse(SocketRole roleOutput, const CSocketAddress& addr, CRefCountedBuffer& spResponse);
virtual bool HasAddress(SocketRole role);
virtual HRESULT GetSocketAddressForRole(SocketRole role, /*out*/ CSocketAddress* pAddr);
ADDREF_AND_RELEASE_IMPL();
};
#endif /* SERVER_H */
/*
Copyright 2011 John Selbie
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "commonincludes.h"
#include "stuncore.h"
#include "stunsocket.h"
#include "stunsocketthread.h"
#include "recvfromex.h"
CStunSocketThread::CStunSocketThread() :
_fNeedToExit(false),
_pthread((pthread_t)-1),
_fThreadIsValid(false),
_rotation(0)
{
;
}
CStunSocketThread::~CStunSocketThread()
{
SignalForStop(true);
WaitForStopAndClose();
}
HRESULT CStunSocketThread::Init(std::vector<CRefCountedStunSocket>& listSockets, IStunResponder* pResponder, IStunAuth* pAuth)
{
HRESULT hr = S_OK;
ChkIfA(_fThreadIsValid, E_UNEXPECTED);
ChkIfA(pResponder == NULL, E_UNEXPECTED);
ChkIfA(listSockets.size() <= 0, E_INVALIDARG);
_socks = listSockets;
_handler.SetResponder(pResponder);
_handler.SetAuth(pAuth);
_fNeedToExit = false;
_rotation = 0;
Cleanup:
return hr;
}
HRESULT CStunSocketThread::Start()
{
HRESULT hr = S_OK;
int err = 0;
ChkIfA(_fThreadIsValid, E_UNEXPECTED);
ChkIfA(_socks.size() <= 0, E_FAIL);
err = ::pthread_create(&_pthread, NULL, CStunSocketThread::ThreadFunction, this);
ChkIfA(err != 0, ERRNO_TO_HRESULT(err));
_fThreadIsValid = true;
Cleanup:
return hr;
}
HRESULT CStunSocketThread::SignalForStop(bool fPostMessages)
{
size_t size = _socks.size();
HRESULT hr = S_OK;
_fNeedToExit = true;
// have the socket send a message to itself
// if another thread is sharing the same socket, this may wake that thread up to
// but all the threads should be started and shutdown together
if (fPostMessages)
{
for (size_t index = 0; index < size; index++)
{
char data = 'x';
::CSocketAddress addr(_socks[index]->GetLocalAddress());
::sendto(_socks[index]->GetSocketHandle(), &data, 1, 0, addr.GetSockAddr(), addr.GetSockAddrLength());
}
}
return hr;
}
HRESULT CStunSocketThread::WaitForStopAndClose()
{
void* pRetValFromThread = NULL;
if (_fThreadIsValid)
{
// now wait for the thread to exit
pthread_join(_pthread, &pRetValFromThread);
}
_fThreadIsValid = false;
_pthread = (pthread_t)-1;
_socks.clear();
return S_OK;
}
// static
void* CStunSocketThread::ThreadFunction(void* pThis)
{
((CStunSocketThread*)pThis)->Run();
return NULL;
}
int CStunSocketThread::WaitForSocketData()
{
fd_set set = {};
int nHighestSockValue = 0;
size_t nSocketCount = _socks.size();
int ret;
CRefCountedStunSocket spSocket;
int result = -1;
UNREFERENCED_VARIABLE(ret); // only referenced in ASSERT
// rotation gives another socket priority in the next loop
_rotation = (_rotation + 1) % nSocketCount;
ASSERT(_rotation >= 0);
FD_ZERO(&set);
for (size_t index = 0; index < nSocketCount; index++)
{
int sock = _socks[index]->GetSocketHandle();
FD_SET(sock, &set);
nHighestSockValue = (sock > nHighestSockValue) ? sock : nHighestSockValue;
}
// wait indefinitely for a socket
ret = ::select(nHighestSockValue+1, &set, NULL, NULL, NULL);
ASSERT(ret > 0); // This will be a benign assert, and should never happen. But I will want to know if it does
// now figure out which socket just got data on it
spSocket.reset();
for (size_t index = 0; index < nSocketCount; index++)
{
int indexconverted = (index + _rotation) % nSocketCount;
int sock = _socks[indexconverted]->GetSocketHandle();
if (FD_ISSET(sock, &set))
{
result = indexconverted;
break;
}
}
return result;
}
void CStunSocketThread::Run()
{
size_t nSocketCount = _socks.size();
bool fMultiSocketMode = (nSocketCount > 1);
int recvflags = fMultiSocketMode ? MSG_DONTWAIT : 0;
CRefCountedStunSocket spSocket = _socks[0];
const int RECV_BUFFER_SIZE = 1500;
CRefCountedBuffer spBuffer(new CBuffer(RECV_BUFFER_SIZE));
int ret;
int socketindex = 0;
CSocketAddress remoteAddr;
CSocketAddress localAddr;
Logging::LogMsg(LL_DEBUG, "Starting listener thread");
while (_fNeedToExit == false)
{
if (fMultiSocketMode)
{
spSocket.reset();
socketindex = WaitForSocketData();
if (_fNeedToExit)
{
break;
}
ASSERT(socketindex >= 0);
if (socketindex < 0)
{
// just go back to waiting;
continue;
}
spSocket = _socks[socketindex];
ASSERT(spSocket != NULL);
}
// now receive the data
spBuffer->SetSize(0);
ret = ::recvfromex(spSocket->GetSocketHandle(), spBuffer->GetData(), spBuffer->GetAllocatedSize(), recvflags, &remoteAddr, &localAddr);
if (Logging::GetLogLevel() >= LL_VERBOSE)
{
char szIPRemote[100];
char szIPLocal[100];
remoteAddr.ToStringBuffer(szIPRemote, 100);
localAddr.ToStringBuffer(szIPLocal, 100);
Logging::LogMsg(LL_VERBOSE, "recvfrom returns %d from %s on local interface %s", ret, szIPRemote, szIPLocal);
}
if (ret < 0)
{
// error
continue;
}
if (_fNeedToExit)
{
break;
}
spBuffer->SetSize(ret);
StunMessageEnvelope msg;
msg.remoteAddr = remoteAddr;
msg.spBuffer = spBuffer;
msg.localSocket = spSocket->GetRole();
msg.localAddr = localAddr;
_handler.ProcessRequest(msg);
}
Logging::LogMsg(LL_DEBUG, "Thread exiting");
}
/*
Copyright 2011 John Selbie
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#ifndef STUNSOCKETTHREAD_H
#define STUNSOCKETTHREAD_H
#include "stunsocket.h"
class CStunServer;
class CStunSocketThread
{
public:
CStunSocketThread();
~CStunSocketThread();
HRESULT Init(std::vector<CRefCountedStunSocket>& listSockets, IStunResponder* pResponder, IStunAuth* pAuth);
HRESULT Start();
HRESULT SignalForStop(bool fPostMessages);
HRESULT WaitForStopAndClose();
/// returns back the index of the socket _socks that is ready for data, otherwise, -1
int WaitForSocketData();
private:
// this is the function that runs in a thread
void Run();
static void* ThreadFunction(void* pThis);
std::vector<CRefCountedStunSocket> _socks;
bool _fNeedToExit;
CStunThreadMessageHandler _handler;
pthread_t _pthread;
bool _fThreadIsValid;
int _rotation;
};
#endif /* STUNSOCKETTHREAD_H */
Usage: stunserver [OPTION]...
Start a STUN server in one of various modes of operation using any of
the [OPTION]s described below:
Available options:
--mode=MODE
Where MODE is either "basic" or "full". In basic mode, the server only listens on one port and does not support STUN CHANGE requests. In full mode, the STUN service listens on two different interfaces and two different ports on each. A client binding request may specify an option for the server to send the response back from one of the alternate interfaces and/or ports. Basic mode is sufficient for basic NAT traversal. Full mode facilitates clients attempting to determine NAT behavior and NAT filtering behavior. Full mode requires two unique network interfaces with different IP addresses. Full mode does not work with TCP or TLS, only UDP. If this parameter is not specified, basic mode is the default.
--primaryinterface=INTERFACE or IPADDRESS
The value for this option may the name of an interface (such as "eth0" or "lo"). Or it may be one of the available IP addresses assigned to a network interface present on the host (such as "128.23.45.67"). The interface chosen will be used by the service as the primary listening address in either full or basic mode. In basic mode, the default primary interface is ALL adapters (socket binds to INADDR_ANY). In full mode, the service binds to the the first non-localhost interface that in the UP state with a valid IP Address.
--altinterface=INTERFACE OR IPADDRESS
Same as the --primaryinterface option, except this option's value specifies the listening address for the alternate address in full mode. It has no meaning in basic mode nor does it have meaning in when TCP or TLS is the listening protocol. In full mode, the service will bind to the the second non-localhost interface that is in the UP state with a valid IP address.
--primaryport=PORTNUM
PORTNUM is a value between 1 to 65535. This is the UDP or TCP port that the primary and alternate interfaces listen on as the primary port for binding requests. The default is 3478 for UDP and TCP. For TLS it is 5349.
--altport=PORTNUM
PORTNUM is a value between 1 to 65535. This is the UDP or TCP port that the primary and alternate interfaces listen on as the alternate port. The default is 3479. It has no meaning in FULL mode.
--family=IPVERSION
IPVERSION is either "4" or "6" to specify the usage of IPV4 or IPV6. If not specified, the default value is "4".
--protocol=PROTO
PROTO is either "udp", "tcp", or "tls". Where "udp" is the default. "tcp" and "tls" modes are only available when the --mode option is "basic". (Note: tcp and tls are not yet available in this version)
--verbosity=LOGLEVEL
Sets the verbosity of the logging level. 0 is the default (minimal output and logging). 1 shows slightly more. 2 and higher shows even more.
--help
Prints this help page
Examples:
stunserver
With no options, starts a basic STUN binding service on UDP port 3478.
stunserver --mode full --primaryinterface 128.34.56.78 --altinterface 128.34.56.79
Above example starts a dual-host STUN service on the the interfaces identified by the IP address "128.34.56.78" and "128.34.56.79". There are four socket listeners:
128.34.56.78:3478 (Primary IP, Primary Port)
128.34.56.78:3479 (Primary IP, Alternate Port)
128.34.56.79:3478 (Primary IP, Primary Port)
128.34.56.79:3479 (Alternate IP, Alternate Port)
An error occurs if the addresses specified do not exist on the local host running the service.
stunserver --mode=full --primaryinterface=eth0 --altinterface=eth1
Same as above, except the interfaces are specified by their names as enumerated by the system. (The "ifconfig" or "ipconfig" command will enumerate available interface names.
Usage: stunserver [OPTION]...
Try 'stunserver --help' for a complete set of options
include ../common.inc
PROJECT_TARGET := libstuncore.a
PROJECT_OBJS := buffer.o datastream.o messagehandler.o socketaddress.o stunbuilder.o stunclientlogic.o stunclienttests.o stunreader.o stunutils.o
INCLUDES := $(BOOST_INCLUDE) $(OPENSSL_INCLUDE) -I../common
all: $(PROJECT_TARGET)
clean:
rm -f $(PROJECT_OBJS) $(PROJECT_TARGET)
$(PROJECT_TARGET): $(PROJECT_OBJS)
rm -f $@
$(AR) rv $@ $^
This diff is collapsed.
/*
Copyright 2011 John Selbie
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#ifndef CBUFFER_H
#define CBUFFER_H
class CBuffer
{
private:
uint8_t* _data;
size_t _size;
size_t _allocatedSize;
boost::scoped_array<uint8_t> _spAllocation;
// disallow copy and assignment.
CBuffer(const CBuffer&);
void operator=(const CBuffer& other);
public:
CBuffer(); // deliberately makes the buffer null
void Reset(); // releases current pointer
CBuffer(size_t nSize);
HRESULT InitWithAllocation(size_t size);
CBuffer(uint8_t* pByteArray, size_t nByteArraySize, bool fCopy);
HRESULT InitWithAllocAndCopy(uint8_t* pByteArray, size_t nByteArraySize);
HRESULT InitNoAlloc(uint8_t* pByteArray, size_t nByteArraySize);
size_t GetSize();
size_t GetAllocatedSize();
HRESULT SetSize(size_t size);
uint8_t* GetData();
bool IsValid();
};
typedef boost::shared_ptr<CBuffer> CRefCountedBuffer;
#endif
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment