среда, 19 сентября 2012 г.

Web Service в Visual Studio 10 и клиент на Delphi 7

Part 1. Веб-сервис (Web Service) в Visual Studio 10 называется "WCF Service Application".


Сразу после создания нового сервиса он содержит класс Service1 (в файле Service1.svc.cs), реализующий интерфейс IService1 (в файле IService1.cs), в котором объявлены два публичных демонстрационных метода GetData и GetDataUsingDataContract и композитный тип данных CompositeType. Веб-сервис можно сразу запустить (Debug). В окне броузера появится следующее сообщение: "Служба создана... необходимо создать клиент... Это можно сделать запустив программу svcutil.exe..."


В результате запуска svcutil.exe будет создан файл Service1.cs, который нужно добавить в тестовый проект (я сделал консольное тестовое приложение TestApplication1). В References тестового проекта нужно добавить System.Runtime.Serialization и System.ServiceModel. Пример вызова веб-службы см. на рисунке выше. 

Теперь немного про развертывание веб-службы на сервере IIS. Можно воспользоваться визардом (Publish), а можно сделать все "ручками":
  1. Установить .Net Framework 4.0, если он еще не установлен.
  2. Создать каталог на сайте, например C:\inetpub\wwwroot\Service1.
  3. Скопировать туда Service1.svc файлы Service1.svc, Web.Config и папку bin с файлами WcfService1.dll и WcfService1.pdb.
  4. Дать права "Чтение, Чтение и выполнение" пользователю IIS (IUSR)  на каталог Service1 и все скопированные в него файлы/подкаталоги.
  5. В диспетчере служб IIS в контекстном меню каталога Service1 выбрать "Преобразовать в приложение". В диалоге "Добавление приложения" нажать кнопку "Выбрать..." справа от "Пул приложений", выбрать пул приложений с версией среды .Net Framework соответствующей веб-сервису (ASP .NET v4.0), и нажать Ok. Каталог Service1 будет преобразован в приложение.
Иногда сразу же приложение не хочет работать в IIS (как случилось и у меня) с формулировкой типа "HTTP Error 404.3 - Not Found The page you are requesting cannot be served because of the extension configuration. If the page is a script, add a handler. If the file should be downloaded, add a MIME map." В таком случае помогает выполнение команды ServiceModelReg.exe -i. ServiceModelReg.exe находится в C:\Windows\Microsoft .NET\Framework\v4.0.30319. Там же находится aspnet_regiis.exe. Он помогает в том случае, если .Net 4 вдруг оказывается не зарегистрированным (см. картинку ниже) и по этой причине веб-сервис не запускается.

Part 2. Создание клиента для веб-сервиса на Delphi 7.

1. Сервис должен использовать привязку basicHttpBinding, а не wsHttpBinding. Регулируется это в Web.Config. Для его правки в Visual Studio 10 можно воспользоваться утилитой WCF Service Configuration Editor (можно выбрать пункт Edit WCF Configuration из контекстного меню для Web.Config).



2. Необходимо обновить утилиту импорта WSDL. Взять ее можно здесь: http://cc.embarcadero.com/item/24535. Пользоваться ею просто, например так:

    WSDLImp.exe -P http://localhost:2708/Service1.svc?wsdl

В результате будет создан файл Service1.pas, который нужно подключить к проекту клиента в Delphi 7. Пользоваться очень просто:

uses Service1.pas;

var
  Client: IWcfService1;
  ws: WideString;
begin
  Client := GetIWcfService1(True, 'http://localhost:2708/Service1.svc?wsdl');
  ws := Client.GetDate(10);
  MessageBoxW(0, PWideChar(ws), 'Result', MB_ICONINFORMATION);
end;

3. Delphi 7 BUG. При попытке обратиться к веб-сервису из клиента, написанного на Delphi 7, может возникать Access Violation (см. картинку ниже). Возникает эта ошибка не везде. У меня на Windows 7 ее нет, а на Windows Server 2008 - проявилась.


Фикс для этой ошибки находится в том же архиве, что и новая утилита импорта WSDL (http://cc.embarcadero.com/item/24535). НО НЕ ТОРОПИТЕСЬ заменять ваши исходники SOAP на файлы из архива. Этот "патч" что-то лечит, а что-то калечит :-) У меня при его использовании перестали передаваться в веб-сервис строковые параметры вызова (в значениях параметров всегда содержался null). Для того, чтобы справиться с ошибкой Access Violation, нам необходимы только два файла из этого патча: PrivateHeap.pas и Rio.pas. Их (только их!) нужно добавить в Delphi-проект клиента веб-сервиса.

===
Перепечатка материалов блога разрешается с обязательной ссылкой на blog.coolsoftware.ru

вторник, 11 сентября 2012 г.

SSL HTTP сервер на Indy 9 (Delphi 7)

HTTP-сервер в Indy 9 представлен компонентом TIdHTTPServer. Чтобы включить использование SSL (HTTPS), нужно у объекта TIdHTTPServer свойство IOHandler связать с объектом типа TIdServerIOHandlerSSL, в котором в поле CertFile прописать путь к файлу сертификата, а в KeyFile - путь к файлу с приватным ключом.

Файл сертификата имеет примерно такое содержимое:

Файл с приватным ключом выглядит примерно так:

Но что делать, если необходимо, чтобы сертификат и ключ были "вшиты" в приложение? Я использую следующую технику - после загрузки и инициализации библиотеки ssleay32.dll подменяю указатели на функции IdSslCtxUseCertificateFile и IdSslCtxUsePrivateKeyFile (они объявлены в IdSSLOpenSSLHeaders) на мои реализации этих функций, которые читают сертификат и ключ не из файлов, а из памяти.

unit uOpenSSL;

(*
Copyright 2012 Coolsoftware. http://blog.coolsoftware.ru/
You can freely use this code for your needs.
Please, don't remove this copyright.
*)

interface

uses Windows, SysUtils, IdSSLOpenSSL, IdSSLOpenSSLHeaders;

function MySslCtxUsePrivateKeyFile(ctx: PSSL_CTX; const _file: PChar; _type: Integer): Integer cdecl;
function MySslCtxUseCertificateFile(ctx: PSSL_CTX; const _file: PChar; _type: Integer): Integer cdecl;

type
  PBIO            = Pointer;
  PPBIO           =^PBIO;
  PBIO_METHOD     = Pointer;
  PRSA         = Pointer;

var
  MySslCtxUsePrivateKey : function(ctx: PSSL_CTX; pkey: PEVP_PKEY): 
    Integer cdecl = nil;
  MySslCtxUseCertificate : function(ctx: PSSL_CTX; x: PX509): 
    Integer cdecl = nil;

  EVP_PKEY_new : function(): PEVP_PKEY cdecl = nil;
  EVP_PKEY_free : procedure(pkey: PEVP_PKEY) cdecl = nil;

  EVP_PKEY_set1_RSA : function(pkey: PEVP_PKEY; key: PRSA) : 
    Integer cdecl = nil;

  BIO_s_mem : function () : PBIO_METHOD cdecl = nil;
  BIO_new : function(t: PBIO_METHOD): PBIO cdecl = nil;
  BIO_free : function(a: PBIO): Integer cdecl = nil;
 BIO_puts : function(bp: PBIO; buf: PChar): Integer cdecl = nil;

  RSA_new: function (): pRSA; cdecl = nil;
  RSA_free: procedure (r: pRSA); cdecl = nil;
  RSA_public_encrypt: function (flen: integer; from: PChar; _to: PChar;
    rsa: pRSA; padding: integer): integer; cdecl = nil;
  RSA_public_decrypt: function (flen: integer; from: PChar; _to: PChar;
    rsa: pRSA; padding: integer): integer; cdecl;

  d2i_RSAPrivateKey : function(a: Pointer; pp: Pointer; length: Integer): 
    PRSA cdecl = nil;

  PEM_ASN1_read_bio : function(d2: Pointer; name: PChar; bp: PBIO; x: Pointer;
    cb: Pointer; u: Pointer): PChar cdecl = nil;
  PEM_read_bio_X509 : function(bp: PBIO; x: Pointer; cb: Pointer; u: Pointer): 
    PX509 cdecl = nil;
  PEM_read_bio_RSAPublicKey: function (bp: pBIO; x: pRSA;
    cb: Pointer; u: pointer): pRSA; cdecl = nil;

implementation

uses SyncObjs;

const
  SSL_DLL_name       = 'ssleay32.dll';  {Do not localize}
  SSLCLIB_DLL_name   = 'libeay32.dll';  {Do not localize}

var
  hIdSSL    : Integer = 0;
  hIdCrypto : Integer = 0;

function LoadFunction(FceName:String):Pointer;
begin
  FceName := FceName+#0;
  Result := GetProcAddress(hIdSSL, @FceName[1]);
//  if (Result = nil) then ShowMessage('Error loading: ' + FceName);  {Do not localize}
end;

function LoadFunctionCLib(FceName:String):Pointer;
begin
  FceName := FceName+#0;
  Result := GetProcAddress(hIdCrypto, @FceName[1]);
//  if (Result = nil) then ShowMessage('Error loading: ' + FceName);  {Do not localize}
end;

const
  fn_SSL_CTX_use_RSAPrivateKey = 'SSL_CTX_use_RSAPrivateKey';  {Do not localize}
  fn_SSL_CTX_use_PrivateKey = 'SSL_CTX_use_PrivateKey';  {Do not localize}
  fn_SSL_CTX_use_certificate = 'SSL_CTX_use_certificate';  {Do not localize}

  fn_EVP_PKEY_new = 'EVP_PKEY_new';  {Do not localize}
  fn_EVP_PKEY_free = 'EVP_PKEY_free';  {Do not localize}

  fn_EVP_PKEY_set1_RSA = 'EVP_PKEY_set1_RSA';

  fn_BIO_s_mem = 'BIO_s_mem';  {Do not localize}
  fn_BIO_new = 'BIO_new';  {Do not localize}
  fn_BIO_free = 'BIO_free';  {Do not localize}
  fn_BIO_puts = 'BIO_puts';  {Do not localize}

  fn_RSA_new = 'RSA_new';  {Do not localize}
  fn_RSA_free = 'RSA_free';  {Do not localize}
  fn_RSA_public_encrypt = 'RSA_public_encrypt';  {Do not localize}
  fn_RSA_public_decrypt = 'RSA_public_decrypt';  {Do not localize}

  fn_d2i_RSAPrivateKey = 'd2i_RSAPrivateKey';  {Do not localize}

  fn_PEM_ASN1_read_bio = 'PEM_ASN1_read_bio';  {Do not localize}
  fn_PEM_read_bio_X509 = 'PEM_read_bio_X509';  {Do not localize}
  fn_PEM_read_bio_RSAPublicKey = 'PEM_read_bio_RSAPublicKey';  {Do not localize}

var
  c_PrivateKeyPassword: PChar = 'put the password of your private key here';
  c_PrivateKey: PChar = 'put your private key here';
  c_Certificate: PChar = 'put your certificate here';

  LockPassCB: TCriticalSection;

function MyPasswordCallback(buf:PChar; size:Integer; rwflag:Integer; 
  userdata: Pointer):Integer; cdecl;
begin
  LockPassCB.Acquire;
  try
    size := StrLen(c_PrivateKeyPassword);
    StrCopy(buf, c_PrivateKeyPassword);
    Result := size;
  finally
    LockPassCB.Release;
  end;
end;

function MySslCtxUsePrivateKeyFile(ctx: PSSL_CTX; const _file: PChar; 
  _type: Integer):Integer cdecl;
var
  pk: PEVP_PKEY;
  rsakey: PRSA;
  pMemBIO: PBIO;
begin
    pk := EVP_PKEY_new();
    try
      pMemBIO := BIO_new(BIO_s_mem());
      try
        BIO_puts(pMemBIO, c_PrivateKey);
        rsakey := PEM_ASN1_read_bio(
          @d2i_RSAPrivateKey, OPENSSL_PEM_STRING_RSA, pMemBIO, nil,
          @MyPasswordCallback, nil);
      finally
        BIO_free(pMemBIO);
      end;
      EVP_PKEY_set1_RSA(pk, rsakey);
      Result := MySslCtxUsePrivateKey(ctx, pk);
    finally
      EVP_PKEY_free(pk);
    end;
end;

function MySslCtxUseCertificateFile(ctx: PSSL_CTX; const _file: PChar; 
  _type: Integer):Integer cdecl;
var
  pCert: PX509;
  pMemBIO: PBIO;
begin
    pMemBIO := BIO_new(BIO_s_mem());
    try
      BIO_puts(pMemBIO, c_Certificate);
      pCert := PEM_read_bio_X509(pMemBIO, nil,
        @MyPasswordCallback, nil);
    finally
      BIO_free(pMemBIO);
    end;
    Result := MySslCtxUseCertificate(ctx, pCert);
end;

var
  ctx: TIdSSLContext;

initialization
  LockPassCB := TCriticalSection.Create;
    
  //this will load the SSL library
  ctx := TIdSSLContext.Create;
  try

    hIdCrypto := GetModuleHandle(SSLCLIB_DLL_name);
    hIdSSL := GetModuleHandle(SSL_DLL_name);

    @MySslCtxUsePrivateKey := LoadFunction(fn_SSL_CTX_use_PrivateKey);
    @MySslCtxUseCertificate := LoadFunction(fn_SSL_CTX_use_certificate);

    @EVP_PKEY_new := LoadFunctionCLib(fn_EVP_PKEY_new);
    @EVP_PKEY_free := LoadFunctionCLib(fn_EVP_PKEY_free);

    @EVP_PKEY_set1_RSA := LoadFunctionCLib(fn_EVP_PKEY_set1_RSA);

    @BIO_s_mem := LoadFunctionCLib(fn_BIO_s_mem);
    @BIO_new := LoadFunctionCLib(fn_BIO_new);
    @BIO_free := LoadFunctionCLib(fn_BIO_free);
    @BIO_puts := LoadFunctionCLib(fn_BIO_puts);

    @RSA_new := LoadFunctionCLib(fn_RSA_new);
    @RSA_free := LoadFunctionCLib(fn_RSA_free);
    @RSA_public_encrypt := LoadFunctionCLib(fn_RSA_public_encrypt);
    @RSA_public_decrypt := LoadFunctionCLib(fn_RSA_public_decrypt);

    @d2i_RSAPrivateKey := LoadFunctionCLib(fn_d2i_RSAPrivateKey);

    @PEM_ASN1_read_bio := LoadFunctionCLib(fn_PEM_ASN1_read_bio);
    @PEM_read_bio_X509 := LoadFunctionCLib(fn_PEM_read_bio_X509);
    @PEM_read_bio_RSAPublicKey := LoadFunctionCLib(fn_PEM_read_bio_RSAPublicKey);

    IdSslCtxUsePrivateKeyFile := MySslCtxUsePrivateKeyFile;
    IdSslCtxUseCertificateFile := MySslCtxUseCertificateFile;

  finally
    ctx.Free;
  end;

finalization
  LockPassCB.Free;
end.

===
Перепечатка материалов блога разрешается с обязательной ссылкой на blog.coolsoftware.ru

среда, 5 сентября 2012 г.

Обновился VHashedStringList

В GitHub я выложил обновленную версию VHashedStringList. Сделаны следующие добавления/изменения:
  1. Добавлено свойство OwnsObjects. Оно позволяет объекту TVHashedStringList контролировать выделение памяти для индексированных объектов - при удалении объекта из списка или при удалении самого списка автоматически освобождается память, отведенная под указанные объекты. (Это аналог одноименного свойства OwnsObjects у TObjectList).
  2. Добавлено свойство ValueObjects. Позволяет обращаться к объекту, хранящемуся в списке, по значению соответствующей ему строки.
  3. При удалении элемента списка hash-индекс остается корректным, т.е. не требуется его полное перестроение перед поиском элементов.
  4. Добавлено свойство AutoUpdateHash (по-умолчанию его значение = True), которое позволяет на время отключать авто-обновление hash-индекса.
===
Перепечатка материалов блога разрешается с обязательной ссылкой на blog.coolsoftware.ru

вторник, 4 сентября 2012 г.

GitHub and TortoieseGit: error code 128 git did not exit cleanly

У меня 2 аккаунта на GitHub. И несколько разных репозиториев для разных проектов - некторые должны заливаться на GitHub под первым аккаунтом, другие - под вторым. Я использую Git-клиент TortoiseGit. И столкнулся со странной проблемой - под первым аккаунтом все хорошо работает, а под вторым Push обламывается в формулировкой "error code 128 git did not exit cleanly". Оказалось, что иногда для второго аккаунта используется ssh-key от первого. Лечится проблема удалением ключей (Remove Key) из Pagent (PuTTY authentication client).

===
Перепечатка материалов блога разрешается с обязательной ссылкой на blog.coolsoftware.ru