From bfbb807dead870e215e2628ee2e9201f7f34789f Mon Sep 17 00:00:00 2001 From: Dmitrii Evdokimov Date: Wed, 3 Jul 2024 22:15:56 +0300 Subject: [PATCH] Add logs, creds, args --- FinCERT-Client/API/Feeds.cs | 2 + FinCERT-Client/Config.cs | 5 + FinCERT-Client/FinCERT-Client.csproj | 1 + FinCERT-Client/Managers/BulletinsManager.cs | 249 +++++++++++------- FinCERT-Client/Managers/ConfigManager.cs | 3 +- FinCERT-Client/Managers/CredentialManager.cs | 229 ++++++++++++++++ FinCERT-Client/Managers/FeedsManager.cs | 13 +- FinCERT-Client/Managers/Helper.cs | 33 ++- FinCERT-Client/Program.cs | 133 ++++++++-- FinCERT-Client/Properties/launchSettings.json | 7 + README.md | 45 +++- 11 files changed, 591 insertions(+), 129 deletions(-) create mode 100644 FinCERT-Client/Managers/CredentialManager.cs create mode 100644 FinCERT-Client/Properties/launchSettings.json diff --git a/FinCERT-Client/API/Feeds.cs b/FinCERT-Client/API/Feeds.cs index 5ae345c..1c174ed 100644 --- a/FinCERT-Client/API/Feeds.cs +++ b/FinCERT-Client/API/Feeds.cs @@ -58,6 +58,8 @@ public static async Task DownloadFeedsAsync(FeedType feed, string path) { try { + Directory.CreateDirectory(path); + var response = await TlsClient.GetAsync($"antifraud/feeds/{feed}/download"); response.EnsureSuccessStatusCode(); diff --git a/FinCERT-Client/Config.cs b/FinCERT-Client/Config.cs index 9c21b91..e949823 100644 --- a/FinCERT-Client/Config.cs +++ b/FinCERT-Client/Config.cs @@ -50,4 +50,9 @@ public class Config /// Папка для сохранения полученных файлов бюллетеней. /// public string BulletinsDownloads { get; set; } = nameof(BulletinsDownloads); + + /// + /// Папка для ведения логов. + /// + public string Logs { get; set; } = nameof(Logs); } diff --git a/FinCERT-Client/FinCERT-Client.csproj b/FinCERT-Client/FinCERT-Client.csproj index 863f353..b2d9a55 100644 --- a/FinCERT-Client/FinCERT-Client.csproj +++ b/FinCERT-Client/FinCERT-Client.csproj @@ -15,6 +15,7 @@ Diev https://github.com/diev/FinCERT-Client https://github.com/diev/FinCERT-Client.git + true diff --git a/FinCERT-Client/Managers/BulletinsManager.cs b/FinCERT-Client/Managers/BulletinsManager.cs index c86a553..b6fa24c 100644 --- a/FinCERT-Client/Managers/BulletinsManager.cs +++ b/FinCERT-Client/Managers/BulletinsManager.cs @@ -17,6 +17,7 @@ limitations under the License. */ #endregion +using System.Diagnostics; using System.Text; using API; @@ -28,6 +29,12 @@ namespace FincertClient.Managers; internal static class BulletinsManager { + /// + /// Получение комплекта файлов и папок для прохождения чек-листа. + /// + /// Путь к папке для формируемого комплекта. + /// Ограничение на несколько записей для примера (4). + /// Комплект получен. public static async Task GetCheckList(string path, int limit = 4) { try @@ -67,126 +74,180 @@ public static async Task GetCheckList(string path, int limit = 4) return false; } - public static async Task LoadBulletinsList(string path, int limit = 100, long offset = 0) + /// + /// Неполноценный вариант загрузки бюллетеней, т.к. нет возможности получить + /// из него дополнительные файлы. + /// Получается список, из него составляется список для получения бюллетеней, + /// а в них нет информации о дополнительных файлах - только id основного. + /// Видимо, это только для отображения таблицы бюллетеней в интерфейсе. + /// + /// Путь к папке, где будет создан файл 'Bulletins.txt'. + /// Если он существует, то он будет перезаписан. + /// Ограничение на число бюллетеней (1-100), максимум 100. + /// Смещение по списку бюллетеней вглубь истории (0 - без смещения). + /// Процесс дошел до конца. + public static async Task LoadBulletinsList(string path, int limit = 100, long offset = 0) { - Console.WriteLine("Получение бюллетеней списком..."); - - var ids = await GetBulletinIdsAsync(limit, offset); - - if (ids is null) + try { - Console.WriteLine("Список бюллетеней не получен."); - Environment.Exit(1); - } + Directory.CreateDirectory(path); - int count = ids.Items.Length; - string[] ids2 = new string[count]; + Trace.WriteLine("Получение бюллетеней списком..."); - Console.WriteLine($"Всего: {ids.Total}, в листе: {count}."); + var ids = await GetBulletinIdsAsync(limit, offset); - for (int i = 0; i < count; i++) - { - ids2[i] = ids.Items[i].Id; - } + if (ids is null) + { + Trace.WriteLine("Список бюллетеней не получен."); + Environment.Exit(1); + } - var BulletinInfos = await GetBulletinInfosAsync(ids2); - StringBuilder sb = new(); + int count = ids.Items.Length; + string[] ids2 = new string[count]; - if (BulletinInfos is null) - { - Console.WriteLine("Список информации по бюллетеням не получен."); - Environment.Exit(1); - } + Trace.WriteLine($"Всего: {ids.Total}, в листе: {count}."); - foreach (var bulletin in BulletinInfos.Items) - { - var id = bulletin.Id; - var date = DateTime.Parse(bulletin.PublishedDate); - - sb.AppendLine(new string('-', 36)) - .AppendLine(id) - .AppendLine($"Дата публикации: {date:g}") - .AppendLine($"Идентификатор: {bulletin.Hrid}") - .AppendLine($"Заголовок: {bulletin.Header}") - .AppendLine($"Тип рассылки: {bulletin.Type}") - .AppendLine($"Подтип рассылки: {bulletin.Subtype}"); - - if (bulletin.AttachmentId != null) + for (int i = 0; i < count; i++) { - sb.AppendLine($"Файл рассылки: {bulletin.AttachmentId}"); - //string name = "attachment.dat"; - //await Bulletins.DownloadAttachmentAsync(bulletin.AttachmentId, PathCombine(dir, name)); + ids2[i] = ids.Items[i].Id; } - sb.AppendLine($"Описание: {bulletin.Description}"); - } - - string text = sb.ToString(); - string file = Path.Combine(path, "Bulletins.txt"); - await File.WriteAllTextAsync(file, text); + var BulletinInfos = await GetBulletinInfosAsync(ids2); + StringBuilder sb = new(); - Console.WriteLine(text); - } + if (BulletinInfos is null) + { + Trace.WriteLine("Список информации по бюллетеням не получен."); + Environment.Exit(1); + } - public static async Task LoadBulletinsDirs(string path, int limit = 100, long offset = 0) - { - Console.WriteLine("Получение бюллетеней по папкам..."); + foreach (var bulletin in BulletinInfos.Items) + { + var id = bulletin.Id; + var date = DateTime.Parse(bulletin.PublishedDate); + + sb.AppendLine(new string('-', 36)) + .AppendLine(id) + .AppendLine($"Дата публикации: {date:g}") + .AppendLine($"Идентификатор: {bulletin.Hrid}") + .AppendLine($"Заголовок: {bulletin.Header}") + .AppendLine($"Тип рассылки: {bulletin.Type}") + .AppendLine($"Подтип рассылки: {bulletin.Subtype}"); + + if (bulletin.AttachmentId != null) + { + sb.AppendLine($"Файл рассылки: {bulletin.AttachmentId}"); + //string name = "attachment.dat"; + //await Bulletins.DownloadAttachmentAsync(bulletin.AttachmentId, PathCombine(dir, name)); + } + + sb.AppendLine($"Описание: {bulletin.Description}"); + } - var ids = await GetBulletinIdsAsync(limit, offset); + string text = sb.ToString(); + string file = Path.Combine(path, "Bulletins.txt"); + await File.WriteAllTextAsync(file, text); - if (ids is null) - { - Console.WriteLine("Список бюллетеней не получен."); - Environment.Exit(1); + Console.WriteLine(text); + return true; } + catch { } + return false; + } - Console.WriteLine($"Всего: {ids.Total}, в листе: {ids.Items.Length}."); - - foreach (var itemId in ids.Items) + /// + /// Полноценный вариант загрузки бюллетеней со всеми файлами. + /// Получается список, по нему запрашиваются поштучно бюллетени, для них - файлы. + /// + /// Путь к папке для скачиваний. + /// Ограничение на число бюллетеней (1-100), максимум 100. + /// Смещение по списку бюллетеней вглубь истории (0 - без смещения). + /// Процесс дошел до конца. + public static async Task LoadBulletinsDirs(string path, int limit = 100, long offset = 0) + { + try { - var id = itemId.Id; - var bulletin = await GetBulletinAttachInfoAsync(id); - var date = DateTime.Parse(bulletin!.PublishedDate); - - // Dirname generator! Do not change it to prevent an unwanted full reload! - - var dir = PathCombine(path, $"{date:yyyy-MM-dd HHmm} {bulletin.Hrid.Trim()}"); + Directory.CreateDirectory(path); - if (Directory.Exists(dir)) break; + Trace.WriteLine("Получение бюллетеней по папкам..."); - Directory.CreateDirectory(dir); + var ids = await GetBulletinIdsAsync(limit, offset); - StringBuilder sb = new(); - sb.AppendLine(id) - .AppendLine($"Дата публикации: {date:g}") - .AppendLine($"Идентификатор: {bulletin.Hrid}") - .AppendLine($"Заголовок: {bulletin.Header}") - .AppendLine($"Тип рассылки: {bulletin.Type}") - .AppendLine($"Подтип рассылки: {bulletin.Subtype}"); - - if (bulletin.Attachment != null) + if (ids is null) { - string name = bulletin.Attachment.Name; - sb.AppendLine($"Файл рассылки: {name}"); - await DownloadAttachmentAsync(bulletin.Attachment.Id, Path.Combine(dir, name)); + Trace.WriteLine("Список бюллетеней не получен."); + Environment.Exit(1); } - int i = 0; - foreach (var additional in bulletin.AdditionalAttachments) + Trace.WriteLine($"Всего: {ids.Total}, в листе: {ids.Items.Length}."); + + foreach (var itemId in ids.Items) { - string name = additional.Name; - sb.AppendLine($"Доп. файл {++i}: {name}"); - await DownloadAttachmentAsync(additional.Id, Path.Combine(dir, name)); + var id = itemId.Id; + var bulletin = await GetBulletinAttachInfoAsync(id); + var date = DateTime.Parse(bulletin!.PublishedDate); + + // Генератор имени папки - не изменяйте его, чтобы не скачать заново лишнее! + + var name = CorrectName($"{date:yyyy-MM-dd HHmm} {bulletin.Hrid.Trim()}"); + var dir = Path.Combine(path, name); + + // Встречена ранее скачанная папка + if (Directory.Exists(dir)) + { + if (offset == 0) + { + // Скачивание прекращается + Trace.WriteLine($"{name} // конец скачивания."); + break; + } + else + { + // Ранее скачанная папка пропускается + Trace.WriteLine($"{name} // есть"); + continue; + } + } + + // Создается новая папка + Trace.WriteLine(name); + Directory.CreateDirectory(dir); + + StringBuilder sb = new(); + sb.AppendLine(id) + .AppendLine($"Дата публикации: {date:g}") + .AppendLine($"Идентификатор: {bulletin.Hrid}") + .AppendLine($"Заголовок: {bulletin.Header}") + .AppendLine($"Тип рассылки: {bulletin.Type}") + .AppendLine($"Подтип рассылки: {bulletin.Subtype}"); + + if (bulletin.Attachment != null) + { + string filename = bulletin.Attachment.Name; + sb.AppendLine($"Файл рассылки: {filename}"); + await DownloadAttachmentAsync(bulletin.Attachment.Id, Path.Combine(dir, filename)); + } + + int i = 0; + foreach (var additional in bulletin.AdditionalAttachments) + { + string filename = additional.Name; + sb.AppendLine($"Доп. файл {++i}: {filename}"); + await DownloadAttachmentAsync(additional.Id, Path.Combine(dir, filename)); + } + + sb.AppendLine($"Описание: {bulletin.Description}"); + + string text = sb.ToString(); + string file = Path.Combine(dir, bulletin.Type + ".txt"); + await File.WriteAllTextAsync(file, text); + + Console.WriteLine(new string('-', 36)); + Console.WriteLine(text); } - - sb.AppendLine($"Описание: {bulletin.Description}"); - - string text = sb.ToString(); - string file = Path.Combine(dir, bulletin.Type + ".txt"); - await File.WriteAllTextAsync(file, text); - - Console.WriteLine(new string('-', 36)); - Console.WriteLine(text); + return true; } + catch { } + return false; } } diff --git a/FinCERT-Client/Managers/ConfigManager.cs b/FinCERT-Client/Managers/ConfigManager.cs index 57421ff..b34a046 100644 --- a/FinCERT-Client/Managers/ConfigManager.cs +++ b/FinCERT-Client/Managers/ConfigManager.cs @@ -17,6 +17,7 @@ limitations under the License. */ #endregion +using System.Diagnostics; using System.Text.Json; namespace FincertClient.Managers; @@ -46,7 +47,7 @@ public static Config Read() using var write = File.OpenWrite(appsettings); JsonSerializer.Serialize(write, new Config(), GetJsonOptions()); - Console.WriteLine(@$"Создан новый файл настроек ""{appsettings}"" - откорректируйте его."); + Trace.WriteLine(@$"Создан новый файл настроек ""{appsettings}"" - откорректируйте его."); throw new Exception(); } catch diff --git a/FinCERT-Client/Managers/CredentialManager.cs b/FinCERT-Client/Managers/CredentialManager.cs new file mode 100644 index 0000000..ddee818 --- /dev/null +++ b/FinCERT-Client/Managers/CredentialManager.cs @@ -0,0 +1,229 @@ +#region License +/* +Copyright 2022-2024 Dmitrii Evdokimov +Open source software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion + +using System.ComponentModel; +using System.Runtime.InteropServices; +using System.Text; + +using Microsoft.Win32.SafeHandles; + +namespace FincertClient.Managers; +using static NativeMethods; + +public enum CredentialType +{ + Generic = 1, + DomainPassword, + DomainCertificate, + DomainVisiblePassword, + GenericCertificate, + DomainExtended, + Maximum, + MaximumEx = Maximum + 1000, +} + +/// +/// Windows Credential Manager credential +/// +/// +/// +/// +/// +public record Credential(CredentialType CredentialType, string TargetName, string? UserName, string? Password) +{ + public override string ToString() + => $"CredentialType: {CredentialType}, TargetName: {TargetName}, UserName: {UserName}, Password: {Password}"; +} + +internal static class CredentialManager +{ + public static Credential ReadCredential(string targetName) + { + if (targetName.Contains('*')) + { + if (CredEnumerate(targetName, 0, out int count, out nint pCredentials)) + { + if (count > 1) + throw new Exception($"Windows Credential Manager has more '{targetName}' entries ({count})."); + + nint credential = Marshal.ReadIntPtr(pCredentials, 0); + var cred = Marshal.PtrToStructure(credential, typeof(NativeCredential)); + return ReadFromNativeCredential((NativeCredential)cred!); + } + + throw new Exception($"Windows Credential Manager has no '{targetName}' entries."); + } + + if (CredRead(targetName, CredentialType.Generic, 0, out nint nCredPtr)) + { + using CriticalCredentialHandle critCred = new(nCredPtr); + var cred = critCred.GetNativeCredential(); + return ReadFromNativeCredential(cred); + } + + throw new Exception($"Windows Credential Manager has no '{targetName}' entries."); + } + + public static void WriteCredential(string targetName, string userName, string secret) + { + NativeCredential credential = new() + { + AttributeCount = 0, + Attributes = nint.Zero, + Comment = nint.Zero, + TargetAlias = nint.Zero, + Type = CredentialType.Generic, + Persist = (uint)CredentialPersistence.LocalMachine, + CredentialBlobSize = 0, + TargetName = Marshal.StringToCoTaskMemUni(targetName), + CredentialBlob = Marshal.StringToCoTaskMemUni(secret), + UserName = Marshal.StringToCoTaskMemUni(userName ?? Environment.UserName) + }; + + if (secret != null) + { + byte[] byteArray = Encoding.Unicode.GetBytes(secret); + + if (byteArray.Length > 512 * 5) + throw new ArgumentOutOfRangeException(nameof(secret), "The secret message has exceeded 2560 bytes."); + + credential.CredentialBlobSize = (uint)byteArray.Length; + } + + bool written = CredWrite(ref credential, 0); + + Marshal.FreeCoTaskMem(credential.TargetName); + Marshal.FreeCoTaskMem(credential.CredentialBlob); + Marshal.FreeCoTaskMem(credential.UserName); + + if (!written) + { + int lastError = Marshal.GetLastWin32Error(); + throw new Exception(string.Format("CredWrite failed with the error code {0}.", lastError)); + } + } + + public static Credential[] EnumerateCrendentials(string? filter = null) + { + if (CredEnumerate(filter, 0, out int count, out nint pCredentials)) + { + Credential[] result = new Credential[count]; + + for (int n = 0; n < count; n++) + { + nint credential = Marshal.ReadIntPtr(pCredentials, n * Marshal.SizeOf(typeof(nint))); + var cred = Marshal.PtrToStructure(credential, typeof(NativeCredential)); + result[n] = ReadFromNativeCredential((NativeCredential)cred!); + } + + return result; + } + + int lastError = Marshal.GetLastWin32Error(); + throw new Win32Exception(lastError); + } + + private enum CredentialPersistence : uint + { + Session = 1, + LocalMachine, + Enterprise + } + + private static Credential ReadFromNativeCredential(NativeCredential credential) + { + string targetName = Marshal.PtrToStringUni(credential.TargetName)!; + string? userName = Marshal.PtrToStringUni(credential.UserName); + string? secret = null; + + if (credential.CredentialBlob != nint.Zero) + { + secret = Marshal.PtrToStringUni(credential.CredentialBlob, (int)credential.CredentialBlobSize / 2); + } + + return new Credential(credential.Type, targetName, userName, secret); + } + + sealed class CriticalCredentialHandle : CriticalHandleZeroOrMinusOneIsInvalid + { + public CriticalCredentialHandle(nint preexistingHandle) + { + SetHandle(preexistingHandle); + } + + public NativeCredential GetNativeCredential() + { + if (!IsInvalid) + { + var cred = Marshal.PtrToStructure(handle, typeof(NativeCredential)); + return (NativeCredential)cred!; + } + + throw new InvalidOperationException("Invalid CriticalHandle!"); + } + + protected override bool ReleaseHandle() + { + if (!IsInvalid) + { + CredFree(handle); + SetHandleAsInvalid(); + + return true; + } + + return false; + } + } +} + +internal static partial class NativeMethods +{ + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct NativeCredential + { + public uint Flags; + public CredentialType Type; + public nint TargetName; + public nint Comment; + public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten; + public uint CredentialBlobSize; + public nint CredentialBlob; + public uint Persist; + public uint AttributeCount; + public nint Attributes; + public nint TargetAlias; + public nint UserName; + } + + [LibraryImport("Advapi32.dll", EntryPoint = "CredReadW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool CredRead(string target, CredentialType type, int reservedFlag, out nint credentialPtr); + + [DllImport("Advapi32.dll", EntryPoint = "CredWriteW", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern bool CredWrite(ref NativeCredential userCredential, uint flags); //TODO + + [LibraryImport("Advapi32", EntryPoint = "CredEnumerateW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool CredEnumerate(string? filter, int flag, out int count, out nint pCredentials); + + [LibraryImport("Advapi32.dll", EntryPoint = "CredFree", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool CredFree(nint cred); +} diff --git a/FinCERT-Client/Managers/FeedsManager.cs b/FinCERT-Client/Managers/FeedsManager.cs index a984a67..c6fb383 100644 --- a/FinCERT-Client/Managers/FeedsManager.cs +++ b/FinCERT-Client/Managers/FeedsManager.cs @@ -17,6 +17,8 @@ limitations under the License. */ #endregion +using System.Diagnostics; + using API; using static API.Feeds; @@ -27,10 +29,9 @@ internal static class FeedsManager { public static async Task LoadFeeds(string path) { - Console.WriteLine("Получение фидов..."); + Trace.WriteLine("Получение фидов..."); - if (!string.IsNullOrEmpty(path)) - Directory.CreateDirectory(path); + Directory.CreateDirectory(path); foreach (var feed in Enum.GetValues()) { @@ -39,7 +40,7 @@ public static async Task LoadFeeds(string path) if (status is null) { - Console.WriteLine($"Статус {feed} не получен."); + Trace.WriteLine($"Статус {feed} не получен."); continue; } @@ -52,12 +53,12 @@ public static async Task LoadFeeds(string path) */ var date = DateTime.Parse(status.UploadDatetime); - Console.WriteLine($"{date:g} {feed}"); + Trace.WriteLine($"{date:g} {feed}"); // Выгрузить из АСОИ ФинЦЕРТ нужный тип фидов if (!await DownloadFeedsAsync(feed, path)) { - Console.WriteLine($"Содержимое {feed} не получено."); + Trace.WriteLine($"Содержимое {feed} не получено."); continue; } } diff --git a/FinCERT-Client/Managers/Helper.cs b/FinCERT-Client/Managers/Helper.cs index d7efd09..e7ea4f7 100644 --- a/FinCERT-Client/Managers/Helper.cs +++ b/FinCERT-Client/Managers/Helper.cs @@ -17,13 +17,42 @@ limitations under the License. */ #endregion +using System.Diagnostics; + namespace FincertClient.Managers; internal static class Helper { - public static string PathCombine(string dir, string name) + public static string CorrectName(string name) { string file = string.Concat(name.Split(Path.GetInvalidFileNameChars())).Trim(); - return Path.Combine(dir, file.Length > 0 ? file : "--"); + return file.Length > 0 ? file : "--"; + } + + public static string GetLogFile(string? logDirectory) + { + string logs = logDirectory ?? nameof(logs); + var now = DateTime.Now; + var logsPath = Directory.CreateDirectory(Path.Combine(logs, now.ToString("yyyy"))); + var file = Path.Combine(logsPath.FullName, now.ToString("yyyyMMdd") + ".log"); + + Console.WriteLine(@$"Лог пишется в файл ""{file}""."); + + return file; + } + + public static void TraceError(string message, Exception ex) + { + Console.WriteLine("Вывод информации об ошибке."); + + Trace.WriteLine($"{DateTime.Now:G} {message}: {ex.Message}."); + string text = $"{DateTime.Now:G} Exception:{Environment.NewLine}{ex}{Environment.NewLine}"; + + if (ex.InnerException != null) + { + text += $"Inner Exception:{Environment.NewLine}{ex.InnerException}{Environment.NewLine}"; + } + + File.AppendAllText("error.log", text); } } diff --git a/FinCERT-Client/Program.cs b/FinCERT-Client/Program.cs index be86c49..3e5fa8f 100644 --- a/FinCERT-Client/Program.cs +++ b/FinCERT-Client/Program.cs @@ -17,6 +17,10 @@ limitations under the License. */ #endregion +#define TRACE + +using System.Diagnostics; + using API; using FincertClient.Managers; @@ -29,33 +33,124 @@ internal class Program static async Task Main(string[] args) { + // defaults + bool checklist = false; + int limit = 100; + long offset = 0; + Console.WriteLine("Hello, World!"); // :) - // Подключиться к АСОИ ФинЦЕРТ - if (!await TlsClient.Login(_config.Tls)) - { - Console.WriteLine("Ошибка подключения к серверу."); - Environment.Exit(1); - } + // Console Trace + using ConsoleTraceListener ConTracer = new() { Name = nameof(ConTracer) }; + Trace.Listeners.Add(ConTracer); + + // Log Trace + string log = Helper.GetLogFile(_config.Logs); + using TextWriterTraceListener FileTracer = new(log, nameof(FileTracer)); + Trace.Listeners.Add(FileTracer); + Trace.AutoFlush = true; - if (args.Length > 0 && args.Contains("-checklist")) + try { - await BulletinsManager.GetCheckList( - Path.Combine(_config.BulletinsDownloads, "CheckList")); - } + // Опциональные параметры + for (int i = 0; i < args.Length; i++) + { + string cmd = args[i]; + + switch (cmd.ToLower()) + { + case "-checklist": + checklist = true; + Trace.WriteLine(cmd); + break; + + case "-limit": + if (args.Length > i) + limit = int.Parse(args[++i]); + Trace.WriteLine($"{cmd} {limit}"); + break; + + case "-offset": + if (args.Length > i) + offset = long.Parse(args[++i]); + Trace.WriteLine($"{cmd} {offset}"); + break; + + default: + Trace.WriteLine($"Непонятная команда '{cmd}'."); + Environment.Exit(1); + break; + } + } + + // Конфиг + Trace.WriteLine("Получение учетных данных..."); + var tls = _config.Tls; - if (_config.Feeds) + if (string.IsNullOrEmpty(tls.Password)) + { + string entry = "FinCERT"; + Trace.WriteLine($"Пароль пуст - получение от Windows ('{entry}')."); + var cred = CredentialManager.ReadCredential(entry); + tls.Login = cred.UserName ?? string.Empty; + tls.Password = cred.Password ?? string.Empty; + } + + // Подключиться к АСОИ ФинЦЕРТ + Trace.WriteLine($"{DateTime.Now:T} Login..."); + + if (!await TlsClient.Login(tls)) + { + Trace.WriteLine("Ошибка подключения к серверу."); + Environment.Exit(1); + } + + // Скачать комплект файлов для чек-листа + if (checklist) + { + await BulletinsManager.GetCheckList( + Path.Combine(_config.BulletinsDownloads, "CheckList")); + Environment.Exit(0); + } + + // Скачать фиды + if (_config.Feeds) + { + await FeedsManager.LoadFeeds(_config.FeedsDownloads); + } + + // Скачать бюллетени + if (_config.Bulletins) + { + // Неполноценный вариант с загрузкой только основного файла + // await BulletinsManager.LoadBulletinsList(_config.BulletinsDownloads, limit, offset); + + // Полноценный вариант с загрузкой основного и дополнительных файлов + await BulletinsManager.LoadBulletinsDirs(_config.BulletinsDownloads, limit, offset); + } + + // Завершить работу с АСОИ ФинЦЕРТ + Trace.WriteLine($"{DateTime.Now:T} Logout..."); + + if (await TlsClient.LogoutAsync()) + { + Trace.WriteLine("end."); + Environment.Exit(0); + } + else + { + Trace.WriteLine("Ошибка завершения сеанса."); + Environment.Exit(1); + } + } + catch (Exception ex) { - await FeedsManager.LoadFeeds(_config.FeedsDownloads); + Helper.TraceError("Program Error", ex); + Environment.Exit(4); } - - if (_config.Bulletins) + finally { - await BulletinsManager.LoadBulletinsList(_config.BulletinsDownloads); - await BulletinsManager.LoadBulletinsDirs(_config.BulletinsDownloads); + Trace.Listeners.Clear(); } - - // Завершить работу с АСОИ ФинЦЕРТ - Environment.Exit(await TlsClient.LogoutAsync() ? 0 : 1); } } diff --git a/FinCERT-Client/Properties/launchSettings.json b/FinCERT-Client/Properties/launchSettings.json new file mode 100644 index 0000000..48ad1cd --- /dev/null +++ b/FinCERT-Client/Properties/launchSettings.json @@ -0,0 +1,7 @@ +{ + "profiles": { + "FinCERT-Client": { + "commandName": "Project" + } + } +} \ No newline at end of file diff --git a/README.md b/README.md index 0b45bfb..f7b1b94 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,13 @@ * FinCERT-Client - новый структурированный проект с добавлением Bulletins API - возможность скачивать бюллетени и файлы к ним - в дополнение к фидам. +Эта программа работает и в ЗОЭ, и в ЗПЭ, в режиме read-only, и она не +медленная, а с паузами между запросами, чтобы не вызвать блокировку DDoS. + ### Feeds API / Фиды Фиды скачиваются актуальные, затирая прежние, контроль необходимости -обновления пока не ведется. +обновления пока не ведется. Ставьте нужное время по Планировщику. ### Bulletins API / Бюллетени @@ -23,9 +26,17 @@ API - возможность скачивать бюллетени и файлы Внутри каждой папки есть файл Bulletin.txt, в котором то же, что видно и в ЛК, а также все файлы, которые были приложениями к бюллетеню. -По умолчанию скачивается до 100 (ограничение API) последних бюллетеней, -пока не обнаружатся папки с именами, которые уже были ранее скачаны. -Параметры для возможности скачивания ранее этих 100 будут добавлены позже. +В первый раз будет скачано 100 (если не ограничить параметром `-limit`) +последних бюллетеней. + +При последующих запусках будут скачиваться новые последние, пока не будет +достигнуто ограничение API в 100, ограничение параметром или встречена +папка, что была уже загружена ранее. + +Чтобы скачать более ранние (за предел 100), нужно использовать параметр +`-offset`. Имейте в виду, что если этот параметр не 0, то отключается +прекращение скачивания при встрече ранее скачанной папки и будут скачаны +все недостающие, попавшие в этот лимит. ## Config / Конфигурация @@ -35,9 +46,13 @@ API - возможность скачивать бюллетени и файлы Важно заполнить вашими данными значения параметров: -* `MyThumbprint` -* `Login` -* `Password` +* `MyThumbprint` - отпечаток сертификата клиента, зарегистрированного на +сервере в ЛК и имеющего допуск к серверу; +* `Login` - учетная запись на сервере (логин); +* `Password` - пароль учетной записи. + +Если пароль пуст, то программа запросит логин и пароль из хранилища +Windows Credential Manager по строке `FinCERT`. Если указываете файловые пути, то по правилам JSON надо удваивать `\\`. @@ -45,6 +60,22 @@ API - возможность скачивать бюллетени и файлы * `-checklist` - сформировать в папке `BulletinsDownloads\CheckList` комплект файлов для приложения к чек-листу на подключение. +* `-limit 100` - число от 1 до 100 - ограничение числа скачиваемых +бюллетеней. 100 - по умолчанию и максимум - это ограничение API - обойти +его невозможно - только указанием параметра `-offset`. +* `-offset 0` - число от 0 и выше - сдвиг начала скачиваемых бюллетеней. +0 - по умочанию. Указание этого параметра не 0 переключает режим поведения +при встрече ранее скачанной папки: + * 0 - скачивание прекращается; + * не 0 - ранее скачанная папка пропускается, продолжается перебор (в +пределах значения `-limit`). + +## Exit codes / Коды возврата + +* 0 - успешно; +* 1 - ошибка; +* 2 - что-то не найдено; +* 4 - внешняя ошибка. ## Requirements / Требования