Страницы

пятница, 24 февраля 2012 г.

Отправляем уведомления на e-mail или SMS.

Для того, чтобы получать уведомления о событиях, происходящих на Вашем ПК или, что на мой взгляд более востребовано, вверенных Вам серверах достаточно приготовить несколько скриптов. Для приготовления потребуется: сервер сценариев Windows, аккаунт на любом почтовом сервере, аккаунт Google. В случае наличия необходимых ингредиентов ничто не мешает нам начать приготовление.

Начнем с закуски - уведомления на e-mail.

Напишем код класса по имени MailNotification. Для работы с электронной почтой используем набор библиотек CDO(Collaboration Data Objects).
'для примера используем сервер mail.ru
Const Login = "mymyail@mail.ru"
Const PW = "mypassword"
Const SMTPSrv = "smtp.mail.ru"

Set objMail = New MailNotification
'можно изменить порт, аутентификацию, таймаут подключения,
'кодировку и использовать SSL
'With objMail
' .Port = 465 'gmail
' .UseSSL = True
'End With
'можно добавить несколько вложений - вместо Nothing - какие-нибудь логи например
'arrLogs = Array("c:\temp\1.log","c:\temp\2.log")
If objMail.Send(SMTPSrv, Login, PW, Login, "Заголовок", "Содержание", Nothing) Then
 MsgBox "Сообщение отправлено", vbInformation
Else
 MsgBox "Не удалось отправить сообщение", vbCritical
End If
Set objMail = Nothing

Class MailNotification
 Private m_Msg, m_Conf
 Private m_SMTPPort, m_SMTPAuth, m_SMTPUseSSL, m_SMTPTimeout, m_Charset
  
 Private Sub Class_Initialize()
  Set m_Msg = CreateObject("CDO.Message")
  Set m_Conf = CreateObject("CDO.Configuration")
  'значения по умолчанию
  m_SMTPPort = 25 'порт
  m_SMTPAuth = 1 'базовая аутентификация    
  m_SMTPUseSSL = False 'не использовать SSL
  m_SMTPTimeout = 60 'таймаут подключения
  m_Charset = "windows-1251" 'кодировка  
 End Sub
 Private Sub Class_Terminate()
  Set m_Msg = Nothing
  Set m_Conf = Nothing
 End Sub 
 
 Public Property Let Port(i)
  m_SMTPPort = i
 End Property 
 Public Property Let Auth(i)
  m_SMTPAuth = i
 End Property
 Public Property Let UseSSL(b)
  m_SMTPUseSSL = b
 End Property
 Public Property Let Timeout(i)
  m_SMTPTimeout = i
 End Property
 Public Property Let Charset(s)
  m_Charset = s
 End Property
 
 Public Function Send(sSMTPSrv, sLogin, sPW, sTo, sSubject, sBody, arrAttachment)
  On Error Resume Next
  With m_Conf.Fields
   'значение 1, которое используется по умолчанию – использовать каталог Pickup
   .Item("http://schemas.microsoft.com/cdo/configuration/sendusing") = 2
   '1 - базовая аутентификация, 0 – без аутентификации (анонимно), 2 – аутентификация NTLM
   .Item("http://schemas.microsoft.com/cdo/configuration/smtpauthenticate") = m_SMTPAuth
   .Item("http://schemas.microsoft.com/cdo/configuration/smtpserver") = sSMTPSrv
   .Item("http://schemas.microsoft.com/cdo/configuration/smtpserverport") = m_SMTPPort
   .Item("http://schemas.microsoft.com/cdo/configuration/sendusername") = sLogin
   .Item("http://schemas.microsoft.com/cdo/configuration/sendpassword") = sPW
   'использовать ssl
   .Item("http://schemas.microsoft.com/cdo/configuration/smtpusessl") = m_SMTPUseSSL
   'таймаут
   .Item("http://schemas.microsoft.com/cdo/configuration/smtpconnectiontimeout") = m_SMTPTimeout
   .Update
  End With
  With m_Msg
   .Configuration = m_Conf
   .From = sLogin
   .To = sTo  
   .Subject = sSubject
   .TextBody = sBody
   .Bodypart.Charset = m_Charset ' выставляем кодировку
   If IsArray(arrAttachment) Then
    For i = 0 To UBound(arrAttachment)
     .AddAttachment arrAttachment(i)
    Next
   End If
   .Send
  End With
  If Err.Number = 0 Then Send = True  
 End Function 
End Class
Таким образом мы можем не только отправить сообщение о наступлении события, например падения целевого сервиса, но и прикрепить к сообщению какие-нибудь логи, содержащие более детальную информацию.

Приступим к приготовлению основного блюда - SMS.

Писать код для работы с каждым sms-оператором - не айс :). Кроме того все sms-операторы для отправки sms через их сервисы используют капчу, что вполне объяснимо.
"Что же делать?", - спросите Вы меня. Ну-ка, спросите меня :).
Предлагаю использовать сервис Google Календарь, который позволяет создавать события и получать уведомления об их наступлении в виде сообщений электронной почты или SMS, для чего необходимо добавить номер мобильного телефона в настройки календаря.
На текущий момент поддерживаются следующие sms-операторы: Билайн, Мегафон, МТС и Скай Линк.
Авторизацию и общение с календарем реализуем с помошью объекта XMLHttpRequest.
Напишем код класса по имени SMSNotification и используем его единственный метод SendMessage по назначению.
Const Login = "mymail@gmail.com"
Const PW = "mypassword"

Set objSMS = New SMSNotification
 If objSMS.SendMessage(Login, PW, "Заголовок", "Содержание", "Место") Then
  MsgBox "Сообщение отправлено", vbInformation
 Else
  MsgBox "Не удалось отправить сообщение", vbCritical
 End If
Set objSMS = Nothing

Class SMSNotification 
 Public Function SendMessage(sLogin, sPW, sTitle, sContent, sWhere)
  'проверяем службу времени
  If Not W32TimeCheck Then Exit Function
  'синхронизируем время
  If Not TimeSinc Then Exit Function
  Dim XMLHttp 'XMLHttpRequest
  Dim sAuthTokens
  Set XMLHttp = CreateObject("Microsoft.XMLHTTP")
  With XMLHttp   
   'авторизуемся
   .Open "POST", "https://www.google.com/accounts/ClientLogin", False
   .SetRequestHeader "Content-Type", "application/x-www-form-urlencoded"
   .Send "Email=" & sLogin & "&Passwd=" & sPW & "&service=cl&source=da440dil-GSMS-1.0"
   'получаем строку аутентификации
   sAuthTokens = Right(.responseText, Len(.responseText)-InStr(.responseText, "Auth=")-4)
   If .Status <> 200 Then Exit Function 'ошибка   
   'работаем с календарем
   .Open "POST", "http://www.google.com/calendar/feeds/default/private/full", False
   .SetRequestHeader "Content-Type", "application/atom+xml"
   .SetRequestHeader "X-If-No-Redirect", "True"
   .SetRequestHeader "Authorization", "GoogleLogin auth=" & sAuthTokens
   .Send "<?xml version='1.0' ?><entry xmlns='http://www.w3.org/2005/Atom' " & _
     "xmlns:gd='http://schemas.google.com/g/2005'>" & _  
     "<title type='text'>" & sTitle & "</title>" & _
     "<content type='text'>" & sContent & "</content>" & _
     "<gd:eventStatus value='http://schemas.google.com/g/2005#event.confirmed'>" & _
     "</gd:eventStatus>" & _
     "<gd:where valueString='" & sWhere & "'></gd:where>" & _
     "<gd:when startTime='" & MakeDateTime() & _
     "' endTime='" & MakeDateTime() & "'>" & _
     "<gd:reminder minutes='1' method='sms' /></gd:when></entry>"
   If .Status <> 201 Then Exit Function 'ошибка   
  End With
  Set XMLHttp = Nothing
  SendMessage = True
 End Function
 
 'собираем время в необходимом формате
 Private Function MakeDateTime() 
  MakeDateTime = Year(Date()) & "-" & Right(0 & Month(Date()),2) & "-" & _
    Right(0 & Day(Date),2) & "T" & Right(0 & DateAdd("s",60,Time()),8) 
 End Function
 
 'запуск службы времени
 Private Function W32TimeCheck()
  Set objShellApp = CreateObject("Shell.Application")
  With objShellApp
   If Not .IsServiceRunning("W32Time") Then
    If .ServiceStart("W32Time", True) = 0 Then Exit Function
   End If
   'вместо WScript.Sleep, т.к. вероятно Shell.Application выполняет ServiceStart асинхронно
   W32TimeCheck = .IsServiceRunning("W32Time")
  End With
  Set objShellApp = Nothing
 End Function
 
 'синхронизация времени с временем интернета
 Private Function TimeSinc()
  Set wshShell = CreateObject("WScript.Shell")
  With wshShell
   If .Run("w32tm /config /syncfromflags:manual /manualpeerlist:" & Chr(34) & _
     "time.windows.com time.nist.gov, time-nw.nist.gov, time-a.nist.gov, time-a.nist.gov" & Chr(34) _
     ,0,True) <> 0 Then Exit Function
   If .Run("w32tm /config /update",0,True) <> 0 Then Exit Function
   
   If .Run("w32Tm /resync /rediscover",0,True) <> 0 Then Exit Function
  End With
  Set wshShell = Nothing
  TimeSinc = True 
 End Function
 
End Class
С помощью календаря Google в случае необходимости можно даже организовать sms-рассылку, но здесь не об этом :).

Переходим к десерту - постоянной подписке на события WMI.

В предыдущей статье я изложил свой взгляд на временную подписку WMI, способы преодоления ее недостатков, и в заключении намекнул о существовании более "неубиваемых" способов мониторинга событий, под которыми подразумевал именно постоянную подписку.
В рамках настоящей статьи мы не сможем осветить эту тему подробно, поэтому сразу перейдем к стандартному потребителю событий CommandLineEventConsumer.
Для использования нашего потребителя необходимо скомпилировать файл wbemcons.mof, проживаюший по адресу %SystemRoot%\system32\Wbem\, в пространство имен root\cimv2 репозитория WMI. В Windows XP достаточно выполнить команду "mofcomp -N:root\cimv2 %SystemRoot%\system32\Wbem\wbemcons.mof", однако в Windows 7 такой способ не прокатит (возможно в Висте тоже - не пробовал). Все дело в строке "#pragma namespace autorecover" файла wbemcons.mof (в самом начале, первая после коммента), которая запрещает компиляцию в пространство имен, отличное от пространства имен по умолчанию. Если верить MSDN, единственный способ преодолеть это ограничение - редактировать mof-файл.
Не будем париться с редактированием файла в коде преодолевая параноидальную систему безопасности Windows 7 путем изменения DACL файла, смены владельца с TrustedInstaller на админский аккаунт и т.п. Просто скопируем файл wbemcons.mof в каталог скрипта, удалим эту злополучную строку руками и скомпилируем mof-файл из нового места проживания.
With CreateObject("Scripting.FileSystemObject")
 If CreateObject("WScript.Shell").Run("mofcomp -N:root\cimv2 " & Chr(34) & _
    .BuildPath(.GetParentFolderName(WScript.ScriptFullName),"WBEMCons.mof") & Chr(34) _
   ,0,True) = 0 Then
  MsgBox "Файл scrcons.mof скомпилирован", vbInformation
 Else
  MsgBox "Не удалось скопмилировать файл scrcons.mof", vbCritical
 End If 
End With
Выполним скрипт.

Далее напишем код создания модального окна, которое будем отображать каждый раз в момент отлова события.
Разумеется можно использовать написанные выше скрипты, но модальное окно по-моему нагляднее.
MsgBox "Вы запустили блокнот",vbInformation,"Achtung!"
Назовем его Achtung.vbs.

Затем создадим подписку на событие запуска стандартного блокнота Notepad.

With CreateObject("Scripting.FileSystemObject")
 If CreateSubscription("MyFilter","MyConsumer","wscript.exe " &  _
   .BuildPath(.GetParentFolderName(WScript.ScriptFullName),"Achtung.vbs")) Then
  MsgBox "Создание подписки на событие завершено", vbInformation
 Else
  MsgBox "Не удалось создать подписку на событие", vbCritical
 End If
End With
 
'функция создания интерактивной подписки на событие запуска блокнота
Function CreateSubscription(sFilterName,sConsumerName,sScriptFilePath)
 On Error Resume Next
 Set objWMI = GetObject("winmgmts:\\.\Root\CIMV2")
 'создание фильтра события
 With objWMI.Get("__EventFilter").SpawnInstance_()
  .Name = sFilterName
  .QueryLanguage = "WQL"
  .Query = "SELECT * FROM __InstanceCreationEvent WITHIN 1 WHERE TargetInstance ISA 'Win32_Process' " & _
    "AND TargetInstance.Name = 'notepad.exe'"
  Set objFilterPath = .Put_()
 End With 
 'создание потребителя события  
 With objWMI.Get("CommandLineEventConsumer").SpawnInstance_()
  .Name = sConsumerName   
  .CommandLineTemplate = sScriptFilePath
  .RunInteractively = True  
  Set objConsumerPath = .Put_()
 End With 
 'связка фильтра и потребителя  
 With objWMI.Get("__FilterToConsumerBinding").SpawnInstance_()
  .Filter = objFilterPath
  .Consumer = objConsumerPath
  .Put_()
 End With
 Set objWMI = Nothing
 If Err.Number = 0 Then
  CreateSubscription = True
 End If 
End Function
Выполняем скрипт. Запускаем блокнот... Кушать подано... :).

Теперь каждый раз при запуске блокнота мы будем видеть наше модальное окно.
"Что же делать?", - продублирую я цитату из одного популярного мультфильма по примеру одного известного режиссера :).
Пишем скрипт удаления постоянной подписки.

If DeleteSubscription("MyFilter","MyConsumer") Then
 MsgBox "Удаление подписки на событие завершено", vbInformation
Else
 MsgBox "Не удалось удалить подписку на событие", vbCritical
End If
 
'функция удаления подписки на событие
Function DeleteSubscription(sFilterName,sConsumerName)
 On Error Resume Next
 Set objWMI = GetObject("winmgmts:\\.\Root\CIMV2")
 'удаляем фильтр
 Set colFilters = objWMI.ExecQuery("SELECT * FROM __EventFilter WHERE Name='" & sFilterName & "'")
 If colFilters.Count Then
  For Each objFilter In colFilters
   objFilter.Delete_
  Next   
 End If
 Set colFilters = Nothing
 
 'удаляем потребителя
 Set colConsumers = objWMI.ExecQuery("SELECT * FROM CommandLineEventConsumer WHERE Name='" & sConsumerName & "'") 
 If colConsumers.Count Then
  For Each objConsumer In colConsumers
   objConsumer.Delete_
  Next   
 End If
 Set colConsumers = Nothing
 Set objWMI = Nothing
 
 If Err.Number = 0 Then
  DeleteSubscription = True
 End If
End Function
Запускаем... Избавились.

В заключение еще пару слов о постоянной подписке.
С моей точки зрения ее преимущество в том, что она не создает никаких собственных процессов, служб и пр., таким образом оставляя гораздо более меньший по сравнению с временной подпиской "угол атаки". Правда у использованного в статье потребителя событий CommandLineEventConsumer есть один небольшой, на мой взгляд, недостаток - файл, прописанный в свойстве CommandLineTemplate можно удалить.
Но существуют иные стандартные потребители событий, обладающие своими преимуществами и недостатками, которые вы сможете использовать, если покопаете тему постоянной подписки на события WMI поглубже. Более того, углубившись в подписку на события WMI основательно, вы сможете написать код своего собственного потребителя событий. Но это уже совсем другая история...