MCP

понедельник, 27 мая 2013 г.

Цифровая подпись больших файлов на .NET в потоковом режиме

Стандартный механизм в .NET по подписи через SignedCms, весьма симпатичный, но есть одна проблема: для подписи необходимо передать массив байт, т.е. если надо подписать большой файл, то вначале прочитать в память в .NET потом засунуть этот же объём системному API, что весьма затратно по памяти.

Конечно, в CryptoPro есть замечательный метод SignHash, который позволяет заранее посчитать хеш и его подписать (что идентично подписи самого файла), но это только у CryptoPro, стандартное API даже на низком уровне не позволяет провести такое.

Гугление выдало код, который может шифровать в потоковом режиме, но у него обнаружилось несколько проблем:

  • Почему-то используется FileStream, хотя достаточно просто Stream
  • Реально всё равно читается всё содержимое (!) а потом подписывается в потоковом режиме
  • Нет поддержки Detached подписи, хотя добавить её несложно
  • Внаглую из сертификата берётся приватный ключ, который опять же считается RSA (наивные! совершенно не знают что в России творится)
В общем, я поправил этот код, и выкладываю его здесь, ибо взял из блога исходники, надо их таким же образом вернуть, чтобы людям было приятно.

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

StreamCms.cs
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Runtime.InteropServices;
using System.ComponentModel;

namespace NS1
{
 [ComVisible(false)]
 public class StreamCms
 {
  // File stream to use in callback function
  private Stream _callbackFile;

  // Streaming callback function for encoding
  private bool StreamOutputCallback(IntPtr pvArg, IntPtr pbData, int cbData, bool fFinal)
  {
   if (cbData == 0) return true;
   // Write all bytes to encoded file
   var bytes = new byte[cbData];
   Marshal.Copy(pbData, bytes, 0, cbData);
   _callbackFile.Write(bytes, 0, cbData);

   if (fFinal)
   {
    // This is the last piece. Close the file
    _callbackFile.Flush();
    _callbackFile.Close();
    _callbackFile = null;
   }

   return true;
  }

  // Encode StreamCms with streaming to support large data
  public void Encode(X509Certificate2 cert, Stream inFile, Stream outFile, bool isDetached)
  {
   // Variables

   IntPtr hProv = IntPtr.Zero;
   IntPtr SignerInfoPtr = IntPtr.Zero;
   IntPtr CertBlobsPtr = IntPtr.Zero;
   IntPtr hMsg = IntPtr.Zero;

   try
   {
    // Prepare stream for encoded info
    _callbackFile = outFile;

    // Get cert chain
    var chain = new X509Chain();
    chain.Build(cert);
    var chainElements = new X509ChainElement[chain.ChainElements.Count];
    chain.ChainElements.CopyTo(chainElements, 0);

    // Get certs in chain
    var certs = new X509Certificate2[chainElements.Length];
    for (int i = 0; i < chainElements.Length; i++)
    {
     certs[i] = chainElements[i].Certificate;
    }

    // Get context of all certs in chain
    var CertContexts = new Win32.CERT_CONTEXT[certs.Length];
    for (int i = 0; i < certs.Length; i++)
    {
     CertContexts[i] = (Win32.CERT_CONTEXT)Marshal.PtrToStructure(certs[i].Handle, typeof(Win32.CERT_CONTEXT));
    }

    // Get cert blob of all certs
    var CertBlobs = new Win32.BLOB[CertContexts.Length];
    for (int i = 0; i < CertContexts.Length; i++)
    {
     CertBlobs[i].cbData = CertContexts[i].cbCertEncoded;
     CertBlobs[i].pbData = CertContexts[i].pbCertEncoded;
    }

    // Get CSP of client certificate

    Win32.CRYPT_KEY_PROV_INFO csp;
    GetPrivateKeyInfo(GetCertContext(cert), out csp);

    bool bResult = Win32.CryptAcquireContext(
     ref hProv,
     csp.pwszContainerName,
     csp.pwszProvName,
     (int)csp.dwProvType,
     0);
    if (!bResult)
    {
     throw new Exception("CryptAcquireContext error #" + Marshal.GetLastWin32Error().ToString(), new Win32Exception(Marshal.GetLastWin32Error()));
    }

    // Populate Signer Info struct
    var SignerInfo = new Win32.CMSG_SIGNER_ENCODE_INFO();
    SignerInfo.cbSize = Marshal.SizeOf(SignerInfo);
    SignerInfo.pCertInfo = CertContexts[0].pCertInfo;
    SignerInfo.hCryptProvOrhNCryptKey = hProv;
    SignerInfo.dwKeySpec = (int)csp.dwKeySpec;
    SignerInfo.HashAlgorithm.pszObjId = cert.SignatureAlgorithm.Value; // Win32.szOID_OIWSEC_sha1;

    // Populate Signed Info struct
    var SignedInfo = new Win32.CMSG_SIGNED_ENCODE_INFO();
    SignedInfo.cbSize = Marshal.SizeOf(SignedInfo);

    SignedInfo.cSigners = 1;
    SignerInfoPtr = Marshal.AllocHGlobal(Marshal.SizeOf(SignerInfo));
    Marshal.StructureToPtr(SignerInfo, SignerInfoPtr, false);
    SignedInfo.rgSigners = SignerInfoPtr;

    SignedInfo.cCertEncoded = CertBlobs.Length;
    CertBlobsPtr = Marshal.AllocHGlobal(Marshal.SizeOf(CertBlobs[0]) * CertBlobs.Length);
    for (int i = 0; i < CertBlobs.Length; i++)
    {
     Marshal.StructureToPtr(CertBlobs[i], new IntPtr(CertBlobsPtr.ToInt64() + (Marshal.SizeOf(CertBlobs[i]) * i)), false);
    }

    SignedInfo.rgCertEncoded = CertBlobsPtr;

    // Populate Stream Info struct
    var StreamInfo = new Win32.CMSG_STREAM_INFO
                      {
                       cbContent = (int)inFile.Length,
                       pfnStreamOutput = StreamOutputCallback
                      };

    // Open message to encode
    hMsg = Win32.CryptMsgOpenToEncode(
     Win32.X509_ASN_ENCODING | Win32.PKCS_7_ASN_ENCODING,
     isDetached ? Win32.CMSG_DETACHED_FLAG : 0,
     Win32.CMSG_SIGNED,
     ref SignedInfo,
     null,
     ref StreamInfo);

    if (hMsg.Equals(IntPtr.Zero))
    {
     throw new Exception("CryptMsgOpenToEncode error #" + Marshal.GetLastWin32Error().ToString(), new Win32Exception(Marshal.GetLastWin32Error()));
    }

    // Process the whole message
    ProcessMessage(hMsg, inFile);
   }
   finally
   {
    // Clean up

    if (inFile != null)
    {
     inFile.Close();
    }

    if (_callbackFile != null)
    {
     _callbackFile.Close();
    }

    if (!CertBlobsPtr.Equals(IntPtr.Zero))
    {
     Marshal.FreeHGlobal(CertBlobsPtr);
    }

    if (!SignerInfoPtr.Equals(IntPtr.Zero))
    {
     Marshal.FreeHGlobal(SignerInfoPtr);
    }

    if (!hProv.Equals(IntPtr.Zero))
    {
     Win32.CryptReleaseContext(hProv, 0);
    }

    if (!hMsg.Equals(IntPtr.Zero))
    {
     Win32.CryptMsgClose(hMsg);
    }
   }
  }

  // Decode StreamCms with streaming to support large data
  public void Decode(Stream dataFile, Stream signFile, Stream outFile, bool isDetached)
  {
   // Variables

   IntPtr hMsg = IntPtr.Zero;
   IntPtr pSignerCertInfo = IntPtr.Zero;
   IntPtr pSignerCertContext = IntPtr.Zero;
   IntPtr hStore = IntPtr.Zero;

   try
   {
    // Get data to decode

    // Prepare stream for decoded info
    _callbackFile = outFile;

    // Populate Stream Info struct
    var StreamInfo = new Win32.CMSG_STREAM_INFO
                      {
                       cbContent = (int)signFile.Length,
                       pfnStreamOutput = StreamOutputCallback
                      };

    // Open message to decode
    hMsg = Win32.CryptMsgOpenToDecode(
     Win32.X509_ASN_ENCODING | Win32.PKCS_7_ASN_ENCODING,
     isDetached ? Win32.CMSG_DETACHED_FLAG : 0,
     0,
     IntPtr.Zero,
     IntPtr.Zero,
     ref StreamInfo);
    if (hMsg.Equals(IntPtr.Zero))
    {
     throw new Exception("CryptMsgOpenToDecode error #" + Marshal.GetLastWin32Error().ToString(), new Win32Exception(Marshal.GetLastWin32Error()));
    }

    // Process the whole message
    if (isDetached)
    {
     ProcessMessage(hMsg, signFile);
     ProcessMessage(hMsg, dataFile);
    }
    else
    {
     ProcessMessage(hMsg, signFile);
    }

    // Get signer certificate info
    int cbSignerCertInfo = 0;
    bool bResult = Win32.CryptMsgGetParam(
     hMsg,
     Win32.CMSG_SIGNER_CERT_INFO_PARAM,
     0,
     IntPtr.Zero,
     ref cbSignerCertInfo);
    if (!bResult)
    {
     throw new Exception("CryptMsgGetParam error #" + Marshal.GetLastWin32Error().ToString(), new Win32Exception(Marshal.GetLastWin32Error()));
    }

    pSignerCertInfo = Marshal.AllocHGlobal(cbSignerCertInfo);

    bResult = Win32.CryptMsgGetParam(
     hMsg,
     Win32.CMSG_SIGNER_CERT_INFO_PARAM,
     0,
     pSignerCertInfo,
     ref cbSignerCertInfo);
    if (!bResult)
    {
     throw new Exception("CryptMsgGetParam error #" + Marshal.GetLastWin32Error().ToString(), new Win32Exception(Marshal.GetLastWin32Error()));
    }

    // Open a cert store in memory with the certs from the message
    hStore = Win32.CertOpenStore(
     Win32.CERT_STORE_PROV_MSG,
     Win32.X509_ASN_ENCODING | Win32.PKCS_7_ASN_ENCODING,
     IntPtr.Zero,
     0,
     hMsg);
    if (hStore.Equals(IntPtr.Zero))
    {
     throw new Exception("CertOpenStore error #" + Marshal.GetLastWin32Error().ToString(), new Win32Exception(Marshal.GetLastWin32Error()));
    }

    // Find the signer's cert in the store
    pSignerCertContext = Win32.CertGetSubjectCertificateFromStore(
     hStore,
     Win32.X509_ASN_ENCODING | Win32.PKCS_7_ASN_ENCODING,
     pSignerCertInfo);
    if (pSignerCertContext.Equals(IntPtr.Zero))
    {
     throw new Exception("CertGetSubjectCertificateFromStore error #" + Marshal.GetLastWin32Error().ToString(), new Win32Exception(Marshal.GetLastWin32Error()));
    }

    // Set message for verifying
    var SignerCertContext = (Win32.CERT_CONTEXT)Marshal.PtrToStructure(pSignerCertContext, typeof(Win32.CERT_CONTEXT));
    bResult = Win32.CryptMsgControl(
     hMsg,
     0,
     Win32.CMSG_CTRL_VERIFY_SIGNATURE,
     SignerCertContext.pCertInfo);
    if (!bResult)
    {
     throw new Exception("CryptMsgControl error #" + Marshal.GetLastWin32Error().ToString(), new Win32Exception(Marshal.GetLastWin32Error()));
    }
   }
   finally
   {
    // Clean up
    if (!pSignerCertContext.Equals(IntPtr.Zero))
    {
     Win32.CertFreeCertificateContext(pSignerCertContext);
    }

    if (!pSignerCertInfo.Equals(IntPtr.Zero))
    {
     Marshal.FreeHGlobal(pSignerCertInfo);
    }

    if (!hStore.Equals(IntPtr.Zero))
    {
     Win32.CertCloseStore(hStore, Win32.CERT_CLOSE_STORE_FORCE_FLAG);
    }

    if (dataFile != null)
    {
     dataFile.Close();
    }

    if (signFile != null)
    {
     signFile.Close();
    }

    if (_callbackFile != null)
    {
     _callbackFile.Close();
    }

    if (!hMsg.Equals(IntPtr.Zero))
    {
     Win32.CryptMsgClose(hMsg);
    }
   }
  }

  private void ProcessMessage(IntPtr hMsg, Stream dataStream)
  {
   long streamSize = dataStream.Length;
   if (streamSize == 0)
    throw new CryptographicException("Cannot encode zero length data");
   var gchandle = new GCHandle();
   const int ChunkSize = 1024 * 1024;
   var dwSize = (int)((streamSize < ChunkSize) ? streamSize : ChunkSize);
   var pbData = new byte[dwSize];

   try
   {
    var dwRemaining = streamSize;
    gchandle = GCHandle.Alloc(pbData, GCHandleType.Pinned);
    var pbPtr = gchandle.AddrOfPinnedObject();

    while (dwRemaining > 0)
    {
     dataStream.Read(pbData, 0, dwSize);

     // Update message piece by piece     
     var bResult = Win32.CryptMsgUpdate(hMsg, pbPtr, dwSize, (dwRemaining <= dwSize));
     if (!bResult)
     {
      throw new Exception(
       "CryptMsgUpdate error #" + Marshal.GetLastWin32Error().ToString(),
       new Win32Exception(Marshal.GetLastWin32Error()));
     }

     dwRemaining -= dwSize;
     if (dwRemaining < dwSize)
      dwSize = (int)dwRemaining;

     // if (gchandle.IsAllocated)
     //  gchandle.Free();
    }
   }
   finally
   {
    if (gchandle.IsAllocated)
    {
     gchandle.Free();
    }
   }
  }

  internal static Win32.CertHandle GetCertContext(X509Certificate2 certificate)
  {
   var handle = Win32.CertDuplicateCertificateContext(certificate.Handle);
   GC.KeepAlive(certificate);
   return handle;
  }

  internal static bool GetPrivateKeyInfo(Win32.CertHandle safeCertContext, out Win32.CRYPT_KEY_PROV_INFO parameters)
  {
   parameters = new Win32.CRYPT_KEY_PROV_INFO();
   var invalidHandle = new Win32.SafeHandle(IntPtr.Zero);
   uint pcbData = 0;
   if (!Win32.CertGetCertificateContextProperty(safeCertContext, 2, invalidHandle.DangerousGetHandle(), ref pcbData))
   {
    if (Marshal.GetLastWin32Error() != -2146885628)
    {
     throw new CryptographicException(Marshal.GetLastWin32Error());
    }

    return false;
   }

   invalidHandle = Win32.LocalAlloc(0, new IntPtr(pcbData));
   if (!Win32.CertGetCertificateContextProperty(safeCertContext, 2, invalidHandle.DangerousGetHandle(), ref pcbData))
   {
    if (Marshal.GetLastWin32Error() != -2146885628)
    {
     throw new CryptographicException(Marshal.GetLastWin32Error());
    }

    return false;
   }

   parameters = (Win32.CRYPT_KEY_PROV_INFO)Marshal.PtrToStructure(invalidHandle.DangerousGetHandle(), typeof(Win32.CRYPT_KEY_PROV_INFO));
   invalidHandle.Dispose();
   return true;
  }
 }
}
Win32.cs
using System;
using System.Runtime.InteropServices;

using Microsoft.Win32.SafeHandles;

namespace NS1
{
 [ComVisible(false)]
 public class Win32
 {
  #region "CONSTS"

  public const int X509_ASN_ENCODING = 0x00000001;

  public const int PKCS_7_ASN_ENCODING = 0x00010000;

  public const int CMSG_SIGNED = 2;

  public const int CMSG_DETACHED_FLAG = 0x00000004;

  public const int AT_KEYEXCHANGE = 1;

  public const int AT_SIGNATURE = 2;

  // public const string szOID_OIWSEC_sha1 = "1.3.14.3.2.26";

  public const int CMSG_CTRL_VERIFY_SIGNATURE = 1;

  public const int CMSG_CERT_PARAM = 12;

  public const int CMSG_SIGNER_CERT_INFO_PARAM = 7;

  public const int CERT_STORE_PROV_MSG = 1;

  public const int CERT_CLOSE_STORE_FORCE_FLAG = 1;

  #endregion

  #region "STRUCTS"

  [StructLayout(LayoutKind.Sequential)]
  [ComVisible(false)]
  public struct CRYPT_ALGORITHM_IDENTIFIER
  {
   public string pszObjId;
   public BLOB Parameters;
  }

  [StructLayout(LayoutKind.Sequential)]
  [ComVisible(false)]
  public struct CERT_ID
  {
   public int dwIdChoice;
   public BLOB IssuerSerialNumberOrKeyIdOrHashId;
  }

  [StructLayout(LayoutKind.Sequential)]
  [ComVisible(false)]
  public struct CMSG_SIGNER_ENCODE_INFO
  {
   public int cbSize;
   public IntPtr pCertInfo;
   public IntPtr hCryptProvOrhNCryptKey;
   public int dwKeySpec;
   public CRYPT_ALGORITHM_IDENTIFIER HashAlgorithm;
   public IntPtr pvHashAuxInfo;
   public int cAuthAttr;
   public IntPtr rgAuthAttr;
   public int cUnauthAttr;
   public IntPtr rgUnauthAttr;
   public CERT_ID SignerId;
   public CRYPT_ALGORITHM_IDENTIFIER HashEncryptionAlgorithm;
   public IntPtr pvHashEncryptionAuxInfo;
  }

  [StructLayout(LayoutKind.Sequential)]
  [ComVisible(false)]
  public struct CERT_CONTEXT
  {
   public int dwCertEncodingType;
   public IntPtr pbCertEncoded;
   public int cbCertEncoded;
   public IntPtr pCertInfo;
   public IntPtr hCertStore;
  }

  [StructLayout(LayoutKind.Sequential)]
  [ComVisible(false)]
  public struct BLOB
  {
   public int cbData;
   public IntPtr pbData;
  }

  [StructLayout(LayoutKind.Sequential)]
  [ComVisible(false)]
  public struct CMSG_SIGNED_ENCODE_INFO
  {
   public int cbSize;
   public int cSigners;
   public IntPtr rgSigners;
   public int cCertEncoded;
   public IntPtr rgCertEncoded;
   public int cCrlEncoded;
   public IntPtr rgCrlEncoded;
   public int cAttrCertEncoded;
   public IntPtr rgAttrCertEncoded;
  }

  [StructLayout(LayoutKind.Sequential)]
  [ComVisible(false)]
  public struct CMSG_STREAM_INFO
  {
   public int cbContent;
   public StreamOutputCallbackDelegate pfnStreamOutput;
   public IntPtr pvArg;
  }

  [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
  [ComVisible(false)]
  internal struct CRYPT_KEY_PROV_INFO
  {
   internal string pwszContainerName;
   internal string pwszProvName;
   internal uint dwProvType;
   internal uint dwFlags;
   internal uint cProvParam;
   internal IntPtr rgProvParam;
   internal uint dwKeySpec;
  }

  #endregion

  #region "DELEGATES"

  [ComVisible(false)]
  public delegate bool StreamOutputCallbackDelegate(IntPtr pvArg, IntPtr pbData, int cbData, Boolean fFinal);

  #endregion

  #region "API"

  [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
  [ComVisible(false)]
  public static extern Boolean CryptAcquireContext(
    ref IntPtr hProv,
    String pszContainer,
    String pszProvider,
    int dwProvType,
    int dwFlags);

  [DllImport("Crypt32.dll", SetLastError = true)]
  [ComVisible(false)]
  public static extern IntPtr CryptMsgOpenToEncode(
   int dwMsgEncodingType,
   int dwFlags,
   int dwMsgType,
   ref CMSG_SIGNED_ENCODE_INFO pvMsgEncodeInfo,
   String pszInnerContentObjID,
   ref CMSG_STREAM_INFO pStreamInfo);

  [DllImport("Crypt32.dll", SetLastError = true)]
  [ComVisible(false)]
  public static extern IntPtr CryptMsgOpenToDecode(
   int dwMsgEncodingType,
   int dwFlags,
   int dwMsgType,
   IntPtr hCryptProv,
   IntPtr pRecipientInfo,
   ref CMSG_STREAM_INFO pStreamInfo);

  [DllImport("Crypt32.dll", SetLastError = true)]
  [ComVisible(false)]
  public static extern Boolean CryptMsgClose(IntPtr hCryptMsg);

  [DllImport("Crypt32.dll", SetLastError = true)]
  [ComVisible(false)]
  public static extern Boolean CryptMsgUpdate(
   IntPtr hCryptMsg,
   Byte[] pbData,
   int cbData,
   Boolean fFinal);

  [DllImport("Crypt32.dll", SetLastError = true)]
  [ComVisible(false)]
  public static extern Boolean CryptMsgUpdate(
   IntPtr hCryptMsg,
   IntPtr pbData,
   int cbData,
   Boolean fFinal);

  [DllImport("Crypt32.dll", SetLastError = true)]
  [ComVisible(false)]
  public static extern Boolean CryptMsgGetParam(
   IntPtr hCryptMsg,
   int dwParamType,
   int dwIndex,
   IntPtr pvData,
   ref int pcbData);

  [DllImport("Crypt32.dll", SetLastError = true)]
  [ComVisible(false)]
  public static extern Boolean CryptMsgControl(
   IntPtr hCryptMsg,
   int dwFlags,
   int dwCtrlType,
   IntPtr pvCtrlPara);

  [DllImport("advapi32.dll", SetLastError = true)]
  [ComVisible(false)]
  public static extern Boolean CryptReleaseContext(
   IntPtr hProv,
   int dwFlags);

  [DllImport("Crypt32.dll", SetLastError = true)]
  [ComVisible(false)]
  public static extern IntPtr CertCreateCertificateContext(
   int dwCertEncodingType,
   IntPtr pbCertEncoded,
   int cbCertEncoded);

  [DllImport("Crypt32.dll", SetLastError = true)]
  [ComVisible(false)]
  public static extern bool CertFreeCertificateContext(IntPtr pCertContext);

  [DllImport("Crypt32.dll", SetLastError = true)]
  [ComVisible(false)]
  public static extern IntPtr CertOpenStore(
   int lpszStoreProvider,
   int dwMsgAndCertEncodingType,
   IntPtr hCryptProv,
   int dwFlags,
   IntPtr pvPara);

  [DllImport("Crypt32.dll", SetLastError = true)]
  [ComVisible(false)]
  public static extern IntPtr CertGetSubjectCertificateFromStore(
   IntPtr hCertStore,
   int dwCertEncodingType,
   IntPtr pCertId);

  [DllImport("Crypt32.dll", SetLastError = true)]
  [ComVisible(false)]
  public static extern IntPtr CertCloseStore(
   IntPtr hCertStore,
   int dwFlags);

  [DllImport("crypt32.dll", CharSet = CharSet.Auto, SetLastError = true)]
  internal static extern CertHandle CertDuplicateCertificateContext([In] IntPtr pCertContext);

  [DllImport("crypt32.dll", CharSet = CharSet.Auto, SetLastError = true)]
  internal static extern bool CertGetCertificateContextProperty([In] CertHandle pCertContext, [In] uint dwPropId, [In, Out] IntPtr pvData, [In, Out] ref uint pcbData);

  [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
  internal static extern SafeHandle LocalAlloc([In] uint uFlags, [In] IntPtr sizetdwBytes);

  [DllImport("kernel32.dll", SetLastError = true)]
  private static extern IntPtr LocalFree(IntPtr handle);

  #endregion

  public class SafeHandle : SafeHandleZeroOrMinusOneIsInvalid
  {
   public SafeHandle(IntPtr handle) : base(true)
   {
    SetHandle(handle);
   }


   public SafeHandle()
    : base(true)
   {
   }

   protected override bool ReleaseHandle()
   {
    return (LocalFree(handle) == IntPtr.Zero);
   }
  }

  public class CertHandle : SafeHandleZeroOrMinusOneIsInvalid
  {
   public CertHandle()
    : base(true)
   {
   }

   public CertHandle(bool ownsHandle)
    : base(ownsHandle)
   {
   }

   protected override bool ReleaseHandle()
   {
    return CertFreeCertificateContext(handle);
   }
  }
 }
}

суббота, 18 мая 2013 г.

Люди деньги и время

Забавный факт, что человек знает проценты, логарифмы, но обычно не умеет считать и не ценит своё время. Большинство людей, увидев батон хлеба за 20 рублей и зная что есть место где он стоит 10 рублей, возьмут тот, что за 10, даже если потратят кучу времени, чтобы добраться до дешёвого магазина.

При этом, покупая, скажем, телевизор, за 31000 руб и зная, что чуть постаравшись, можно взять его за 30000, не будут возиться и возьмут здесь и сейчас вариант, которой дороже. Разница-то в процентах небольшая! Хотя на сэкономленную тысячу можно тупо покупать дорогие батоны и не париться с поиском дешёвых.

То же самое и с оплатой всяких услуг — люди готовы час простоять в очереди на почте или в сбере (иногда ещё потратить деньги на транспорт, чтобы добраться до нужного отделения), вместо того, чтобы заплатить с небольшой комиссией через интернет-банк.

Что это значит в программировании? Если вы путём неимоверных усилий сократите расчёты с одного часа до 55 минут, никто этого и не заметит. А если с двух секунд до одной, то вы просто гениальные оптимизаторы. Имейте это ввиду, и вкладывайте ресурсы туда, где пользователи их оценят, даже если другие части будут совершенно неоптимальными. 

понедельник, 6 мая 2013 г.

Торренты vs лицензия

Я довольно-таки редко играю в игры, но если уж играю, и игра понравилась, то стараюсь купить лицензию. При этом играю в ломанную версию с торрентов, а лицензионный диск часто даже не распаковываю (как пример, рядом лежит диск с XCOM Enemy Unknown, который я достаточно долго искал где бы купить, но таки купил, ибо надо платить за хороший продукт.

Но недавно, в связи с выходом Heart of Swarm от близзарда решил не ждать кряков а просто купить лицензионный диск... Лучше бы я этого не делал.
По факту оказалось, что ещё надо докупить Wings of Liberty, без него работать ничего не будет, но это мой личный факап, и надо было внимательнее гуглить про это. Проблемы же начались гораздо раньше.

Во-первых, нужно завести аккаунт на Battle.net, указав о себе тучу личной информации, чуть ли не заставили указать цвет носков. Блин, я тупо хочу поиграть локально, не надо мне всё это! Но меня никто не спрашивал.
Минут 10 я потратил на придумывание пароля. Он был то слишком длинным, то слишком сложным (в нём нельзя использовать спецсимволы, но обязательно цифры и большие буквы, ну что за бред?). Письмо с подтверждением регистрации я получил только через час. Т.е. я сижу с диском, который не работает и ничего не могу сделать.

Но в коцне-концов зарегистрировался, выяснил, что надо купить ещё одну игру, я её купил и... И я теперь могу играть в Wings of Liberty! Но не могу играть в Heart of Swarm, потому что близзарду нужно 72 часа, чтобы что-то там сделать у себя перед тем как вводить ключ из коробки с диском. Пипец. Приехали. Я купил диск, купил ещё одну игру, но должен ждать, пока что-то там произойдёт.

Слава яйцам, прошли всего сутки и я смог ввести этот ключ. Дальше всё почти хорошо, кроме постоянного ввода пароля для игры (блин, где галка "запомнить пароль", а?).

Но... вчера произошла история, которая и заставила меня написать этот пост. Ибо мы решили посмотреть, как Starcraft ведёт себя на маке, а в результате я получил заблокированный аккаунт. Приехали. Не понравилась близзарду моя активность, и вместо письма со ссылкой — если это ты, нажми сюда, мне просто заблокировали аккаунт. Хорошо хоть, что можно пройти стандартную процедуру восстановления пароля и разблокировать его, но что блин, за наезд? Нам не понравился твой компьютер, мы тебе больше не дадим играть!

В общем, после такого, только торренты! Там нет никаких идиотских защит, не надо постоянно вводить пароли и выслушивать предупреждения о том, как я незащищён. Я просто хочу запустить игру и начать играть! А лицензии так и буду покупать задним числом, а коробку и распаковывать не буду. Всё равно внутри ничего полезного нет.

Update: Мой аккаунт блокировали 3 раза, пока не добавил мобильный аутентификатор.

суббота, 6 апреля 2013 г.

Странные изменения в Такси

Уже постепенно перехожу на сервисы по заказу такси через интернет со смартфона, но вчера решил позвонить обычным способом и заказать машину. Удивлению моему не было предела. По какой-то причине у такси сменился алгоритм заказа (я в 3 звонил), и появились идиотские информаторы. Теперь по-порядку.

Раньше, я звонил, говорил: "Хочу машину из пункта А в пункт Б". Мне уточняли по срокам и принимали заказ. Сейчас политика изменилась на: "Я вам перезвоню" без уточнения заказа. Т.е. тебя сразу ставят уже в позу ожидания, хотя ты даже не выяснил будут ли принимать заказ.

Возможно, они думали, что я на этом остановлюсь... Хех... я позвонил в 3 такси. Ну а чего, с таким качеством информирования об услуге — сами напросились.

Потом оказалось, что 20 минут меня дожидалось одно из такси, при этом по телефону говорили что машина едет, а с водителем связаться не получалось. Это уже просто качество сервиса говно.

Дальше началось интересно. Я уже ехал на машине, как всё-таки одни позвонили и уточнили, нужна ли ещё машина. А вот вторые 3 раза звонили роботом чтобы сказать: Вашей машины ещё нет! Хех, я только в третий раз дослушал, оказывается, чтобы отказаться нужно нажать 1, а если просто отбиться, это означает, что ты ждёшь. Да не жду я уже! Впрочем, спустя час мне всё-таки отправили смс и позвонили голосом, что машина приехала. Я уже был дома. Наверное стал даже стал плохим клиентом.

В общем, жду с нетерпением, когда можно у меня в городе заказывать Такси через телефон с выбором. Ибо несмотря на удобство, выбора у нас нет (всего 2 службы такси и заказываются разными программами).

среда, 27 марта 2013 г.

HttpWebRequest некоторые неочевидности

Я последнее время достаточно много работал с классом HttpWebRequest (позволяет делать запросы к серверу на .NET) и столкнулся с некоторыми моментами, которые весьма неочевидны, но о которых следует знать, если им активно пользоваться.


  • Сколько бы вы не создали клиентов, в реальности одновременно к серверу будут идти 2 запроса, остальные попадут в очередь. Планируйте это при реализации параллельности. Изменить количество можно установив ServicePoint.ConnectionLimit или уникальный ConnectionGroupName
  • Балансировка запросов в настоящий момент достаточно туповатая, поэтому, если вам в принципе достаточно небольшого количества одновременных коннекций, но некоторые могут занимать долгое время, вы можете получить нехорошую проблему: запрос зависнет в очереди. Происходит это тогда, когда кончаются все доступные коннекшены, запрос уходит в очередь к какому-либо из них, и продолжится когда тот закончится. Если при этом освобождаются другие — это ни на что не влияет. Т.е. вы можете получить пиковую нагрузку, очередь и толпу отвалившихся запросов, в случае если один будет долгим, а другие пристроятся ему в хвост.
  • Timeout на самом деле ограничивает время выполнения всего запроса, а не ожидания данных. Т.е. если вы не спеша получаете файл в 10Gb, вам плюнется таймаут. Не забудьте увеличить. А проблемы с долгим соединением решайте вручную парой Begin/End
  • Если вы используете Begin/End (BeginGetRequestStream, например), и решаете забросить запрос в случае долгого ответа, не забудьте сделать Abort, а то запрос может дойти, когда его уже никто не ждёт и не хочет обрабатывать
  • Несмотря на возможность установить AllowWriteStreamBuffering в false, память всё равно будет сжираться где-то в его недрах (возможно поправят поведение, ибо кажется багом, т.к. данные уходят по факту), так что при передаче гигабайт данных для надёжноти включите чанкинг
  • Размер данных, при которых будет отправка не регулируется, опытным путём установлено, что при чанкинге он 1024 байта. Т.е. если вы хотите медленно посылать небольшие порции данных на сервер, вы можете посылать их тупо в буфер, так что лучше не делать подобное и посылать всё целиком (получать данные с сервера маленькими кусками можно)
  • Если на сервере используется недоверенный https сертификат, то чтобы не получать ошибку соединения, надо подписаться на ServicePointManager.ServerVerificationCallback и там проверять сертификат на корректность. Метод статичный, так что следите за тем, чтобы не перехватывать чужие запросы (если в вашем приложении ещё кто-то посылает данные)
  • HttpWebRequest использует своё кеширование DNS, по умолчанию 2 минуты. Что странно, т.к. резолв идёт через системный DNS-кеш, т.е. накладных расходов не происходит. Можно изменить это время в ServicePointManager.DnsRefreshTimeout, и аккуратнее с KeepAlive, т.к. при его наличии будет держаться коннекшен к серверу некоторе время, и резолва не произойдёт.
  • Если сервер вам посылает данные очень медленно, то проверить не умер ли он, можно установив ServicePoint.SetTcpKeepAlive, что позволит пинговать сервер средствами TCP, и получить ошибку в случае если сервер реально пропал.
На этом пока всё, если будут ещё интересные моменты, то буду обновлять пост.

пятница, 8 февраля 2013 г.

История про винду поганую и крипто-про не лучше

Жил был компьютер. И жил он долго и счастливо, и решил владелец обновить его, да с седьмой винды, да на восьмую.
И обновился компьютер, и сказал, буду я работать и всё у меня будет хорошо, но не буду я апдейты ставить. Ибо 800B0001!

И пошёл владелец папки catroot2 да SoftwareDistribution удалять, очередь у службы bits вычищать, регистрировать и перерегистрировать COM-компоненты. Да не помогло это ни хрена. А гугл буржуйский только тех же страдальцев показывает, да переустановить систему с нуля предлагает.
И выяснил тут совершенно случайно владелец, что CryptoPro, продукт православный может козни подобные строить, и удалил он этот продукт с концами. И заработали апдейты, и поставились патчи, и заговорила Винда человеческим голосом: "Не выключай меня, добрый молодец, ибо обновляюсь я!". И всё стало хорошо до поры до времени.

Суть да дело, а геморроище злобное подобралось со стороны неожиданной (что для геморроев действительно неожиданно). Выяснил добрый молодец, что оказывается весь SSL'юшка сломан на виндоузе да на восьмом. Точнее не весь, а ровно половина: исходящие пакеты уходят, а локально опять же да и не работает ни хрена, как и было ранее сказано, да про апдейты. Причём, что презобавненько, коннекшен вроде бы устанавливается, сертификат отдаётся, ну а дальше-то жопенько: Сервер неожиданно взял да и разорвал соединение.

Но не лыком был шит добрый молодец, помнил он ещё про крипто про подлое, ну да делать нечего, ставить крипто про надо заново. Авось да починится.
И пошёл добрый молодец крипто про ставить злобное. Семь дней и ночей ставил он крипто про без устали, а ему в лицо мерзость всякая падала: дескать, сервис мой, да интерактивный, и работать он и не будет, а и драйвер мой удаляется, да не может удалиться, ибо процесс идёт установочный!

Но одолел страшилище добрый молодец, и включился тут SSL православный и апдейты, и те не сломались!

И я там был, винду материл, крипто про материл, всё подряд материл, да невыматериволся!

вторник, 5 февраля 2013 г.

Программы для восстановления полноценной кнопки Пуск в Windows 8

Я как-то писал, что сделал суррогатную замену кнопки Пуск, но в реальности, конечно жить с этим трудно и подходит только для узкого редкого применения в виртуалках, когда кнопки Win тупо нет.

Собственно, люди уже написали просто гору различных программ, которые восстанавливают или эмулируют эту кнопку, и почему бы их и не использовать?

В общем, я проверил около десятка программ, которые восстанавливают именно классическое семёрочное меню, те, которые делают что-то своё не рассматривал. Например, весьма популярная Classic Shell имеет хороший функционал, бесплатная, но меню совершенно другое, так что для моих условий не подошла. Т.е. я упростил себе задачу и мне не пришлось перебирать 30 программ. Всего лишь десяток.  

В общем, большинство решений пришлось сразу отбросить. Причины:
  1. У авторов нет вкуса и выглядит всё ущербно
  2. Совсем непохоже или сильно непохоже в мелочах на стандартное меня
  3. Глючит, тормозит и тупит
И по факту оказалось, что достойны внимания всего 3 программы, две из которых платные, а одна хак. 

Собственно делюсь:

Ex7forW8

ссылка

Это, собственно хак, который заключается в запуске проводника из Windows 7 под Windows 8. В результате исчезает вся восьмёрочность, и выглядит всё классически. Пользовался я им мало, поскольку восьмёрочность всё-таки иногда нужна, но особых глюков не заметил.

StartIsBack

ссылка

Судя по всему сделана русским разработчиком. Триал на 30 дней, программа стоит $3 за 2 лицензии (т.е. на двух компьютерах можно поставить). Т.е. вполне бюджетно. По внешнему виду выглядит почти идентично семёрочному, но есть скины, которые могут сделать небольшой микс в сторону восьмёрки, и результат будет совсем замечательным. Проблем не выявлено.

Start8

ссылка

Очень навороченный продукт от StarDock с кучей скинов и настроек. В общем, рюшечек хоть отбавляй. Стоит $5 за одну лицензию. Проблем не выявлено. Работает как надо.




Есть куча других бесплатных аналогов, но как я говорил уже — в них какие-то недостатки есть, которые при работе весьма раздражают, а если уж восстанавливать кнопку пуск, то надо сделать её максимально "правильной", чтобы не раздражала и не увеличивала количество ненависти во вселенной.