Click here to Skip to main content
Click here to Skip to main content

Tool to traverse MIB tree using SNMP

, 11 Dec 2004
Rate this:
Please Sign up or sign in to vote.
This is a simple tool to walk MIB tree. It also demonstrates receiving traps along with client side SNMP requests.

Sample Image - MySNMP.jpg

Introduction

This is a simple tool to walk through MIB tree using Simple Network Management Protocol (SNMP). I developed this tool solely for learning. Here, I'm presenting this tool to explain the techniques behind SNMP manager (client) development. This tool is not intended for serious managing stuff. But if you want to use it for serious system management, go ahead and use it. I won't stop you. But after that, don't complain to me about the bugs and lacking features Wink | ;) . You are most welcome to modify or fix the bugs (if you have time Wink | ;) ).

This tool will do two things. One is capturing the traps and the other is sending request and getting the response. See the above figure, first list control displays response and the bottom one displays the traps. Traps are captured independently by a thread, which is started in OnInitDialog function. So whenever you run this tool, it will start displaying the traps coming to your system. On this figure, displayed traps are from MyAgent.DLL agent, which I presented on the previous articles. Even in the rest of this article, I'm using MyAgent.DLL agent to explain this tool functionality.

Requesting To Agent

Under this heading, I am explaining the first part of this tool in detail. Like all communication, here also there is a need to call Open before making any request to server. Making the request is simple. All you have to do is, first open it using SnmpMgrOpen API, then make a request using SnmpMgrRequest and close it using SnmpMgrClose API. To open it, you should specify the agent's IP address and the community name. This seems simple, isn't it? Yeh!... it is simple. OK, now, take a look in to request types.

Example: Type IP address of your agent, here that is "127.0.0.1". Type the community name as "public" and click "Open". Make sure "public" community has READ WRITE access.

Set request (SNMP_PDU_SET): Set request can be performed only on to the variable with write access. Also, you should have opened the session with READ & WRITE enabled community in SnmpMgrOpen API. First, this tool will get the string from the value edit box and translate to the type specified by the type combo box (combo next to the value edit box), and then it will pass it to the following function below as AsnObjectIdentifier variable. This function will make the request based on the selected OID after putting this variable in to a SnmpVarBindList list. (Even though it is a list, only one variable name & value will be placed.)

Example: Type ".1.3.6.1.4.1.15.0.0.2" on the OID edit box, type "Some thing" on value edit box, and select "octet" in the type combo next to the value edit box. Then, click "Set". Now you can see traps coming with "Some thing". Which shows that the variable is set successfully.

int CMySNMPDlg::SetRequest(AsnObjectIdentifier &asnOid)
{
    char *asciiStr = (char*)malloc(sizeof(char)*255);
    char *pBuff, *pBuff2;
    int i;

    SnmpVarBindList snmpVarList;
    
    AsnInteger    errorStatus=0;    // Error if encountered
        AsnInteger    errorIndex=0;    // Works with variable above

    snmpVarList.list = NULL;
        snmpVarList.len = 0;

    snmpVarList.list = (SnmpVarBind *)SNMP_realloc(
                    snmpVarList.list, 
                    sizeof(SnmpVarBind) *snmpVarList.len); 

    snmpVarList.len++;

    // Assigning OID to variable bindings list
    SnmpUtilOidCpy(&snmpVarList.list[0].name,&asnOid);
    snmpVarList.list[0].value.asnType = ASN_NULL;
    
    int nSel = ((CComboBox*)GetDlgItem(IDC_CMBOIDTYPE))->GetCurSel();

    snmpVarList.list[0].value.asnType = 
        (INT)((CComboBox*)GetDlgItem(IDC_CMBOIDTYPE))->GetItemData(nSel);

    GetDlgItemText(IDC_EDTVALUE,asciiStr,255);
    
    // check the type of the variable 
    //            and convert the string in the edit box to that type
    switch (snmpVarList.list[0].value.asnType)
    {
    case ASN_OCTETSTRING:
        snmpVarList.list[0].value.asnValue.string.dynamic = TRUE;
        snmpVarList.list[0].value.asnValue.string.length = strlen(asciiStr);
        snmpVarList.list[0].value.asnValue.string.stream = 
            (unsigned char*)malloc(
                    snmpVarList.list[0].value.asnValue.string.length*
                                    sizeof(char));

        strcpy((char*)snmpVarList.list[0].value.asnValue.string.stream,asciiStr);
        break;

    case ASN_INTEGER32:
        snmpVarList.list[0].value.asnValue.number = atoi(asciiStr);
        break;

    case ASN_TIMETICKS:
        snmpVarList.list[0].value.asnValue.ticks = atoi(asciiStr);
        break;

    case ASN_GAUGE32:
        snmpVarList.list[0].value.asnValue.gauge = atoi(asciiStr);
        break;

    case ASN_COUNTER32:
        snmpVarList.list[0].value.asnValue.counter = atoi(asciiStr);
        break;

    case ASN_IPADDRESS:
        pBuff = asciiStr;
        pBuff2 = pBuff;

        snmpVarList.list[0].value.asnValue.address.dynamic = TRUE;
        snmpVarList.list[0].value.asnValue.address.length = 4;
        snmpVarList.list[0].value.asnValue.address.stream = 
                            (UCHAR*)SnmpUtilMemAlloc(4);

        for(i=0;i<4;i++)
        {
            pBuff2 = strchr(pBuff,'.');

            if(pBuff2)
            {
                pBuff2[0]='\0';
            }
            else
            {
                pBuff = pBuff;
                pBuff[strlen(pBuff)] = '\0';
            }

            snmpVarList.list[0].value.asnValue.address.stream[i] = 
                                    atoi(pBuff);

            pBuff = ++pBuff2;
        }
        break;
    case ASN_OBJECTIDENTIFIER:
        if(!SnmpMgrStrToOid(asciiStr,&snmpVarList.list[0].value.asnValue.object))
        {
            MessageBox("Invalid value","Snmp Value Error",MB_OK|MB_ICONERROR);
            SnmpUtilVarBindListFree(&snmpVarList);
            SnmpUtilOidFree(&asnOid);
            return 1;
        }
        break;
    case ASN_SEQUENCE:
        snmpVarList.list[0].value.asnValue.sequence.dynamic = TRUE;
        snmpVarList.list[0].value.asnValue.sequence.length = strlen(asciiStr)+1;
        snmpVarList.list[0].value.asnValue.sequence.stream =     
                    (UCHAR*) SnmpUtilMemAlloc(strlen(asciiStr) + 1);

        strncpy((char*)snmpVarList.list[0].value.asnValue.sequence.stream,
                        asciiStr,
                        strlen(asciiStr)+1);
        break;
    case ASN_OPAQUE:
        snmpVarList.list[0].value.asnValue.arbitrary.dynamic = TRUE;
        snmpVarList.list[0].value.asnValue.arbitrary.length = strlen(asciiStr)+1;
        snmpVarList.list[0].value.asnValue.arbitrary.stream =  
                    (UCHAR*) SnmpUtilMemAlloc(strlen(asciiStr) + 1);

        strncpy((char*)snmpVarList.list[0].value.asnValue.arbitrary.stream,
                        asciiStr,
                        strlen(asciiStr)+1);
    case ASN_NULL:
    default:
        strcpy(asciiStr,"");
        SnmpUtilVarBindListFree(&snmpVarList);
        SnmpUtilOidFree(&asnOid);
        MessageBox("Error Unsupported Type","Error",MB_OK|MB_ICONERROR);
        return 1;
    }
    
    // initiates the set request
    if(!SnmpMgrRequest(m_lpMgrSession,SNMP_PDU_SET,
                &snmpVarList,&errorStatus,&errorIndex))
    {
        MessageBox("Snmp Request Failed","Snmp Error",MB_OK|MB_ICONERROR);
        SnmpUtilVarBindListFree(&snmpVarList);
        SnmpUtilOidFree(&asnOid);
        return 1;
    }
    if(errorStatus > 0)
    {
        PrintStatusError(errorStatus,pBuff);
        sprintf(asciiStr,"Snmp Request Failed\nErrorStatus: %s  ErrorIndex: %d",
                                pBuff,errorIndex);

        MessageBox(asciiStr,"Snmp Error",MB_OK|MB_ICONERROR);
        free(asciiStr);
        free(pBuff);

        SnmpUtilVarBindListFree(&snmpVarList);
        SnmpUtilOidFree(&asnOid);
        return 1;
    }

    // free the list, to avoid memory leak
    SnmpUtilVarBindListFree(&snmpVarList);
    return 0;
}

Get Request (SNMP_PDU_GET): This is reverse of the SET request. This tool will take the OID and make the request. The returning value will be translated to string and the edit box will be populated with that.

Example: Type ".1.3.6.1.4.1.15.0.0.1" on the OID and click "Get". You can see the "Author : Ramanan.T" coming on the value edit box and "octet" on the type combo.

int CMySNMPDlg::GetRequest(AsnObjectIdentifier &asnOid)
{
    char *asciiStr, *tmpStr;
    CComboBox *pCmdType = (CComboBox *)GetDlgItem(IDC_CMBOIDTYPE);

    AsnInteger    errorStatus=0;    // Error if encountered
        AsnInteger    errorIndex=0;    // Works with variable above

    SnmpVarBindList snmpVarList;
    snmpVarList.list = NULL;
        snmpVarList.len = 0;

    snmpVarList.list = 
        (SnmpVarBind *)SNMP_realloc(
                snmpVarList.list, 
                sizeof(SnmpVarBind) *snmpVarList.len); 
    snmpVarList.len++;

    // Assigning OID to variable bindings list
    SnmpUtilOidCpy(&snmpVarList.list[0].name,&asnOid);
    snmpVarList.list[0].value.asnType = ASN_NULL;
        
    // initiates the GET request
    if(!SnmpMgrRequest(m_lpMgrSession,SNMP_PDU_GET,
            &snmpVarList,&errorStatus,&errorIndex))
    {
        SnmpUtilVarBindListFree(&snmpVarList);
        SnmpUtilOidFree(&asnOid);
        
        asciiStr = (char*)malloc(sizeof(char)*128);
        PrintStatusError(errorStatus,tmpStr);
        sprintf(asciiStr,"Snmp Request Failed\nErrorStatus: %s  ErrorIndex: %d",
                                tmpStr,errorIndex);
        MessageBox(asciiStr,"Snmp Error",MB_OK|MB_ICONERROR);
        free(asciiStr);
        free(tmpStr);
        return 1;
    }
    if(errorStatus > 0)
    {
        SnmpUtilVarBindListFree(&snmpVarList);
        SnmpUtilOidFree(&asnOid);

        asciiStr = (char*)malloc(sizeof(char)*128);
        PrintStatusError(errorStatus,tmpStr);
        sprintf(asciiStr,"ErrorStatus: %s  ErrorIndex: %d",tmpStr,errorIndex);
        MessageBox(asciiStr,"Snmp Error",MB_OK|MB_ICONERROR);
        free(asciiStr);
        free(tmpStr);
        return 1;
    }

    asciiStr = SNMP_AnyToStr(&snmpVarList.list[0].value);
    int nTypes = pCmdType->GetCount();
    for(int i=0;i<nTypes;i++)
    {
        if(pCmdType->GetItemData(i) == snmpVarList.list[0].value.asnType)
        {
            pCmdType->SetCurSel(i);
            break;
        }
    }

    SetDlgItemText(IDC_EDTVALUE,asciiStr);
    
    SnmpUtilVarBindListFree(&snmpVarList);
    
    if(asciiStr)
        SnmpUtilMemFree(asciiStr);

    return 0;
}

Get Next Request (SNMP_PDU_GETNEXT): This is same like the GET request, but instead of returning the requested OID value, it will return the next available OID and its value. It's very useful to traverse the MIB tree.

Example: To experiment this, type ".1.3.6.1.4.1.15" on the OID and click "Get All". You can see all the three nodes in MyAgent.DLL displayed on the list box. Another interesting thing, type ".1.3.6" on the OID, click "Get All", and wait for a while. You can see a long list showing most of your computer details.

void CMySNMPDlg::OnBtnGetAll() 
{
    char *szOID = (char*)malloc(sizeof(char)*255);
    char *asciiStr, *tmpStr;
    char *szListEntry;

    SnmpVarBindList snmpVarList;
    AsnObjectIdentifier asnOid, asnOidTemp;

    AsnInteger    errorStatus=0;    // Error if encountered
        AsnInteger    errorIndex=0;    // Works with variable above

    snmpVarList.list = NULL;
        snmpVarList.len = 0;
    
    memset(szOID,0,255);
    GetDlgItemText(IDC_CMBOID,szOID,255);

    if(!SnmpMgrStrToOid(szOID, &asnOid))
    {
        MessageBox("Invalid Oid","Error",MB_OK|MB_ICONERROR);
        return;
    }

    snmpVarList.len++;

    snmpVarList.list = 
            (SnmpVarBind *)SNMP_realloc(snmpVarList.list, 
                    sizeof(SnmpVarBind) *snmpVarList.len); 

    // Assigning OID to variable bindings list
    SnmpUtilOidCpy(&snmpVarList.list[0].name,&asnOid);
    snmpVarList.list[0].value.asnType = ASN_NULL;

    ((CListBox*)GetDlgItem(IDC_LSTOID))->ResetContent();
    for(;;)
    {
        // one by one get the variables
        if(!SnmpMgrRequest(m_lpMgrSession,SNMP_PDU_GETNEXT, 
                &snmpVarList, &errorStatus, &errorIndex))
        {
            asciiStr = (char*)malloc(sizeof(char)*255);
            PrintStatusError(errorStatus,tmpStr);
            sprintf(asciiStr,"Snmp Request Failed\nErrorStatus: %s  ErrorIndex: %d",
                                        tmpStr,errorIndex);
            MessageBox(asciiStr,"Snmp Error",MB_OK|MB_ICONERROR);
            free(asciiStr);
            free(tmpStr);
            free(szOID);

            SnmpUtilVarBindListFree(&snmpVarList);
            SnmpUtilOidFree(&asnOid);
            return;
        }
        if(errorStatus == SNMP_ERRORSTATUS_NOSUCHNAME||
            SnmpUtilOidNCmp(&snmpVarList.list[0].name,&asnOid, asnOid.idLength))
            break;

        if(errorStatus > 0)
        {
            asciiStr = (char*)malloc(sizeof(char)*255);
            PrintStatusError(errorStatus,tmpStr);
            sprintf(asciiStr,"ErrorStatus: %s  ErrorIndex: %d",tmpStr,errorIndex);
            MessageBox(asciiStr,"Snmp Error",MB_OK|MB_ICONERROR);
            free(asciiStr);
            free(tmpStr);
            free(szOID);
            break;
        }
        else
        {
            char *szOidString = NULL;
            if(snmpVarList.list[0].name.idLength)
            {
                szOidString = 
                    (char *)SnmpUtilMemAlloc(
                        snmpVarList.list[0].name.idLength * 5);

                if(szOidString)
                {
                    UINT i;
                    char szBuf[17];
                    strcpy(szOidString,".");
                    for(i = 0; i < snmpVarList.list[0].name.idLength; i++)
                    {
                        lstrcat(szOidString, 
                            itoa(snmpVarList.list[0].name.ids[i], 
                                szBuf, 10 ) );

                        if(i < snmpVarList.list[0].name.idLength-1)
                            lstrcat(szOidString, ".");
                    }
                }
            }
            //SnmpMgrOidToStr(&snmpVarList.list[0].name, &szOidString);

            // i had problems in SnmpMgrOidToStr 
            //            so I created this custome func to convert to string
            asciiStr = SNMP_AnyToStr(&snmpVarList.list[0].value);
            if(!asciiStr)
            {
                asciiStr = (char*)SnmpUtilMemAlloc(5);
                strcpy(asciiStr,"");
            }

            szListEntry = 
                (char*)malloc(sizeof(char)*(strlen(asciiStr)+
                            strlen(szOidString)+5));

            strcpy(szListEntry,szOidString);
            strcat(szListEntry," : ");
            strcat(szListEntry,asciiStr);

            ((CListBox*)GetDlgItem(IDC_LSTOID))->AddString(szListEntry);

            SnmpUtilMemFree(asciiStr);
            SNMP_free(szOidString);
            free(szListEntry);
        }
        // Prepare for the next iteration.  Make sure returned oid is
        // preserved and the returned value is freed.

        SnmpUtilOidCpy(&asnOidTemp, &snmpVarList.list[0].name);

        SnmpUtilVarBindFree(&snmpVarList.list[0]);

        SnmpUtilOidCpy(&snmpVarList.list[0].name, &asnOidTemp);
        snmpVarList.list[0].value.asnType = ASN_NULL;

        SnmpUtilOidFree(&asnOidTemp);
    }
    SnmpUtilVarBindListFree(&snmpVarList);
    SnmpUtilOidFree(&asnOid);
    free(szOID);
}

Capturing Traps

Only the following thread is dealing with traps. It is independent from the requests. To capture traps, you don't have to open it (click "Open" button). To capture traps, event is necessary. Event should be created (using CreateEvent) and passed to SnmpMgrTrapListen API. Then, using WaitForSingleObject API, wait for any traps, and when it appears, get it using SnmpMgrGetTrap API. Here in this tool, these captured traps are translated to string type and list control is populated with that.

unsigned long __stdcall CMySNMPDlg::TrapCaptureThread(void *lpVoid)
{
    CMySNMPDlg *pDlg = (CMySNMPDlg*)lpVoid;

    DWORD dwResult;

    // listening to any traps coming and 
    //    triggers the event assigned (here it is m_hNewTrapsEvent) 
    if(!(dwResult = SnmpMgrTrapListen(&pDlg->m_hNewTrapsEvent)))
    {
        LPVOID lpMsgBuf;
        dwResult = GetLastError();
        FormatMessage(    FORMAT_MESSAGE_ALLOCATE_BUFFER|
                FORMAT_MESSAGE_FROM_SYSTEM|
                FORMAT_MESSAGE_IGNORE_INSERTS,
                NULL,
                dwResult,
                MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
                (LPTSTR) &lpMsgBuf,
                0,
                NULL);

        ::MessageBox(pDlg->m_hWnd,(char*)lpMsgBuf,
                "Listen Error",MB_OK|MB_ICONERROR);

        free(lpMsgBuf);
        return 1;
    }

    for(;!pDlg->m_bStopTrapCaptureThread;)
    {
        // waiting for the event to get triggered
        dwResult = WaitForSingleObject(pDlg->m_hNewTrapsEvent, 0xffffffff);
        if(dwResult != WAIT_OBJECT_0)
            continue;
        if(pDlg->m_bStopTrapCaptureThread)
            break;
        ResetEvent(pDlg->m_hNewTrapsEvent);

        AsnObjectIdentifier enterprise;
        AsnNetworkAddress   IPAddress;
        AsnInteger          genericTrap;
        AsnInteger          specificTrap;
        AsnTimeticks        timeStamp;
        RFC1157VarBindList  variableBindings;

        UINT i;
        char *oidStr = NULL;
        char *asciiStr =NULL;
        char *tempStr = NULL;
        char *ipStr = NULL;

        // gets all the traps one by one
        //  several traps can at a time, 
        //    in this case only once the event will get triggered 
        //    but we have to get all the traps one by one
        while(SnmpMgrGetTrap(&enterprise, &IPAddress, 
            &genericTrap,&specificTrap, &timeStamp, &variableBindings))
        {
            if (IPAddress.length == 4) 
            {
                ipStr = (char*)malloc(sizeof(char)*(64));
                sprintf(ipStr,"%d.%d.%d.%d",
                        (int)IPAddress.stream[0], 
                        (int)IPAddress.stream[1],
                        (int)IPAddress.stream[2], 
                        (int)IPAddress.stream[3]);

            }
            ;
            if(IPAddress.dynamic) 
            {
                SnmpUtilMemFree(IPAddress.stream);
            }

            for(i=0; i < variableBindings.len; i++)
            {
                // SNMP api to convert the oid bane to string , 
                //    so that we can populate the list box
                SnmpMgrOidToStr(&variableBindings.list[i].name, &oidStr);
                int nErr = GetLastError(); // for debugging

                // SNMP api to convert any types to string
                asciiStr = pDlg->SNMP_AnyToStr(&variableBindings.list[i].value);

                tempStr = 
                    (char*)malloc(sizeof(char)*
                        (strlen(ipStr)+strlen(oidStr)+
                        strlen(asciiStr)+128));

                strcpy(tempStr,"Trap: IP:");
                strcat(tempStr, ipStr);

                strcat(tempStr," OID:");
                strcat(tempStr, oidStr);

                strcat(tempStr," Value:");
                strcat(tempStr, asciiStr);

                ((CListBox*)pDlg->GetDlgItem(IDC_LSTTRAP))->AddString(tempStr);

                if(oidStr)
                {
                    SnmpUtilMemFree(oidStr);
                    oidStr = NULL;
                }
                if(tempStr)
                {
                    free(tempStr);
                    tempStr = NULL;
                }
                if(asciiStr)
                {
                    SnmpUtilMemFree(asciiStr);
                    asciiStr = NULL;
                }
            }

            if(ipStr)
            {
                free(ipStr);
                ipStr = NULL;
            }
                SnmpUtilOidFree(&enterprise);
                SnmpUtilVarBindListFree(&variableBindings);
        }
    }
    if(pDlg->m_hNewTrapsEvent)
    {
        CloseHandle(pDlg->m_hNewTrapsEvent);
        pDlg->m_hNewTrapsEvent = NULL;
    }
    pDlg->m_bStopTrapCaptureThread = FALSE;
    return 0;
}

Some Issues

  • This may crash when you try to exit this dialog while receiving traps.
  • It can't display UNICODE string from agent.
  • There can be some memory leaks (I didn't check this thoroughly).

Conclusion

That's all folks. Hope you got a clear picture of SNMP and its functionality. Using this, you might mess up network printers, hubs, etc. Please refrain from annoying others. Also, make sure you set appropriate security settings when you configure your system for SNMP. Others can peek in to your system (may be they can uninstall some thing Wink | ;) ).

If you want, just take a look at the free and powerful tool GetIf to walk MIB tree.

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

About the Author

Ramanan.T
Software Developer (Senior)
Australia Australia
No Biography provided

Comments and Discussions

 
QuestionCan you send me full source code? Pinmemberworldtrain28-Apr-14 23:27 
QuestionA need help Chinese beginners Pinmembercoperator16-Nov-13 14:51 
QuestionOID translate PinmemberRichie ng30-Oct-12 21:08 
QuestionUnable to see any trap Pinmemberaprilialand20-Nov-11 22:31 
GeneralJust a minor bug on exit [modified] Pinmemberjzyang12-May-11 7:39 
GeneralSNMP through firewall Pinmemberharishksh20-Jan-09 17:12 
QuestionIdentify all devices Pinmemberprashant.majhwar16-Dec-08 21:25 
Generalrequest failed Pinmembermanh_duc27-Oct-08 16:56 
GeneralPlease Help: SNMP_ERRORSTATUS_NOSUCHNAME PinmemberTom Marasco1-May-08 2:03 
GeneralRe: Please Help: SNMP_ERRORSTATUS_NOSUCHNAME PinmemberMark A Stevens2-May-08 11:20 
GeneralRe: Please Help: SNMP_ERRORSTATUS_NOSUCHNAME Pinmembertwokkel4-May-10 22:14 
GeneralFull source PinmemberBlue Wonder1-Aug-07 3:31 
GeneralMIB tree Pinmemberrtyson28-Feb-07 3:50 
GeneralThe WinSnmp & Snmp Api Pinmembermosfet3311-Jan-07 22:47 
Generalnever get Traps Capture Pinmemberdrift200021-Aug-06 1:52 
GeneralRe: never get Traps Capture Pinmemberacpetkimo19-Oct-08 23:06 
Generalerror in getrequest Pinmembermhwlng2-May-06 0:50 
GeneralI can't get trap with MySNMP, help please PinmemberGoogle Fun22-Feb-06 17:54 
GeneralError of... PinmemberKamran Bukhari12-Sep-05 21:56 
GeneralIncase of SNMP Broadcast Pinmemberjsaroj5-Jun-05 4:54 
GeneralRe: Incase of SNMP Broadcast PinsussAnonymous24-Jul-05 16:02 
GeneralError for Get and Set Pinmemberzhu_david7-Apr-05 14:30 
GeneralRe: Error for Get and Set Pinmemberj20932-Jun-05 22:42 
GeneralRe: Error for Get and Set Pinmemberjsaroj5-Jun-05 5:03 
GeneralRe: Error for Get and Set Pinmemberj20935-Jun-05 15:18 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140721.1 | Last Updated 11 Dec 2004
Article Copyright 2004 by Ramanan.T
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid