Как воспользоваться встроенным конвейером IIS 7.0

Майк Володарский

IIS 6.0 и предыдущих версий позволили разрабатывать компоненты приложений .NET с помощью платформы ASP.NET. ASP.NET интегрирован с IIS через расширение ISAPI и предоставляет собственную модель обработки приложений и запросов. Это эффективно предоставляет два отдельных конвейера сервера, один для собственных фильтров ISAPI и компонентов расширений, а другой — для компонентов управляемых приложений. ASP.NET компоненты будут выполняться полностью внутри контейнера расширения ISAPI ASP.NET и только для запросов, сопоставленных с ASP.NET в конфигурации карт сценариев IIS.

IIS 7.0 и более поздних версий интегрирует среду выполнения ASP.NET с основным веб-сервером, предоставляя единый конвейер обработки запросов, который предоставляется как собственным, так и управляемым компонентам, называемым модулями. К многочисленным преимуществам интеграции относятся:

  • Разрешая службам, предоставляемым как собственными, так и управляемыми модулями, применяться ко всем запросам независимо от обработчика. Например, для всего содержимого можно использовать управляемую проверку подлинности форм, включая страницы ASP, CGIs и статические файлы.
  • Расширение возможностей компонентов ASP.NET для предоставления функциональности, которая ранее была недоступна им из-за их размещения в конвейере сервера. Например, управляемый модуль, предоставляющий функции перезаписи запросов, может переписать запрос до любой обработки сервера, включая проверку подлинности.
  • Единое место для реализации, настройки, мониторинга и поддержки функций сервера, таких как конфигурация сопоставления модуля и обработчика, конфигурация пользовательских ошибок, конфигурация авторизации URL-адресов.

В этой статье рассматривается, как ASP.NET приложения могут использовать интегрированный режим в IIS 7.0 и выше, иллюстрирует следующие задачи:

  • Включение и отключение модулей на уровне приложения.
  • Добавление модулей управляемых приложений на сервер и включение их применения ко всем типам запросов.
  • Добавление управляемых обработчиков.

Дополнительные сведения о создании модулей IIS 7.0 и более поздних версий см. в разделе "Разработка модулей и обработчиков IIS 7.0 и выше" с помощью .NET Framework.

Дополнительные советы по использованию интегрированного режима и разработке модулей IIS, использующих интеграцию ASP.NET в IIS 7.0 и более поздних версиях, см. в блоге http://www.mvolo.com/. Там скачайте ряд таких модулей, включая перенаправление запросов к приложению с помощью модуля HttpRedirection, списки каталогов для веб-сайта IIS с помощью DirectoryListingModule и отображение красивых значков файлов в приложениях ASP.NET с помощью IconHandler.

Необходимые условия

Чтобы выполнить действия, описанные в этом документе, необходимо установить следующие компоненты IIS 7.0 и выше.

ASP.NET

Установите ASP.NET с помощью панели управления Windows Vista. Выберите "Программы и компоненты" — "Включить или отключить функции Windows". Затем откройте "Интернет-информационные службы" — "World Wide Web Services" — "Функции разработки приложений" и проверьте "ASP.NET".

Если у вас есть сборка Windows Server® 2008, откройте "Диспетчер серверов" — "Роли" и выберите "Веб-сервер (IIS)". Нажмите кнопку "Добавить службы ролей". В разделе "Разработка приложений" установите флажок "ASP.NET".

Классический ASP

Мы хотим показать, как ASP.NET модули теперь работают со всем содержимым, а не только с ASP.NET страницами, поэтому используйте панель управления Windows Vista для установки классического ASP. Выберите "Программы" — "Включить или отключить функции Windows". Затем откройте "Интернет-информационные службы" — "World Wide Web Services" — "Функции разработки приложений" и проверьте "ASP".

Если у вас есть сборка Windows Server 2008, откройте "Диспетчер серверов" — "Роли" и выберите "Веб-сервер (IIS)". Нажмите кнопку "Добавить службы ролей". В разделе "Разработка приложений" установите флажок "ASP".

Добавление проверки подлинности форм в приложение

В рамках этой задачи мы включаем аутентификацию на основе форм для приложения ASP.NET. В следующей задаче мы включим модуль проверки подлинности форм для выполнения всех запросов к приложению независимо от типа контента.

Сначала настройте проверку подлинности форм, как и для обычного приложения ASP.NET.

Создание примера страницы

Чтобы проиллюстрировать эту функцию, мы добавим страницу default.aspx в корневой каталог веб-сайта. Откройте блокнот (чтобы убедиться, что у вас есть доступ к каталогу wwwroot ниже, необходимо запустить от имени администратора правой кнопкой мыши значок "Программы\Аксессуары\Блокнот" и нажать кнопку "Запустить от имени администратора" и создать следующий файл: %systemdrive%\inetpub\wwwroot\default.aspx Вставьте в него следующие строки:

<%=Datetime.Now%> 
<BR> 
Login Name: <asp:LoginName runat="server"/>

Все default.aspx отображает текущее время и имя пользователя, вошедшего в систему. Мы используем эту страницу позже для отображения проверки подлинности форм в действии.

Настройка правил проверки подлинности форм и управления доступом

Теперь для защиты default.aspx с помощью проверки подлинности форм. Создайте файл web.config в каталоге %systemdrive%\inetpub\wwwroot и добавьте конфигурацию, показанную ниже:

<configuration> 
  <system.web> 
    <!--membership provider entry goes here--> 
    <authorization> 
      <deny users="?"/> 
      <allow users="*"/> 
    </authorization> 
    <authentication mode="Forms"/> 
  </system.web> 
</configuration>

Эта конфигурация задает режим проверки подлинности ASP.NET для использования проверки подлинности на основе форм и добавляет параметры авторизации для управления доступом к приложению. Этот параметр запрещает доступ анонимным пользователям (?) и разрешает только прошедших проверку подлинности пользователей (*).

Создание поставщика членства

Шаг 1. Необходимо предоставить хранилище проверки подлинности, для которого будут проверены учетные данные пользователя. Чтобы проиллюстрировать глубокую интеграцию между ASP.NET и IIS 7.0 и более поздними версиями, мы используем собственный поставщик членства на основе XML (вы также можете использовать поставщик членства SQL Server по умолчанию при установке SQL Server).

Добавьте следующую запись сразу после начального элемента <конфигурации>/<system.web> в файле web.config:

<membership defaultProvider="AspNetReadOnlyXmlMembershipProvider"> 
  <providers> 
    <add name="AspNetReadOnlyXmlMembershipProvider" type="AspNetReadOnlyXmlMembershipProvider" description="Read-only XML membership provider" xmlFileName="~/App_Data/MembershipUsers.xml"/> 
  </providers> 
</membership>

Шаг 2. После добавления записи конфигурации, необходимо сохранить предоставленный в приложении код поставщика членства как XmlMembershipProvider.cs в каталоге%systemdrive%\inetpub\wwwroot\App_Code. Если этот каталог не существует, необходимо создать его.

Замечание

Если используется Блокнот, обязательно установите флажок "Сохранить как": все файлы, чтобы предотвратить сохранение файла как XmlMembershipProvider.cs.txt.

Шаг 3. Все, что остается, является фактическим хранилищем учетных данных. Сохраните фрагмент XML ниже, как MembershipUsers.xml файл в каталоге %systemdrive%\inetpub\wwwroot\App_Data .

Замечание

Если используется Блокнот, обязательно установите флажок "Сохранить как": все файлы, чтобы предотвратить сохранение файла как MembershipUsers.xml.txt.

<Users>    
    <User>        
        <UserName>Bob</UserName>
        <Password>contoso!</Password>
        <Email>bob@contoso.com</Email>        
    </User>    
    <User>        
        <UserName>Alice</UserName>        
        <Password>contoso!</Password>        
        <Email>alice@contoso.com</Email>        
    </User>    
</Users>

Если каталог App_Data не существует, необходимо создать его.

Замечание

Из-за изменений безопасности в Windows Server 2003 и Windows Vista с пакетом обновления 1 (SP1) вы больше не можете использовать средство администрирования IIS для создания учетных записей пользователей для поставщиков членства, не входящих в GAC.

После выполнения этой задачи перейдите к средству администрирования IIS и добавьте или удалите пользователей для приложения. Запустите INETMGR из меню «Выполнить…». Откройте знаки "+" в представлении дерева слева, пока не отобразится "Веб-сайт по умолчанию". Выберите "Веб-сайт по умолчанию", а затем перейдите вправо и выберите категорию "Безопасность". Остальные функции отображают пользователей .NET. Щелкните "Пользователи ". NET" и добавьте одну или несколько учетных записей пользователей.

Просмотрите MembershipUsers.xml, чтобы найти только что созданных пользователей.

Создание страницы входа

Чтобы использовать проверку подлинности форм, необходимо создать страницу входа. Откройте блокнот (Чтобы убедиться, что у вас есть доступ к каталогу wwwroot ниже, необходимо запустить от имени администратора, щелкнув правой кнопкой мыши значок "Программы\Аксессуары\Блокнот" и щелкнув "Запустить от имени администратора"), и создать файл login.aspx в каталоге %systemdrive%\inetpub\wwwroot . Примечание. Не забудьте задать значение "Сохранить как": все файлы, чтобы предотвратить сохранение файла как login.aspx.txt. Вставьте в него следующие строки:

<%@ Page language="c#" %>    
<form id="Form1" runat="server">    
    <asp:LoginStatus runat="server" />        
    <asp:Login runat="server" />    
</form>

Это страница входа, на которую вы перенаправляетесь, когда правила авторизации запрещают доступ к определенному ресурсу.

Тестирование

Откройте окно Internet Explorer и запросите http://localhost/default.aspx. Вы видите, что вы перенаправляетесь на login.aspx, поскольку изначально вы не прошли аутентификацию, и мы ограничили доступ для пользователей без аутентификации ранее. Если вы успешно выполнили вход с помощью одной из пар имени пользователя и пароля, указанных в MembershipUsers.xml, вы будете перенаправлены обратно на страницу default.aspx первоначально запрошенной. На этой странице отображается текущее время и удостоверение пользователя, с помощью которого вы прошли проверку подлинности.

На этом этапе мы успешно развернули пользовательское решение проверки подлинности с помощью проверки подлинности форм, элементов управления входа и членства. Эта функция не является новой в IIS 7.0 или более поздней версии. Она была доступна с ASP.NET 2.0 в предыдущих выпусках IIS.

Однако проблема заключается в том, что защищено только содержимое, обрабатываемое ASP.NET.

При закрытии и повторном открытии окна браузера и запросе http://localhost/iisstart.htm вам не запрашиваются учетные данные. ASP.NET не участвует в запросе на статический файл, например iisstart.htm. Поэтому он не может защитить его с помощью проверки подлинности форм. Вы видите то же поведение с классическими страницами ASP, программами CGI, PHP или Perl скриптами. Проверка подлинности форм — это ASP.NET функция, которая просто недоступна во время запросов к этим ресурсам.

Включение проверки подлинности форм для всего приложения

В этой задаче мы устраняем ограничение ASP.NET в предыдущих выпусках и активируем функции аутентификации форм и авторизации URL в ASP.NET для всего приложения.

Чтобы воспользоваться преимуществами интеграции ASP.NET, приложение должно быть настроено для запуска в интегрированном режиме. Режим интеграции ASP.NET настраивается для каждого пула приложений, что позволяет ASP.NET приложениям в разных режимах размещаться параллельно на одном сервере. Пул приложений по умолчанию, в котором живет приложение, уже использует интегрированный режим по умолчанию, поэтому нам не нужно ничего делать здесь.

Итак, почему мы не смогли испытать преимущества интегрированного режима, когда мы пытались получить доступ к статической странице ранее? Ответ состоит в параметрах по умолчанию для всех модулей ASP.NET, поставляемых с IIS 7.0 и более поздних версий.

Использование встроенного конвейера

Конфигурация по умолчанию для всех управляемых модулей, поставляемых с IIS 7.0 и выше, включая модули проверки подлинности форм и авторизации URL-адресов, использует предварительные условия, чтобы эти модули применялись только к содержимому, управляемому обработчиком (ASP.NET). Это делается по причинам обратной совместимости.

Удаляя предварительные условия, мы делаем нужный управляемый модуль выполняемым для всех запросов к приложению независимо от содержимого. Это необходимо для защиты статических файлов и любого другого содержимого приложения с помощью проверки подлинности на основе форм.

Для этого откройте файл web.config приложения, расположенный в %systemdrive%\inetpub\wwwroot каталоге, и вставьте следующие строки сразу под первым <элементом конфигурации> :

<system.webServer> 
<modules> 
    <remove name="FormsAuthenticationModule" />    
    <add name="FormsAuthenticationModule" type="System.Web.Security.FormsAuthenticationModule" />    
    <remove name="UrlAuthorization" />    
    <add name="UrlAuthorization" type="System.Web.Security.UrlAuthorizationModule" />    
    <remove name="DefaultAuthentication" />    
    <add name="DefaultAuthentication" type="System.Web.Security.DefaultAuthenticationModule" />    
</modules> 
</system.webServer>

Эта конфигурация повторно добавляет элементы модуля без предварительных условий, что позволяет выполнять все запросы к приложению.

Тестирование

Закройте все экземпляры Internet Explorer, чтобы введенные учетные данные больше не кэшировались. Откройте Internet Explorer и выполните запрос к приложению по следующему URL-адресу:

http://localhost/iisstart.htm

Вы перенаправляетесь на страницу login.aspx, чтобы войти в систему.

Войдите с помощью пары имени пользователя и пароля, используемой ранее. При успешном входе вы перенаправляетесь обратно на исходный ресурс, который отображает страницу приветствия IIS.

Замечание

Несмотря на запрос статического файла, модуль проверки подлинности управляемых форм и модуль авторизации URL-адресов предоставили свои службы для защиты ресурса.

Чтобы проиллюстрировать это еще больше, мы добавим классическую страницу ASP и защитим ее с помощью проверки подлинности форм.

Откройте Блокнот (чтобы убедиться, что у вас есть доступ к каталогу wwwroot, необходимо запустить программу от имени администратора. Для этого щелкните правой кнопкой мыши на иконке "Блокнот" в "Программы\Аксессуары" и выберите пункт "Запустить от имени администратора"), затем создайте файл page.asp в вашем каталоге %systemdrive%\inetpub\wwwroot.

Замечание

Если используется Блокнот, обязательно установите флажок "Сохранить как": все файлы, чтобы предотвратить сохранение файла как page.asp.txt. Вставьте приведенные ниже строки в него:

<% 
for each s in Request.ServerVariables
   Response.Write s & ": "&Request.ServerVariables(s) & VbCrLf
next
%>

Закройте все экземпляры Internet Explorer еще раз. В противном случае учетные данные по-прежнему кэшируются и запрашиваются http://localhost/page.asp. Вы снова перенаправляетесь на страницу входа и после успешной проверки подлинности отобразите страницу ASP.

Поздравляем: вы успешно добавили управляемые службы на сервер, что позволяет им выполнять все запросы к серверу независимо от обработчика!

Сводка

В этом пошаговом руководстве показано, как можно использовать режим интеграции ASP.NET, чтобы сделать мощные функции ASP.NET доступными не только для ASP.NET страниц, но и для всего приложения.

Кроме того, теперь вы можете создавать новые управляемые модули с помощью знакомых API-интерфейсов ASP.NET 2.0, которые могут выполняться для всех содержимого приложения и предоставляют расширенный набор служб обработки запросов для приложения.

Ознакомьтесь с блогом, https://www.mvolo.com/чтобы получить дополнительные советы по использованию интегрированного режима и разработке модулей IIS, использующих интеграцию ASP.NET в IIS 7 и более поздних версиях. Там также можно скачать ряд таких модулей, включая перенаправление запросов к приложению с помощью модуля HttpRedirection, списки каталогов для веб-сайта IIS с помощью DirectoryListingModule и отображение красивых значков файлов в приложениях ASP.NET с помощью IconHandler.

Приложение

Этот поставщик членства основан на примере XML-поставщика членства, найденного в этом разделе Membership Providers.

Чтобы использовать этот поставщик членства, сохраните код как XmlMembershipProvider.cs в каталоге %systemdrive%\inetpub\wwwroot\App\_Code . Если этот каталог не существует, вам придется создать его. Примечание: Не забудьте выбрать "Сохранить как: Все файлы", если вы используете блокнот, чтобы предотвратить сохранение файла как XmlMembershipProvider.cs.txt.

Замечание

Этот пример поставщика членства предназначен только для этой демонстрации. Он не соответствует рекомендациям и требованиям безопасности для поставщика членства в рабочей среде, включая безопасное хранение паролей и аудит действий пользователей. Не используйте этого поставщика членства в приложении вообще!

using System; 
using System.Xml; 
using System.Collections.Generic; 
using System.Collections.Specialized; 
using System.Configuration.Provider; 
using System.Web.Security; 
using System.Web.Hosting; 
using System.Web.Management; 
using System.Security.Permissions; 
using System.Web; 

public class AspNetReadOnlyXmlMembershipProvider : MembershipProvider 
{ 
    private Dictionary<string, MembershipUser> _Users; 
    private string _XmlFileName; 
                  // MembershipProvider Properties 

    public override string ApplicationName 
    { 
        get { throw new NotSupportedException(); } 
        set { throw new NotSupportedException(); } 
    } 

    public override bool EnablePasswordRetrieval 
    { 
        get { return false; } 
    } 

    public override bool EnablePasswordReset 
    { 
        get { return false; } 
    } 

    public override int MaxInvalidPasswordAttempts 
    { 
        get { throw new NotSupportedException(); } 
    } 

    public override int MinRequiredNonAlphanumericCharacters 
    { 
        get { throw new NotSupportedException(); } 
    } 

    public override int MinRequiredPasswordLength 
    { 
        get { throw new NotSupportedException(); } 
    } 
   
    public override int PasswordAttemptWindow 
    { 
        get { throw new NotSupportedException(); } 
    } 

    public override MembershipPasswordFormat PasswordFormat 
    { 
        get { throw new NotSupportedException(); } 
    } 

    public override string PasswordStrengthRegularExpression 
    { 
        get { throw new NotSupportedException(); } 
    } 
   
    public override bool RequiresQuestionAndAnswer 
    { 
        get { return false; } 
    } 

    public override bool RequiresUniqueEmail 
    { 
        get { throw new NotSupportedException(); } 
    } 
  
   // MembershipProvider Methods 

    public override void Initialize(string name, 
        NameValueCollection config) 
    { 
        // Verify that config isn't null 
        if (config == null) 
            throw new ArgumentNullException("config"); 

        // Assign the provider a default name if it doesn't have one 
        if (String.IsNullOrEmpty(name)) 
            name = "ReadOnlyXmlMembershipProvider"; 
  
        // Add a default "description" attribute to config if the 
        // attribute doesn't exist or is empty 
        if (string.IsNullOrEmpty(config["description"])) 
        { 
            config.Remove("description"); 
            config.Add("description", 
                "Read-only XML membership provider"); 
        } 
  
        // Call the base class's Initialize method 
        base.Initialize(name, config); 
  
        // Initialize _XmlFileName and make sure the path 
        // is app-relative 
        string path = config["xmlFileName"]; 

        if (String.IsNullOrEmpty(path)) 
            path = "~/App_Data/MembershipUsers.xml"; 

        if (!VirtualPathUtility.IsAppRelative(path)) 
            throw new ArgumentException 
                ("xmlFileName must be app-relative"); 

        string fullyQualifiedPath = VirtualPathUtility.Combine 
            (VirtualPathUtility.AppendTrailingSlash 
            (HttpRuntime.AppDomainAppVirtualPath), path); 

        _XmlFileName = HostingEnvironment.MapPath(fullyQualifiedPath); 
        config.Remove("xmlFileName"); 
  
        // Make sure we have permission to read the XML data source and 
        // throw an exception if we don't 
        FileIOPermission permission = 
            new FileIOPermission(FileIOPermissionAccess.Read, 
            _XmlFileName); 
        permission.Demand(); 
  
        // Throw an exception if unrecognized attributes remain 
        if (config.Count > 0) 
        { 
            string attr = config.GetKey(0); 
            if (!String.IsNullOrEmpty(attr)) 
                throw new ProviderException 
                    ("Unrecognized attribute: " + attr); 
        } 
    } 

    public override bool ValidateUser(string username, string password) 
    { 
        // Validate input parameters 
        if (String.IsNullOrEmpty(username) || 
            String.IsNullOrEmpty(password)) 
            return false; 
  
        // Make sure the data source has been loaded 
        ReadMembershipDataStore(); 
  
        // Validate the user name and password 
        MembershipUser user; 

        if (_Users.TryGetValue(username, out user)) 
        { 
            if (user.Comment == password) // Case-sensitive 
            { 
                return true; 
            } 
        } 

        return false; 
    } 
   
    public override MembershipUser GetUser(string username, 
        bool userIsOnline) 
    { 
  
        // Note: This implementation ignores userIsOnline 
        // Validate input parameters 

        if (String.IsNullOrEmpty(username)) 
            return null; 
  
        // Make sure the data source has been loaded 
        ReadMembershipDataStore(); 
  
        // Retrieve the user from the data source 
        MembershipUser user; 

        if (_Users.TryGetValue(username, out user)) 
            return user; 

        return null; 
    } 
   
    public override MembershipUserCollection GetAllUsers(int pageIndex, 
        int pageSize, out int totalRecords) 
    { 
        // Note: This implementation ignores pageIndex and pageSize, 
        // and it doesn't sort the MembershipUser objects returned 
        // Make sure the data source has been loaded 

        ReadMembershipDataStore(); 

        MembershipUserCollection users = 
            new MembershipUserCollection(); 

        foreach (KeyValuePair<string, MembershipUser> pair in _Users) 
            users.Add(pair.Value); 

        totalRecords = users.Count; 

        return users; 
    } 

    public override int GetNumberOfUsersOnline() 
    { 
        throw new NotSupportedException(); 
    } 

    public override bool ChangePassword(string username, 
        string oldPassword, string newPassword) 
    { 
        throw new NotSupportedException(); 
    } 

    public override bool 
        ChangePasswordQuestionAndAnswer(string username, 
        string password, string newPasswordQuestion, 
        string newPasswordAnswer) 
    { 
        throw new NotSupportedException(); 
    } 

    public override MembershipUser CreateUser(string username, 
        string password, string email, string passwordQuestion, 
        string passwordAnswer, bool isApproved, object providerUserKey, 
        out MembershipCreateStatus status) 
    { 
        throw new NotSupportedException(); 
    } 

    public override bool DeleteUser(string username, 
        bool deleteAllRelatedData) 
    { 
        throw new NotSupportedException(); 
    } 

    public override MembershipUserCollection 
        FindUsersByEmail(string emailToMatch, int pageIndex, 
        int pageSize, out int totalRecords) 
    { 
        throw new NotSupportedException(); 
    } 

    public override MembershipUserCollection 
        FindUsersByName(string usernameToMatch, int pageIndex, 
        int pageSize, out int totalRecords) 
    { 
        throw new NotSupportedException(); 
    } 

    public override string GetPassword(string username, string answer) 
    { 
        throw new NotSupportedException(); 
    } 
   
    public override MembershipUser GetUser(object providerUserKey, 
        bool userIsOnline) 
    { 
        throw new NotSupportedException(); 
    } 

    public override string GetUserNameByEmail(string email) 
    { 
        throw new NotSupportedException(); 
    } 
   
    public override string ResetPassword(string username, 
        string answer) 

    { 
        throw new NotSupportedException(); 
    } 
   
    public override bool UnlockUser(string userName) 
    { 
        throw new NotSupportedException(); 
    } 
   
    public override void UpdateUser(MembershipUser user) 
    { 
        throw new NotSupportedException(); 

    } 
   
    // Helper method 

    private void ReadMembershipDataStore() 
    { 
        lock (this) 
        { 
            if (_Users == null) 
            { 
                _Users = new Dictionary<string, MembershipUser> 
                   (16, StringComparer.InvariantCultureIgnoreCase); 
                XmlDocument doc = new XmlDocument(); 
                doc.Load(_XmlFileName); 
                XmlNodeList nodes = doc.GetElementsByTagName("User"); 
  
                foreach (XmlNode node in nodes) 
                { 
                    MembershipUser user = new MembershipUser( 
                        Name,                       // Provider name 
                        node["UserName"].InnerText, // Username 
                        null,                       // providerUserKey 
                        node["Email"].InnerText,    // Email 
                        String.Empty,               // passwordQuestion 
                        node["Password"].InnerText, // Comment 
                        true,                       // isApproved 
                        false,                      // isLockedOut 
                        DateTime.Now,               // creationDate 
                        DateTime.Now,               // lastLoginDate 
                        DateTime.Now,               // lastActivityDate 
                        DateTime.Now,               // lastPasswordChangedDate 
                        new DateTime(1980, 1, 1)    // lastLockoutDate 
                 ); 

                 _Users.Add(user.UserName, user); 

                } 
            } 
        } 
    } 
}