Friday, January 16, 2004

Accessing Network Files And Resources From ASP.NET

Accessing network resources through ASP/ASP.NET has never been a terribly simple task, not if you want to maintain a high level of security for your server.

Despite not being as simple as I'd like, it is not difficult to do. Microsoft's recommendation comes in the following steps:

  1. Create an assembly that works with the network resources.
  2. Add the assembly to Component Services (COM+).
  3. Set the user the COM+ object runs under to one with rights to the network resources.
  4. Add a reference to your COM+ assembly to the web project and access the network resources using it.


I'm going to show you step by step how to do these things. It really isn't difficult, but breaking up your network resources library into a seperate project does fragment things a bit.

1. Create an assembly that works with the network resources.

This is pretty simple. Here's some example code:

Imports System

Imports System.Reflection
Imports System.EnterpriseServices

<Assembly: AssemblyKeyFileAttribute("example.snk")>
<Assembly: ApplicationActivation(ActivationOption.Server)>
<Assembly: ApplicationName("NetworkResourcesExample")>
Public Class AccessNetworkResources
Inherits ServicedComponent

Public Function ExampleFunction() As String

' Put your code that access the network resources here.

End Function

End Class


---
An assembly that will go into Component Services is not much different than any other assembly. All you have to do is have your class inherit from ServicedComponent and add a few properties. The AssemblyKeyFileAttribute property gives the name/path of the key pair file that will be used to strong name the assembly. To create a new key pair file, use the sn.exe utility like so:
sn -k example.snk

---
The ApplicationActivation property tells COM+ to run the application as a Server (as opposed to a Library). You need to do this so that you can assign a different user for the assembly to run under other than the caller. The third property, ApplicationName, tells COM+ the name to list your assembly under in the component services manager (also, if you use any legacy code to access the object, such as classic ASP, you can call the object using CreateObject("ApplicationName.ClassName")).


2. Add the assembly to Component Services (COM+).
This is a very simple step. Once you've compiled the assembly, go the command line and change into the directory where the compiled assembly is located. Then execute the following:
regsvcs [assemblyname]

---
This will install the assembly as a COM+ component.


3. Set the user the COM+ object runs under to one with rights to the network resources.
Now you need to set the COM+ object to run under a user who has rights to the network resources that you need to access. To do this go to: Control Panel -> Administrative Tools -> Component Services. Drill into the Component Services node and into the computer the component is installed on. In the COM+ Applications folder you will find your newly installed component under the name you gave it in the ApplicationName property mentioned earlier.

Right-click on the application and go to properties. In the properties dialog go to the Identity tab. Select either the "Network Service" account under "System Account" or select "This User" and enter the username and password that has access to the resources. I would recommend creating a seperate account that only has access to the specific resources needed, just for safety. Click OK.


4. Add a reference to your COM+ assembly to the web project and access the network resources using it.
This step is pretty self explanatory, but I do have one comment about it. When you add the reference, you don't need to go to the COM tab of the references dialog and select your object from there (in fact, you can't do that, it wont work). You just browse to the compiled assembly you created for the library and select it directly. The .NET framework recognizes that the assembly was installed under COM+ and will activate it using the proper credentials.


That's it! You should be securely accessing the network resources from your web project now.

Thursday, January 15, 2004

Retreiving Performance Counter Values With VB.Net

From Windows 98 on Microsoft has included performance counters in their O/S that measure all kinds of things going on in the system: memory usage, disk access, network bandwidth use, etc. Your software can access these values through the performance counters for reporting, notification of system problems, etc. I've recently finished an application which sits on a server and regularly pulls performance counter values from all of the computers on the network, alerting the network administrator if any of the values rise above of fall below values set as the acceptable range.

It's very easy to access performance counters in Visual Basic .NET. Here's how you do it:


Imports System.Diagnostics

Public Class CounterTest

Public CounterTestValue As Single

Public Sub New()

Dim counter As New PerformanceCounter("Processor", _
"% Processor Time", _
"_Total")

CounterTestValue = counter.NextValue

End Sub

End Class


---

Here's the syntax of the PerformanceCounter constructor:

New PerformanceCounter(CategoryName, CounterName, InstanceName)


---

The category name is which group of counters the counter you want belongs to (such as Processor or Memory or Thread), the counter name is the text name of a counter that is in that category. Those two things are pretty obvious.

The InstanceName is not always so obvious, because it's not always required. A counter's instances break down the counter into all of its smaller parts. For example, the Process category has a counter called "% Processor Time" that has instances for each application currently running, as well as one called "_Total" (a common instance name) which gives you the processor time of all processes. Not all counters have an instance. A few do. If a counter does not have an instance you can pass an empty string, or better yet, just use the constructor overload that does not require the instance name.

The PerformanceCounter class also has a constructor overload which takes the network id of the machine you want to get the counter from. So, for example, if I wanted to get the processor usage on a machine on the LAN called "Loris", I would change the above class like so:


Dim counter As New PerformanceCounter("Processor",
"% Processor Time",
"_Total",
"Loris")


---

It's that simple.

NOTE: Be careful when pulling performance counters from machines on the LAN running different versions of Windows. Microsoft introduced new performance counters with each new Windows version, so make sure that the counter you are able to pull from your Windows XP machine exists on that Windows 2000 box before you try it. Also, the user under which your program is running must have rights to access performance counters on other machines. It is usually best to run the program under a Domain Administrator user, as this guarantees its ability to read the counters.

To find out what performance counters are available to you, go to Control Panel -> Administrative Tools -> Performance. Right-click in the graph and select "Add Counters". There you can see a list of all of the categories and their specified counters and the instances of those counters.

TIP: Many of Microsoft's Software Packages create their own performance counters when installed. A big one is MS Sql Server, which creates dozens of counters for database administrators to use in monitoring the performance of their databases. Your software can access these as well. In addition, .NET makes it very easy to create your own performance counters. How to create your own counter will be the subject of a future blog.

Wednesday, January 14, 2004

Code Generation

In a recent project I decided to explore the idea of code generation, even writing a tool that automatically generated the classes for all the database access to a SQL Server database.

My tool was pretty specific to my needs, and in a later project I found that it just wasn't flexible enough. In a link sent to me I happened to find a great freeware code generation tool created by Eric J. Smith called CodeSmith. It lets you write your own templates for code generation based on tables from any database (OLE DB or ODBC, including Access and SQL Server). I used it to generate dozens of database access classes for one of my team's projects. Saved literally hundreds of hours of coding.

Check it out and let me know what you think.

CodeSmith
http://www.ericjsmith.net/codesmith/

Tuesday, January 13, 2004

Disable Printing Of A Web Page

And still another trick I have learned while working on my current ASP.NET project is how to prevent a user from printing the web page that they are viewing. This is a two step process. The first step is very simple. Add this tag in the HEAD section of your page:

<style media="print">
BODY { DISPLAY: none }
</style>


---

What this stylesheet entry does is tell the browser to replace everything in between the BODY tags of the page with nothing when the user tries to print the page. All the user will get is a blank page.

However, nothing prevents a user from saving the page to his hard drive and editing the style tag out of the page. So much for that security. JavaScript to the rescue! Add this code to HEAD of the page:


// Prevent selecting text for copying.
function deselect(){
if (document.selection) {
document.selection.empty();
Copied=document.body.createTextRange();
}
else if (window.getSelection){window.getSelection().removeAllRanges();}}

timerID=setTimeout('setInterval("deselect()", 1)',1);

// Capture right-click and prevent it to prevent copying text or viewing source.
function clickIE4(){
if (event.button==2){
return false;
}
}

function clickNS4(e){
if (document.layers||document.getElementById&&!document.all){
if (e.which==2||e.which==3){
return false;
}
}
}

if (document.layers){
document.captureEvents(Event.MOUSEDOWN);
document.onmousedown=clickNS4;
}
else if (document.all){
document.onmousedown=clickIE4;
}

document.oncontextmenu=new Function("return false")



---

This code prevents right-clicking and prevents selecting of text in Netscape (and IE). To make absolutely sure that nobody can copy text from the page in IE, add this code to the BODY tag:

<BODY onselectstart="return false;" ondragstart="return false;">


---

That will prevent any selecting of text at all (but it is not supported in Netscape, so you'll need the previous code for that).

Of course, this still does not prevent the user from doing a File->Save on your page. To prevent that, just use javascript to open the page that should not be printed in a new window without the toolbars. This also has the added security of preventing the user from disabling javascript as a trick to get the page contents. If javascript is disabled in the browser, the page will not come up since javascript is used to launch the window.

The final possibility is that the user could enable javascript, open the window, disable javascript while the window is open and then hit F5 to refresh the opened page. With the javascript disabled, the user would be able to copy the page contents.

Now, you can trap the refresh key with javascript, but you cannot prevent refreshing the page. All you can do is preform some task before the page is refreshed. In order to be totally secure, you can have the window close if the user tries to refresh the page. Here's the code (goes in the HEAD tag):


function preventRefresh(e)
{
// Netscape uses e, IE window.event
var eventObj = (e)?e:window.event;

if(eventObj.which)
{
//NS4+
if(e.which==116)
{
alert('No refreshing of this page is allowed.' +
'This window will now close.');
window.close();
}
} else {
//IE
if(window.event.keyCode==116)
{
alert('No refreshing of this page is allowed.' +
'This window will now close.');
window.close();
}
}
}
if(document.layers)
{
//NS4+
document.captureEvents(Event.ONKEYDOWN);
}
document.onkeydown=preventRefresh;



---

Voila! An unprintable, uncopyable secured web page.

Monday, January 12, 2004

Creating PDF Reports For ASP.NET with Active PDF

Yet another winning product I discovered while working on my current ASP.NET project is Active PDF (www.activepdf.com), an incredible toolset for creating PDF reports from .NET. The tool I'm using from Active PDF is Active PDF Server.

Active PDF Server sets up a printer which can be selected and print to from your application. You print to the PDF printer, and the object returns the path to where the PDF was created. Very spiffy.

It took a little bit of tinkering and a few registry entries to get everything working right (since I have it installed on Windows 2003 running in terminal server mode--Active PDF requires some registry entries to run it in terminal server mode). But once it was all setup and installed--WOOHOO! I was very impressed.

Great compression, support for linearization (where the PDF is downloaded in pieces so that the web user can be reading the first page or two while the rest of the file downloads in the background), security settings (disable printing--which I needed--disable copy/paste, password protection, etc.), very fast PDF creation.

If you need a solid PDF solution, check 'em out.

NOTE: I tried using a companion product, Active PDF DocConverter, but I had little success. Although it purports to convert MS Word files and Rich Text Format files, I could never get either to convert with any consistancy. I must say, though, that after talking (well, typing) to their excellent tech support guys, that it appears to be a Windows 2003 permissions issue. I have had more Win2003 permissions issue than you can shake a stick at... At any rate, I gave up on the DocConverter and just wrote my own custom reports in .NET (I was originally creating HTML templates and trying to convert them to PDF using the DocConverter). One more thing: When I was able to get DocConverter to convert MS Word, RTF and HTML files, the quality of the output PDF was not good. Most of the lines and tables were misformatted, fonts were wrong and missized. It just wasn't a good solution.

Sunday, January 11, 2004

JavaScript Hotkeys

I am currently working on an ASP.NET project that required the use of hot keys. Using a JavaScript include on each page I was able to implement the hotkeys. A sample of the code follows:

As you will see by looking through the code, the KeyDown event is captured and various ALT-Key combinations are trapped which redirect the user to different pages of the site. On the page I have the first letter of the hotkey underlined in the menu so that users know which ALT-Key is the hotkey.

In addition to global hotkeys, certain pages of the site also implement their own set of additional hotkeys, and the script checks to see if the page that the user is on has its own set of hotkeys, and if so it calls the function for handling those hot keys (those functions are not shown, but are very similiar to the one shown).

The Code:


function searchHotkeys(e)
{
try
{
// Netscape uses e.which, IE event.keyCode
var eventObj = (e)?e:window.event;
var keyPressed = (e)?e.which:window.event.keyCode;
var altPressed = (e)?(e.altKey):window.event.altKey;

if (altPressed)
{
switch(keyPressed)
{
case 67: // C - Civil
{
if(document.getElementById("SiteMenu_searchCivil"))
{
document.location = "searchCivil.aspx";
}
return;
}
case 82: // R - Criminal.
{
if(document.getElementById("SiteMenu_searchCriminal"))
{
document.location = "searchCriminal.aspx";
}
return;
}
case 77: // M - Marriage.
{
if(document.getElementById("SiteMenu_searchMarriage"))
{
document.location = "searchMarriage.aspx";
}
return;
}
case 80: // P - Property.
{
if(document.getElementById("SiteMenu_searchRecords"))
{
document.location = "searchRecords.aspx";
}
return;
}
}
}

// If we're on a civil suit, check those hot keys.
if (document.location.href.indexOf("Civil") > - 1)
{
return civilHotkeys(altPressed, keyPressed);
}

// If we're on a criminal suit, check those hot keys.
if (document.location.href.indexOf("Criminal") > - 1)
{
return criminalHotKeys(altPressed, keyPressed);
}

// If we're on a marriage license listing, check those hot keys.
if (document.location.href.indexOf("Marriage") > - 1)
{
return marriageHotKeys(altPressed, keyPressed);
}

// Check records hot keys.
if (document.location.href.indexOf("Records") > -1 |
document.location.href.indexOf("Mortgage") > -1 |
document.location.href.indexOf("Chattel") > -1)
{
return recordsHotKeys(altPressed, keyPressed);
}
}
catch(er)
{
// Trap all errors in case of browser javascript
// incompatibilities. We don't want ugly error
// messages popping up everywhere.
//alert(er);
}
}

// Trap key down for hot-key functionality.
if (document.layers)
{
//Needed for Netscape.
document.captureEvents(Events.ONKEYDOWN);
}
document.onkeydown=searchHotkeys;