Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

ATL Server - Web Application/Web Service

0.00/5 (No votes)
21 Aug 2003 2  
Web Application/Web Service development using ATL Server classes

Introduction

Other ATL7 changes are covered here and here. First let's go over some of the new classes added for simplifying the ATL Server development. These classes can be used anywhere you need them not just for ATL Server development.

Cryptography (CCryptProv, CCryptKey, CCryptKeyedHash, etc):

Hashing:

CCryptProv prov;
prov.Initialize();    
char rand[256];
memset(rand, 0, sizeof(rand));
//generating cryptographically random bytes

HRESULT hr = prov.GenRandom(sizeof(rand), (BYTE*)rand);
if(hr != S_OK) 
    return 0;

/////////////////////////////////////////////

//hashing

//TODO: input password from user. never hard code it like this!

const char* szPassword = "#$%^!SDD%$^&%^&~";

char Hash1st[256]; DWORD dwSize=256;
char Hash2nd[256];
memset(Hash1st, 0, sizeof(Hash1st));
memset(Hash2nd, 0, sizeof(Hash2nd));

CCryptKeyedHash hash;
//use SHA-1 or any other such as MD5, HMAC, etc

hash.Initialize(prov, CALG_SHA1, CCryptKey::EmptyKey, 0);

// add data to hash

hr = hash.AddString(szPassword);
if(hr != S_OK) 
    return 0;

//get the hashed value

hr = hash.GetValue((BYTE*)Hash1st, &dwSize);
if(hr != S_OK)
    return 0;
hash.Destroy();

// now we can check the hashed value against original whenever we want

hash.Initialize(prov, CALG_SHA1, CCryptKey::EmptyKey, 0);

//add data to hash

hr = hash.AddString(szPassword);
if(hr != S_OK)
    return 0;
//get hashed value

hr = hash.GetValue((BYTE*)Hash2nd, &dwSize);
if(hr != S_OK)
    return 0;

//hash values should matched

if(memcmp(Hash1st, Hash2nd, sizeof(Hash1st)) == 0)
    ATLTRACE("\npassword matched.\n\n");
 

Encryption:

const int ENCRYPT_BLOCK_SIZE = 64;
//since TripleDES is used, which uses block cipher mode, 

//the data size must be a multiple of block size (8 for TripleDes) 

DWORD dwBlockLen = 2000 - 1000 % ENCRYPT_BLOCK_SIZE;
ATLASSERT((dwBlockLen%8)==0);
CHeapPtr<BYTE> buff;
buff.Allocate(dwBlockLen);

CCryptProv prov;
//need MS_ENHANCED_PROV or MS_STRONG_PROV for TripleDES algo

HRESULT hr = prov.Initialize(PROV_RSA_FULL, NULL, MS_STRONG_PROV);

//TODO: create some file named testpln.txt with some data for example code 

// to work


//encrypt file using TripleDES and based on password

{
    CCryptDerivedKey derKey;

    //let's use md5 hash to hash the password for the file to be encrypted

    //normally you would use sha, this is only to show how you can easily use
//md5 if needed
CCryptMD5Hash md5; hr = md5.Initialize(prov); //hash file password md5.AddString(szPassword); //initialize the key to use TripleDES and md5 based hash of the password hr = derKey.Initialize(prov,md5,CALG_3DES); //16 for AES, 8 for 3DES BYTE iv[8]; //generate cryptographically strong random bytes hr = prov.GenRandom(8, iv); //set above bytes as IV hr = derKey.SetIV(iv); //set mode to CBC hr = derKey.SetMode(CRYPT_MODE_CBC); //set padding hr = derKey.SetPadding(PKCS5_PADDING); FILE* pFilePl = fopen("testpln.txt", "rb"); //original file if(pFilePl == NULL) return 0; FILE* pFileEn = fopen("testenc.txt", "wb"); //to be encrypted file DWORD dwEncLen = 0; DWORD dwOrigBlockLen = dwBlockLen; while(!feof(pFilePl)) { DWORD nRead = fread(buff, 1, dwBlockLen, pFilePl); //read plain data DWORD nEnc = nRead; //encrypt data, data and size is returned back to us hr = derKey.Encrypt(feof(pFilePl), (BYTE*)buff, &nEnc, dwBlockLen); if(hr == HRESULT_FROM_WIN32(ERROR_MORE_DATA)) //error, buffer size // not enough { buff.Reallocate(nEnc); dwBlockLen = nEnc; hr = derKey.Encrypt(feof(pFilePl), (BYTE*)buff, &nRead,
dwBlockLen); nEnc = nRead; } if(hr != S_OK) break; fwrite(buff, 1, nEnc, pFileEn); //write encrypted data } fclose(pFileEn); fclose(pFilePl); //resize buffer back, in case it was changed dwBlockLen = dwOrigBlockLen; buff.Reallocate(dwBlockLen); } //decrypt same file using TripleDES and based on password { CCryptDerivedKey derKey; CCryptMD5Hash md5; hr = md5.Initialize(prov); //file password md5.AddString(szPassword); //initialize the key to use TripleDES and md5 based hash of the password hr = derKey.Initialize(prov,md5,CALG_3DES); //set IV, MODE, Padding like before FILE* pFileEn = fopen("testenc.txt", "rb"); //encrypted file FILE* pFileDec = fopen("testdec.txt", "wb");//to be decrypted file DWORD dwEncLen = 0; while(!feof(pFileEn)) { //read encrypted data DWORD nRead = fread(buff, 1, dwBlockLen, pFileEn); //decrypt data, data and size is returned back to us hr = derKey.Decrypt(feof(pFileEn), buff, &nRead); if(hr != S_OK) break; fwrite(buff, 1, nRead, pFileDec); //write decrypted data } fclose(pFileDec); fclose(pFileEn); }

Regular Expressions:

Regular Expression Reference(MSDN)
CAtlRegExp<> regexp;
CAtlREMatchContext<> mc;
// match any line that starts with any number of digits,

// has a dash, and ends with any number of digits

if(regexp.Parse("^\\d+-\\d+$") == REPARSE_ERROR_OK)
{
    const char* szNumDashNum="5663-4662";
    if(regexp.Match(szNumDashNum, &mc))
    {
        ATLTRACE("Matched");
    }
}

Support for sending SMTP email:

MIME & SMTP Reference(MSDN)
CoInitialize(0);
{
    CSMTPConnection conn;
    conn.Connect("SMTP Server Address");
    CMimeMessage msg;
    msg.SetSender(_T("sender@address"));
    msg.AddRecipient(_T("recepient@address"));
    msg.SetPriority(ATL_MIME_HIGH_PRIORITY);
    //msg.AttachFile(_T("filename"));

    msg.AddText(_T("message text"));
    msg.SetSubject(_T("Subject line"));
    conn.SendMessage(msg);
}
CoUninitialize();

Encoding:

Encoding Reference(MSDN)
BEncode, Base64Encode, UUEncode, QEncode, QPEncode, AtlHexEncode.

CString sSource = "some string";
int nDestLen = Base64EncodeGetRequiredLength(sSource.GetLength());
CString str64;
Base64Encode((const BYTE*)(LPCSTR)sSource, sSource.GetLength(), 
str64.GetBuffer(nDestLen), &nDestLen); str64.ReleaseBuffer(nDestLen); cout<<(LPCSTR)str64; int nDecLen = Base64DecodeGetRequiredLength(nDestLen); CString strOrig; Base64Decode(str64, str64.GetLength(), (BYTE*)strOrig.GetBuffer(nDecLen),
&nDecLen); strOrig.ReleaseBuffer(nDecLen); cout<<(LPCSTR)strOrig;

HTML Generation:

HTML Generation Reference(MSDN)
CHtmlGen html;
CWriteStreamOnCString stream;
html.Initialize(&stream);
html.html();
html.head();
html.title(_T("Sample Page"));
html.headEnd();
html.body(_T("Gray"));
html.WriteRaw(_T("Using ATL7!"));
html.bodyEnd();
html.htmlEnd();
cout<<(LPCTSTR)stream.m_str;

HTTP Client:

HTTP Client Reference(MSDN)
class CHttpClientWithCookie : public CAtlHttpClient
{
    template<>
    void OnSetCookie(LPCTSTR cookie)
    {
        std::cout<<"Got Cookie from server: "<<std::endl<<cookie<<std::endl;
        return;
    }
};

{
CHttpClientWithCookie client;
//client.NavigateChunked()

client.Navigate("http://www.microsoft.com");
const char* pszBody = (const char*)client.GetBody();
CString sRawHdr; DWORD dwLen;
client.GetRawResponseHeader(0, &dwLen);
client.GetRawResponseHeader((BYTE*)sRawHdr.GetBufferSetLength(dwLen), &dwLen);
std::cout<<"Raw Response Header: "<<std::endl<<(LPCTSTR)sRawHdr;
}
There are many other miscellaneous helper classes and more specific ones that deal with Web Application/Web Service development.

Debugging:

Debugging(MSDN)

ISAPI Filter/Extension Overview:

ISAPI stands for Internet Server Application Programming Interface. ISAPI programming is divided into Filters and Extensions. Both are DLLs with specific exported functions. ISAPI filters are loaded with the IIS and stay in memory until HTTP Web service shuts down. ISAPI Extensions are loaded on demand and provide extended functionality to web applications. ISAPI filters are useful for examining/filtering incoming and outgoing HTTP requests under IIS (for example, implementing custom authentication, encryption, compression, logging, etc). ISAPI extensions are useful for developing dynamic web pages and work per URL reference.

There is no class for ISAPI filter under ATL7. ATL Server provides support for ISAPI extensions in form of DLL cache, file cache, page cache, memory cache, data source cache, thread pool, remote management with web based or web service based interfaces, predefined performance counters, session support, etc. As you can see that's quite a lot of functionality/services. In addition it's easy to add your own services to the existing ones.

Web Application:

ATL7 Web Application is a combination of ISAPI extension and a handler DLL (or both can be combined into one dll) to create dynamic web pages. ISAPI extension is a DLL that exports three functions: HttpExtensionProc, GetExtensionVersion, TerminateExtension. The Handler DLL/Web Application DLL also exports three predefined functions for ATL: InitializeAtlHandlers, GetAtlHandlerByName, UninitializeAtlHandlers.

ATL7 web applications are built upon stencils. Stencil is a text file normally with .srf extension, resides in the virtual directory of ISAPI extension, and can have a mixed content of static html and dynamic/run-time replaceable tags. In GetExtensionVersion, ATL initializes the heap it uses, predefined performance monitors, worker thread, thread pool, dllcache, page cache, and stencil cache. TerminateExtension does the opposite.

The main function that does all the work is HttpExtensionProc. The class responsible for implementing it is CIsapiExtension. The CIsapiExtension::HttpExtensionProc queues the incoming request unto an I/O completion port. This completion port is checked by the worker threads that run in the thread pool (CThreadPool). All these worker threads do is block on the GetQueuedCompletionStatus on the afore mentioned completion port (on which requests were queued from HttpExtensionProc). By default there are 2 threads started in the pool (controlled by the ATLS_DEFAULT_THREADSPERPROC or IThreadPoolConfig which CThreadPool implements).

So when any of these worker threads get the chance and successfully dequeue the completion request from the port, they execute the request. Executing request means processing the stencil file, first the whole processed page is looked up in page cache (controlled by overriding CachePage() method and returning TRUE from the CRequestHandlerT derived class). If found the whole page file simply gets transmitted to user and all is done. Otherwise, the stencil file is looked up in stencil cache. If it's not in the cache, the (.srf) file is read from disk, the Handler DLL tag is parsed, and DLL loaded (if it's not in DLL cache already).

The handler's DLL entry point is called, that is InitializeAtlHandlers, and the handler class that handles the request is found through GetAtlHandlerByName. Then all remaining replacement tags are parsed out of stencil file. The stencil file is Rendered, meaning, for all the tags that represent methods the address of the method in handler class is found through the GetAttrReplacementMethodMap defined by tag_name if using attributes or REPLACEMENT_METHOD_MAP in the CRequestHandlerT derived class. Then methods are called and generate the dynamic content for the web page. The generated content is sent back to user. It's nice to go through in debugger to see the sequence of events and suggest you do the same. It's interesting to see how it works internally.

To step through the ATL code place breakpoints at HttpExtensionProc and step through up to PostQueuedCompletionStatus. The other place to put breakpoint is in the while loop for GetQueuedCompletionStatus. Now you can hit F5 and follow the whole sequence of steps.

The wizard generated code (when you choose Data Source Cache, you're free to do it yourself also, but this can serve as an example) also produces a custom CIsapiWorker derived class for per-thread services. This is the class that is created for each thread in the thread pool. What this allows is to add your own methods and data members per thread. This means it doesn't need to be synchronized, and your request handler class can get the pointer to this per-thread worker by calling IIsapiExtension::GetThreadWorker (link has an example on using it also).

ATL Server also provides a number of ways to control and configure your web application. It provides default implementation for remotely controlling the Thread Pool, SRF file cache, and Web App DLL Cache. All you do is define the appropriate symbols for whatever service you want to expose through HTML web interface or Web services, restrict to specific users, etc and you get the full implementation for free! You can follow the steps described in Extension Management Services on how to do it step by step!
Extension Management XML Web Service Client

The services that you choose when you create new project: Blob cache, File Cache, Data Source Cache, Browser capabilities, Session services and in addition the framework provided services: DllCache, StencilCache, ThreadPoolConfig, and AtlMemMgr are all exposed through IServiceProvider::QueryService, which CIsapiExtension derived class implements. You can also add any of your own services at runtime through IIsapiExtension::AddService and later QueryService for them. The code for first group of services will be added for you to the request handler class and will be commented out. All you need to do is uncomment it and apply it to your needs.

In addition there are Predefined ISAPI Performance Counters available. Adding your own performance counters is easy. There is a simple example on this and adding custom service under the Gathering Statistics section of the ATL Server tutorial.

Web Service:

ATL Web service support is also implemented as ISAPI extension. You can either use the wizard or try it manually for learning purposes. Create simple Win32 DLL project. Add .def file to export the 3 ISAPI extension functions. Then in its most simplest form the web service implemented with ATL7 can look as follows:

//websvc.h

#pragma once
namespace ATL7WebService
{
    [export]
    struct S
    {
        int n;
        double d;
    };

    [uuid("86C903DD-4D9F-4928-A3B5-AE242EA86D7A")]
    __interface IWebSvc
    {
        HRESULT GetString([out,retval] BSTR* pVal);
        HRESULT GetData([out, retval] ATLSOAP_BLOB* pBlob);
        HRESULT GetStruct([out, retval] S* pVal);
        //etc

    };

    [request_handler(name="Default", sdl="WSDL")]
    [soap_handler(name="WebSvc", namespace="urn:ATL7WebService")]
    class CWebSvc : public IWebSvc
    {
    public:
        [soap_method]
        HRESULT GetString(/*[out,retval]*/ BSTR* pVal)
        {
            CComBSTR strOut("Some String");
            *pVal = strOut.Detach();
            return S_OK;
        }
        [soap_method]
        HRESULT GetData(/*[out, retval]*/ ATLSOAP_BLOB* pBlob)
        {
            IAtlMemMgr* pMem = GetMemMgr();
            pBlob->size = 1000;
            pBlob->data = (unsigned char*)pMem->Allocate(pBlob->size);
            memset(pBlob->data, 'a', pBlob->size);
            return S_OK;
        }
        [soap_method]
        HRESULT GetStruct(/*[out, retval]*/ S* pVal)
        {
            pVal->n = 10;
            pVal->d = 1.1;
            return S_OK;
        }
        //etc

    };
}
//websvc.cpp

#define _WIN32_WINNT 0x0403
//need for attributes

#define _ATL_ATTRIBUTES
//not using COM for this simple project

#define _ATL_NO_COM_SUPPORT 
#include <atlisapi.h> 

#include <atlsoap.h>

#include "websvc.h"


[ module(name="WebSvc", type="dll") ];
//don't need any .idl, .tlb, etc files generated

[ emitidl(restricted) ]; 
 
CIsapiExtension<> theExtension;

extern "C" DWORD WINAPI HttpExtensionProc(LPEXTENSION_CONTROL_BLOCK lpECB) 
{  return theExtension.HttpExtensionProc(lpECB); } 

extern "C" BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO* pVer) 
{  return theExtension.GetExtensionVersion(pVer); } 

extern "C" BOOL WINAPI TerminateExtension(DWORD dwFlags) 
{ return theExtension.TerminateExtension(dwFlags); }

Either use the VC++.NET project properties for Web Deployment and add .dll extension under Application Mappings or create the virtual directory and register the App mappings manually under IIS. After that you can access the WSDL of the web service in web browser as follows http://localhost/websvc/WebSvc.dll?Handler=WSDL
WebSvc.dll has the implementation of your Web Service, and WSDL is the compiler generated handler that you specified in request_handler for sdl parameter.

Now when you access it again as in Web Application in previous section, HttpExtensionProc will be called by IIS. It will delegate to CIsapiExtension. CIsapiExtension will queue the request on I/O completion port. One of the worker threads will dequeue the request. Then the Handler DLL will be loaded (websvc.dll). Handler will be initialized. What it does is, it gets all the functions exposed by your web service class to write the info about them to WSDL. ATL already has the template for generating the WSDL for your web service. It can be found in atlspriv.h (const char * const s_szAtlsWSDLSrf). You'll see that it's a simple stencil text suitable for Stencil processing (with replaceable tags). So this string is loaded using CStencil::LoadFromString and all the tags are parsed and replaced/rendered.

Now this WSDL output can be used to generate the client to your web service. You can either use VS.NET built-in support for adding Web Services, by running wsdl.exe manually for .NET clients, or running ATL7 provided tool called sproxy.exe to produce proxy client file. For example: sproxy /out:client.h http://localhost/websvc/WebSvc.dll?Handler=WSDL

If using sproxy.exe, the generated file will have the methods exposed by your Web Service nicely wrapped in a C++ class. To use it, simply include the generated file (for example client.h) file in your project:

//client.cpp

#include <iostream>

#include "client.h"


int main()
{
    CoInitialize(NULL);
    {
        WebSvc::CWebSvc svc;
        
        CComBSTR str;
        if(svc.GetString(&str.m_str) == S_OK)
        {
            std::wcout<<str.m_str<<std::endl;
            ATLSOAP_BLOB blob;
            if(svc.GetData(&blob) == S_OK)
            {
                std::cout<<"blob of size: "<<blob.size<<std::endl;
                std::cout<<blob.data[0]<<blob.data1><<std::endl;
                IAtlMemMgr* pMem = svc.GetMemMgr();
                pMem->Free(blob.data);
           WebSvc::S s;
                svc.GetStruct(&s);
            }
        }
    }
    CoUninitialize();
    return 0;
}

If you want to use Windows Integrated Authentication or Basic, you can use CNTLMAuthObject/CBasicAuthObject(CSoapSocketClientT specific):

CoInitialize(NULL);
{
    //by default uses CSoapSocketClientT

    WebSvc::CWebSvc svc;
    
    //this code is specific to CSoapSocketClientT

    CAtlHttpClient& httpClient = svc.m_socket;
    CNTLMAuthObject authUser;
    httpClient.AddAuthObj(_T("NTLM"), &authUser);
    httpClient.NegotiateAuth(true);

    CComBSTR str;
    if(svc.GetString(&str.m_str) == S_OK)
    {
        //etc

    }
}
CoUninitialize();

If you want to use HTTPS connection you can look at the Secure SOAP sample:
SecureSOAP Sample: Implements a Secure SOAP Communication (HTTPS) (MSDN)

In addition you have a choice of using predefined classes for communication such as CSoapSocketClientT (by default), CSoapWininetClient, CSoapMSXMLInetClient:

WebSvc::CWebSvcT<CSoapWininetClient> svc;
CComBSTR str;
if(svc.GetString(&str.m_str) == S_OK)
{
   //etc

}

Or your own implementation for the SOAP transport. This sample demonstrates the creation of SOAP servers and clients that communicate using different transports: sockets, Microsoft Message Queue, the file system, and a custom HTTP listener:
SOAPTransport Sample: Communicates SOAP Messages Over Sockets, MSMQ, the File System, and HTTP Listener(MSDN)

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here