Microsoft's Remote Access Server (RAS) can be used to establish a connection to a remote RAS server. Typically, this means dialing a modem to connect to a remote network, such as an Internet Service Provider (ISP). RAS isn't a terribly complex API, but there are significant differences in the RAS implementation between Windows NT and Windows 95. This article will explain how to use RAS to establish a connection to a remote machine. In part two of this series, we'll discuss some of the more advanced RAS functions and the built-in RAS dialogs.
Most of the time when dealing with RAS, you're dealing with a modem dialing out over a phone line. That isn't always true, however. Users using ISDN, for example, don't have a modem per se. Windows handles these details for you and you don't have to worry too much about the specific hardware on a particular user's machine.
Determine the phonebook entry that will be used to make the connection.
| Determine whether a connection already exists.
| Dial the modem (or other device).
| |
RASENTRYNAMEA
{
DWORD dwSize;
CHAR szEntryName[RAS_MaxEntryName + 1];
};
As you can see, the szEntryName member is the only one of significance. When
RasEnumEntries returns, this member will contain the name of a specific
phonebook entry.
At this point an example might help. Here's the first step in retrieving
phonebook entries using RasEnumEntries:
RASENTRYNAME* entries = new RASENTRYNAME[1]; entries[0].dwSize = sizeof(RASENTRYNAME); DWORD numEntries; DWORD size = entries[0].dwSize;The first line in this code snippet declares an array of RASENTRYNAME structures. We initially assume there's only one entry in the phonebook so the array size is 1. Following that, we set the dwSize member of the structure to the size of a RASENTRYNAME structure. This is a necessary step and is typical of many Windows API functions. Then we declare a variable called numEntries. This variable will contain the number of entries in the phonebook after RasEnumEntries returns. Finally, we declare a variable called size. This variable must initially be set to the size of the RASENTRYNAME structure. When RasEnumEntries returns, this variable will be set to the size of the buffer required to hold all of the entry names. Now we can actually call the RasEnumEntries function. Here's the code:
DWORD res = RasEnumEntries( 0, 0, entries, &size, &numEntries);The first parameter to RasEnumEntries is reserved and must be set to 0. The second parameter is used to specify the phonebook to enumerate. Under Windows 95, this parameter is ignored. In Windows NT you can set this parameter to the path and filename of a particular phonebook. If you specify 0 for this parameter under NT, then Windows will use the current default phonebook. Most of the time, this is sufficient. Some applications, however, need to take multiple phonebooks into account. If you're writing an application that must take multiple phonebooks into account, then you need to write code to handle multiple phonebooks. The third parameter in RasEnumEntries is the address of the buffer that will contain the list of RASENTRYNAME structures if RasEnumEntries returns successfully. The final two parameters are the buffer size and number of entries parameters. Notice that we pass the addresses of our size and numEntries variables for these parameters.
One of two scenarios will develop when you call RasEnumEntries. First, (and most likely) the function may return success on the first call (a return value of 0 indicates success). If this happens, then you know there was only one entry in the phonebook. The numEntries variable will contain the value 1 and the entries array will contain the entry. In this instance, you can simply use the value of entries[0].szEntryName to dial RAS.
In the second scenario, RasEnumEntries will return ERROR_BUFFER_TOO_SMALL. This return value indicates more than one entry in the phonebook. You need to re-allocate the buffer and call RasEnumEntries again.
You can re-allocate the buffer in one of two ways. The size variable will contain the required size of the buffer. Using the size variable you can re-allocate the buffer like this:
delete[] entries; entries = (RASENTRYNAME*)new char[size];Or, if you prefer, you can use the numEntries to re-allocate the buffer like this:
delete[] entries; entries = new RASENTRYNAME[numEntries];Once you've re-allocated the buffer, you can call RasEnumEntries again. Before you do, however, you must set the dwSize member of the first structure in the array. For example:
entries = new RASENTRYNAME[numEntries]; entries[0].dwSize = sizeof(RASENTRYNAME); res = RasEnumEntries( 0, 0, entries, &size, &numEntries);If RasEnumEntries returns 0 you can enumerate the array to retrieve the list of entry names. Typically, you'd place this list in a combo box to allow the user to select the entry he wants to use to dial. The code might look like this:
for (int i=0;i<(int)numEntries;i++) EntriesCb->Items->Add(entries[i].szEntryName);Now you can move on to the second step, determining whether a connection currently exists.
Detecting an active connection is a two-step operation:
RASCONN rc; rc.dwSize = sizeof(rc); DWORD numConns; DWORD size; DWORD res = RasEnumConnections(&rc, &size, &numConns);This code should look familiar, as it's very similar to the code we used when we called RasEnumEntries. We're technically cheating by assuming that there will only be one active connection. If you need to account for more than one active connection, then you should use the allocate-check-re-allocate method described earlier for RasEnumEntries. If RasEnumConnections returns 0 (success), then you can check the value of the numConns variable. If numConns is 0, you know that there are no active connections. Then you can move directly to dialing RAS. If numConns is 1, then there's one active connection and you should check that connection's status. If numConns is greater than 1, you should enumerate the connections and look for the connection that matches the phonebook entry name obtained earlier.
The RASCONN structure has two members of note. The hrasconn member contains a handle to an active connection. The szEntryName member contains the phonebook entry used to make the connection.
Once you've detected an active connection, you need to check the status of that connection. This is accomplished using the RasGetConnectStatus function. Here's the code:
RASCONNSTATUS status; status.dwSize = sizeof(status); res = RasGetConnectStatus(rc.hrasconn, &status);In this code snippet, we're calling RasGetConnectStatus passing the hrasconn parameter of the RASCONN structure obtained earlier with the call to RasEnumConnections and the address of a RASCONNSTATUS structure. If RasGetConnectStatus returns 0 (indicating success), you should check the rasconnstate member of the RASCONNSTATUS structure to determine the status. The rasconnstate member can contain either RASCS_Connected or RASCS_Disconnected. If rasconnstate is RASCS_Connected, then you can use the active connection. A value of RASCS_Disconnected indicates an active connection that, for whatever reason, is no longer valid. If the status is RASCS_Connected, you can save the hrascon parameter of the RASCONN structure to be used later. For example:
if (status.rasconnstate == RASCS_Connected) // Good connection handle, save it TheHandle = rc.hrasconn; else // A connection was detected but its // status is RASCS_Disconnected. TheHandle = 0;Now we can move on to actually dialing RAS.
RASDIALPARAMS params; params.dwSize = sizeof(params); strcpy(params.szEntryName, PBEntry.c_str()); strcpy(params.szPhoneNumber, ""); strcpy(params.szCallbackNumber, ""); strcpy(params.szUserName, ""); strcpy(params.szPassword, ""); strcpy(params.szDomain, "TURBOPOWER");First, note how we assign the szEntryName member. In this code example, PBEntry is an AnsiString that contains the name of the phonebook entry to use. This value is obtained as described earlier in the section, "Getting the phonebook entry." Alternatively, you can specify an empty string for the szEntryName member. When you do that, Windows will establish a simple modem connection on the first available port. If you use this method, you must provide a phone number to dial in the szPhoneNumber member. Note that in this example, the szPhoneNumber member is set to a blank string. This causes Windows to use the phone number as determined by the phonebook entry. If you want to override the phonebook entry's phone number, you can specify the new number here. The szCallbackNumber is set to an empty string, as well. This member is used when the caller requests the remote machine to call back the local machine (Windows NT only).
The szUserName and szPassword members are used to log on to the remote machine. When these members contain an empty string, Windows behaves in two different ways depending on whether the operating system is Windows NT or Windows 95. With NT, Windows uses the current user's logon credentials to log on to the remote server and no further user input is required. Windows 95, on the other hand, can't use the current logon credentials. Under Windows 95, the user will be presented with a log on dialog where they can specify their user name and password. In either situation, you can specifically set the szUserName and szPassword in code if you prefer.
The szDomain member of the RASDIALPARAMS structure can be set to the domain name of the machine to which you are connecting or to one of the following values:
"" The domain in which the remote access server is a member
"*" The domain specified in the phonebook entry settings
In most situations, you should set this member to an asterisk so the phonebook entry settings are used.
Synchronous or asynchronous operation is determined by the way RasDial is called. If the address of a callback function is provided, then RasDial operates asynchronously. If no callback function is provided, then RasDial operates synchronously.
Table A: Possible dwNotifierType values
| A window handle which receives WM_RASDIALEVENT messages | |
| A RasDialFunc callback function | |
|
| A RasDialFunc1 callback function |
|
| A RasDialFunc2 callback function |
You'll probably use a RasDialFunc1 callback function. This type of callback provides both status and error messages (the RasDialFunc callback doesn't provide error messages). In situations where multilink connections are possible, you may elect to use the RasDialFunc2. This callback provides more information than RasDialFunc1 does. A typical RAS callback function might look like this:
VOID WINAPI RasCallback(HRASCONN hrasconn,
UINT unMsg, RASCONNSTATE rascs,
DWORD dwError, DWORD dwExtendedError)
{
String S = "";
if (dwError) {
char buff[256];
RasGetErrorString(
dwError, buff, sizeof(buff));
// display error message
return;
}
switch (rascs) {
case RASCS_DeviceConnected :
S = "Connected..."; break;
case RASCS_Authenticate :
S = "Logging on..."; break;
case RASCS_Connected :
S = "Logon Complete"; break;
// additional case statements
}
Form1->Memo1->Lines->Add(S);
}
This callback function provides error and status information. You can make your
callback as simple or as complex as you like, as determined by the type of
application you're writing.
A common mistake is to try to make the callback function a member of your main
form's class. A callback must be a stand-alone function and cannot be a class
member function.
DWORD res = RasDial( 0, 0, ¶ms, 1, RasCallback, &hRas);The first parameter of RasDial is used to specify extended RAS features. The extended RAS features are only available under Windows NT. We aren't using the extended features here, so we set this parameter to 0.
The second parameter is used to specify the phonebook to be used for dialing. We set this parameter to 0 so Windows will use the default phonebook. Under Windows NT, you can specify a phonebook other than the default if multiple phonebooks are defined.
The third parameter is a pointer to a RASDIALPARAMS structure. We filled this structure out previously in step one of the dialing operation.
The fourth parameter is used to specify which RAS messaging mechanism to use. Now, we pass the value 1 to indicate that we're using a RasDialFunc1 callback type. The fourth parameter is the address of the callback function itself. In this example, the name of the callback function is RasCallback. The fourth parameter is also the address of a variable that will contain a RAS connection handle. This variable will contain the connection handle when RasDial returns. If RasDial returns 0, the call was successful. If a non-zero value is returned, an error occurred. If that happens, you can use RasGetErrorString to obtain an error message to display to the user.
RasHangUp(hRas);
DWORD res = 0;
while (res != ERROR_INVALID_HANDLE) {
RASCONNSTATUS status;
status.dwSize = sizeof(status);
res = RasGetConnectStatus(hRas, &status);
Sleep(0);
}
Notice that we call RasHangUp, passing the handle to the RAS connection we're
terminating. The while loop insures that RAS has completed the disconnect
process before allowing the application to continue on its way.
Listing A shows the code for the main unit of an example program that
illustrates the concepts discussed in this article. This application
establishes a RAS connection on a button click. It then downloads a file from
TurboPower Software's FTP site on a second button click. Finally, it hangs up
the connection on a third button click. Status messages are shown in a memo on
the form. We don't show the main form's header or the code for a second unit,
which allows the user to select a phonebook entry. You can download the
complete example program from our Web site.
Listing A: RASEXU.CPP
#include <vcl.h>
#pragma hdrstop
#include "RasExU.h"
#include "EntriesU.h"
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
// The RAS callback function
VOID WINAPI RasCallback(HRASCONN hrasconn,
UINT unMsg, RASCONNSTATE rascs,
DWORD dwError, DWORD dwExtendedError)
{
String S = "";
if (dwError) {
// Error occurred, show the error string.
char buff[256];
RasGetErrorString(
dwError, buff, sizeof(buff));
Form1->Memo1->Lines->Add(buff);
return;
}
switch (rascs) {
// Build a status string based on the
// status message.
case RASCS_PortOpened :
S = "Port opened..."; break;
case RASCS_DeviceConnected :
S = "Connected..."; break;
case RASCS_Authenticate :
S = "Logging on..."; break;
case RASCS_Authenticated :
S = "Authenticated"; break;
case RASCS_Connected : {
S = "Logon Complete";
Form1->DownloadBtn->Enabled = true;
break;
}
case RASCS_Disconnected :
S = "Disconnected"; break;
}
// Show the status message in the memo.
if (S != "")
Form1->Memo1->Lines->Add(S);
}
void __fastcall
TForm1::ConnectBtnClick(TObject *Sender)
{
// Get the phonebook entry to use to dial.
String PBEntry = GetPhoneBookEntry();
if (PBEntry == "") {
ShowMessage("Operation Cancelled");
return;
}
// Check for existing connections.
hRas = CheckForConnections();
if (hRas) {
Memo1->Lines->Add(
"Using existing connection...");
Memo1->Lines->Add("Click the "
"Download button to transfer files.");
DownloadBtn->Enabled = true;
hRas = 0;
// No need to call RasDial for an
// existing connection.
return;
}
Memo1->Lines->Add(
"No current connection, dialing...");
// Set up the connection params.
RASDIALPARAMS params;
params.dwSize = sizeof(params);
strcpy(params.szEntryName, PBEntry.c_str());
strcpy(params.szPhoneNumber, "");
strcpy(params.szCallbackNumber, "");
strcpy(params.szUserName, "");
strcpy(params.szPassword, "");
strcpy(params.szDomain, "*");
// Dial.
DWORD res = RasDial(
0, 0, ¶ms, 1, RasCallback, &hRas);
if (res) {
char buff[256];
RasGetErrorString(res, buff, sizeof(buff));
Memo1->Lines->Add(buff);
}
}
HRASCONN TForm1::CheckForConnections()
{
char buff[256];
RASCONN rc;
rc.dwSize = sizeof(rc);
DWORD numConns;
DWORD size;
// Enumerate the connections.
DWORD res =
RasEnumConnections(&rc, &size, &numConns);
if (!res) {
// No connections, return 0.
if (numConns == 0)
return 0;
// Multiple connections. Should add code to
// handle multiple connections and decide
// which one to use.
if (numConns > 1)
Memo1->Lines->Add("Multiple connections");
}
if (res) {
// Error. Report it.
RasGetErrorString(res, buff, sizeof(buff));
Memo1->Lines->Add(buff);
} else {
// Get the connection status.
RASCONNSTATUS status;
status.dwSize = sizeof(status);
res = RasGetConnectStatus(
rc.hrasconn, &status);
if (res) {
// Error. Report it.
RasGetErrorString(
res, buff, sizeof(buff));
Memo1->Lines->Add(buff);
return 0;
} else {
// Found connection, show details.
if (status.rasconnstate
== RASCS_Connected) {
Memo1->Lines->Add("Device type: " +
String(status.szDeviceType));
Memo1->Lines->Add("Device name: " +
String(status.szDeviceName));
Memo1->Lines->Add("Connected to: " +
String(rc.szEntryName));
return rc.hrasconn;
} else {
// A connection was detected but its
// status is RASCS_Disconnected.
Memo1->Lines->Add
("Connection Error");
return 0;
}
}
}
return 0;
}
String TForm1::GetPhoneBookEntry()
{
RASENTRYNAME* entries = new RASENTRYNAME[1];
entries[0].dwSize = sizeof(RASENTRYNAME);
DWORD numEntries;
DWORD size = entries[0].dwSize;
DWORD res = RasEnumEntries(0, 0, entries, &size, &numEntries);
if (numEntries == 1)
return entries[0].szEntryName;
if (res == ERROR_BUFFER_TOO_SMALL) {
// allocate enough memory to get all the phonebook entries
delete[] entries;
entries = new RASENTRYNAME[numEntries];
entries[0].dwSize = sizeof(RASENTRYNAME);
res = RasEnumEntries(0, 0, entries, &size, &numEntries);
if (res) {
char buff[256];
RasGetErrorString(res, buff, sizeof(buff));
ShowMessage(buff);
}
}
TPBEntriesForm* form = new TPBEntriesForm(this);
for (int i=0;i<(int)numEntries;i++)
form->EntriesCb->Items->Add(entries[i].szEntryName);
form->EntriesCb->ItemIndex = 0;
String S;
if (form->ShowModal() == mrCancel)
S = "";
else
S = form->EntriesCb->Text;
delete form;
delete[] entries;
return S;
}
void __fastcall
TForm1::HangUpBtnClick(TObject *Sender)
{
if (!hRas) return;
// Hang up.
RasHangUp(hRas);
// Be sure the RAS state machine has cleared.
DWORD res = 0;
while (res != ERROR_INVALID_HANDLE) {
RASCONNSTATUS status;
status.dwSize = sizeof(status);
res = RasGetConnectStatus(hRas, &status);
Sleep(0);
}
hRas = 0;
}
void __fastcall
TForm1::DownloadBtnClick(TObject *Sender)
{
FTP->Connect();
}
void __fastcall
TForm1::FTPConnect(TObject *Sender)
{
Memo1->Lines->Add(
"Connected to TurboPower FTP Site");
FTP->ChangeDir("pub");
}
void __fastcall
TForm1::FTPSuccess(TCmdType Trans_Type)
{
switch (Trans_Type) {
case cmdChangeDir : {
Memo1->Lines->Add("Changed directory");
FTP->Download(
"00index.txt", "00index.txt");
break;
}
case cmdDownload : {
Memo1->Lines->Add("File downloaded!");
break;
}
}
}
void __fastcall
TForm1::FormCreate(TObject *Sender)
{
hRas = 0;
}
void __fastcall
TForm1::FormDestroy(TObject *Sender)
{
// Terminate the connection if necessary
if (hRas)
HangUpBtnClick(this);
}