FileSecurity.cs source code in C# .NET

Source code for the .NET framework in C#

                        

Code:

/ 4.0 / 4.0 / DEVDIV_TFS / Dev10 / Releases / RTMRel / ndp / clr / src / BCL / System / Security / AccessControl / FileSecurity.cs / 1305376 / FileSecurity.cs

                            // ==++== 
//
//   Copyright (c) Microsoft Corporation.  All rights reserved.
//
// ==--== 
/*============================================================
** 
** Class:  FileSecurity 
**
** 
** Purpose: Managed ACL wrapper for files & directories.
**
**
===========================================================*/ 

using System; 
using System.Collections; 
using System.Security.AccessControl;
using System.Security.Permissions; 
using System.Security.Principal;
using Microsoft.Win32;
using Microsoft.Win32.SafeHandles;
using System.Runtime.InteropServices; 
using System.IO;
using System.Runtime.Versioning; 
using System.Diagnostics.Contracts; 

namespace System.Security.AccessControl 
{
    // Constants from from winnt.h - search for FILE_WRITE_DATA, etc.
    [Flags]
    public enum FileSystemRights 
    {
        // No None field - An ACE with the value 0 cannot grant nor deny. 
        ReadData                     = 0x000001, 
        ListDirectory                = ReadData,     // For directories
        WriteData                    = 0x000002, 
        CreateFiles                  = WriteData,    // For directories
        AppendData                   = 0x000004,
        CreateDirectories            = AppendData,   // For directories
        ReadExtendedAttributes       = 0x000008, 
        WriteExtendedAttributes      = 0x000010,
        ExecuteFile                  = 0x000020,     // For files 
        Traverse                     = ExecuteFile,  // For directories 
        // DeleteSubdirectoriesAndFiles only makes sense on directories, but
        // the shell explicitly sets it for files in its UI.  So we'll include 
        // it in FullControl.
        DeleteSubdirectoriesAndFiles = 0x000040,
        ReadAttributes               = 0x000080,
        WriteAttributes              = 0x000100, 
        Delete                       = 0x010000,
        ReadPermissions              = 0x020000, 
        ChangePermissions            = 0x040000, 
        TakeOwnership                = 0x080000,
        // From the Core File Services team, CreateFile always requires 
        // SYNCHRONIZE access.  Very tricksy, CreateFile is.
        Synchronize                  = 0x100000,  // Can we wait on the handle?
        FullControl                  = 0x1F01FF,
 
        // These map to what Explorer sets, and are what most users want.
        // However, an ACL editor will also want to set the Synchronize 
        // bit when allowing access, and exclude the synchronize bit when 
        // denying access.
        Read = ReadData | ReadExtendedAttributes | ReadAttributes | ReadPermissions, 
        ReadAndExecute = Read | ExecuteFile,
        Write = WriteData | AppendData | WriteExtendedAttributes | WriteAttributes,
        Modify = ReadAndExecute | Write | Delete,
    } 

 
    public sealed class FileSystemAccessRule : AccessRule 
    {
        #region Constructors 

        //
        // Constructor for creating access rules for file objects
        // 

        public FileSystemAccessRule( 
            IdentityReference identity, 
            FileSystemRights fileSystemRights,
            AccessControlType type ) 
            : this(
                identity,
                AccessMaskFromRights( fileSystemRights, type ),
                false, 
                InheritanceFlags.None,
                PropagationFlags.None, 
                type ) 
        {
        } 

        public FileSystemAccessRule(
            String identity,
            FileSystemRights fileSystemRights, 
            AccessControlType type )
            : this( 
                new NTAccount(identity), 
                AccessMaskFromRights( fileSystemRights, type ),
                false, 
                InheritanceFlags.None,
                PropagationFlags.None,
                type )
        { 
        }
 
        // 
        // Constructor for creating access rules for folder objects
        // 

        public FileSystemAccessRule(
            IdentityReference identity,
            FileSystemRights fileSystemRights, 
            InheritanceFlags inheritanceFlags,
            PropagationFlags propagationFlags, 
            AccessControlType type ) 
            : this(
                identity, 
                AccessMaskFromRights( fileSystemRights, type ),
                false,
                inheritanceFlags,
                propagationFlags, 
                type )
        { 
        } 

        public FileSystemAccessRule( 
            String identity,
            FileSystemRights fileSystemRights,
            InheritanceFlags inheritanceFlags,
            PropagationFlags propagationFlags, 
            AccessControlType type )
            : this( 
                new NTAccount(identity), 
                AccessMaskFromRights( fileSystemRights, type ),
                false, 
                inheritanceFlags,
                propagationFlags,
                type )
        { 
        }
 
        // 
        // Internal constructor to be called by public constructors
        // and the access rule factory methods of {File|Folder}Security 
        //

        internal FileSystemAccessRule(
            IdentityReference identity, 
            int accessMask,
            bool isInherited, 
            InheritanceFlags inheritanceFlags, 
            PropagationFlags propagationFlags,
            AccessControlType type ) 
            : base(
                identity,
                accessMask,
                isInherited, 
                inheritanceFlags,
                propagationFlags, 
                type ) 
        {
        } 

        #endregion

        #region Public properties 

        public FileSystemRights FileSystemRights 
        { 
            get { return RightsFromAccessMask( base.AccessMask ); }
        } 

        #endregion

        #region Access mask to rights translation 

        // ACL's on files have a SYNCHRONIZE bit, and CreateFile ALWAYS 
        // asks for it.  So for allows, let's always include this bit, 
        // and for denies, let's never include this bit unless we're denying
        // full control.  This is the right thing for users, even if it does 
        // make the model look asymmetrical from a purist point of view.
        internal static int AccessMaskFromRights( FileSystemRights fileSystemRights, AccessControlType controlType )
        {
            if (fileSystemRights < (FileSystemRights) 0 || fileSystemRights > FileSystemRights.FullControl) 
                throw new ArgumentOutOfRangeException("fileSystemRights", Environment.GetResourceString("Argument_InvalidEnumValue", fileSystemRights, "FileSystemRights"));
            Contract.EndContractBlock(); 
 
            if (controlType == AccessControlType.Allow) {
                fileSystemRights |= FileSystemRights.Synchronize; 
            }
            else if (controlType == AccessControlType.Deny) {
                if (fileSystemRights != FileSystemRights.FullControl &&
                    fileSystemRights != (FileSystemRights.FullControl & ~FileSystemRights.DeleteSubdirectoriesAndFiles)) 
                    fileSystemRights &= ~FileSystemRights.Synchronize;
            } 
 
            return ( int )fileSystemRights;
        } 

        internal static FileSystemRights RightsFromAccessMask( int accessMask )
        {
            return ( FileSystemRights )accessMask; 
        }
 
        #endregion 
    }
 

    public sealed class FileSystemAuditRule : AuditRule
    {
        #region Constructors 

        public FileSystemAuditRule( 
            IdentityReference identity, 
            FileSystemRights fileSystemRights,
            AuditFlags flags ) 
            : this(
                identity,
                fileSystemRights,
                InheritanceFlags.None, 
                PropagationFlags.None,
                flags ) 
        { 
        }
 
        public FileSystemAuditRule(
            IdentityReference identity,
            FileSystemRights fileSystemRights,
            InheritanceFlags inheritanceFlags, 
            PropagationFlags propagationFlags,
            AuditFlags flags ) 
            : this( 
                identity,
                AccessMaskFromRights( fileSystemRights ), 
                false,
                inheritanceFlags,
                propagationFlags,
                flags ) 
        {
        } 
 
        public FileSystemAuditRule(
            String identity, 
            FileSystemRights fileSystemRights,
            AuditFlags flags )
            : this(
                new NTAccount(identity), 
                fileSystemRights,
                InheritanceFlags.None, 
                PropagationFlags.None, 
                flags )
        { 
        }

        public FileSystemAuditRule(
            String identity, 
            FileSystemRights fileSystemRights,
            InheritanceFlags inheritanceFlags, 
            PropagationFlags propagationFlags, 
            AuditFlags flags )
            : this( 
                new NTAccount(identity),
                AccessMaskFromRights( fileSystemRights ),
                false,
                inheritanceFlags, 
                propagationFlags,
                flags ) 
        { 
        }
 
        internal FileSystemAuditRule(
            IdentityReference identity,
            int accessMask,
            bool isInherited, 
            InheritanceFlags inheritanceFlags,
            PropagationFlags propagationFlags, 
            AuditFlags flags ) 
            : base(
                identity, 
                accessMask,
                isInherited,
                inheritanceFlags,
                propagationFlags, 
                flags )
        { 
        } 

        #endregion 

        #region Private methods

        private static int AccessMaskFromRights( FileSystemRights fileSystemRights ) 
        {
            if (fileSystemRights < (FileSystemRights) 0 || fileSystemRights > FileSystemRights.FullControl) 
                throw new ArgumentOutOfRangeException("fileSystemRights", Environment.GetResourceString("Argument_InvalidEnumValue", fileSystemRights, "FileSystemRights")); 
            Contract.EndContractBlock();
 
            return ( int )fileSystemRights;
        }

        #endregion 

        #region Public properties 
 
        public FileSystemRights FileSystemRights
        { 
            get { return FileSystemAccessRule.RightsFromAccessMask( base.AccessMask ); }
        }

        #endregion 
    }
 
 
    public abstract class FileSystemSecurity : NativeObjectSecurity
    { 
        #region Member variables

        private const ResourceType s_ResourceType = ResourceType.FileObject;
 
        #endregion
 
        [System.Security.SecurityCritical]  // auto-generated 
        internal FileSystemSecurity( bool isContainer )
            : base(isContainer, s_ResourceType, _HandleErrorCode, isContainer) 
        {
        }

        [System.Security.SecurityCritical]  // auto-generated 
        [ResourceExposure(ResourceScope.Machine)]
        [ResourceConsumption(ResourceScope.Machine)] 
        internal FileSystemSecurity( bool isContainer, String name, AccessControlSections includeSections, bool isDirectory ) 
            : base( isContainer, s_ResourceType, name, includeSections, _HandleErrorCode, isDirectory )
        { 
        }

        [System.Security.SecurityCritical]  // auto-generated
        internal FileSystemSecurity( bool isContainer, SafeFileHandle handle, AccessControlSections includeSections, bool isDirectory ) 
            : base( isContainer, s_ResourceType, handle, includeSections, _HandleErrorCode, isDirectory )
        { 
        } 

        [System.Security.SecurityCritical]  // auto-generated 
        private static Exception _HandleErrorCode(int errorCode, string name, SafeHandle handle, object context)
        {
            System.Exception exception = null;
 
            switch (errorCode) {
            case Win32Native.ERROR_INVALID_NAME: 
                exception = new ArgumentException(Environment.GetResourceString("Argument_InvalidName"),"name"); 
                break;
 
            case Win32Native.ERROR_INVALID_HANDLE:
                exception = new ArgumentException(Environment.GetResourceString("AccessControl_InvalidHandle"));
                break;
 
            case Win32Native.ERROR_FILE_NOT_FOUND:
                if ((context != null) && (context is bool) && ((bool)context)) { // DirectorySecurity 
 
                    if ((name != null) && (name.Length != 0))
                        exception = new DirectoryNotFoundException(name); 
                    else
                        exception = new DirectoryNotFoundException();
                }
                else { 
                    if ((name != null) && (name.Length != 0))
                        exception = new FileNotFoundException(name); 
                    else 
                        exception = new FileNotFoundException();
                } 
                break;

            default:
                break; 
            }
 
            return exception; 
        }
 
#region Factories

        public sealed override AccessRule AccessRuleFactory(
            IdentityReference identityReference, 
            int accessMask,
            bool isInherited, 
            InheritanceFlags inheritanceFlags, 
            PropagationFlags propagationFlags,
            AccessControlType type ) 
        {
            return new FileSystemAccessRule(
                identityReference,
                accessMask, 
                isInherited,
                inheritanceFlags, 
                propagationFlags, 
                type );
        } 

        public sealed override AuditRule AuditRuleFactory(
            IdentityReference identityReference,
            int accessMask, 
            bool isInherited,
            InheritanceFlags inheritanceFlags, 
            PropagationFlags propagationFlags, 
            AuditFlags flags )
        { 
            return new FileSystemAuditRule(
                identityReference,
                accessMask,
                isInherited, 
                inheritanceFlags,
                propagationFlags, 
                flags ); 
        }
 
        #endregion

        #region Internal Methods
 
        internal AccessControlSections GetAccessControlSectionsFromChanges()
        { 
            AccessControlSections persistRules = AccessControlSections.None; 
            if ( AccessRulesModified )
                persistRules = AccessControlSections.Access; 
            if ( AuditRulesModified )
                persistRules |= AccessControlSections.Audit;
            if ( OwnerModified )
                persistRules |= AccessControlSections.Owner; 
            if ( GroupModified )
                persistRules |= AccessControlSections.Group; 
            return persistRules; 
        }
 
        [System.Security.SecurityCritical]  // auto-generated
        [SecurityPermission(SecurityAction.Assert, UnmanagedCode=true)]
        [ResourceExposure(ResourceScope.Machine)]
        [ResourceConsumption(ResourceScope.Machine)] 
        internal void Persist( String fullPath )
        { 
            new FileIOPermission( FileIOPermissionAccess.NoAccess, AccessControlActions.Change, fullPath ).Demand(); 

            WriteLock(); 

            try
            {
                AccessControlSections persistRules = GetAccessControlSectionsFromChanges(); 
                base.Persist( fullPath, persistRules );
                OwnerModified = GroupModified = AuditRulesModified = AccessRulesModified = false; 
            } 
            finally
            { 
                WriteUnlock();
            }
        }
 
        [System.Security.SecuritySafeCritical]  // auto-generated
        [SecurityPermission(SecurityAction.Assert, UnmanagedCode=true)] 
        internal void Persist( SafeFileHandle handle, String fullPath ) 
        {
            if ( fullPath != null ) 
                new FileIOPermission( FileIOPermissionAccess.NoAccess, AccessControlActions.Change, fullPath ).Demand();
            else
                new FileIOPermission( PermissionState.Unrestricted ).Demand();
 
            WriteLock();
 
            try 
            {
                AccessControlSections persistRules = GetAccessControlSectionsFromChanges(); 
                base.Persist( handle, persistRules );
                OwnerModified = GroupModified = AuditRulesModified = AccessRulesModified = false;
            }
            finally 
            {
                WriteUnlock(); 
            } 
        }
 
        #endregion

        #region Public Methods
 
        public void AddAccessRule( FileSystemAccessRule rule )
        { 
            base.AddAccessRule( rule ); 

            //PersistIfPossible(); 
        }

        public void SetAccessRule( FileSystemAccessRule rule )
        { 
            base.SetAccessRule( rule );
        } 
 
        public void ResetAccessRule( FileSystemAccessRule rule )
        { 
            base.ResetAccessRule( rule );
        }

        [System.Security.SecuritySafeCritical]  // auto-generated 
        public bool RemoveAccessRule( FileSystemAccessRule rule )
        { 
            if ( rule == null ) 
                throw new ArgumentNullException("rule");
            Contract.EndContractBlock(); 

            // If the rule to be removed matches what is there currently then
            // remove it unaltered. That is, don't mask off the Synchronize bit.
            // This is to avoid dangling synchronize bit 

            AuthorizationRuleCollection rules = GetAccessRules(true, true, rule.IdentityReference.GetType()); 
 
            for (int i=0; i FileSystemRights.FullControl) 
                throw new ArgumentOutOfRangeException("fileSystemRights", Environment.GetResourceString("Argument_InvalidEnumValue", fileSystemRights, "FileSystemRights"));
            Contract.EndContractBlock(); 
 
            if (controlType == AccessControlType.Allow) {
                fileSystemRights |= FileSystemRights.Synchronize; 
            }
            else if (controlType == AccessControlType.Deny) {
                if (fileSystemRights != FileSystemRights.FullControl &&
                    fileSystemRights != (FileSystemRights.FullControl & ~FileSystemRights.DeleteSubdirectoriesAndFiles)) 
                    fileSystemRights &= ~FileSystemRights.Synchronize;
            } 
 
            return ( int )fileSystemRights;
        } 

        internal static FileSystemRights RightsFromAccessMask( int accessMask )
        {
            return ( FileSystemRights )accessMask; 
        }
 
        #endregion 
    }
 

    public sealed class FileSystemAuditRule : AuditRule
    {
        #region Constructors 

        public FileSystemAuditRule( 
            IdentityReference identity, 
            FileSystemRights fileSystemRights,
            AuditFlags flags ) 
            : this(
                identity,
                fileSystemRights,
                InheritanceFlags.None, 
                PropagationFlags.None,
                flags ) 
        { 
        }
 
        public FileSystemAuditRule(
            IdentityReference identity,
            FileSystemRights fileSystemRights,
            InheritanceFlags inheritanceFlags, 
            PropagationFlags propagationFlags,
            AuditFlags flags ) 
            : this( 
                identity,
                AccessMaskFromRights( fileSystemRights ), 
                false,
                inheritanceFlags,
                propagationFlags,
                flags ) 
        {
        } 
 
        public FileSystemAuditRule(
            String identity, 
            FileSystemRights fileSystemRights,
            AuditFlags flags )
            : this(
                new NTAccount(identity), 
                fileSystemRights,
                InheritanceFlags.None, 
                PropagationFlags.None, 
                flags )
        { 
        }

        public FileSystemAuditRule(
            String identity, 
            FileSystemRights fileSystemRights,
            InheritanceFlags inheritanceFlags, 
            PropagationFlags propagationFlags, 
            AuditFlags flags )
            : this( 
                new NTAccount(identity),
                AccessMaskFromRights( fileSystemRights ),
                false,
                inheritanceFlags, 
                propagationFlags,
                flags ) 
        { 
        }
 
        internal FileSystemAuditRule(
            IdentityReference identity,
            int accessMask,
            bool isInherited, 
            InheritanceFlags inheritanceFlags,
            PropagationFlags propagationFlags, 
            AuditFlags flags ) 
            : base(
                identity, 
                accessMask,
                isInherited,
                inheritanceFlags,
                propagationFlags, 
                flags )
        { 
        } 

        #endregion 

        #region Private methods

        private static int AccessMaskFromRights( FileSystemRights fileSystemRights ) 
        {
            if (fileSystemRights < (FileSystemRights) 0 || fileSystemRights > FileSystemRights.FullControl) 
                throw new ArgumentOutOfRangeException("fileSystemRights", Environment.GetResourceString("Argument_InvalidEnumValue", fileSystemRights, "FileSystemRights")); 
            Contract.EndContractBlock();
 
            return ( int )fileSystemRights;
        }

        #endregion 

        #region Public properties 
 
        public FileSystemRights FileSystemRights
        { 
            get { return FileSystemAccessRule.RightsFromAccessMask( base.AccessMask ); }
        }

        #endregion 
    }
 
 
    public abstract class FileSystemSecurity : NativeObjectSecurity
    { 
        #region Member variables

        private const ResourceType s_ResourceType = ResourceType.FileObject;
 
        #endregion
 
        [System.Security.SecurityCritical]  // auto-generated 
        internal FileSystemSecurity( bool isContainer )
            : base(isContainer, s_ResourceType, _HandleErrorCode, isContainer) 
        {
        }

        [System.Security.SecurityCritical]  // auto-generated 
        [ResourceExposure(ResourceScope.Machine)]
        [ResourceConsumption(ResourceScope.Machine)] 
        internal FileSystemSecurity( bool isContainer, String name, AccessControlSections includeSections, bool isDirectory ) 
            : base( isContainer, s_ResourceType, name, includeSections, _HandleErrorCode, isDirectory )
        { 
        }

        [System.Security.SecurityCritical]  // auto-generated
        internal FileSystemSecurity( bool isContainer, SafeFileHandle handle, AccessControlSections includeSections, bool isDirectory ) 
            : base( isContainer, s_ResourceType, handle, includeSections, _HandleErrorCode, isDirectory )
        { 
        } 

        [System.Security.SecurityCritical]  // auto-generated 
        private static Exception _HandleErrorCode(int errorCode, string name, SafeHandle handle, object context)
        {
            System.Exception exception = null;
 
            switch (errorCode) {
            case Win32Native.ERROR_INVALID_NAME: 
                exception = new ArgumentException(Environment.GetResourceString("Argument_InvalidName"),"name"); 
                break;
 
            case Win32Native.ERROR_INVALID_HANDLE:
                exception = new ArgumentException(Environment.GetResourceString("AccessControl_InvalidHandle"));
                break;
 
            case Win32Native.ERROR_FILE_NOT_FOUND:
                if ((context != null) && (context is bool) && ((bool)context)) { // DirectorySecurity 
 
                    if ((name != null) && (name.Length != 0))
                        exception = new DirectoryNotFoundException(name); 
                    else
                        exception = new DirectoryNotFoundException();
                }
                else { 
                    if ((name != null) && (name.Length != 0))
                        exception = new FileNotFoundException(name); 
                    else 
                        exception = new FileNotFoundException();
                } 
                break;

            default:
                break; 
            }
 
            return exception; 
        }
 
#region Factories

        public sealed override AccessRule AccessRuleFactory(
            IdentityReference identityReference, 
            int accessMask,
            bool isInherited, 
            InheritanceFlags inheritanceFlags, 
            PropagationFlags propagationFlags,
            AccessControlType type ) 
        {
            return new FileSystemAccessRule(
                identityReference,
                accessMask, 
                isInherited,
                inheritanceFlags, 
                propagationFlags, 
                type );
        } 

        public sealed override AuditRule AuditRuleFactory(
            IdentityReference identityReference,
            int accessMask, 
            bool isInherited,
            InheritanceFlags inheritanceFlags, 
            PropagationFlags propagationFlags, 
            AuditFlags flags )
        { 
            return new FileSystemAuditRule(
                identityReference,
                accessMask,
                isInherited, 
                inheritanceFlags,
                propagationFlags, 
                flags ); 
        }
 
        #endregion

        #region Internal Methods
 
        internal AccessControlSections GetAccessControlSectionsFromChanges()
        { 
            AccessControlSections persistRules = AccessControlSections.None; 
            if ( AccessRulesModified )
                persistRules = AccessControlSections.Access; 
            if ( AuditRulesModified )
                persistRules |= AccessControlSections.Audit;
            if ( OwnerModified )
                persistRules |= AccessControlSections.Owner; 
            if ( GroupModified )
                persistRules |= AccessControlSections.Group; 
            return persistRules; 
        }
 
        [System.Security.SecurityCritical]  // auto-generated
        [SecurityPermission(SecurityAction.Assert, UnmanagedCode=true)]
        [ResourceExposure(ResourceScope.Machine)]
        [ResourceConsumption(ResourceScope.Machine)] 
        internal void Persist( String fullPath )
        { 
            new FileIOPermission( FileIOPermissionAccess.NoAccess, AccessControlActions.Change, fullPath ).Demand(); 

            WriteLock(); 

            try
            {
                AccessControlSections persistRules = GetAccessControlSectionsFromChanges(); 
                base.Persist( fullPath, persistRules );
                OwnerModified = GroupModified = AuditRulesModified = AccessRulesModified = false; 
            } 
            finally
            { 
                WriteUnlock();
            }
        }
 
        [System.Security.SecuritySafeCritical]  // auto-generated
        [SecurityPermission(SecurityAction.Assert, UnmanagedCode=true)] 
        internal void Persist( SafeFileHandle handle, String fullPath ) 
        {
            if ( fullPath != null ) 
                new FileIOPermission( FileIOPermissionAccess.NoAccess, AccessControlActions.Change, fullPath ).Demand();
            else
                new FileIOPermission( PermissionState.Unrestricted ).Demand();
 
            WriteLock();
 
            try 
            {
                AccessControlSections persistRules = GetAccessControlSectionsFromChanges(); 
                base.Persist( handle, persistRules );
                OwnerModified = GroupModified = AuditRulesModified = AccessRulesModified = false;
            }
            finally 
            {
                WriteUnlock(); 
            } 
        }
 
        #endregion

        #region Public Methods
 
        public void AddAccessRule( FileSystemAccessRule rule )
        { 
            base.AddAccessRule( rule ); 

            //PersistIfPossible(); 
        }

        public void SetAccessRule( FileSystemAccessRule rule )
        { 
            base.SetAccessRule( rule );
        } 
 
        public void ResetAccessRule( FileSystemAccessRule rule )
        { 
            base.ResetAccessRule( rule );
        }

        [System.Security.SecuritySafeCritical]  // auto-generated 
        public bool RemoveAccessRule( FileSystemAccessRule rule )
        { 
            if ( rule == null ) 
                throw new ArgumentNullException("rule");
            Contract.EndContractBlock(); 

            // If the rule to be removed matches what is there currently then
            // remove it unaltered. That is, don't mask off the Synchronize bit.
            // This is to avoid dangling synchronize bit 

            AuthorizationRuleCollection rules = GetAccessRules(true, true, rule.IdentityReference.GetType()); 
 
            for (int i=0; i

                        

Link Menu

Network programming in C#, Network Programming in VB.NET, Network Programming in .NET
This book is available now!
Buy at Amazon US or
Buy at Amazon UK