|
|
/*++
Copyright (c) 1999-2000 Microsoft Corporation
File Name:
server.c
Abstract:
This file contains code which implements rpc server start and stop.
Author: Ting Cai
Created: 07/10/1999
--*/
#include <pch.h>
#include "ssdp.h"
#include "status.h"
#include "ssdpfunc.h"
#include "ssdptypes.h"
#include "ssdpnetwork.h"
#include "ncbase.h"
#include "event.h"
#include "ncinet.h"
#include "eventsrv.h"
#include "iphlpapi.h"
#include "announce.h"
#include "notify.h"
#include "search.h"
#include "cache.h"
#include "ReceiveData.h"
#include "InterfaceList.h"
#include "ReceiveData.h"
#include "InterfaceHelper.h"
#define SSDP_MSG_MAX_THROTTLE_SIZE 16384
/*********************************************************************/ /* Global vars for debugging */ /*********************************************************************/
// To-Do: Auto-restart the ssdpsrv.exe.
// Updated through interlocked exchange
static LONG bRegisteredIf = 0; // static long bRegisteredEp = 0;
LONG bShutdown = 0; HANDLE ShutDownEvent = NULL; HWND hWnd = NULL; SOCKET g_socketTcp; HANDLE g_hAddrChange = NULL; OVERLAPPED g_ovAddrChange = {0}; HANDLE g_hEventAddrChange = NULL; HANDLE g_hAddrChangeWait = NULL;
static const CHAR c_szWindowClassName[] = "SSDP Server Window";
HWND SsdpCreateWindow(); LRESULT CALLBACK SsdpWindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam); VOID RunMessageLoop();
VOID NTAPI InterfaceChange(PVOID pvContext, BOOLEAN fFlag) { DWORD dwStatus; HWND hwnd = (HWND)pvContext;
TraceTag(ttidSsdpNetwork, "InterfaceChange called!!!!!!!");
ResetNetworkList(hwnd);
dwStatus = NotifyAddrChange(&g_hAddrChange, &g_ovAddrChange);
if (dwStatus != ERROR_SUCCESS && dwStatus != ERROR_IO_PENDING) { TraceTag(ttidSsdpNetwork, "NotifyAddrChange returned %d", dwStatus); } }
BOOL FRegisterAddrChange(HWND hwnd) { DWORD dwStatus; BOOL fResult = FALSE;
TraceTag(ttidSsdpNetwork, "RegisterAddrChange() entered...");
g_hEventAddrChange = CreateEvent(NULL, FALSE, FALSE, NULL);
if (g_hEventAddrChange) { if (RegisterWaitForSingleObject(&g_hAddrChangeWait, g_hEventAddrChange, InterfaceChange, (LPVOID)hwnd, INFINITE, 0)) {
TraceTag(ttidSsdpNetwork, "RegisterWaitForSingleObject() " "succeeded...");
g_ovAddrChange.hEvent = g_hEventAddrChange;
dwStatus = NotifyAddrChange(&g_hAddrChange, &g_ovAddrChange);
if (dwStatus != ERROR_SUCCESS && dwStatus != ERROR_IO_PENDING) { TraceTag(ttidSsdpNetwork, "NotifyAddrChange returned %d", dwStatus); } else { fResult = TRUE;
TraceTag(ttidSsdpNetwork, "NotifyAddrChange succeeded", dwStatus); } } }
return fResult; }
VOID UnregisterAddrChange() { if (g_hAddrChangeWait) { UnregisterWait(g_hAddrChangeWait); }
if (g_hEventAddrChange) { CloseHandle(g_hEventAddrChange); } }
INT SsdpMain(SERVICE_STATUS_HANDLE ssHandle, LPSERVICE_STATUS pStatus) { TraceTag(ttidSsdpRpcIf, "SsdpMain - Enter");
unsigned long hThread;
#ifdef DBG
InitializeDebugging(); #endif
// Initialize data structures
HRESULT hr = S_OK;
InitializeListNetwork(); InitializeListOpenConn();
hr = CTimerQueue::Instance().HrInitialize(); if(FAILED(hr)) { TraceHr(ttidSsdpRpcIf, FAL, hr, FALSE, "SsdpMain - CTimerQueue::Instance().HrInitialize failed"); goto cleanup; }
hr = CInterfaceHelper::Instance().HrInitialize(); if(FAILED(hr)) { goto cleanup; }
hr = CUPnPInterfaceList::Instance().HrInitialize(); if(FAILED(hr)) { goto cleanup; TraceHr(ttidSsdpRpcIf, FAL, hr, FALSE, "SsdpMain - CUPnPInterfaceList::Instance().HrInitialize failed"); }
// SSDP socket initialization
if (SocketInit() != 0) { TraceTag(ttidError, "SsdpMain - SocketInit failed"); goto cleanup; }
g_socketTcp = CreateHttpSocket(); if (g_socketTcp == INVALID_SOCKET) { TraceTag(ttidError, "SsdpMain - CreateHttpSocket failed"); // Should we continue without eventing?
goto cleanup; }
if (RpcServerStart() != 0) { TraceTag(ttidError, "SsdpMain - RpcServerStart failed"); goto cleanup; }
hr = CReceiveDataManager::Instance().HrInitialize(); if(FAILED(hr)) { TraceHr(ttidSsdpRpcIf, FAL, hr, FALSE, "SsdpMain - CReceiveDataManager::Instance().HrInitialize failed"); RpcServerStop(); goto cleanup; } // Initializes Max Cache Entries
hr = CSsdpCacheEntryManager::Instance().HrInitialize(); if(FAILED(hr)) { TraceHr(ttidSsdpRpcIf, FAL, hr, FALSE, "SsdpMain - CSsdpCacheEntryManager::Instance().HrInitialize failed"); RpcServerStop(); goto cleanup; }
hWnd = SsdpCreateWindow(); if (hWnd == NULL) { TraceTag(ttidError, "SsdpMain - SsdpCreateWindow failed"); RpcServerStop(); goto cleanup; }
if (ListenOnAllNetworks(hWnd) != 0) { TraceTag(ttidError, "SsdpMain - ListenOnAllNetworks failed"); RpcServerStop(); goto cleanup; }
if (StartHttpServer(g_socketTcp, hWnd, SM_TCP) != 0) { TraceTag(ttidError, "SsdpMain - StartHttpServer failed"); RpcServerStop(); goto cleanup; }
if (!FRegisterAddrChange(hWnd)) { TraceTag(ttidError, "SsdpMain - FRegisterAddrChange failed"); RpcServerStop(); goto cleanup; }
TraceTag(ttidSsdpRpcIf, "SsdpMain - Performed initialization");
pStatus->dwCurrentState = SERVICE_RUNNING; if (SetServiceStatus(ssHandle, pStatus) == FALSE) { TraceTag(ttidError, "SsdpMain - SetServiceStatus failed"); RpcServerStop(); goto cleanup; }
TraceTag(ttidSsdpRpcIf, "SSDPSRV service is now started");
RunMessageLoop();
TraceTag(ttidSsdpRpcIf, "SsdpMain - Doing shutdown");
RpcServerStop();
UnregisterAddrChange();
TraceTag(ttidSsdpRpcIf, "Waiting for the shut down event.");
if (ShutDownEvent) { WaitForSingleObject(ShutDownEvent,INFINITE); }
TraceTag(ttidSsdpRpcIf, "Shut down event signaled.");
cleanup: TraceTag(ttidSsdpRpcIf, "SsdpMain - Doing cleanup");
CReceiveDataManager::Instance().HrShutdown(); CleanupHttpSocket(); CleanupListOpenConn(); CSsdpCacheEntryManager::Instance().HrShutdown(); CTimerQueue::Instance().HrShutdown(INVALID_HANDLE_VALUE); CUPnPInterfaceList::Instance().HrShutdown(); CInterfaceHelper::Instance().HrShutdown();
CleanupListNetwork(FALSE);
SocketFinish();
TraceTag(ttidSsdpRpcIf, "Finished shutdown cleanup.");
#ifdef DBG
// CloseLogFileHandle(fileLog);
UnInitializeDebugging(); #endif // DBG
if (ShutDownEvent) { CloseHandle(ShutDownEvent); ShutDownEvent = NULL; }
#ifdef NEVER
if (g_hInetSess) { InternetCloseHandle(g_hInetSess); } #endif
if (hWnd) { DestroyWindow(hWnd); hWnd = NULL; } UnregisterClass(c_szWindowClassName, NULL);
return 0; }
/*********************************************************************/ /* MIDL allocate and free */ /*********************************************************************/
VOID __RPC_FAR * __RPC_USER midl_user_allocate(size_t len) { return(malloc(len)); }
VOID __RPC_USER midl_user_free(VOID __RPC_FAR * ptr) { free(ptr); }
BOOL IsAuthenticatedUser() { BOOL fAuthenticated = FALSE; DWORD dwSidSize = SECURITY_MAX_SID_SIZE; SID* pSidAuthenticated = (SID*)midl_user_allocate(dwSidSize);
if (NULL == pSidAuthenticated) { goto Cleanup; } //
// create SID for the authenticated users
//
if (!CreateWellKnownSid(WinAuthenticatedUserSid, NULL, // not a domain sid
pSidAuthenticated, &dwSidSize)) { // check the error for debug builds, normally we don't care about the error
// because all we can do is to fail the call :)
#ifdef DBG
HRESULT hr = (HRESULT)GetLastError(); #endif
goto Cleanup; } //
// check whether current client token has this sid
//
if (!CheckTokenMembership(NULL, //current token
pSidAuthenticated, // sid for the authenticated user
&fAuthenticated)) { // check the error for debug builds, normally we don't care about the error
// because all we can do is to fail the call :)
#ifdef DBG
HRESULT hr = (HRESULT)GetLastError(); #endif
// just to be on the safe side (as we don't know that CheckTokenMembership
// does not modify fAuthenticated in case of error)
fAuthenticated = FALSE;
goto Cleanup; }
Cleanup: if (pSidAuthenticated) midl_user_free(pSidAuthenticated);
return fAuthenticated; }
BOOL IsAnonymousUser() { BOOL fAnonymous = FALSE; DWORD dwSidSize = SECURITY_MAX_SID_SIZE; SID* pSidAnonymous= (SID*)midl_user_allocate(dwSidSize);
if (NULL == pSidAnonymous) { goto Cleanup; } //
// create SID for the authenticated users
//
if (!CreateWellKnownSid(WinAnonymousSid, NULL, // not a domain sid
pSidAnonymous, &dwSidSize)) { // check the error for debug builds, normally we don't care about the error
// because all we can do is to fail the call :)
#ifdef DBG
HRESULT hr = (HRESULT)GetLastError(); #endif
goto Cleanup; } //
// check whether current client token has this sid
//
if (!CheckTokenMembership(NULL, //current token
pSidAnonymous, // sid for the authenticated user
&fAnonymous)) { // check the error for debug builds, normally we don't care about the error
// because all we can do is to fail the call :)
#ifdef DBG
HRESULT hr = (HRESULT)GetLastError(); #endif
// just to be on the safe side (as we don't know that CheckTokenMembership
// does not modify fAnonymous in case of error)
fAnonymous = FALSE;
goto Cleanup; }
Cleanup: if (pSidAnonymous) midl_user_free(pSidAnonymous);
return fAnonymous; }
RPC_STATUS RPC_ENTRY RpcSecurityCallback( RPC_IF_HANDLE* handle, void* pCtx) { RPC_STATUS status = RPC_S_OK; UINT uiTransportType = 0; ULONG ulAuthLevel = 0; ULONG ulAuthSvc = 0; BOOL fImpersonated = FALSE; //
// check to make sure incoming call comes through LRPC
//
status = I_RpcBindingInqTransportType(NULL, // current caller
&uiTransportType); if (status) goto Cleanup;
if (TRANSPORT_TYPE_LPC != uiTransportType) { status = RPC_S_ACCESS_DENIED; goto Cleanup; }
//
// retrieve client's authentication information
//
status = RpcBindingInqAuthClient(NULL, // current call
NULL, // don't care about authz handle
NULL, // don't need client' principal name, as he is local
&ulAuthLevel, &ulAuthSvc, NULL); if (status) goto Cleanup;
//
// we require packet privacy (encryption and signing). with lrpc
// it is set by default, but still, the check is simple
//
if (!(RPC_C_AUTHN_LEVEL_PKT_PRIVACY & ulAuthLevel)) { status = RPC_S_ACCESS_DENIED; goto Cleanup; }
//
// client used some kind of authentication service
//
if (RPC_C_AUTHN_NONE == ulAuthSvc) { status = RPC_S_ACCESS_DENIED; goto Cleanup; }
//
// impersonate, in order to check caller's token
//
status = RpcImpersonateClient(NULL); if (status) goto Cleanup;
fImpersonated = TRUE;
//
// check whether this is an authenticated user (client)
//
if (!IsAuthenticatedUser()) { status = RPC_S_ACCESS_DENIED; goto Cleanup; }
//
// do a separate check for the anonymous user
// (even though i am not sure whether it is applicable for LRPC)
//
if (IsAnonymousUser()) { status = RPC_S_ACCESS_DENIED; goto Cleanup; }
Cleanup: if (fImpersonated) RpcRevertToSelf();
//
// this routine is expected to return either success (RPC_S_OK) or
// RPC_S_ACCESS_DENIED, so all other erros should be masked
//
if (status != RPC_S_OK) status = RPC_S_ACCESS_DENIED; return status; }
INT RpcServerStart() { RPC_STATUS status = RPC_S_OK; BOOL fRegisteredRpc = FALSE; RPC_BINDING_VECTOR* pBindingVector = NULL;
//
// analyze all existing networks available.
//
status = GetNetworks(); if (status) goto Error; //
// create the event, to be used to synchronize shutdown
//
ShutDownEvent = CreateEvent(NULL, TRUE, FALSE, NULL); if (ShutDownEvent == NULL) { TraceTag(ttidSsdpRpcInit, "Failed to create shut down event (%d)", GetLastError()); goto Error; }
//
// now, start the RPC interface
//
//
// Use LPC protocol sequence
//
status = RpcServerUseProtseq((unsigned char*)"ncalrpc", RPC_C_PROTSEQ_MAX_REQS_DEFAULT, NULL); if (status) goto Error;
//
// use NTLM or kerberos
//
status = RpcServerRegisterAuthInfo(NULL, RPC_C_AUTHN_GSS_NEGOTIATE, NULL, NULL); if (status) goto Error;
//
// register our interface,
// RPC_IF_ALLOW_SECURE_ONLY - to allow only users with authentication level higher than
// RPC_C_AUTHN_LEVEL_NONE, even though it is
// unnecessary, as it is superceeded by the security callback
// RPC_IF_AUTOLISTEN - start listening on the interface as soon as interface is registred and
// and stops listening as soon as interface is unregistered
//
status = RpcServerRegisterIfEx(_ssdpsrv_v1_0_s_ifspec, NULL, // MgrTypeUuid
NULL, // MgrEpv; null means use default
RPC_IF_ALLOW_SECURE_ONLY | RPC_IF_AUTOLISTEN, RPC_C_LISTEN_MAX_CALLS_DEFAULT, (RPC_IF_CALLBACK_FN*)RpcSecurityCallback); if (status) goto Error;
//
// get the list of available bindings
//
status = RpcServerInqBindings(&pBindingVector); if (status) goto Error;
//
// register all the endpoints with the map database
//
status = RpcEpRegister(_ssdpsrv_v1_0_s_ifspec, pBindingVector, NULL, NULL); // no annotation
if (status) goto Error;
// if we reached this point, everything must have registered successfully
// so we should mark that
fRegisteredRpc = TRUE;
TraceTag(ttidSsdpRpcInit, "RPC server is started");
Cleanup: if (pBindingVector) RpcBindingVectorFree(&pBindingVector); return status;
Error: TraceTag(ttidSsdpRpcInit, "StartRpcServer failed (%x)", status);
//
// don't cleanup everything, as all this globl stuff, like ShutdownEvent
// is expected to be cleaned from the SsdpMain.
// Rpc is the only stuff that needs to be cleaned up
//
if (fRegisteredRpc) { RpcServerStop(); }
goto Cleanup; }
// Unregister RPC interface, endpoint and close the file if necessary.
INT RpcServerStop() { RPC_STATUS status = RPC_S_OK;
status = RpcServerUnregisterIfEx(_ssdpsrv_v1_0_s_ifspec, NULL, // MgrTypeUuid
1); // call rundown now
TraceTag(ttidSsdpRpcStop, "Leaving RpcServerStop");
return status; }
VOID ProcessSsdpRequest(PSSDP_REQUEST pSsdpRequest, RECEIVE_DATA *pData) { // Ensure that the socket is in the network list before attempting to
// get its name
//
if (pData->fIsTcpSocket || FReferenceSocket(pData->socket)) { sockaddr_in addr; int nSize = sizeof(addr);
getsockname(pData->socket, reinterpret_cast<sockaddr*>(&addr), &nSize);
CInterfaceHelper::Instance().HrResolveAddress(addr.sin_addr.S_un.S_addr, pSsdpRequest->guidInterface); if (!pData->fIsTcpSocket) { UnreferenceSocket(pData->socket); } } else { FreeSsdpRequest(pSsdpRequest); return; }
if (!pData->fIsTcpSocket && !pData->fMCast && pSsdpRequest->Method != SSDP_M_SEARCH) { FreeSsdpRequest(pSsdpRequest); return; }
if (pSsdpRequest->Method == SSDP_M_SEARCH) {
if (0 == lstrcmpA(pSsdpRequest->RequestUri, "*")) { TraceTag(ttidSsdpSocket, "Searching for ST (%s)", pSsdpRequest->Headers[SSDP_ST]); CSsdpServiceManager::Instance().HrAddSearchResponse(pSsdpRequest, &pData->socket, &pData->RemoteSocket); } else { TraceTag(ttidSsdpSocket, "Not searching for ST, since URI != '*' (URI='%s')", pSsdpRequest->RequestUri); } } else if (pSsdpRequest->Method == SSDP_NOTIFY) {
TraceTag(ttidSsdpSocket, "Receive notification of type (%s)", pSsdpRequest->Headers[SSDP_NT]);
if (!lstrcmpi(pSsdpRequest->Headers[SSDP_NTS], "upnp:propchange")) { if(pSsdpRequest->Headers[GENA_SID]) { TraceTag(ttidEvents, "ProcessSsdpRequest - upnp:propchange - SID:%s", pSsdpRequest->Headers[GENA_SID]); } CSsdpNotifyRequestManager::Instance().HrCheckListNotifyForEvent(pSsdpRequest);
if (pData->fIsTcpSocket || FReferenceSocket(pData->socket)) { SocketSend(OKResponseHeader, pData->socket, NULL);
if (!pData->fIsTcpSocket) { UnreferenceSocket(pData->socket); } } } else if (!lstrcmpi(pSsdpRequest->Headers[SSDP_NTS], "ssdp:alive") || !lstrcmpi(pSsdpRequest->Headers[SSDP_NTS], "ssdp:byebye")) { BOOL IsSubscribed;
// preserve source address if possible.
// hack here where we use szSID to hold address
// this should not be used for alive normally.
if (pSsdpRequest->Headers[GENA_SID] == NULL) { char* pszIp = GetSourceAddress(pData->RemoteSocket); pSsdpRequest->Headers[GENA_SID] = (CHAR *) midl_user_allocate( sizeof(CHAR) * (strlen(pszIp) + 1)); if (pSsdpRequest->Headers[GENA_SID]) { strcpy(pSsdpRequest->Headers[GENA_SID], pszIp); } }
// We only cache notification that clients has subscribed.
IsSubscribed = CSsdpNotifyRequestManager::Instance().FIsAliveOrByebyeInListNotify(pSsdpRequest);
CSsdpCacheEntryManager::Instance().HrUpdateCacheList(pSsdpRequest, IsSubscribed); } else { // unrecognized NTS type
}
// SsdpMessage fields are freed when clean up cache entry.
// FreeSsdpRequest(pSsdpRequest);
} else { TraceTag(ttidSsdpSocket, "Unrecognized SSDP request."); } FreeSsdpRequest(pSsdpRequest); }
VOID RunMessageLoop() { MSG msg;
while (GetMessage(&msg, 0, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); }
TraceTag(ttidSsdpRpcInit, "Message loop is done"); }
HWND SsdpCreateWindow() { WNDCLASS WndClass; HWND hwnd;
//
// Register the window class.
//
WndClass.style = 0; WndClass.lpfnWndProc = SsdpWindowProc; WndClass.cbClsExtra = 0; WndClass.cbWndExtra = 0; WndClass.hInstance = NULL; WndClass.hIcon = NULL; WndClass.hCursor = NULL; WndClass.hbrBackground = NULL; WndClass.lpszMenuName = NULL; WndClass.lpszClassName = c_szWindowClassName;
if (!RegisterClass(&WndClass)) { TraceTag(ttidSsdpRpcInit, "RegisterClassEx failed."); return NULL; }
//
// Create the window.
//
hwnd = CreateWindow( c_szWindowClassName, "", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, NULL, NULL );
if (hwnd == NULL) { TraceTag(ttidSsdpRpcInit, "CreateWindow failed."); return NULL; }
TraceTag(ttidSsdpRpcInit, "Created window %x", hwnd);
ShowWindow(hwnd, SW_HIDE); UpdateWindow(hwnd);
return hwnd; }
LRESULT CALLBACK SsdpWindowProc( HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam )
/*++
Routine Description:
Window message dispatch procedure for our hidden window.
Arguments:
hwnd - The target window handle.
msg - The current message.
wParam - WPARAM value.
lParam - LPARAM value.
Return Value:
LRESULT - The result of the message.
--*/
{ LRESULT Result = 0; INT EventCode; INT ErrorCode; SOCKET Socket; CHAR * szData; CHAR * szTcpData; SOCKADDR_IN RemoteSocket; DWORD cbBuffSize = 0; BOOL bMCast;
switch (msg) { case SM_SSDP: Socket = (SOCKET) wParam; EventCode = WSAGETSELECTEVENT(lParam); ErrorCode = WSAGETSELECTERROR(lParam);
switch (EventCode) { case FD_READ: if (FReferenceSocket(Socket)) { cbBuffSize = 0; if (SocketReceive(Socket, &szData, &cbBuffSize, &RemoteSocket, TRUE, &bMCast) == TRUE) { if(cbBuffSize <= SSDP_MSG_MAX_THROTTLE_SIZE ) { CReceiveDataManager::Instance().HrAddData( szData, Socket, bMCast, reinterpret_cast<SOCKADDR_IN*>(&RemoteSocket)); } else { // Typical SSDP MSG is less than 1K. If its greater than 16K we suspect a Buffer flood attack.
free(szData); TraceTag(ttidSsdpRpcInit, "Received SSDP Msg more than 16k"); } }
UnreferenceSocket(Socket); } break; } break;
case SM_TCP: Socket = (SOCKET) wParam; EventCode = WSAGETSELECTEVENT(lParam); ErrorCode = WSAGETSELECTERROR(lParam);
switch (EventCode) { case FD_READ: DWORD cbBuffer;
if (SocketReceive(Socket, &szTcpData, &cbBuffer, &RemoteSocket, FALSE, &bMCast) == TRUE) { RECEIVE_DATA * pData = NULL; pData = (RECEIVE_DATA *)malloc(sizeof(RECEIVE_DATA)); if (pData) { CopyMemory(&pData->RemoteSocket, &RemoteSocket, sizeof(SOCKADDR_IN)); pData->socket = Socket; pData->szBuffer = szTcpData; pData->cbBuffer = cbBuffer; pData->fIsTcpSocket = TRUE; pData->fMCast = FALSE;
QueueUserWorkItem(LookupListOpenConn, pData, 0); } else { TraceError("Couldn't allocate sufficient memory!", E_OUTOFMEMORY); } } break;
case FD_ACCEPT:
TraceTag(ttidSsdpRpcInit, "Ready to accept connection");
HandleAccept(Socket);
break;
case FD_CLOSE:
// To-Do: Do I need to call recv to make sure all available data are read?
TraceTag(ttidSsdpRpcInit, "Closing socket %d", Socket); closesocket(Socket); RemoveOpenConn(Socket); }
break;
case WM_QUERYENDSESSION:
TraceTag(ttidSsdpRpcInit, "Received WM_QUERYENDSESSION message"); if (!(lParam & ENDSESSION_LOGOFF)) { TraceTag(ttidSsdpRpcInit, "System is shutting down"); } Result = TRUE;
break;
default: //
// Pass it through.
//
Result = DefWindowProc( hwnd, msg, wParam, lParam ); break; }
return Result; }
|