Import a NPAPI DLL in a C++ application - c++

I have to import a NPAPI DLL in a C++ application for my project.
I follow the excellent tutorial : http://colonelpanic.net/2009/03/building-a-firefox-plugin-part-one/
However I have some trouble to access to the methods of the dll. (Note: in a browser the dll is completly functional). After calling the main functions : NP_GetEntryPoints and NP_Initialize and retrieving the ScriptableNPObject, the invocation of the methods return no value with no error. The property or method names are the same used in javascript in the browser (functional case).
For information, the property and method names and the mime-type have been replaced in this sample.
Anyone has an idea to invoke the methods of the dll by simulating what the browser does?
Here is a part of the main program:
if (hDLL == 0)
{
std::cout << "DLL failed to load!" << std::endl;
}
else
{
std::cout << "DLL loaded!" << std::endl;
//WRAP NP_GETENTRYPOINTS FUNCTION:
_GetEntryPointsFunc = (GetEntryPointsFunc)GetProcAddress(hDLL, "NP_GetEntryPoints");
if (_GetEntryPointsFunc)
{
std::cout << "Get NP_GetEntryPoints Function!" << std::endl;
status = _GetEntryPointsFunc(pFuncs);
}
//WRAP NP_INITIALIZE FUNCTION:
_InitializeFunc = (InitializeFunc)GetProcAddress(hDLL, "NP_Initialize");
if (_InitializeFunc)
{
std::cout << "Get NP_Initialize Function!" << std::endl;
status = _InitializeFunc(&sBrowserFuncs);
}
int32_t mode = NP_EMBED;
int32_t argc = 7;
static const char mimetype[] = "application/x-mime_type_of_my_plugin";
char * argn[] = {"param1", "param2", "param3", "param4", "param5", "param6", "param7"};
char * argv[] = { "value1", "value2", "value3", "value4", "value5", "value6", "value7" };
NPObject np_object;
uint16_t size;
char* descritpionString;
char* nameString;
instance = &(plugin_instance.mNPP);
status = pFuncs->newp((char*)mimetype, instance, (uint16_t)mode, argc, argn, argv, &saved);
status = pFuncs->version; //OK
status = pFuncs->getvalue(instance,NPPVpluginDescriptionString,&descritpionString); //OK
status = pFuncs->getvalue(instance,NPPVpluginNameString,&nameString); //OK
status = pFuncs->getvalue(instance,NPPVpluginScriptableNPObject,&np_object); //ISSUE STARTS HERE
std::cin.get();
}
Here is my create_object function called after getting the scriptable NPObject with the getvalue function:
NPObject* _createobject(NPP npp, NPClass* aClass)
{
if (!npp) {
return nullptr;
}
if (!aClass) {
return nullptr;
}
NPObject *npobj;
if (aClass->allocate) {
npobj = aClass->allocate(npp, aClass);
} else {
npobj = (NPObject *)malloc(sizeof(NPObject));
}
if (npobj) {
npobj->_class = aClass;
npobj->referenceCount = 1;
//TEST:
NPError status;
NPString url;
NPVariant result;
NPVariant variant;
NPIdentifier property = "existing_property";
NPIdentifier *arrayId;
uint32_t count = 2;
const char *str = "https://test_url.com";
url.UTF8Characters = str;//;
url.UTF8Length = 20;
variant.type = NPVariantType_String;
variant.value.stringValue = url;
NPVariant args[] = { variant };
status = 1; //GENERIC ERROR VALUE
status = npobj->_class->structVersion; //OK
status = npobj->_class->hasMethod(npobj,L"existing_set_function"); //STATUS OK
status = npobj->_class->enumerate(npobj, &arrayId, &count); //Privileged instruction ERROR
status = npobj->_class->hasProperty(npobj, property); //STATUS OK
status = npobj->_class->getProperty(npobj, property, &result); //STATUS OK BUT NO RESULT
status = npobj->_class->invoke(npobj,L"existing_set_function",args,1,&result); //STATUS OK
status = npobj->_class->invoke(npobj,L"existing_get_function",args,0,&result); //STATUS OK BUT NO RESULT
status = npobj->_class->invokeDefault(npobj,args,0,&result); //STATUS OK BUT NO RESULT
//END TEST
}
return npobj;
}
Finally, here is the plugin_instance methods to declare ndata and pdata:
nsNPAPIPluginInstance::nsNPAPIPluginInstance()
{
mNPP.pdata = NULL;
mNPP.ndata = this;
}
nsNPAPIPluginInstance::~nsNPAPIPluginInstance()
{
}
Thanks in advance.

While it is unclear what "no result" means, a privileged instruction error suggests the function pointer is off.
I'd start with:
Checking that structVersion >= NP_CLASS_STRUCT_VERSION_ENUM (otherwise NPClass::enumerate is not available)
Using the proper npapi-sdk headers - your function pointer type names suggest you are not doing that
Using a simple test plugin to see what happens on the plugin side
Take care that the NPIdentifiers match up between your host application and the plugin, i.e. use a common string->NPIdentifier mapping for the host code and NPN_GetStringIdentifier() - as posted this can't work
Don't test the NPObject right in the creation function - the plugin may set things up to work properly only after NPN_CreateObject() returned

Related

Retrieve ptr from function call asmjit

I am trying to generate a function call using AsmJit to which I pass an char*. This char* is in itself retrieved from another function call. I tried out this:
typedef
const char* getStr();
const char* getStrImpl() {
return "hello pie";
}
void use_str_impl(int id, const char* c_str) {
// do stuff...
}
int main() {
JitRuntime rt;
CodeHolder code;
code.init(rt.getCodeInfo());
X86Compiler c(&code);
auto jitted_func = c.addFunc(FuncSignature0<const char*>(code.getCodeInfo().getCdeclCallConv()));
auto err = c.getLastError();
auto call = c.call((uint64_t) fooFuncImpl, FuncSignature0<intptr_t>());
X86Gpd res(call->getRet().getId());
auto call2 = c.call((uint64_t) send_input, FuncSignature2<void, int, intptr_t>());
err = !call2->setArg(0, Imm(42));
err = !call2->setArg(1, res);
c.ret();
c.endFunc();
err = c.finalize();
if(err) return 0;
size_t size = code.getCodeSize();
VMemMgr vm;
void* p = vm.alloc(size);
if (!p) return 0;
code.relocate(p);
auto fun = (entrypoint*) p;
fun();
}
It turns out this does not generate any instructions for the second parameter or second call to setArg. I also tried to use .newIntPtr and using move instructions to move the result of call into place. But this generated dec and add instructions which made no sense to me and my small experience with assembly. What is the correct way of doing this type of thing?
Btw I am using the AsmJit next branch.
I have done few corrections to your sample with some comments.
Better Usage of JitRuntime:
JitRuntime rt;
size_t size = code.getCodeSize();
VMemMgr vm;
....
void* p = vm.alloc(size);
if (!p) return 0;
code.relocate(p);
auto fun = (entrypoint*) p;
You have used JitRuntime just to setup the parameters for CodeHolder, but then avoided it and allocated the memory for the function yourself. While that's a valid use case it's not what most people do. Using runtime's add() is sufficient in most cases.
Invalid use of CCFuncCall::getRet():
X86Gpd res(call->getRet().getId());
The call node at this point doesn't have any return register assigned so it would return an invalid id. If you need to create a virtual register you always have to call compiler's newSomething(). AsmJit's compiler provides API to check for that case at runtime, if you are unsure:
// Would print 0
printf("%d", (int)c.isVirtRegValid(call->getRet().getId()));
The solution is to create a new virtual register and ASSIGN it to the function's return value. Assigning return value requires an index (like assigning an argument), the reason is that some functions may return multiple values(like 64-bit value in 32-bit mode), using 0 as index is sufficient most of the time.
X86Gp reg = c.newIntPtr("reg");
call->setRet(0, reg);
You can verify getRet() functionality:
X86Gp reg = c.newIntPtr("reg");
assert(call->getRet(0).isNone());
call->setRet(0, reg);
assert(call->getRet(0) == reg);
Fully working example:
#include <stdio.h>
#include <asmjit/asmjit.h>
const char* func_a() {
printf("func_a(): Called\n");
return "hello pie";
}
void func_b(int id, const char* c_str) {
printf("func_b(%d, %s): Called\n", id, c_str);
}
int main() {
using namespace asmjit;
JitRuntime rt;
CodeHolder code;
code.init(rt.getCodeInfo());
X86Compiler c(&code);
X86Gp reg = c.newIntPtr("reg");
// Compilation step...
c.addFunc(FuncSignature0<void>(code.getCodeInfo().getCdeclCallConv()));
auto call_a = c.call((uint64_t)func_a, FuncSignature0<intptr_t>());
call_a->setRet(0, reg);
auto call_b = c.call((uint64_t)func_b, FuncSignature2<void, int, intptr_t>());
call_b->setArg(0, Imm(42));
call_b->setArg(1, reg);
c.ret();
c.endFunc();
// Finalize does the following:
// - allocates virtual registers
// - inserts prolog / epilog
// - assembles to CodeHolder
auto err = c.finalize();
if (err) {
printf("COMPILER FAILED: %s\b", DebugUtils::errorAsString(err));
return 1;
}
typedef void (*EntryPoint)(void);
EntryPoint entry;
// Adds function to the runtime. Should be freed by rt.release().
// Function is valid until the runtime is valid if not released.
err = rt.add(&entry, &code);
if (err) {
printf("RUNTIME FAILED: %s\b", DebugUtils::errorAsString(err));
return 1;
}
entry();
return 0;
}
I am trying to create a function that receives and returns a double. For the call method I used the approach with Mem. At the end I need to save the result in the variable xmm1.
I can't identify the error. The sine function is called correctly. But for the final assembler generation error occurs.
JitRuntime rt;
CodeHolder code;
code.init(rt.codeInfo());
asmjit::x86::Compiler cc(&code);
asmjit::x86::Gp reg = cc.newIntPtr("reg");
asmjit::Zone zonee(1024);
asmjit::ConstPool constPool(&zonee);
asmjit::Label constPoolLabel = cc.newLabel();
// Compilation step...
// c.addFunc(asmjit::FuncSignatureT<void>(code.codeInfo().getCdeclCallConv()));
cc.addFunc(asmjit::FuncSignatureT<void>());
auto call_a = cc.call((uint64_t)func_a, FuncSignatureT<intptr_t>());
call_a->setRet(0, reg);
auto call_b = cc.call((uint64_t)func_b, FuncSignatureT<void, int, intptr_t>());
call_b->setArg(0, Imm(42));
call_b->setArg(1, reg);
auto seno = [&](double value) {
size_t valueOffset;
double seno = static_cast<double_t>(std::sin(value));
cout << " seno " << seno << endl;
constPool.add(&seno, sizeof(double), valueOffset);
return asmjit::x86::ptr(constPoolLabel, valueOffset);
};
asmjit::x86::Mem mem;
double test = 180.5;
auto call_c = cc.call(seno(test), asmjit::FuncSignatureT<double_t>());
call_c->setArg(0, asmjit::Imm(test));
call_c->_setRet(0, mem);
cc.movsd(asmjit::x86::xmm1, mem);
cc.ret();
cc.endFunc();
// Finalize does the following:
// - allocates virtual registers
// - inserts prolog / epilog
// - assembles to CodeHolder
auto err = cc.finalize();
if (err) {
printf("COMPILER FAILED: %s\b", DebugUtils::errorAsString(err));
return;
}
typedef void (*EntryPoint)(void);
EntryPoint entry;
// Adds function to the runtime. Should be freed by rt.release().
// Function is valid until the runtime is valid if not released.
err = rt.add(&entry, &code);
if (err) {
printf("RUNTIME FAILED: %s\b", DebugUtils::errorAsString(err));
return;
}
entry();
return;
perhaps the memory object should relate to some memory address?
Mem mem = qword_ptr ((uint64_t) &test);

"Fatal signal 11 (SIGSEGV) at 0xdeadbaad" with JNI call from java to .so

I use my own .so library to build an Android App. It goes well last night. But it comes to this err: **A/libc﹕ Fatal signal 11 (SIGSEGV) at 0xdeadbaad (code=1) ** this morning when I Debug it. This is my .so code:
config_t config_obj_ptr = NULL;
module_t module_ptr = NULL;
int module_interface_init(const char * path)
{
LOGD("==========wrapper init==========\n");
if(path == NULL)
{
LOGD("path is not available");
return -1;
}
const char * app_path = path;
const char * cache_path = app_path;
config_obj_ptr = lib_create_config();
LOGD("Mark===1!\n");
if(NULL == config_obj_ptr)
{
LOGD("create config object err!\n");
getchar();
return -1;
}
set_app_path(config_obj_ptr, app_path);
set_cache_path(config_obj_ptr, cache_path);
LOGD("Mark===2!\n");
module_ptr = create_module(config_obj_ptr);
LOGD("Mark===3!\n");
if(NULL == p2p_module_ptr)
{
LOGD("create p2p module err!");
getchar();
return -1;
}
return 0;
}
I call this method through JNI in java :
private wrapper(String path)
{
nInit(path);
}
I find that crash occurred in this function (from my .so): I can get Log to "Mark===2!", no "Mark===3!". so it must be module_ptr = create_module(config_obj_ptr); cause this problem. config_obj_ptr and module_ptr are all global variable declared outside the function. config_obj_ptr can work well but module_ptr can't.

How to use StartRTPDump() function in WebRTC correctly?

I am sending an audio stream from my browser (Chrome) to the "peerconnection_client" sample application (trunk\talk\examples\peerconnection) and trying to dump it into a file using StartRTPDump:
int StartRTPDump(int channel, const char fileNameUTF8[1024], RTPDirections direction = kRtpIncoming);
Here is my test code:
void Conductor::OnPeerConnected(int id, const std::string& name) {
LOG(INFO) << __FUNCTION__;
// Refresh the list if we're showing it.
if (main_wnd_->current_ui() == MainWindow::LIST_PEERS)
main_wnd_->SwitchToPeerList(client_->peers());
// My test code
{
voe_ = webrtc::VoiceEngine::Create();
webrtc::VoEBase* voeBase = webrtc::VoEBase::GetInterface(voe_);
rtp_rtcp_ = webrtc::VoERTP_RTCP::GetInterface(voe_);
voeBase->Init();
channelId_ = voeBase->CreateChannel();
bool isActive = rtp_rtcp_->RTPDumpIsActive(channelId_); // "isActive" is false
int ret = rtp_rtcp_->StartRTPDump(channelId_, "AudioStreamFromRemotePeer.rtp"); // "ret" is 0
isActive = rtp_rtcp_->RTPDumpIsActive(channelId_); // "isActive" is true
}
}
void Conductor::OnPeerDisconnected(int id) {
LOG(INFO) << __FUNCTION__;
if (id == peer_id_) {
LOG(INFO) << "Our peer disconnected";
main_wnd_->QueueUIThreadCallback(PEER_CONNECTION_CLOSED, NULL);
} else {
// Refresh the list if we're showing it.
if (main_wnd_->current_ui() == MainWindow::LIST_PEERS)
main_wnd_->SwitchToPeerList(client_->peers());
}
// My test code
{
bool isActive = rtp_rtcp_->RTPDumpIsActive(channelId_); // "isActive" is true
int ret = rtp_rtcp_->StopRTPDump(channelId_); // "ret" is 0
isActive = rtp_rtcp_->RTPDumpIsActive(channelId_); // "isActive" is false
rtp_rtcp_->Release();
webrtc::VoiceEngine::Delete(voe_);
}
}
When I set a breakpoint inside the following function after calling OnPeerConnected() but before OnPeerDisconnected():
int32_t Channel::ReceivedRTPPacket(const int8_t* data, int32_t length, const PacketTime& packet_time)
{
WEBRTC_TRACE(kTraceStream, kTraceVoice, VoEId(_instanceId,_channelId), "Channel::ReceivedRTPPacket()"); // "_channelId" here matches to "channelId_" above
....
}
I can see incoming data.
But the generated .rtp file after calling OnPeerDisconnected() looks like:
Why am I not getting any data in my .rtp file?

PAM Authentication for a Legacy Application

I have a legacy app that receives a username/password request asynchronously over the wire. Since I already have the username and password stored as variables, what would be the best way to authenticate with PAM on Linux (Debian 6)?
I've tried writing my own conversation function, but I'm not sure of the best way of getting the password into it. I've considered storing it in appdata and referencing that from the pam_conv struct, but there's almost no documentation on how to do that.
Is there a simpler way to authenticate users without the overkill of a conversation function? I'm unable to use pam_set_data successfully either, and I'm not sure that's even appropriate.
Here's what I'm doing:
user = guiMessage->username;
pass = guiMessage->password;
pam_handle_t* pamh = NULL;
int pam_ret;
struct pam_conv conv = {
my_conv,
NULL
};
pam_start("nxs_login", user, &conv, &pamh);
pam_ret = pam_authenticate(pamh, 0);
if (pam_ret == PAM_SUCCESS)
permissions = 0xff;
pam_end(pamh, pam_ret);
And initial attempts at the conversation function resulted in (password is hard-coded for testing):
int
my_conv(int num_msg, const struct pam_message **msg, struct pam_response **resp, void *data)
{
struct pam_response *aresp;
if (num_msg <= 0 || num_msg > PAM_MAX_NUM_MSG)
return (PAM_CONV_ERR);
if ((aresp = (pam_response*)calloc(num_msg, sizeof *aresp)) == NULL)
return (PAM_BUF_ERR);
aresp[0].resp_retcode = 0;
aresp[0].resp = strdup("mypassword");
*resp = aresp;
return (PAM_SUCCESS);
}
Any help would be appreciated. Thank you!
This is what I ended up doing. See the comment marked with three asterisks.
#include <stdlib.h>
#include <iostream>
#include <fstream>
#include <security/pam_appl.h>
#include <unistd.h>
// To build this:
// g++ test.cpp -lpam -o test
// if pam header files missing try:
// sudo apt install libpam0g-dev
struct pam_response *reply;
//function used to get user input
int function_conversation(int num_msg, const struct pam_message **msg, struct pam_response **resp, void *appdata_ptr)
{
*resp = reply;
return PAM_SUCCESS;
}
int main(int argc, char** argv)
{
if(argc != 2) {
fprintf(stderr, "Usage: check_user <username>\n");
exit(1);
}
const char *username;
username = argv[1];
const struct pam_conv local_conversation = { function_conversation, NULL };
pam_handle_t *local_auth_handle = NULL; // this gets set by pam_start
int retval;
// local_auth_handle gets set based on the service
retval = pam_start("common-auth", username, &local_conversation, &local_auth_handle);
if (retval != PAM_SUCCESS)
{
std::cout << "pam_start returned " << retval << std::endl;
exit(retval);
}
reply = (struct pam_response *)malloc(sizeof(struct pam_response));
// *** Get the password by any method, or maybe it was passed into this function.
reply[0].resp = getpass("Password: ");
reply[0].resp_retcode = 0;
retval = pam_authenticate(local_auth_handle, 0);
if (retval != PAM_SUCCESS)
{
if (retval == PAM_AUTH_ERR)
{
std::cout << "Authentication failure." << std::endl;
}
else
{
std::cout << "pam_authenticate returned " << retval << std::endl;
}
exit(retval);
}
std::cout << "Authenticated." << std::endl;
retval = pam_end(local_auth_handle, retval);
if (retval != PAM_SUCCESS)
{
std::cout << "pam_end returned " << retval << std::endl;
exit(retval);
}
return retval;
}
The way standard information (such as a password) is passed for PAM is by using variables set in the pam handle with pam_set_item (see the man page for pam_set_item).
You can set anything your application will need to use later into the pam_stack. If you want to put the password into the pam_stack you should be able to do that immediately after calling pam_start() by setting the PAM_AUTHTOK variable into the stack similar to the pseudo code below:
pam_handle_t* handle = NULL;
pam_start("common-auth", username, NULL, &handle);
pam_set_item( handle, PAM_AUTHTOK, password);
This will make the password available on the stack to any module that cares to use it, but you generally have to tell the module to use it by setting the standard use_first_pass, or try_first_pass options in the pam_configuration for the service (in this case /etc/pam.d/common-auth).
The standard pam_unix module does support try_first_pass, so it wouldn't hurt to add that into your pam configuration on your system (at the end of the line for pam_unix).
After you do this any call to pam_authenticate() that are invoked from the common-auth service should just pick the password up and go with it.
One small note about the difference between use_first_pass and try_first_pass: They both tell the module (in this case pam_unix) to try the password on the pam_stack, but they differ in behavior when their is no password/AUTHTOK available. In the missing case use_first_pass fails, and try_first_pass allows the module to prompt for a password.
Fantius' solution worked for me, even as root.
I originally opted for John's solution, as it was cleaner and made use of PAM variables without the conversation function (really, there isn't a need for it here), but it did not, and will not, work. As Adam Badura alluded to in both posts, PAM has some internal checks to prevent direct setting of PAM_AUTHTOK.
John's solution will result in behaviour similar to what is mentioned here, where any password value will be allowed to login (even if you declare, but do not define, the pam_conv variable).
I would also recommend users be aware of the placement of the malloc, as it will likely differ in your application (remember, the code above is more of a test/template, than anything else).
struct pam_conv {
int (*conv)(int num_msg, const struct pam_message **msg,
struct pam_response **resp, void *appdata_ptr);
void *appdata_ptr;
};
The second field(appdata_ptr) of the struct pam_conv is passed to the conversation function,
therefore we can use it as our password pointer.
static int convCallback(int num_msg, const struct pam_message** msg,
struct pam_response** resp, void* appdata_ptr)
{
struct pam_response* aresp;
if (num_msg <= 0 || num_msg > PAM_MAX_NUM_MSG)
return (PAM_CONV_ERR);
if ((aresp = (pam_response*)calloc(num_msg, sizeof * aresp)) == NULL)
return (PAM_BUF_ERR);
aresp[0].resp_retcode = 0;
aresp[0].resp = strdup((char*)appdata_ptr);
*resp = aresp;
return (PAM_SUCCESS);
}
int main()
{
....
pam_handle_t* pamH = 0;
char *password = strdup("foopassword");
struct pam_conv conversation = {convCallback, password};
int retvalPam = pam_start("check_user", "foousername", &conversation, &pamH);
//Call pam_authenticate(pamH, 0)
//Call pam_end(pamH, 0);
...
...
free(password);
}

How to make upnp action?

I want to implement port-forwarding using intel-upnp.
I got XML data like:
Device found at location: http://192.168.10.1:49152/gatedesc.xml
service urn:schemas-upnp-org:service:WANIPConnection:1
controlurl /upnp/control/WANIPConn1
eventsuburl : /upnp/control/WANIPConn1
scpdurl : /gateconnSCPD.xml
And now, I want to make upnp-action. But, I don't know how to make it.
If you know some code snippet or helpful URL in C, please tell me.
char actionxml[250];
IXML_Document *action = NULL;
strcpy(actionxml, "<u:GetConnectionTypeInfo xmlns:u=\"urn:schemas-upnp- org:service:WANCommonInterfaceConfig:1\">");
action = ixmlParseBuffer(actionxml);
int ret = UpnpSendActionAsync( g_handle,
"http:192.168.10.1:49152/upnp/control/WANCommonIFC1",
"urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1",
NULL,
action,
upnp_callback,
NULL);
I know this is an old question, but it can be kept for reference. You can take a look at the sample code in the libupnp library here: https://github.com/mrjimenez/pupnp/blob/master/upnp/sample/common/tv_ctrlpt.c
The relevant code is in the function TvCtrlPointSendAction():
int TvCtrlPointSendAction(
int service,
int devnum,
const char *actionname,
const char **param_name,
char **param_val,
int param_count)
{
struct TvDeviceNode *devnode;
IXML_Document *actionNode = NULL;
int rc = TV_SUCCESS;
int param;
ithread_mutex_lock(&DeviceListMutex);
rc = TvCtrlPointGetDevice(devnum, &devnode);
if (TV_SUCCESS == rc) {
if (0 == param_count) {
actionNode =
UpnpMakeAction(actionname, TvServiceType[service],
0, NULL);
} else {
for (param = 0; param < param_count; param++) {
if (UpnpAddToAction
(&actionNode, actionname,
TvServiceType[service], param_name[param],
param_val[param]) != UPNP_E_SUCCESS) {
SampleUtil_Print
("ERROR: TvCtrlPointSendAction: Trying to add action param\n");
/*return -1; // TBD - BAD! leaves mutex locked */
}
}
}
rc = UpnpSendActionAsync(ctrlpt_handle,
devnode->device.
TvService[service].ControlURL,
TvServiceType[service], NULL,
actionNode,
TvCtrlPointCallbackEventHandler, NULL);
if (rc != UPNP_E_SUCCESS) {
SampleUtil_Print("Error in UpnpSendActionAsync -- %d\n",
rc);
rc = TV_ERROR;
}
}
ithread_mutex_unlock(&DeviceListMutex);
if (actionNode)
ixmlDocument_free(actionNode);
return rc;
}
The explanation is that you should create the action with UpnpMakeAction() if you have no parameters or UpnpAddToAction() if you have parameters to create your action, and then send it either synchronously or asynchronously.