Novell is now a part of Micro Focus

Using Novell's Web Sever LCGI To Develop an NDS Web Browser

Articles and Tips: article

Developer Support Engineer
Developer Support

01 Apr 1997

Describes how to use Novell's Local Common Gateway Interface (LCGI) to develop an NDS browser that can configure a user's desktop through Novell Application Launcher (NAL).

Common Gateway Interface

This article describes how to use Novell's Local Common Gateway Interface (LCGI) to develop an NDS browser that can configure a user's desktop through Novell Application Launcher (NAL). The NDS web browser developed here allows standard web browsers such as Netscape to walk the NDS tree to find NAL objects. A NAL object can then be assigned to a user through an HTML form, and will then appear in the user's NAL window on the next refresh cycle. Hence an administrator can configure a user's desktop through an IP connection from a remote site, rather than requiring a direct connection.

While the code described in this article can be used in practice it should not be considered as a finished product. For example, there is no mention of viewing a user's NAL desktop nor removing applications; also authentication through the web browser is left at a minimal level. This article is mainly concerned with demonstrating how LCGI can be used to view and configure NDS through a web browser.

Throughout this article it will be assumed that the reader has a modest knowledge of HTML and HTTP.

Common Gateway Interface

Common Gateway Interface is a method by which a web server can dynamically respond to a request from a client. Normally a server will respond to a client's request by sending a copy of the file specified by the client. This static relationship between a client request and the response is not suitable for such things as database queries or browsing an NDS tree; in both cases the server must return an HTML page that can be generated dynamically.

Often a CGI request from a client is not a static request, but one generated by an HTML form or from a page generated by a previous CGI request. This is because a static request can normally be handled by a static response; however, this is not always the case since the request could be for such things as the current time.

Figure 1: Novell Application Launcher Web Browser.

Universal Resource Locator (URL) standard defines the format of the requests that the client can send to the web server. These requests have the following format.


In this article the service will always be HTTP, which is the normal default used by web browsers. In the case of a static request the directory-path-filename will point to an HTML file, relative to a base directory used by the web server. In the case of a CGI request the directory-path-filename will contain a path to an executable file that the web server recognises as a CGI program. The format for CGI requests can contain option parameters as shown below.

http://machine-name/path-to-cgi-executable/ optional-path?optional-data

The optional-path and optional-data values are passed to the CGI executable through the environment variables PATH_INFO and QUERY_STRING.

The client web browser is responsible for collecting any client data and sending it to the web server. If the HTML page contains a simple link containing a URL, such as

<A HREF=</cgi-bin/cgitime<<TIME</A<

then the URL can be used directly. (In this example the web browser automatically adds the service and machine name to complete the URL.) In the case of an HTML form the data can be transmitted as part of the URL or separate from it depending on how the form was defined.

If the form was defined using

<FORM METHOD=GET ACTION="/cgi-bin/cginal"<

then the data associated with the form will be sent as part of the URL through the optional-data field.

Alternatively, if the form is defined using

<FORM METHOD=POST ACTION="/cgi-bin/cginal"<

then the data associated with the form will be sent separately. Using the GET method makes writing CGI applications slightly easier since the data is more easily available. However, there is a limit on the amount of data that can be transmitted using the GET method since there are system limits on the length of URL strings and environment data.

The web server is responsible for collecting the data from the client browser and presenting it to the CGI application. As stated earlier the server uses environment variables to pass data to the CGI application. Data that is posted using the POST method is made available to the CGI application through standard input.

Once the CGI application has received the data from client via the web server it can start to process the data and produce an HTML page to send back to the client. It is vital that the CGI application does respond since the client browser will pause until the application replies. The CGI application generates the reply by printing an HTML page to standard output. The web server will then send the HTML page to the client. In generating the HTML page the CGI application can use any resource that the application can access. This includes communicating with other web servers, accessing databases or, as in this case, using NDS.

Common Gateway Interface on Novell Web Server

Novell Web Server implements a 'C' version CGI using NetWare Loadable Modules. Experienced NLM developers will realize that the previous description of CGI cannot be implemented in a NetWare environment because of the use of environment variables and standard input/output. To overcome this problem the Novell Web Server uses a modified form of CGI called Local Common Gateway Interface. LCGI provides a set of functions that simulate the environment used by CGI applications to communicate with the web server.

Upon receiving a request from a client for a CGI application the web server will load the NLM implementing the application. NLM then registers two callback functions with the web server. The first callback is used by the web server to handle CGI requests. The second callback is used to warn the NLM that it will soon be unloaded.

Once the NLM has registered the callback functions with the web server, the web server can use the request handler callback to process CGI requests. The web server passes to the request handler a handle to an internal structure used by the LCGI functions. It is through this handle that LCGI functions can simulate the environment variables and standard input/output used by CGI applications.

The NLM will remain loaded in memory until the web server decides that it should be unloaded. This is normally because no requests for it have been received in the recent past. The NLM can also be unloaded from console. When the NLM is unloaded it should de-register the callback functions with the web server.

LCGI NLMs are registered with the Novell Web Server through the configuration file SRM.CFG in the CONFIG directory of the web server. This file links the path specified in a URL to the NLM implementing the CGI application. Neither path has to be complete and it is possible to associate a general URL with a specific NLM or directory containing several NLMs. Each LCGI entry in the SRM.CFG file is entered using the LoadableModule command as below.

LoadableModule /cgi-bin/cginal


LoadableModule /cgi-bin/       sys:web/cgi-bin/

In the first example any URL with the path field prefix /cgi-bin/cginal will be handled by cginal.nlm. Any suffix after the /cgi-bin/cginal/ on the URL will be passed to the NLM through the PATH_INFO environment field. In the second example an NLM is not specified. In this case the web server will try to locate an NLM which matches the path field suffix after /cgi-bin/. As can be seen the second example covers the first since the suffix and NLM are the same.

Creating an NDS Web Browser

Developing an NDS Web Browser using LCGI is relatively straightforward. The optional-path or optional-data fields of the URL can be used to specify the context to browse. The CGI NLM can then list the contents of the NDS container and generate an HTML page to display the contents. For each sub-container listed the NLM can create a link using the same URL but with the sub-container name inserted. Hence when the link is used the CGI NLM is executed with the sub-container's context. The HTML page can also have an "up" link to the parent container, however an alternative is to just use the browser's "Back" button.

The NDS web browser can be enhanced by adding icons to depict the object types and links to view object attributes. The standard NDS web browser supplied with the Novell Web Server can also browse multiple trees rather than just the local server's tree.

The NDS web browser developed in the article is specifically intended for NAL objects. In this case only container and NAL objects are listed. Container objects are linked to view their contents, while NAL objects are linked to an HTML form. The HTML form allows the administrator to enter the distinguished name of a user plus a security key. The form data is posted to the NLM where the security key is verified. If the key is valid then the user object's NAL properties are modified to include the NAL application.

Given that the last operation requires administrator rights over the user object and NAL object it is best left to a separate NLM rather than the NLM running the CGI application. In the implementation used in this article the key security and NDS modifications are handled by a registration NLM that must be authenticated to the NDS tree before it can perform the necessary modifications. The registration NLM exports a function that the CGI NLM can use to send the form data to modify the NDS tree.

In the current implementation the registration NLM opens a dialog box when it is loaded asking for the administrator's distinguished name and password. While the CGI NLM will still allow a administrator to browse the tree it will not be able to modify a user's NDS object until the registration NLM has been authenticated. Various methods for automatic NLM authentication were discussed in "NLM Issues and Strategies When Accessing NDS," Novell Developer Notes, August 1996.

As stated earlier the web browser will pause until the CGI application returns an HTML page, hence care should be taken when a CGI NLM depends on other NLMs being available. For example, if the CGI NLM imported the registration function on loading and the registration NLM was not loaded, then the CGI NLM would not load and no HTML page returned. Similarly if the registration function waited for the registration NLM to be authenticated then the CGI NLM would be blocked.

The technique used with the NDS/NAL browser is for the CGI NLM to auto-load the registration NLM and then use ImportSymbol() to access the registration function. The registration NLM will then display the login dialog box until authenticated. If the registration function is called before authentication then the function returns an error code immediately.

Novell Application Launcher and User NDS Objects

Novell Application Launcher uses NDS to record which application objects are associated with which users. A NAL object records the users that use the application and a user object records the NAL applications that are used. The NAL application reads the user's NDS object when launched to determine which NAL objects should appear on the user's NAL window. To add an application to the NAL window requires modifying both the user and application NDS objects.

There are two kinds of NAL application classes- platform specific classes and the generic platform class. The properties used to link applications and users depends on the type of application class. The following table lists the properties that are used to link applications and user objects.

Class name
Application property
User property




Application (DOS)

Application (Windows 3.x)

Application (Windows 95)

Application (Windows NT)

See Also


All the properties are distinguished names other than App:Associates which is a typed name.


The structure of CGINAL main() function is similar to many NLMs that implement callback functions. The main() function first registers a SIGTERM handler, then registers the callback functions and suspends itself. On unloading, the SIGTERM handler resumes the main thread and waits until everything is quite. The resumed main thread waits until any outstanding requests have been handled before de-registering the callback functions and terminating. The code for main() is given in Listing 1.

Listing 1

void main(int, char * [])


    NlmID           = GetNLMID();

    NlmHandle       = GetNLMHandle();

    ThreadID        = GetThreadID();

    ThreadGroupID   = GetThreadGroupID();

    ThreadCount++ ;

    GetTreeName(treeName) ;


    // Register new CGI extensions with web server

    LONG ccode = CgiExtReg(RequestHandler,UnloadHandler, CgiC_ParseHeaders,& CgiExtensionID) ;& CgiExtensionID) ;
    if (ccode){

        ConsolePrintf("\r\nCGINAL: Cannot load CGI extension\n") ;

        return ;


    // Suspend main thread until unloaded


    // Wait until any outstanding requests have finished

    while (ThreadCount > 1)


    // Deregister extensions with web server

    if (CgiExtensionID){


        CgiExtensionID = 0 ;


    // Unload imported symbols

    if (AdministrateNAL){


        AdministrateNAL = 0 ;




The LCGI callback functions are registered by CgiExtReg(). This function takes a pointer to the request and unload functions, a long value for passing flags and a pointer to a long to return the extension identifier. The flags field can be used to indicate to the server whether this process should be treated as a parsed header style process, as in this case. The extension ID is used de-register the extension when the NLM is unloaded and also to create mutual exclusions zones using the functions CgiMutexLock() and CgiMutexUnlock().

Before attempting to browse the NDS tree it is necessary to determine the name of the NDS tree containing the server. The function NWDSCreateContext() used to create a context handle does not necessarily set the tree name to the local tree. Ideally it should be possible to get the local tree name by using the function NWIsDSServer() or NWCCGetConnInfo() by passing a connection handle of zero. Unfortunately this does not work, so a connection handle to local server has to be opened explicitly. The code for determining the tree name is given in Listing 2.

Listing 2

int GetTreeName(char * tree)


    char          server[48];

    NWCONN_HANDLE conn = 0;

    tree[0] = 0 ;


    if (NWCCOpenConnByName(0,server, NWCC_NAME_FORMAT_BIND,NWCC_OPEN_UNLICENSED,0,& conn))

        return 0 ;


    return NWCCCloseConn(conn);


The listing for the SIGTERM handler is given in Listing 3. The UnloadHandler() function used by the web server to warn the NLM that it is about to be unload just resumes the main thread.

Listing 3

void SigTermHandler(int)


    ThreadExitNow = 1 ;


    while (ThreadCount > 0)



LONG UnloadHandler(LONG)


    ThreadExitNow = 1 ;


    return 0 ;


The flag ThreadExitNow is used to indicate that the NLM is being unloaded and that all requester threads should complete what they are doing and terminate. It should be noted that the SIGTERM handler is run by the console thread and that while it is waiting for the requester threads to terminate the console is locked. Moreover, the SIGTERM handler should not terminate itself since this will lock the console until the server is reset.

The code for the request handler is given in Listing 4. The request handler's function is to parse the URL, process any associated data and return an HTML page. In this case the request handler prints an HTML header and determines type of request. The request is handled by separate procedures which prints the main body of the HTML page. Finally the request handler prints the HTML page tail.

Listing 4

LONG RequestHandler(struct

Cgi_t * cgiInfo)


    BYTE *      envValue = 0 ;

    NWDSCCODE   ccode = 0 ;

    ThreadCount++ ;

// Generate the HTML heading that appears on all the pages. Consists

// of a title, background colour, table and the main logo.

    CgiStrmPrintf(cgiInfo,0,(BYTE*) "<HTML<<HEAD<<TITLE<NAL Object Browser</TITLE<</HEAD<\n") ;<
    CgiStrmPrintf(cgiInfo,0,(BYTE*) "<BODY BGCOLOR=\"FFFFFF\"<<CENTER<\n");<
    CgiStrmPrintf(cgiInfo,0,(BYTE*) "<TABLE<\n");<
    CgiStrmPrintf(cgiInfo,0,(BYTE*) "<TR<<TD ColSpan=2<" "<IMG Src=\"/images/cginal.gif\" align=bottom<</TD<</TR<\n") ;<

// Get the query string that came with the URL. This consists of a

// command (LIST,READ,POST) and an NDS context. The main body of the

// HTML page is generated by one of the three Tree() functions.

    if (CgiEnvGet(cgiInfo,(BYTE *)"QUERY_STRING",& envValue) == 0){

        switch (toupper(envValue[0])){

            case  0 :

                ccode = ListTree(cgiInfo,""); break ;

            case 'L':

                ccode = ListTree(cgiInfo,(char *)envValue + 5) ; break ;

            case 'R':

                ccode = ReadTree(cgiInfo,(char *)envValue + 5) ; break ;

            case 'P':

                ccode = PostTree(cgiInfo,(char *)envValue + 5) ; break ;



    if (ccode)

        CgiStrmPrintf(cgiInfo,0,HeadingError,"NetWare error code",ccode) ;

// Generate the HTML close.



    return 0 ;


The URL used to call CGINAL can have the following formats.





The LIST function is for listing the contents of an NDS container, this returns an HTML page listing containers and NAL objects. The READ function is called when a NAL object is selected, and returns an HTML form to enter the user name and security key. The POST function is called when the HTML form is submitted and returns an HTML page indicating whether the operation to modify the user's NDS object was completed.

The function and distinguished name contained in the URL are obtained by reading the environment variable QUERY_STRING. Due to the NetWare environment it is not possible for the web server and CGI NLM to share environment variables using standard ANSI functions. The CGI environment variables are accessed using LCGI functions that take a handle supplied by the web server to the request handler. The function to access environment variables is CgiEnvGet() and is used in Listing 4 to access QUERY_STRING to determine the function and context.

The HTML page is printed using the function CgiStrmPrintf(). This function also uses the handle supplied by the web server so that the output can be collected and sent to the client browser. The function behaves similarly to printf() and can take a variable number of arguments.

An abridged version of the control loop used to list the NDS objects in a container is given in Listing 5. The object's name and base class is returned by NWDSList() in the output buffer and are obtained by using NWDSGetObjectName(). The standard container classes and NAL classes are checked using the function TestClass(), which returns true if the object is of a class given in a list passed as a parameter (the full parameter list for TestClass() is not shown in Listing 5). Container objects belonging to extended schema classes are detected by testing the subordinate count of the object. If an object is found to have subordinates then it is treated similarly to an OU object.

Listing 5

if ((handle = NWDSCreateContext()) == ERR_CONTEXT_CREATION)


NWDSCCODE ccode = NWDSAllocBuf(DEFAULT_MESSAGE_LEN,& buffer) ;& buffer) ;
if (ccode) return NWDSFreeContext(handle), ccode ;

// Set the directory handle's tree name and context.

if (* treeName) NWDSSetContext(handle,DCK_TREE_NAME,treeName);


long iteration = NO_MORE_ITERATIONS;

LONG objectCount = 0 ;

LONG attrCount = 0 ;

// Use NWDSList() to find all NAL and container objects.

do {

   ccode = NWDSList(handle,context,& iteration,buffer) ;& iteration,buffer) ;
   if (ccode) break ;

   NWDSGetObjectCount(handle,buffer,& objectCount) ;& objectCount) ;
   for (int i = 0 ; i < objectCount ; i++){

      // Get name and convert spaces to underscores

      NWDSGetObjectName(handle,buffer,objectName, & attrCount,& objectInfo);& attrCount,& objectInfo);

      for (char * cp = objectName ; * cp ; cp++)

      if (isspace(* cp)) * cp = '_' ;

      // Test object's type against container types.

      if (TestClass(...,objectInfo.baseClass,...)){

         // CgiStrmPrintf(...) ;

         continue ;


      // Test object's type against NAL class types.

      if (TestClass(...,objectInfo.baseClass ...)){

         // CgiStrmPrintf(...) ;

         continue ;


      // Test object for extended schema container types.

      if (objectInfo.subordinateCount){

         // CgiStrmPrintf(...) ;

         continue ;



} while (iteration != NO_MORE_ITERATIONS);

NWDSFreeBuf(buffer) ;

NWDSFreeContext(handle) ;

All the container objects found in the NWDSList() loop require an HTML link to allow the NDS tree to be browsed. For example, if CGINAL was invoked using the URL


and the Sydney container had a container called Users then the following HTML link is created on the output page.

<A HREF=</cgi-bin/cginal?LIST:OU=Users.OU=Sydney.O=DeveloperNet<<<
<IMG Src=</images/ndsOU.GIF< align=<middle<<</A<<

Similarly if the Sydney container had a NAL object called NWAdmin then the following link is created.

  <A HREF=</cgi-bin/cginal?READ:CN=NWAdmin.OU=Sydney.O=DeveloperNet<<<
  <IMG Src=</images/appNAL.GIF< align=<middle<<</A<<

On the HTML page the object name is displayed beside a GIF image showing the standard NWAdmin icon for the object, with the link attach to the image.

CGI applications have to be able to read standard input to process HTML form data that has been posted to the application. Posted data can be accessed using the LCGI function CgiStrmStrGet(). This function will read a specified number of characters into a buffer. The number of characters that have been posted is given in the environment variable CONTENT_LENGTH. Listing 6 shows how to read the posted data into a dynamically allocated buffer.

Listing 6

if (CgiEnvGet(cgiInfo,(BYTE *)"CONTENT_LENGTH",& envValue) == 0){

   LONG length = atoi((char *)envValue) ;

   if (CgiMemGet((void **)& buffer,length+1) == 0)

      CgiStrmStrGet(cgiInfo,& length,buffer) ;& length,buffer) ;

   if (buffer) CgiMemFree((void **)& buffer) ;& buffer) ;

CGI posted data are transmitted as a sequence of assignments concatenated with ampersand symbols. The assignments are of the form name=value, where name is the title attached to the input field on the HTML form. Spaces characters are converted to >+= symbols, and >+=, >>=, >==, >%= with a few others are encoded as >%xy= where xy is the hexadecimal value of the character.

Listing 7 shows how the registration function is imported from the registration NLM. The typedef declares a pointer to a function returning an integer taking a pointer to a PropertyNode structure. This typedef is used to cast the pointer returned by the ImportSymbol() function. The PropertyNode structure contains information on the user name, application and security key.

Listing 7

typedef int (* PF_AdministrateNAL)(PropertyNode *) ;

PF_AdministrateNAL  AdministrateNAL = 0 ;

int SendRequest(PropertyNode * property)


   if (AdministrateNAL == 0){

      AdministrateNAL = (PF_AdministrateNAL) ImportSymbol(NlmHandle,"AdministrateNAL");

      if (AdministrateNAL == 0) return PN_NOT_LOADED ;


   return (*AdministrateNAL)(property);


The LCGI SDK contains many more functions for controlling the CGI environment that are not required in CGINAL. These functions include environment variable manipulation, file creation, reading and writing, memory allocation, standard input/output manipulation.

REGNAL.NLM - NAL Registration NLM

The registration NLM is required to perform three actions, authenticate to NDS on loading, modify user and NAL objects on request and detach from NDS on unloading. The code for authentication and detaching is given in Listing 8.

Listing 8

NWDSCCODE LoginToTree(char * admin, char * password)


   if ((Handle = NWDSCreateContext()) == ERR_CONTEXT_CREATION)


   if (TreeName[0])



   LoginStatus = NWDSLogin(Handle,0,admin,password,0);

   NWDSAllocBuf(DEFAULT_MESSAGE_LEN,& Input) ;& Input) ;
   NWDSAllocBuf(DEFAULT_MESSAGE_LEN,& Output) ;& Output) ;

   return LoginStatus ;


NWDSCCODE LogoutFromTree()


   if (LoginStatus == 0) NWDSLogout(Handle);

   if (Input)  NWDSFreeBuf(Input);

   if (Output) NWDSFreeBuf(Output);

   if (Handle != ERR_CONTEXT_CREATION) NWDSFreeContext(Handle) ;

      return 0 ;


The admin account and password are obtained through a crude dialog box when the NLM is loaded. The dialog box uses the console I/O functions getch(), putchar()and gotoxy() to allow the admin name and password to be entered with and without echo respectively. An important implication of using getch() is that it blocks until a character is entered. If the same technique as in CGINAL of using a SIGTERM handler to control the unloading of the NLM is used, then the console thread will block until a key is pressed if the NLM is unloaded while the dialog box is open.

To avoid this the SIGTERM handler should force a character on the NLM screen using ungetch(). This will simulate a key stroke and unblock getch(). The SIGTERM handler should not call LogoutFromNDS() since the thread did authenticate to NDS. The handler should resume the main NLM thread and allow that thread to detach from NDS.

An abridged version of the code for modifying the user and application objects is given in Listing 9. The function NWDSReadObjectInfo() is used to determine whether the objects exist and get their distinguished names. The distinguished names are later used when entering the new property values. The input buffer has to be configured according to the versions of the NAL object, so a comparison on the NAL object type is made. The object type is obtained from the previous call to NWDSReadObjectInfo(). The error code of ERR_DUPLICATE_VALUE is ignored since this implies that the user/application are already linked.

Listing 9

NWDSCCODE ModifyApplicationObject(char * app, char * user, char * key)


   NWDSCCODE       ccode ;

   Object_Info_T   objectInfo;

   // Verify that this NLM has permission to modify NDS.

   if (LoginStatus)


   // Verify the originator has permission to use this NLM.

   if (! KeyValid(key))

      return PN_KEY_INVALID ;

   // Verify user exists and get typed distinguished name.

   if (NWDSReadObjectInfo(Handle,user,UserName,& objectInfo))

      return PN_USER_INVALID;

   // Verify application exists and get typed distinguished name.

   if (NWDSReadObjectInfo(Handle,app,AppsName,& objectInfo))

      return PN_APP_INVALID ;

   // Determine the type of application object is being modified.

   int newAppClass = stricmp(objectInfo.baseClass,"App:Application") == 0 ;


   // Modify the application object to link it to the user.

   if (newAppClass){




   } else {

      NWDSPutChange(Handle,Input,DS_ADD_ATTRIBUTE,"See Also") ;

      NWDSPutChange(Handle,Input,DS_ADD_VALUE,"See Also") ;



   ccode = NWDSModifyObject(Handle,app,0,0,Input);

   if (ccode == ERR_DUPLICATE_VALUE) ccode = 0 ;

   if (ccode) return ccode ;

   Typed_Name_T typedName;

   typedName.objectName = AppsName ;

   typedName.level = 1 ;

   typedName.interval = 0 ;


   // Modify the user object to link it to the application.

   if (newAppClass){



      NWDSPutAttrVal(Handle,Input,SYN_TYPED_NAME,& typedName) ;& typedName) ;
   } else {





   ccode = NWDSModifyObject(Handle,user,0,0,Input);

   if (ccode == ERR_DUPLICATE_VALUE) ccode = 0 ;

   return ccode ;


Experienced NLM developers would realize that the above code is not reentrant since it uses common NDS buffers. Hence, if two requests from CGINAL occur simultaneously then the buffers will be in an undefined state. This can be avoided by allocating the buffers from within the above procedure or by using the mutual exclusion fuctions so that the procedure can only be accessed by one thread.


The LCGI SDK is included with the Novell Web server. The Samples directory contains some examples of LCGI applications. The Inc directories contain the LCGI header files and the Etc directory contains the NLM import files.

* Originally published in Novell AppNotes


The origin of this information may be internal or external to Novell. While Novell makes all reasonable efforts to verify this information, Novell does not make explicit or implied claims to its validity.

© Copyright Micro Focus or one of its affiliates