|
|
| White Papers Home |  White Papers | Message Board |  Search |  Products |  Purchase | News |   |
Ó Rick
Strahl, West Wind Technologies, 1998-2002
http://www.west-wind.com/
Updated: March 22, 2002
This document discusses how you can use Visual FoxPro COM objects in your Active Server Applications. COM is the primary mechanism employed by ASP to extend the base functionality provided by this popular Web development tool.
This document covers the following topics:
¨ A brief review of Active Server Pages architecture
¨ The basics how to build a VFP COM object for use with ASP
¨ Discussion of how to pass and use the built-in ASP objects with VFP
¨ Sharing VFP generated objects with the calling ASP page
¨ Sharing data between ASP and VFP using ADO objects
¨ Tips on how to debug your servers as you build them and catch errors as you run them
¨ Discussion of how VFP COM objects affect ASP's scalability to serve large loads of Web requests
¨ How Web security affects your COM ASP applications
Active
Server Pages is Microsoft's primary architecture for building Web backend
logic. The concept of ASP is based on a scripting metaphor that allows users to
mix HTML with scripted code in the same document to provide dynamic Web
content. Scripting is not a new concept from Microsoft – tools like Cold Fusion
and even script languages like Perl preceeded ASP many years before Microsoft even
considered the Web platform. However, with ASP Microsoft has legitimized the
concept of scripting with an environment that closely ties into Microsoft's
general Windows system architecture and COM.
One
misconception about ASP is that it's 'way beyond CGI and ISAPI' and other low
level interfaces. The truth is that ASP is really just a specific
implementation of ISAPI in this case that hides the complexity of the
underlying protocols and system interfaces. The Active Server Pages engine is
implemented as a script mapped ISAPI
server extension. The driving engine—ASP.dll—is an ISAPI extension that is called whenever someone accesses a page
with an .ASP extension. A script map in the server's metabase (or registry
settings on IIS 3) causes the ASP script to be redirected to this DLL. Script
maps are meant to give the impression that the developer or user is
"executing" the .ASP page directly, but in reality ASP.dll is invoked
for each .ASP file. ASP.dll hosts the VBScript or JScript interpreter and HTML
parser, which in turn parses the ASP pages, expanding any code inside the page
and converting it to HTTP-compliant output to return to the Web server. The
figure below shows how the architecture is put together.
The
script code can use any feature that the scripting language supports. One
important feature of the engine is its ability to support COM objects. ASP
starts up with several built-in COM objects that are always available to you in
your ASP pages. The Request and Response objects handle retrieving input and
sending output for the active Web page. The Server object provides an interface
to system services, the most important of which is the ability to launch COM
objects. The Session and Application objects provide state management to allow
creation of persistent data that has a lifetime of the application or of a
user's session, respectively.
The
figure also shows Active Data Objects. Although ADO is not an intrinsic
component of ASP that must be explicitly created with Server.CreateObject(), it
is so closely related to ASP that it should be listed here. ADO is used to
access any system data source (system DSN or OLE DB provider) on your system
through familiar SQL connection and execution syntax. Finally, you can load any COM object available on the local
system. But there are some limitations: The object must support the IDispatch
interface and late binding in order to be used by ASP, but that's hardly a
limitation since most objects support the dual interface standard. All COM objects
built with Visual FoxPro or Visual Basic support this interface.
Notice
that the ISAPI DLL is the actual host of the scripting engine. ASP.dll is
hosted in the IIS process and the scripting engine, and any in-process COM
objects launched from it also run inside this process.
This
is an important fact—since components run inside of IIS they have the potential
to crash or hang the Web server on failure. Moreover, component development can
be very tricky because components running inside IIS can be difficult or
impossible to debug at runtime.
I don't want to go into great detail here, but I'll briefly discuss the base ASP syntax for review here. ASP scripting works via markup tags that are embedded directly inside of an ASP document, which is nothing more than a text file. The ASP engine reads the ASP page and parses the code inside of the page, expanding any expressions or code blocks into plain HTTP/HTML output. The result from an ASP page is always plain HTTP/HTML output that contains the expanded output of each script expression in the document. As such, the resulting output can run on any Web browser assuming the author used HTML markup that is compatible on each browser.
Script markup can take three forms:
¨
Expressions
<%=
Expression %>
¨
Code Blocks
<%
Any number of lines of code to execute
%>
¨
Structure
Statements mixed with Markup and/or script code
<%
IF Sex %>
Yes, Please!
<%
ELSE %>
No, Thank you!
<%
END IF %>
The
following example demonstrates these three combinations:
<HTML>
Function/Expression access:
The current time is: <%= now %>
The browser is:
<%= Request.ServerVariables(“USER_AGENT”)
%>
Code can be accessed in blocks:
<%
for x=1 to 100
Response.Write(“Processing line: “ + x)
next x
%>
And you can mix HTML within script constructs:
<% For x =
100 %>
Processing line: <%= x %>
<% next %>
</HTML>
ASP
scripting is deceptively simple and that's really the purpose behind a
scripting engine. The limitations of a scripting engine really lie with the
scripting languages and the syntax that they support. You'll find all of the
basics in VBScript and Jscript, but these languages are very plain.
The
real power of ASP lies in its ability to hook into COM and extend the scripting
interface with both internal and external objects.
VBScript by itself provides no direct support for Web development. Neither does Jscript. ASP provides the Web interface functionality through several built in objects:
|
Object |
What it does |
|
Request |
Handles all input from
HTML forms, the Web server and the browser. The Request object is responsible
for providing the information you need to create queries or act on requests.
The Request object provides a number of collection objects you can access, which
include Form, QueryString, ServerVariables and cookies. |
|
Response |
This object is the
counterpart to the Request object that is responsible for dynamic output. The
basic method is Response.Write(), which allows dynamic creation of text
output to an HTML document that gets sent back to the server and then the
browser. With this object you can control direct output as well as the HTTP
header, including cookies and special features such as redirection and
authentication. |
|
ADO |
Active Data Objects is
an external object that you can use to access any ODBC or OLE DB data source.
This object must be explicitly created with
SERVER.CREATEOBJECT("ADODB.Connection") and/or ADODB.RecordSet. |
|
Server |
The Server object
provides an interface to system services. The most important aspect of the
Server object is its ability to instantiate external COM objects including
Visual FoxPro Automation servers. |
|
Session/Application |
These two powerful
objects allow you to manage state between individual requests. By its nature,
HTTP requests are stateless, meaning the current request knows nothing about
the previous request unless you pass the relevant data to it. These objects
allow attachment of external property values to dynamic objects so you can
create persistent variables that are available for the duration of a user's
connection or an approximation of "global" variables via the
Application object. |
These
objects are crucial to making ASP work by providing the interface to the Web
server allowing to read input, send output and manage state and the system.
If you've looked at Active Server
Pages, you're probably familiar with using the scripting features of ASP and
accessing database data directly using the Active Data Objects (ADO). Direct database access provides the easiest way
to get at FoxPro or other ODBC/OleDb data quickly. But there are some rather
serious issues with ASP scripting that come down to the limitations of the
scripting model and the limited support for error handling. These issues make
it relatively hard to build mission-critical, bulletproof code with scripting
alone. Furthermore, the scripting nature that mixes code and HTML can easily
lead to heinous spaghetti code that's hard to debug and even harder to fix a
few months after the application goes live. Doing everything in script code
violates a fundamental rule of good application design: Separate the user
interface from the data access code. While it's possible to do all these things
with scripting code, the environment does not encourage it. Scripting requires
you to maintain strict discipline, which may result in bypassing many of the conveniences
that make ASP such an attractive solution in the first place. Finally, script
code is interpreted at runtime and very slow – even when compared to a
non-compiled language like Visual FoxPro. Running similar looping and parsing
code and even ADO recordset loops can run as much as a hundred times faster in
VFP than in an ASP page!
According
to Microsoft scripting was never meant to be the end-all development
environment – rather the scripting engine was designed as a gateway to
interface with databases via ADO and the use of COM for providing the business
layer. Microsoft has long realized that no development tool is an island, and
thus should be extensible. Active Server Pages is no different. With ASP you
have the ability to create any Automation-capable (IDispatch-compatible) COM
object using the Server.CreateObject() method. Once that object has been created, you can access its methods
and properties the same way you can in Visual FoxPro, Visual Basic or any other
Automation-capable client.
COM
is everywhere in Active Server Pages. The entire environment is built on COM.
All the built-in objects – Request, Response, Session, Application and Server -
are COM objects that are exposed with public scope to your ASP pages. ADO which
you use for database access with ASP is implemented as a COM object that you
actually have to create with Server.CreateObject() like any other COM object.
Even the scripting engine is a COM object with a specific interface that can be
extended and allows for custom implementations that can provide their own
syntax (like a third party Perl engine for example).
What's
even more important is ASP's ability to create any COM object and use it from within the script code of the page.
It's very easy to create a VFP COM object and take advantage of Visual FoxPro's
powerful language and fast database access to provide functionality that would
be hard or slow to implement with ASP script code. It also allows you to
partition off business logic into a middle tier rather than creating spaghetti
code at the ASP script level.
COM
is a very powerful extension for ASP, but you should also realize that its use
will make ASP application dramatically more complex from an administrative
point of view. No longer do you deal simply with text documents that you can
upload and replace on a server. No
longer can you easily debug your applications or even simply stop and restart
it. COM objects must be installed and maintained on the server and IIS makes
this process far from trivial when it comes to updating these components in a
live environment. More on this later in the article.
Let's
start with a very simple COM example that you might also find useful. I'm sure
you've seen the counters in use by many Web sites. I'll implement one with
Visual FoxPro by creating a COM object that counts up hits in a database file.
In this example, ASP is used to handle all the HTML and Web-related tasks while
VFP provides the business logic of managing and incrementing the counter.
Here's
the skeleton server code and the implementation of the IncCounter method:
*************************************************************
DEFINE CLASS ASPTools AS Custom OLEPUBLIC
*************************************************************
cAppStartPath = ""
lError = .F.
cErrorMsg = ""
************************************************************************
* WebTools :: Init
*********************************
*** Function: Set the server's environment. VERY IMPORTANT!
************************************************************************
FUNCTION INIT
SET RESOURCE OFF
SET EXCLUSIVE OFF
SET CPDIALOG OFF
SET DELETED ON
SET EXACT OFF
SET SAFETY OFF
SET REPROCESS TO 2 SECONDS
*** Force server into unattended mode – any dialog
*** will cause an error with error message
SYS(2335,0)
*** If you use a SQL backend use this to prevent login dialogs!
* SQLSetProp(0,"DispLogin",3)
*** Utility routines like GetAppStartPath etc.
SET PROCEDURE TO wwUtils ADDITIVE
THIS.cAppStartPath = AddBS(JustPath(Application.ServerName))
*** Important: We need to get at our data
SET PATH TO (THIS.cAppStartpath)
DO PATH WITH "DATA"
ENDFUNC
************************************************************************
* WebTools :: IncCounter
*********************************
*** Function: Increments a counter in the registry.
*** Assume: Key:
*** HKEY_LOCAL_MACHINE\
*** SOFTWARE\West Wind Technologies\Web Connection\Counters
*** Pass: lcCounter - Name of counter to increase
*** lnValue - (optional) Set the value of the counter
*** -1 delete the counter.
*** Return: Increased Counter value - -1 on failure
************************************************************************
FUNCTION IncCounter
LPARAMETER lcCounter, lnSetValue
LOCAL lnValue
THIS.lError = .F.
lnSetValue=IIF(EMPTY(lnSetValue),0,lnSetValue)
IF !USED("WebCounters")
IF !FILE(THIS.cAppStartPath + "WebCounters.dbf")
SELE 0
CREATE table (THIS.cAppStartPath + "WEBCOUNTERS") ;
( NAME C (20),;
VALUE I )
USE
ENDIF
USE (THIS.cAppStartPath + "WEBCOUNTERS") IN 0 ALIAS WebCounters
ENDIF
SELECT WebCounters
LOCATE FOR UPPER(name) = UPPER(lcCounter)
IF !FOUND()
INSERT INTO WEBCounters VALUES (lcCounter,1)
lnValue = 1
ELSE
IF RLOCK()
IF lnSetValue > 0
REPLACE value with lnSetValue
ELSE
IF lnSetValue < 0
REPLACE value with 0
DELETE
ELSE
REPLACE value with value + 1
ENDIF
ENDIF
lnValue = value
UNLOCK
ENDIF
ENDIF
RETURN lnValue
ENDFUNC
************************************************************************
* aspTools :: Error
*********************************
*** Function: Error Method. Capture errors here in a string that
*** you can read from the ASP page to check for errors.
************************************************************************
FUNCTION ERROR
LPARAMETER nError, cMethod, nLine
THIS.lError = .T.
THIS.cErrorMsg=THIS.cErrorMsg + "<BR>Error No: " + STR(nError) + "<BR> Method: " + cMethod + "<BR> LineNo: " +STR(nLine) + "<BR> Message: "+ message() + Message(1) + "<HR>"
ENDFUNC
ENDDEFINE
To create a COM object you can use with ASP from this object:
¨ Make sure that the class you want to use is marked as OLEPUBLIC
¨ Create a project and name it ASPServer
¨ Add the PRG file (or VCX that contains your OLEPUBLIC class) to the project
¨ Create a DLL server from it with BUILD MTDLL (VFP6 SP3 or later)
Once the server is built test it from Visual FoxPro to make sure it works (actually you should first test and debug it before you build the COM object):
OServer = CREATEOBJECT("ASPServer.ASPTools")
? o.IncCounter("HomePage")
? o.IncCounter("HomePage")
? o.IncCounter("ProductPage")
? o.IncCounter("HomePage")
? o.IncCounter("ProductPage")
The meat of this class is the IncCounter method, which has the job of incrementing a counter that is to be used on a Web page. To add a counter to an ASP page named COMCounter.asp, you would do the following:
<% Set loCounter = Server.CreateObject("AspDemo.ASPTools") %>
<HTML>
My new counter value: <%= loCounter.IncCounter("HomePage") %>
</HTML>
You could also keep the declaration and use of the object together in one block with:
<% Set loCounter = Server.CreateObject("AspDemo.ASPTools")
Response.Write( loCounter.IncCounter("HomePage")
%>
When this code runs for the first time, a table named WebCounters is created. Any new counter value that is specified causes a new record to be added to this table. So a record with the ID of Homepage is created and the value is increased by one. On each successive hit, the counter is simply incremented by locating the record pointer on the specific counter record and updating the value. Because it's a VFP table that holds the data the value is automatically synchronized through VFP's record locking mechanism – if two users try to access the page exactly at the same time VFP's record locking and the SET REPROCESS setting cause one of the requests to be forced to wait for the record lock to clear.
The class above is very basic, but
it includes some very important features that you should implement in every
server that you create. First, take a look
at the Init method of the class. It's used for setting the VFP and application
environment. It's crucial at this point that you set all settings that you
expect your server to have. Most importantly, you want to turn off EXCLUSIVE
access on startup or you'll run into problems when multiple instances of your
server try to access data files simultaneously. Remember that ASP is
multi-threaded so multiple requests may be running concurrently accessing the
same data files! This is probably the most common error that hangs servers!
It's a good idea to turn off the resource file for the same reason. Any
environment settings you expect to be in place should also be set at this time.
SET DELETED, SET REPROCESS and SET EXACT are a couple that I always set.
Keep
in mind that a DLL object cannot have any
user interface. This means it's not legal to have a dialog of any kind popping
up. This means file open dialogs, SQL Server login dialogs, code page dialogs,
print dialogs as well as any modal state in your code. Using SYS(2335,0) forces
the server into unattended mode causing a trappable error to occur on any use
of user interface operations (actually, this is not required for DLL servers as
this is the default, but it's a good idea to include anyway in case you build
your object as an EXE server at some time).
When you load a server from an ASP you should also realize that the server loads your object when the page is accessed, then unloads the server when the page is done. This means your object is recreated on every hit to the ASP page. This means that Init() and Destroy() of your server also fire on every hit. Hence, you want to try to minimize the amount of code that runs in these methods to optimize performance. This basically means you should design stateless servers that don't require extensive construction and property settings set via code. Use common default values so you don't have to externally set property values either via Init() code or property settings over the ASP COM interface, which is also comparatively slow.
Your
server should also contain an error handler. This is vital, because any error
that occurs will hang your server to the point that you'll have to kill the
process (if it's an EXE server) or the client process (if it's a DLL server –
in this case IIS) in order to clear the server of the error. By trapping the
error you prevent this tragic end of your server's life in most circumstances.
Using
the Error method code from the above class, you can check for errors in ASP pages
with the following code:
<% IF loServer.lError
Response.Write(loServer.cErrorMsg)
Response.End
<% END IF %>
This is a great debugging tool when your server has problems. You can conditionally include this code after something didn't work and check the error message for the problem in terms of VFP error messages. Keep in mind that the error handler above simply ignores errors that occur – the code continues to run through the rest of the method that is running. Variations on this scheme include using VFP's COMRETURNERROR() to immediately cause an exception in the ASP client code to be displayed as a a standard ASP error, or to use RETURN TO (StartMethod) to shortcut the currently executing code to an entry code handler where the error can be manually handled and presented properly. In either case Error handling is crucial to keep your server from hanging.
This development cycle for developing any programming code should be familiar to you. COM components make this process a lot more complex because COM is a binary standard. It's not possible to debug your VFP COM components while they're running through COM. In order to debug your objects you should follow these steps:
¨ Build your components modular so that they can be tested outside of the COM environment. This includes using smart parameters rather than extensively relying on passing objects that may not exist during the debugging phase.
¨ Test your components inside of VFP. Since COM objects are really VFP class instances use the classes natively from a VFP test program. At this point you have the chance to debug the class methods with help of the VFP environment using the debugger to catch and step through problem code.
¨ Once things run OK, build the COM Server and then re-test the classes using COM. It's still easier to debug a bonked COM object in VFP than it is in ASP, since VFP doesn't lock COM objects into memory.
¨ Now you're ready to try your component under ASP.
¨ If it runs without problems – great! If not, read on to find out how update your COM components on the server.
Unfortunately, the debugging process for ASP components is far from trivial. Remember at the beginning of this paper I mentioned that the ASP development is fairly simple, but as soon as COM is thrown into the mix the complexity goes way up. Here's why!
ASP COM components get loaded into the IIS process space and are locked into memory. This means that even though ASP loads and unloads your component on each page (Init and Destroy fire in your object), IIS permanently locks the DLL into memory. IIS does this as a caching mechanism that improves performance of the object. For VFP/VB this provides great gains because the runtime libraries load only once and stay loaded in the IIS process after that.
The flip side is that once you've loaded your COM object, it will never unload until the Web server is shut down or the ASP application is released. There's no programmatic way to do this, and the process to unload IIS or an IIS Virtual application requires a number of manual steps (which you get used to in a hurry).
If you're using IIS/PWS 5 in Windows 2000 you can use the new IISRESET utility to kill the Web Service and restart it:
IISRESET
IISRESET has a number of options that allow you to stop or start or restart all the configured Web Services which includes if set up WEB, FTP, SMTP and the News Service. In other words all the service depending on the IISAdmin service.
In Windows NT the process requires a few more steps. To shut down IIS in completely in NT you can use a batch file like the following:
KILL INETINFO
pause
NET START IISADMIN
NET START W3SVC
Kill is a utility from the NT resource kit that will kill any process dead in its tracks. It's the quickest way to shut down IIS completely. Note that after killing the process you need to wait about 5-10 seconds or so before you can restart it. This is because the NT Service Manager polls running services and if it hasn't updated the status of the service as Not Running it will not restart it. The Pause in the batch file takes care of this, but you do have to press a key to make it happen (or you can use a third party DOS timed wait program).
I don't recommend you do this with a production server, but it works great for a development box. BTW, if IIS is hung and will not shut down via the service manager this same batch file will shut down and restart your server! It's a handy routine to have available.
You can also take a slightly more official variation for shutdown:
NET STOP W3SVC
NET STOP IISADMIN
NET START IISADMIN
NET START W3SVC
but this will take a while and is not a good idea for development. It also relies on the service manager, so if IIS is hopelessly hung this will not shut it down.
IIS 4.0 also introduced the concept of a virtual application. A virtual application is a virtual directory that runs in its own process space via an Microsoft Transaction Server module. The MTS module hosts the ASP application and all COM objects loaded by this virtual application load into the MTX process, rather than into the IIS process. To set up a virtual directory in this fashion select the Virtual directory in the IIS service manager and check the 'Run in separate memory space (isolated process)' checkbox on the Directory tab for the virtual:

From this point on any request that uses this virtual directory and any directories below it belong to this application. Any ASP page loaded from there run in this 'protected' application in an MTS package (NT) or COM+ Application (Windows 2000) and load their COM objects into the MTS/COM+ process of this application rather than into the IIS process.
The benefit to all of this is:
¨ The virtual application cannot crash IIS because it's running in a separate process
¨ The virtual application can be unloaded separately from IIS
The latter can be accomplished by clicking on the Unload button in the dialog above. You can also unload the application from the MTS/COM+ Management console by selecting the virtual application and using the Unload option from there. The result is that only this application unloads and with it all those COM objects that may have been loaded of ASP pages from this virtual application!
This is fairly nice, but beware of
the following caveats with this approach:
¨
Virtual applications are slower than default
applications because there's overhead in the passthrough from IIS to the MTS/COM+
process.
¨ In online environments the Unload option is of very limited value because Unload does not guarantee that servers stay unloaded. As soon as somebody hits a page with a COM object the object once again becomes locked. On busy sites it would be next to impossible to update a component quickly enough. The only safe way to update components is to shut down the Web server!
As you can see, the only reliable way to update components on IIS is by shutting down the Web server. This is a very serious issue if you think about it. Every time a code change is made you'll have to shut down the server to do this. Now think of the scenario of a big time E-Commerce site that's constantly busy – an update of a server could take a few minutes to get done right. You have to shut down the server (and you would not want to use KILL in this case!), then copy in your new component, re-register it (this may not be necessary, but to be safe you should), then restart the Web service. This is a lot of time and a large number of pissed off customers who get a message that the server is not available for even a few minutes.
Now think of a smaller application that is hosted at an ISP. The ISP has a hundred virtual directories and everytime somebody running a COM object wants to update their application the ISP has to do the job for them! Depending on the application the update may require a server shutdown. You think the ISP will willingly shut down their Web server of 100 or more clients just to update your COM object? Not happily anyway and not more than a few times…
I know of no workaround for this issue, although I've thought about this quite a bit in my own Web Connection framework, where remote code updates can unload objects, replace the server, then automatically restart as part of the Web interface. IIS is in desperate need of a similar mechanism – let's hope Microsoft figures this out at some point (and they have with ASP+ and .Net where objects are cached and can be replaced inline).
COM objects are very susceptible to IIS's security environment. The primary reason for this is that a COM object loaded from an ASP page inherits IIS's security context, which typically is the anonymous Web user accessing the page. The Anonymous Web user account by default is named IUSR_MachineName where machine name is the name of your server (du-uh!). The account name is configurable in the IIS Management Console, but it's recommended that you leave the account set for compatibility reasons (some applications may rely on the IUSR_ account). If you're running in a virtual application that runs in an isolated memory space the user account for this application will be IWAM_ plus some unique ID which you can look up in the security properties of the site. Under Windows 2000 an additional user called IWAM_MachineName also exists for Pooled applications. Pooled application share a single COM+ application that runs out of process of IIS. This provides isolation but better performance than private applications.
What this is means is that your component is called under the IUSR_ or IWAM_ security context rather than under the Interactive User account which is what you use when you're logged on to the NT box (which in turn maps to your actual user account). IUSR_ is a very low security account that has next to no rights on the machine. All file access rights it possesses must be manually assigned! IIS does this automatically for any Web enabled directories, but for any other directories that may contain data or other support files you're responsible for configuring the file access rights!
In order to create a COM object and access data the IUSR_ or Everyone account must have at least the following rights:
¨ Read and Execute rights on the actual DLL server that is to be launched
¨ Full rights on any VFP data files to be accessed
You'll probably want to set these rights at the directory level. Notice that this is a potentially serious security risk as this gains the anonymous user access to your server's hard disk, given that the user knows where to look for the data. Read access is all that's needed to high-jack a file from the server! Be very careful with security!!!
To
demonstrate how the security works, add the following method to your server:
************************************************************************
* ASPTools :: GetUsername
*********************************
*** Function: returns the currently active username
************************************************************************
Function GetUserName
DECLARE Integer GetUserName ;
IN WIN32API AS GUserName ;
STRING @nBuffer, ;
INTEGER @nBufferSize
lcUserName = SPACE(255)
lnLength = LEN(lcUserName)
lnError = GUserName(@lcUserName,@lnLength)
RETURN SUBSTR(lcUserName, 1, lnLength - 1)
Then
create an ASP page that creates the server and accesses this method:
<% SET oServer = Server.CreateObject("ASPTools.ASPDemo") %>
<b>Current User name: </b> <%= oServer.GetUserName() %>
You'll
find that the username displays "IUSR_MachineName" (or
possibly "WAM_USR_APP1" or
something like that if you created a "separate application").
Now
try the following (this will work only if you're running NT with NTFS—make sure
you note the permissions on the file before changing them): Open Windows
Explorer, select the COMCounter.asp file and change the permissions on it, so
that only your user account has access to it. In my case I'll remove IUSR_,
Everyone and also Administrator (because my development IUSR_ account has admin
rights). So now only my current user account has access to this file. Rerun the
request.
Now when you try to access the page you'll be presented with a password dialog box asking for username and password. Type in your account info and look at the username returned by the ASP page: It's the username you logged in as in the password dialog—in my case rstrahl. Once you've tested this, make sure you reset the permission for IUSR_ and Everyone (if it was there before).
This
demonstrates how IIS passes down security to your COM object. The good news is
that the security of the ASP page determines how your server is accessed. The
bad news is that the security of the ASP page determines how your server is
accessed! Here's why it's a problem: Normally the IUSR_ account is meant to be
a low-impact, user account that has practically no rights other than to read
and execute Web pages and scripts on your site.
When your COM object gets called
from the Web, it usually will load under the IUSR_MachineName account, which by
default will not have rights to read and write data in your application path!
No files outside of the Web space can be accessed unless permissions are
changed!
Read
the previous sentence again, because it's very important! So how to get around
this nasty problem? There are several ways:
¨
Give directory access to the IUSR_ account.
Assign full access rights to the
IUSR_ account for every directory in which you'd expect to write data. This is
tedious and prone to many errors because the directory permissions must be
configured on every machine that you move files to. You also need to remember
that this may include the system TEMP path and the SYSTEM path in order to make
Windows API calls! It also opens up your system to serious security issues.
This option is not recommended.
¨
Give the IUSR_ account additional rights.
You can add the IUSR_ account to
the Administrator group, and all your COM objects will magically have access to
the system! Sure, but so will anybody else who accesses your system over the
Web. If they can find a way into a directory (via Web Mapping, or whatever) the
system is open to them. This may be a bad idea for a production system, but
it's probably the best way to work on your development system—just be sure to
test with the Admin rights off before you take your application online!
¨
Use Microsoft Transaction Server/COM+ for your
COM object.
MTS/COM+ allows you to configure a security role for your COM component, and by
running through MTS you're allowing MTS to manage the security context for you.
By doing so, you're changing the security only for the component—not the entire
Web site. This is a decent way to implement security, but it complicates both
installation and development of applications. Use this only as a deployment
solution.
¨
Impersonate a user account from within your COM
object.
NT supports a concept called user
impersonation, which allows you to temporarily change the user context to
another user and then reset it to the original. In fact, this is how IIS itself
handles user contexts such as IUSR_ that it passes to you. Setting user
accounts is complex and requires usernames and passwords, which is unsuitable
for generic applications. However, knowing how IIS creates user accounts can
help here: IIS is a service so it runs under the SYSTEM account, but it impersonates
the Web user to become whatever user is logged in. An API function called RevertToSelf()strips off all impersonations. When you do this to a Web request in your
COM object, the server reverts to the SYSTEM account, which typically does have
rights in most places on your system unless it was explicitly disabled.
The
most consistent mechanism is the last item above, so here's how to implement
it. Add the following to the top of the GetUserName method presented above:
Function GetUserName
LPARAMETER llForceToSystem
IF llForceToSystem AND "NT" $ OS()
DECLARE INTEGER RevertToSelf ;
IN WIN32API
RevertToSelf()
ENDIF
and
add the following to the COMCounter.asp page:
<b>Current User name: </b> <%= oServer.GetUserName() %>
<b>Current User name: </b> <%= oServer.GetUserName(True) %><br>
<b>Current User name: </b> <%= oServer.GetUserName() %>
When
you run this, I get:
Current User name: IUSR_RAS_NOTE
Current User name: SYSTEM
Current User name: SYSTEM
which
demonstrates that an anonymous user comes in as IUSR_RAS_NOTE, then the IIS
Impersonation is removed with RevertToSelf() and switches to SYSTEM. (Note: If
you're logged into the Web server somehow, like through VID debug mode, your user account might
show up instead of SYSTEM). The last call is in there to demonstrate that once
you've changed the user context it doesn't change back for each method call but
is scoped to the current page. There is no easy way to reset it back to IUSR_
unless you know the password. Generally this should not be a problem.
What
does this mean? Since IUSR_ is not supposed to have any rights, you can run
into problems with accessing data in paths where IUSR_ doesn't have rights.
When running your COM components, you can use RevertToSelf() to essentially
grant the user temporary rights while you're executing the current ASP page. So
once you've reverted to the SYSTEM or specific user account, the user has
rights to access the system as needed, be it for data or the registry or a
network resource. Once the page completes, the lax security is released and
reverts back to IUSR_ on the next access to the Web server.
Keep
in mind that the SYSTEM account is a local account and it has no network
rights. It most likely will not have access to remote machine drives across the
network. If that's required, you might have to set up a specific account to
impersonate and use it for remote access, or else use the Transaction Server
mechanism mentioned in the bullet list above.
I've detoured a little here digging into ASP infrastructure that explains how things go wrong. The counter example above was a very simple example how you can use a COM component that provides specific data like a business object to an ASP page. You directly interface with the object from the ASP page and let the component do it's piece of work – in this case manage and increment the counter value.
There are three general ways that you can take advantage of objects in an ASP page:
¨
Using a COM
object as a business object
In this scenario you simply use the business logic in the COM object by
accessing properties and methods of the component. This is the most common use
of COM objects in general.
¨
Using a COM
object as an HTML generator
You can also use COM as a Web interface to Visual FoxPro with ASP. Rather
than having most of the code in the ASP and having the ASP code driving the COM
object, the ASP page is requesting the Fox code to perform some operation that
results in HTML output. For example it's possible to create an ASP page that
does only this:
<%
SET oServer = Server.CreateObject("ASPDemo.ASPTools")
Response.Write(
oServer.CreateHTML() )
%>
This would be all that's required from the ASP page if the
CreateHTML method returns a full HTML document. The same approach can be used
for creating portions of HTML pages, like data-driven tables or things like Fox
forms rendered as DHTML.
This can be very useful for taking advantage of VFP's strengths and superior
performance. Generating HTML in VFP can be drastically more efficient than
using ASP script code as well as providing features that would be impossible to
achieve in ASP altogether.
¨
Using the COM
object as an object factory
It's also possible to use a COM object to return another COM object, even one
that's not explicitly defined. In its simplest form you can do things like
retrieve a record in VFP then return that record to ASP as an object. You can
also create a business object that accesses data and contains other logic and
pass that back to ASP, allowing the ASP page to drive that business object that
was created on the fly in FoxPro code.
We've already seen an example of
the first scenario. Let's take a quick look at the HTML generating scenario. Physically this approach is no different from
using method calls and properties directly, but conceptually it's changing the
role of the VFP server into an HTML-generation tool in addition to processing
the business logic.

I'll
add a few more methods to the ASPTOOLS server now. The following code queries
some data from the TasTrade customer and invoice tables provided as sample data
with Visual FoxPro. Here's what the code looks like:
************************************************************************
* AspTools :: CustList
*********************************
*** Function: Retrieves customer list and creates an HTML Table from it
************************************************************************
FUNCTION Custlist
*** VFP Sample Data Path
lcDataPath = home(2)+"data\"
lcWhere = ""
IF !EMPTY(THIS.cCompany)
lcWhere = " AND Company = THIS.cCompany "
ENDIF
IF LEN(THIS.cInvNo) > 0
lcWhere = lcWhere + " AND orders.order_id = PADL('" +THIS.cInvNo + "',6) "
ENDIF
SELECT customer.company, customer.cust_id, orders.order_id,orders.order_date, Order_amt ;
FROM (lcDataPath + "Orders"), (lcDataPath + "Customer") ;
WHERE customer.cust_id = orders.cust_id &lcWhere ;
ORDER BY company,order_date DESC ;
INTO CURSOR TQuery
lcLastCustId = " "
lcOutput = ""
SCAN
lcOutput = lcOutput + ;
[<table border="0" width="570" class="bodytext">]
IF lcLastCustId <> Tquery.Cust_id
lcOutput = lcOutput + ;
[<tr><td bgcolor="#000000" colspan="3" width="564"><font face="Arial" color="#FFFFFF"><strong>] + Trim(Tquery.Company) +[</strong></font></td></tr>]+CHR(13)+CHR(10)
ENDIF
lcLastCustId = Tquery.Cust_Id
lcOutput = lcOutput + [<tr>]+;
[<td align="center" width="179">] + Transform(TQuery.Order_date) + [</td>] + ;
[<td align="center" width="198"><a href="Invoice.asp?Orderid=]+TQuery.Order_id+[">]+Tquery.Order_id+[</a> </td>]+;
[<td align="Right" width="175">]+ Transform(order_amt) + [</td>]+;
[</tr></table>] + CHR(13)+CHR(10)
ENDSCAN
USE IN TQuery
RETURN lcOutput
This
code looks at several input properties to determine the values of the submitted
values from the ASP page, which looks like this (truncated for size):
<%
Set oServer = Server.CreateObject("aspdemos.asptools")
lcCompany = Request("txtCompany")
lcInvNo = Request("txtInvoiceNo")
lcFromdate= Request("txtFromDate")
lcToDate = Request("txtToDate")
llFirstHit = False
IF LEN(lcToDate)=0 and LEN(lcFromDate)=0 then
llFirstHit = True
lcFromDate = "01/01/90"
lcToDate = FormatDateTime(now,vbShortDate)
END If
%>
<form action="COMinvlookup.asp" method="POST">
<table border="1" cellPadding="1" cellSpacing="1" width="75%" class="bodytext">
<tr>
<td>Invoice Number: </td>
<td><input id="txtInvoiceNo" name="txtInvoiceNo" size="20" value="<%= lcInvNo %>"></td>
</tr>
<tr>
<td>Company: </td>
<td><input id="txtCompany" name="txtCompany" size="20" value="<%= lcCompany %>"></td>
</tr>
<tr>
<td>Date:</td>
<td><input id="txtFromDate" name="txtFromDate" size="8" value="<%= lcFromDate %>"> to <input id="txtCompany" name="txtToDate" size="8" value="<%= lcToDate %>"></td>
</tr>
<tr>
<td> </td>
<td><input type="submit" value="Show List" name="btnSubmit"></td>
</tr>
</table>
</form>
<% oServer.cCompany = lcCompany
oServer.cInvNo = lcInvNo
oServer.CFROMDATE = lcFromDate
oServer.CTODATE = lcToDate
%>
<%= oServer.CustList() %>
The
form acts as both an input and output form. The difference here is that Visual
FoxPro is used to do all data access, as well as providing the majority of the
HTML generation for the table list below. The key code is in the last five
lines of the ASP page, which populates the query properties and then calls the
Custlist() method to render the HTML, which is returned as a string.
You
might be telling yourself, "This is some ugly code," since it's
generating HTML manually via code. It might not be very easy to type this
stuff, but it's actually very fast and efficient code compared to scripted ASP
code. For one thing there's no COM access involved here for each field access –
a lot of overhead is involved in making COM calls for every data access of ADO
recordsets. For large tables you may see a 10 times or better performance
increase.
In
addition, once you start using VFP for HTML generation you'll quickly take to
creating some base classes that can create HTML automatically for many tasks. I
don't want to get into this too much further here—but it's relatively trivial
to generate generic HTML from a Fox table with 30 or so lines of code:
************************************************************************
* ASPTools :: ShowCursor
*********************************
*** Function: Renders the current select cursor/table as HTML.
*** Pass: llNoOutput - If .T. returns a string. Otherwise
*** sends directly to output.
*** Return: "" or output if llNooutput = .t.
************************************************************************
FUNCTION ShowCursor
LPARAMETER llNoOutput
LOCAL lcOutput, lnFields, lcFieldname,x
lcOutput = ;
[<TABLE BGCOLOR="#EEEEEE" Width="98%" ALIGN="CENTER" Border=1>]+CR
IF EMPTY(ALIAS())
RETURN ""
ENDIF
lnFields = AFIELDS(laFields)
*** Build the header first
lcOutput = lcOutput + "<tr>"
FOR x=1 to lnFields
lcfieldname=Proper(lafields[x,1])
lcOutput = lcOutput + "<th BGCOLOR=#FFFFCC>"+lcFieldName+"</th>"
ENDFOR
lcOutput = lcOutput + "</td></tr>"
SCAN
lcOutput = lcOutput + "<TR>"
*** Just loop through fields and display
FOR x=1 to lnFields
lcfieldtype=lafields[x,2]
lcfieldname=lafields[x,1]
lvValue=EVAL(lcfieldname)
DO CASE
CASE lcFieldType = "M"
lcOutput = lcOutput + "<TD>" + STRTRAN(lvValue,CHR(13),"<BR>") + "</TD>"
OTHERWISE
lcOutput = lcOutput + "<TD>" + TRANSFORM(lvValue) + "</TD>"
ENDCASE
ENDFOR && x=1 to lnFieldCount
lcOutput = lcOutput + "</TR>" + CR
ENDSCAN
RETURN lcOutput
ENDFUNC
The order list in the example above can get very long—the VFP demo data contains more than 1000 records. One problem that you run into is how to display all that data at once. Surprisingly, getting the data and generating HTML is relatively fast—what's really slow is the table rendering inside the browser. HTML tables in IE don't display until all data for that table has been retrieved, including the final </table> tag. This means the entire set of data needs to down