삽질하다 도저히 문제 파악이 안돼서 opcUaManager로 분리 테스트 중
This commit is contained in:
523602
OpcConnectionTest/Document/Honeywell_FullMap.csv
Normal file
523602
OpcConnectionTest/Document/Honeywell_FullMap.csv
Normal file
File diff suppressed because it is too large
Load Diff
BIN
OpcConnectionTest/Document/ProjectSchema.png
Normal file
BIN
OpcConnectionTest/Document/ProjectSchema.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 107 KiB |
1
OpcConnectionTest/Document/프로젝트 개발계획.md
Normal file
1
OpcConnectionTest/Document/프로젝트 개발계획.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|

|
||||||
523602
OpcConnectionTest/Honeywell_FullMap.csv
Normal file
523602
OpcConnectionTest/Honeywell_FullMap.csv
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
// using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
//using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Security.Cryptography.X509Certificates;
|
using System.Security.Cryptography.X509Certificates;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
@@ -21,6 +21,139 @@ namespace OpcConnectionTest
|
|||||||
public string Description { get; set; } = "";
|
public string Description { get; set; } = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class TagMaster
|
||||||
|
{
|
||||||
|
public string TagName {get; set; } = string.Empty;
|
||||||
|
public string FullNodeId {get; set; }= string.Empty;
|
||||||
|
public string NodeClass {get; set; }= string.Empty;
|
||||||
|
public string DataType {get; set; }= string.Empty;
|
||||||
|
public int Level {get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class HoneywellCrawler
|
||||||
|
{
|
||||||
|
private readonly Session _session;
|
||||||
|
private readonly List<TagMaster> _discoveredTags = []; // IDE0028 적용
|
||||||
|
|
||||||
|
public HoneywellCrawler(Session session)
|
||||||
|
{
|
||||||
|
// 의존성 주입 및 Null 체크
|
||||||
|
_session = session ?? throw new ArgumentNullException(nameof(session));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 저인망 탐사 시작점
|
||||||
|
/// </summary>
|
||||||
|
public async Task RunAsync(string startNodeId)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"\n🚀 비동기 저인망 탐사 시작: {startNodeId}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
NodeId rootNode = NodeId.Parse(startNodeId);
|
||||||
|
await BrowseRecursiveAsync(rootNode, 0);
|
||||||
|
|
||||||
|
Console.WriteLine("\n===============================================");
|
||||||
|
Console.WriteLine($"✅ 탐사 완료! 총 {_discoveredTags.Count}개의 항목 발견.");
|
||||||
|
Console.WriteLine("===============================================");
|
||||||
|
|
||||||
|
SaveToCsv();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"❌ 실행 중 치명적 오류: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task BrowseRecursiveAsync(NodeId nodeId, int level)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
BrowseDescription description = new() {
|
||||||
|
NodeId = nodeId,
|
||||||
|
BrowseDirection = BrowseDirection.Forward,
|
||||||
|
ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences,
|
||||||
|
IncludeSubtypes = true,
|
||||||
|
NodeClassMask = (uint)(NodeClass.Variable | NodeClass.Object),
|
||||||
|
ResultMask = (uint)BrowseResultMask.All
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1. BrowseAsync는 객체 방식 (에러 CS1061/CS8129 방지)
|
||||||
|
BrowseResponse response = await _session.BrowseAsync(null, null, 0, [description], default);
|
||||||
|
|
||||||
|
if (response?.Results == null || response.Results.Count == 0) return;
|
||||||
|
|
||||||
|
foreach (var result in response.Results)
|
||||||
|
{
|
||||||
|
await ProcessReferencesAsync(result.References, level);
|
||||||
|
|
||||||
|
byte[] cp = result.ContinuationPoint;
|
||||||
|
while (cp != null && cp.Length > 0)
|
||||||
|
{
|
||||||
|
// 2. BrowseNextAsync는 튜플 방식 (에러 CS0029 방지)
|
||||||
|
var (nextHeader, nextCp, nextRefs) = await _session.BrowseNextAsync(null, false, cp, default);
|
||||||
|
|
||||||
|
await ProcessReferencesAsync(nextRefs, level);
|
||||||
|
cp = nextCp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// 특정 노드 접근 권한 에러 등은 무시하고 진행
|
||||||
|
// Console.WriteLine($"⚠️ [Level {level}] {nodeId} 탐색 건너뜀: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessReferencesAsync(ReferenceDescriptionCollection references, int level)
|
||||||
|
{
|
||||||
|
if (references == null || references.Count == 0) return;
|
||||||
|
|
||||||
|
foreach (var rd in references)
|
||||||
|
{
|
||||||
|
// ExpandedNodeId를 실제 NodeId로 변환 (Namespace 관리용)
|
||||||
|
NodeId childId = ExpandedNodeId.ToNodeId(rd.NodeId, _session.NamespaceUris);
|
||||||
|
|
||||||
|
// 마스터 리스트에 추가
|
||||||
|
_discoveredTags.Add(new TagMaster
|
||||||
|
{
|
||||||
|
TagName = rd.BrowseName.Name ?? "Unknown",
|
||||||
|
FullNodeId = childId.ToString(),
|
||||||
|
NodeClass = rd.NodeClass.ToString(),
|
||||||
|
Level = level
|
||||||
|
});
|
||||||
|
|
||||||
|
// 콘솔 출력 (진행 상황 확인용)
|
||||||
|
string indent = new string(' ', level * 2);
|
||||||
|
Console.WriteLine($"{indent} [{rd.NodeClass}] {rd.BrowseName.Name} (ID: {childId})");
|
||||||
|
|
||||||
|
// 3. Object(폴더/태그)인 경우 재귀 탐색 (하니웰 구조에 맞춰 깊이 5단계 제한)
|
||||||
|
if (rd.NodeClass == NodeClass.Object && level < 5)
|
||||||
|
{
|
||||||
|
await BrowseRecursiveAsync(childId, level + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveToCsv()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using StreamWriter sw = new("Honeywell_FullMap.csv");
|
||||||
|
sw.WriteLine("Level,Class,Name,NodeId");
|
||||||
|
foreach (var tag in _discoveredTags)
|
||||||
|
{
|
||||||
|
sw.WriteLine($"{tag.Level},{tag.NodeClass},{tag.TagName},{tag.FullNodeId}");
|
||||||
|
}
|
||||||
|
Console.WriteLine($"💾 CSV 저장 완료: {Path.GetFullPath("Honeywell_FullMap.csv")}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"❌ CSV 저장 실패: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class Program
|
class Program
|
||||||
{
|
{
|
||||||
static Dictionary<string, StatusCodeInfo> _statusCodeMap = new(StringComparer.OrdinalIgnoreCase);
|
static Dictionary<string, StatusCodeInfo> _statusCodeMap = new(StringComparer.OrdinalIgnoreCase);
|
||||||
@@ -117,11 +250,30 @@ namespace OpcConnectionTest
|
|||||||
|
|
||||||
var endpoint = new ConfiguredEndpoint(null, selected, endpointConfig);
|
var endpoint = new ConfiguredEndpoint(null, selected, endpointConfig);
|
||||||
var identity = new UserIdentity(userName, Encoding.UTF8.GetBytes(password));
|
var identity = new UserIdentity(userName, Encoding.UTF8.GetBytes(password));
|
||||||
|
|
||||||
session = await Session.Create(config, endpoint, false, "OpcTestSession", 60000, identity, null);
|
#pragma warning disable CS0618
|
||||||
|
session = await Session.Create(
|
||||||
|
config,
|
||||||
|
endpoint,
|
||||||
|
false,
|
||||||
|
"OpcTestSession",
|
||||||
|
60000,
|
||||||
|
identity,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
#pragma warning restore CS0618
|
||||||
Console.WriteLine($"✅ Connected! SessionID: {session.SessionId}");
|
Console.WriteLine($"✅ Connected! SessionID: {session.SessionId}");
|
||||||
|
|
||||||
// 3. 데이터 읽기 및 DB 저장 루프 (테스트용으로 5회 반복)
|
Console.WriteLine("\n🔍 하니웰 전체 노드 탐사(저인망) 시작...");
|
||||||
|
// ISession을 실제 Session 클래스로 형변환하여 전달
|
||||||
|
if (session is Session clientSession)
|
||||||
|
{
|
||||||
|
HoneywellCrawler crawler = new HoneywellCrawler(clientSession);
|
||||||
|
// 'shinam' 노드를 기점으로 바닥까지 훑고 CSV 저장까지 수행합니다.
|
||||||
|
await crawler.RunAsync("ns=1;s=$assetmodel");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 데이터 읽기 및 DB 저장 루프 (테스트용으로 5회 반복)
|
||||||
string nodeID = "ns=1;s=shinam:p-6102.hzset.fieldvalue";
|
string nodeID = "ns=1;s=shinam:p-6102.hzset.fieldvalue";
|
||||||
for (int i = 0; i < 5; i++)
|
for (int i = 0; i < 5; i++)
|
||||||
{
|
{
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
BIN
OpcConnectionTest/image.png
Normal file
BIN
OpcConnectionTest/image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 150 KiB |
@@ -13,10 +13,10 @@ using System.Reflection;
|
|||||||
[assembly: System.Reflection.AssemblyCompanyAttribute("OpcConnectionTest")]
|
[assembly: System.Reflection.AssemblyCompanyAttribute("OpcConnectionTest")]
|
||||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+20ee22ae0c85a902f6767918a8ebece87301c37f")]
|
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+4ea351946aa5ffb76c8c693b768cb7f460f0cb79")]
|
||||||
[assembly: System.Reflection.AssemblyProductAttribute("OpcConnectionTest")]
|
[assembly: System.Reflection.AssemblyProductAttribute("OpcConnectionTest")]
|
||||||
[assembly: System.Reflection.AssemblyTitleAttribute("OpcConnectionTest")]
|
[assembly: System.Reflection.AssemblyTitleAttribute("OpcConnectionTest")]
|
||||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||||
|
|
||||||
// MSBuild WriteCodeFragment 클래스에서 생성되었습니다.
|
// Generated by the MSBuild WriteCodeFragment class.
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
f71e0f6a7ff186b47ef80c0b9a2fc70fce275730017c4f885ccba6c266a8ced1
|
5dc72724b0828bf3589118e6e5edcb9a4e4008e775a094b3a90862f8de1efba1
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
/home/pacer/projects/OpcConnectionTest/obj/Debug/net8.0/OpcConnectionTest.csproj.AssemblyReference.cache
|
||||||
|
/home/pacer/projects/OpcConnectionTest/obj/Debug/net8.0/OpcConnectionTest.GeneratedMSBuildEditorConfig.editorconfig
|
||||||
|
/home/pacer/projects/OpcConnectionTest/obj/Debug/net8.0/OpcConnectionTest.AssemblyInfoInputs.cache
|
||||||
|
/home/pacer/projects/OpcConnectionTest/obj/Debug/net8.0/OpcConnectionTest.AssemblyInfo.cs
|
||||||
|
/home/pacer/projects/OpcConnectionTest/obj/Debug/net8.0/OpcConnectionTest.csproj.CoreCompileInputs.cache
|
||||||
/home/pacer/projects/OpcConnectionTest/bin/Debug/net8.0/OpcConnectionTest
|
/home/pacer/projects/OpcConnectionTest/bin/Debug/net8.0/OpcConnectionTest
|
||||||
/home/pacer/projects/OpcConnectionTest/bin/Debug/net8.0/OpcConnectionTest.deps.json
|
/home/pacer/projects/OpcConnectionTest/bin/Debug/net8.0/OpcConnectionTest.deps.json
|
||||||
/home/pacer/projects/OpcConnectionTest/bin/Debug/net8.0/OpcConnectionTest.runtimeconfig.json
|
/home/pacer/projects/OpcConnectionTest/bin/Debug/net8.0/OpcConnectionTest.runtimeconfig.json
|
||||||
@@ -11,6 +16,7 @@
|
|||||||
/home/pacer/projects/OpcConnectionTest/bin/Debug/net8.0/Microsoft.Extensions.Options.dll
|
/home/pacer/projects/OpcConnectionTest/bin/Debug/net8.0/Microsoft.Extensions.Options.dll
|
||||||
/home/pacer/projects/OpcConnectionTest/bin/Debug/net8.0/Microsoft.Extensions.Primitives.dll
|
/home/pacer/projects/OpcConnectionTest/bin/Debug/net8.0/Microsoft.Extensions.Primitives.dll
|
||||||
/home/pacer/projects/OpcConnectionTest/bin/Debug/net8.0/Newtonsoft.Json.dll
|
/home/pacer/projects/OpcConnectionTest/bin/Debug/net8.0/Newtonsoft.Json.dll
|
||||||
|
/home/pacer/projects/OpcConnectionTest/bin/Debug/net8.0/Npgsql.dll
|
||||||
/home/pacer/projects/OpcConnectionTest/bin/Debug/net8.0/Opc.Ua.Client.dll
|
/home/pacer/projects/OpcConnectionTest/bin/Debug/net8.0/Opc.Ua.Client.dll
|
||||||
/home/pacer/projects/OpcConnectionTest/bin/Debug/net8.0/Opc.Ua.Configuration.dll
|
/home/pacer/projects/OpcConnectionTest/bin/Debug/net8.0/Opc.Ua.Configuration.dll
|
||||||
/home/pacer/projects/OpcConnectionTest/bin/Debug/net8.0/Opc.Ua.Core.dll
|
/home/pacer/projects/OpcConnectionTest/bin/Debug/net8.0/Opc.Ua.Core.dll
|
||||||
@@ -26,15 +32,9 @@
|
|||||||
/home/pacer/projects/OpcConnectionTest/bin/Debug/net8.0/System.Text.Encodings.Web.dll
|
/home/pacer/projects/OpcConnectionTest/bin/Debug/net8.0/System.Text.Encodings.Web.dll
|
||||||
/home/pacer/projects/OpcConnectionTest/bin/Debug/net8.0/System.Text.Json.dll
|
/home/pacer/projects/OpcConnectionTest/bin/Debug/net8.0/System.Text.Json.dll
|
||||||
/home/pacer/projects/OpcConnectionTest/bin/Debug/net8.0/runtimes/browser/lib/net8.0/System.Text.Encodings.Web.dll
|
/home/pacer/projects/OpcConnectionTest/bin/Debug/net8.0/runtimes/browser/lib/net8.0/System.Text.Encodings.Web.dll
|
||||||
/home/pacer/projects/OpcConnectionTest/obj/Debug/net8.0/OpcConnectionTest.csproj.AssemblyReference.cache
|
|
||||||
/home/pacer/projects/OpcConnectionTest/obj/Debug/net8.0/OpcConnectionTest.GeneratedMSBuildEditorConfig.editorconfig
|
|
||||||
/home/pacer/projects/OpcConnectionTest/obj/Debug/net8.0/OpcConnectionTest.AssemblyInfoInputs.cache
|
|
||||||
/home/pacer/projects/OpcConnectionTest/obj/Debug/net8.0/OpcConnectionTest.AssemblyInfo.cs
|
|
||||||
/home/pacer/projects/OpcConnectionTest/obj/Debug/net8.0/OpcConnectionTest.csproj.CoreCompileInputs.cache
|
|
||||||
/home/pacer/projects/OpcConnectionTest/obj/Debug/net8.0/OpcConnectionTest.csproj.CopyComplete
|
/home/pacer/projects/OpcConnectionTest/obj/Debug/net8.0/OpcConnectionTest.csproj.CopyComplete
|
||||||
/home/pacer/projects/OpcConnectionTest/obj/Debug/net8.0/OpcConnectionTest.dll
|
/home/pacer/projects/OpcConnectionTest/obj/Debug/net8.0/OpcConnectionTest.dll
|
||||||
/home/pacer/projects/OpcConnectionTest/obj/Debug/net8.0/refint/OpcConnectionTest.dll
|
/home/pacer/projects/OpcConnectionTest/obj/Debug/net8.0/refint/OpcConnectionTest.dll
|
||||||
/home/pacer/projects/OpcConnectionTest/obj/Debug/net8.0/OpcConnectionTest.pdb
|
/home/pacer/projects/OpcConnectionTest/obj/Debug/net8.0/OpcConnectionTest.pdb
|
||||||
/home/pacer/projects/OpcConnectionTest/obj/Debug/net8.0/OpcConnectionTest.genruntimeconfig.cache
|
/home/pacer/projects/OpcConnectionTest/obj/Debug/net8.0/OpcConnectionTest.genruntimeconfig.cache
|
||||||
/home/pacer/projects/OpcConnectionTest/obj/Debug/net8.0/ref/OpcConnectionTest.dll
|
/home/pacer/projects/OpcConnectionTest/obj/Debug/net8.0/ref/OpcConnectionTest.dll
|
||||||
/home/pacer/projects/OpcConnectionTest/bin/Debug/net8.0/Npgsql.dll
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
221
OpcPksPlatform/Documents/CertificateGenerator.cs
Normal file
221
OpcPksPlatform/Documents/CertificateGenerator.cs
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.NetworkInformation;
|
||||||
|
using OpcPks.Core.Models;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace OpcPks.Core.Services
|
||||||
|
{
|
||||||
|
public class CertificateGenerator
|
||||||
|
{
|
||||||
|
private readonly string _baseDataPath;
|
||||||
|
private readonly OpcSessionManager _sessionManager;
|
||||||
|
|
||||||
|
public CertificateGenerator(string baseDataPath, OpcSessionManager sessionManager)
|
||||||
|
{
|
||||||
|
_baseDataPath = baseDataPath;
|
||||||
|
_sessionManager = sessionManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> CreateHoneywellCertificateAsync(CertRequestModel model)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string appName = "OpcPksClient";
|
||||||
|
// 초기 성공 모델과 동일한 URI 패턴 유지
|
||||||
|
string applicationUri = $"urn:{Environment.MachineName}:OpcPksClient";
|
||||||
|
|
||||||
|
string derPath = Path.Combine(_baseDataPath, "own", "certs", $"{appName}.der");
|
||||||
|
string pfxPath = Path.Combine(_baseDataPath, "own", "private", $"{appName}.pfx");
|
||||||
|
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(derPath)!);
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(pfxPath)!);
|
||||||
|
|
||||||
|
// 1. RSA 키 생성 (2048-bit)
|
||||||
|
using var rsa = RSA.Create(2048);
|
||||||
|
|
||||||
|
// 2. 인증서 요청 생성
|
||||||
|
var request = new CertificateRequest(
|
||||||
|
$"CN={appName}",
|
||||||
|
rsa,
|
||||||
|
HashAlgorithmName.SHA256,
|
||||||
|
RSASignaturePadding.Pkcs1);
|
||||||
|
|
||||||
|
// 3. [Critical] 하니웰이 요구하는 '신분증 지문' 및 '도장' 추가
|
||||||
|
|
||||||
|
// A. Subject Key Identifier (이게 없으면 하니웰 엔진이 인증서를 무시합니다)
|
||||||
|
request.CertificateExtensions.Add(
|
||||||
|
new X509SubjectKeyIdentifierExtension(request.PublicKey, false));
|
||||||
|
|
||||||
|
// B. Subject Alternative Name (SAN)
|
||||||
|
var sanBuilder = new SubjectAlternativeNameBuilder();
|
||||||
|
sanBuilder.AddUri(new Uri(applicationUri));
|
||||||
|
sanBuilder.AddDnsName(Environment.MachineName);
|
||||||
|
sanBuilder.AddDnsName("localhost");
|
||||||
|
foreach (var ip in GetLocalIpAddresses()) sanBuilder.AddIpAddress(ip);
|
||||||
|
request.CertificateExtensions.Add(sanBuilder.Build());
|
||||||
|
|
||||||
|
// C. Key Usage (하니웰 보안 4종 세트)
|
||||||
|
request.CertificateExtensions.Add(new X509KeyUsageExtension(
|
||||||
|
X509KeyUsageFlags.DigitalSignature |
|
||||||
|
X509KeyUsageFlags.KeyEncipherment |
|
||||||
|
X509KeyUsageFlags.NonRepudiation |
|
||||||
|
X509KeyUsageFlags.DataEncipherment,
|
||||||
|
true));
|
||||||
|
|
||||||
|
// D. Enhanced Key Usage (EKU)
|
||||||
|
request.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(
|
||||||
|
new OidCollection {
|
||||||
|
new Oid("1.3.6.1.5.5.7.3.1"), // Server Auth
|
||||||
|
new Oid("1.3.6.1.5.5.7.3.2") // Client Auth
|
||||||
|
}, false));
|
||||||
|
|
||||||
|
// E. Basic Constraints (End Entity 명시)
|
||||||
|
request.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, true));
|
||||||
|
|
||||||
|
// 4. 인증서 생성 (초기 코드처럼 넉넉하게 10년)
|
||||||
|
using var certificate = request.CreateSelfSigned(
|
||||||
|
DateTimeOffset.UtcNow.AddDays(-1),
|
||||||
|
DateTimeOffset.UtcNow.AddYears(10));
|
||||||
|
|
||||||
|
// 5. 파일 저장
|
||||||
|
// PFX는 암호 없이 추출 (호환성)
|
||||||
|
await File.WriteAllBytesAsync(pfxPath, certificate.Export(X509ContentType.Pfx));
|
||||||
|
// DER은 하니웰 서버 전송용
|
||||||
|
await File.WriteAllBytesAsync(derPath, certificate.Export(X509ContentType.Cert));
|
||||||
|
|
||||||
|
Console.WriteLine($"✅ [Step 1] 하니웰 지문(SKI) 포함 인증서 제작 완료: {derPath}");
|
||||||
|
|
||||||
|
// 6. 전송 및 검증
|
||||||
|
string primaryIp = model.PrimaryIpA?.Trim() ?? "";
|
||||||
|
if (string.IsNullOrEmpty(primaryIp)) throw new Exception("Primary IP 주소가 비어있습니다.");
|
||||||
|
|
||||||
|
await TransferAndVerifyWithRetryAsync(model, primaryIp, $"{appName}.der", derPath);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"❌ 작업 최종 실패: {ex.Message}");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task TransferAndVerifyWithRetryAsync(CertRequestModel model, string ip, string fileName, string localPath)
|
||||||
|
{
|
||||||
|
string winTrustedDir = @"ProgramData\Honeywell\Experion PKS\Server\data\CertStore\opcuaserver\pki\DefaultApplicationGroup\trusted\certs";
|
||||||
|
string winRejectedDir = @"ProgramData\Honeywell\Experion PKS\Server\data\CertStore\opcuaserver\pki\DefaultApplicationGroup\rejected\certs";
|
||||||
|
string auth = $"{model.AdminId}%{model.AdminPassword}";
|
||||||
|
|
||||||
|
int maxRetries = 3;
|
||||||
|
bool isAuthorized = false;
|
||||||
|
|
||||||
|
for (int attempt = 1; attempt <= maxRetries; attempt++)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"\n🔄 [{ip}] {attempt}회차 시퀀스 시작...");
|
||||||
|
string trustedFile = $"{winTrustedDir}\\{fileName}";
|
||||||
|
string rejectedFile = $"{winRejectedDir}\\{fileName}";
|
||||||
|
|
||||||
|
// 1. 기존 파일 삭제 및 새 인증서 전송
|
||||||
|
await DeleteFileFromHoneywell(ip, trustedFile, auth);
|
||||||
|
await PutFileToHoneywell(ip, localPath, trustedFile, auth);
|
||||||
|
await Task.Delay(3000);
|
||||||
|
|
||||||
|
// 2. 접속 시도 (하니웰 반응 유도)
|
||||||
|
isAuthorized = await _sessionManager.SayHelloAsync(ip);
|
||||||
|
if (isAuthorized)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"🎯 [{ip}] 인증 성공!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"⚠️ [{ip}] 거절됨(0x80830000). Rejected 폴더 확인 중...");
|
||||||
|
await Task.Delay(2000);
|
||||||
|
|
||||||
|
// 3. Rejected 유배지로 갔다면 강제 이동 (하니웰의 '불신' 극복)
|
||||||
|
if (await CheckFileExists(ip, rejectedFile, auth))
|
||||||
|
{
|
||||||
|
Console.WriteLine($"🚩 [{ip}] Rejected 유배 확인! Trusted로 강제 이동합니다.");
|
||||||
|
string moveArgs = $"//{ip}/C$ -U \"{auth}\" -c \"rename \\\"{rejectedFile}\\\" \\\"{trustedFile}\\\"\"";
|
||||||
|
await ExecuteSmbCommandAsync(moveArgs);
|
||||||
|
|
||||||
|
await Task.Delay(2000);
|
||||||
|
if (await _sessionManager.SayHelloAsync(ip)) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAuthorized) throw new Exception($"❌ [{ip}] 지문 인식 실패 또는 Extensions 불일치. 수동 Trust 필요.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task PutFileToHoneywell(string ip, string localPath, string remotePath, string auth)
|
||||||
|
{
|
||||||
|
string args = $"//{ip}/C$ -U \"{auth}\" -c \"put \\\"{localPath}\\\" \\\"{remotePath}\\\"\"";
|
||||||
|
await ExecuteSmbCommandAsync(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DeleteFileFromHoneywell(string ip, string remotePath, string auth)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
string args = $"//{ip}/C$ -U \"{auth}\" -c \"del \\\"{remotePath}\\\"\"";
|
||||||
|
await ExecuteSmbCommandAsync(args);
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> CheckFileExists(string ip, string remotePath, string auth)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
string args = $"//{ip}/C$ -U \"{auth}\" -c \"allinfo \\\"{remotePath}\\\"\"";
|
||||||
|
string result = await ExecuteSmbCommandWithOutputAsync(args);
|
||||||
|
return !result.Contains("NT_STATUS_OBJECT_NAME_NOT_FOUND");
|
||||||
|
} catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ExecuteSmbCommandAsync(string args)
|
||||||
|
{
|
||||||
|
using var process = new Process {
|
||||||
|
StartInfo = new ProcessStartInfo {
|
||||||
|
FileName = "smbclient",
|
||||||
|
Arguments = args,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
process.Start();
|
||||||
|
string error = await process.StandardError.ReadToEndAsync();
|
||||||
|
await process.WaitForExitAsync();
|
||||||
|
if (process.ExitCode != 0 && !error.Contains("NT_STATUS_OBJECT_NAME_NOT_FOUND"))
|
||||||
|
throw new Exception($"SMB 실패: {error.Trim()}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> ExecuteSmbCommandWithOutputAsync(string args)
|
||||||
|
{
|
||||||
|
using var process = new Process {
|
||||||
|
StartInfo = new ProcessStartInfo {
|
||||||
|
FileName = "smbclient",
|
||||||
|
Arguments = args,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
process.Start();
|
||||||
|
string output = await process.StandardOutput.ReadToEndAsync();
|
||||||
|
string error = await process.StandardError.ReadToEndAsync();
|
||||||
|
await process.WaitForExitAsync();
|
||||||
|
return output + error;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<IPAddress> GetLocalIpAddresses()
|
||||||
|
{
|
||||||
|
return NetworkInterface.GetAllNetworkInterfaces()
|
||||||
|
.Where(i => i.OperationalStatus == OperationalStatus.Up)
|
||||||
|
.SelectMany(i => i.GetIPProperties().UnicastAddresses)
|
||||||
|
.Where(a => a.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
|
||||||
|
.Select(a => a.Address).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
OpcPksPlatform/Documents/pki/trusted/certs/application_rsa_sha256.der
Executable file
BIN
OpcPksPlatform/Documents/pki/trusted/certs/application_rsa_sha256.der
Executable file
Binary file not shown.
BIN
OpcPksPlatform/Documents/rootCA.der
Executable file
BIN
OpcPksPlatform/Documents/rootCA.der
Executable file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -13,10 +13,10 @@ using System.Reflection;
|
|||||||
[assembly: System.Reflection.AssemblyCompanyAttribute("OpcPks.Collector")]
|
[assembly: System.Reflection.AssemblyCompanyAttribute("OpcPks.Collector")]
|
||||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+4e006a5a5f65f597fafaa3777b8a1073944eada2")]
|
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+4ea351946aa5ffb76c8c693b768cb7f460f0cb79")]
|
||||||
[assembly: System.Reflection.AssemblyProductAttribute("OpcPks.Collector")]
|
[assembly: System.Reflection.AssemblyProductAttribute("OpcPks.Collector")]
|
||||||
[assembly: System.Reflection.AssemblyTitleAttribute("OpcPks.Collector")]
|
[assembly: System.Reflection.AssemblyTitleAttribute("OpcPks.Collector")]
|
||||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||||
|
|
||||||
// MSBuild WriteCodeFragment 클래스에서 생성되었습니다.
|
// Generated by the MSBuild WriteCodeFragment class.
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
cc5c3e28019345878790a222ce17c89ce44d1c1d74447a18859ea42c9a3adb70
|
54f6f1ad0709deb6cb2f3668e2c474e9539ddf45ae288d8f7bae089724875a72
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
OpcPksPlatform/OpcPks.Core/Data/own/certs/OpcPksClient.der
Normal file
BIN
OpcPksPlatform/OpcPks.Core/Data/own/certs/OpcPksClient.der
Normal file
Binary file not shown.
BIN
OpcPksPlatform/OpcPks.Core/Data/own/private/OpcPksClient.pfx
Normal file
BIN
OpcPksPlatform/OpcPks.Core/Data/own/private/OpcPksClient.pfx
Normal file
Binary file not shown.
@@ -1,20 +1,28 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace OpcPks.Core.Models
|
namespace OpcPks.Core.Models
|
||||||
{
|
{
|
||||||
public class CertRequestModel
|
public class CertRequestModel
|
||||||
{
|
{
|
||||||
|
// UI에서 asp-for가 이 이름을 찾습니다.
|
||||||
|
[Required]
|
||||||
|
public string AdminId { get; set; } = "mngr";
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[DataType(DataType.Password)]
|
||||||
|
public string AdminPassword { get; set; } = "mngr";
|
||||||
|
|
||||||
public string ApplicationName { get; set; } = "OpcPksClient";
|
public string ApplicationName { get; set; } = "OpcPksClient";
|
||||||
public bool IsRedundant { get; set; } = false;
|
public bool IsRedundant { get; set; } = false;
|
||||||
|
|
||||||
// 서버 호스트네임 (예: HONPKS)
|
public string PrimaryHostName { get; set; } = "192.168.0.20";
|
||||||
public string PrimaryHostName { get; set; } = "";
|
public string SecondaryHostName { get; set; } = "192.168.1.20";
|
||||||
public string SecondaryHostName { get; set; } = ""; // Redundant일 때 필수
|
|
||||||
|
|
||||||
// FTE 네트워크 구조 반영 (네트워크 대역 분리)
|
public string? PrimaryIpA { get; set; } = ""; // Yellow
|
||||||
public string PrimaryIpA { get; set; } = ""; // 192.168.0.x 대역
|
public string? PrimaryIpB { get; set; } = ""; // Green
|
||||||
public string PrimaryIpB { get; set; } = ""; // 192.168.1.x 대역 (FTE 이중화 시)
|
|
||||||
|
|
||||||
public string SecondaryIpA { get; set; } = ""; // 192.168.0.y 대역
|
public string SecondaryIpA { get; set; } = "";
|
||||||
public string SecondaryIpB { get; set; } = ""; // 192.168.1.y 대역
|
public string SecondaryIpB { get; set; } = "";
|
||||||
|
|
||||||
public int ValidityYears { get; set; } = 5;
|
public int ValidityYears { get; set; } = 5;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,120 +1,96 @@
|
|||||||
using Opc.Ua;
|
using System.Security.Cryptography;
|
||||||
using System.Security.Cryptography.X509Certificates;
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.NetworkInformation;
|
||||||
using OpcPks.Core.Models;
|
using OpcPks.Core.Models;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
namespace OpcPks.Core.Services
|
namespace OpcPks.Core.Services
|
||||||
{
|
{
|
||||||
public class CertificateGenerator
|
public class CertificateGenerator
|
||||||
{
|
{
|
||||||
private readonly string _pfxPath;
|
private readonly string _baseDataPath;
|
||||||
private readonly string _ownCertPath;
|
private readonly OpcSessionManager _sessionManager;
|
||||||
|
|
||||||
public CertificateGenerator(string baseDataPath)
|
public CertificateGenerator(string baseDataPath, OpcSessionManager sessionManager)
|
||||||
{
|
{
|
||||||
// 경로 설정 (현재 tree 구조 반영)
|
_baseDataPath = baseDataPath;
|
||||||
_pfxPath = Path.Combine(baseDataPath, "pki/own/private/OpcTestClient.pfx");
|
_sessionManager = sessionManager;
|
||||||
_ownCertPath = Path.Combine(baseDataPath, "pki/own/certs/OpcTestClient.der");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> CreateHoneywellCertificateAsync(CertRequestModel model)
|
public async Task<bool> CreateHoneywellCertificateAsync(CertRequestModel model)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// 1. [중요] 기존 인증서 백업
|
string appName = "OpcPksClient";
|
||||||
BackupExistingCertificate();
|
// [중요] 호스트네임은 대문자로 처리 (윈도우 기본값 준수)
|
||||||
|
string hostName = Environment.MachineName.ToUpper();
|
||||||
Console.WriteLine("🔐 하니웰 맞춤형 인증서 생성을 시작합니다...");
|
string applicationUri = $"urn:{hostName}:{appName}";
|
||||||
|
|
||||||
// SAN 리스트 구축 (localhost 기본 포함)
|
|
||||||
var subjectAlternativeNames = new List<string> { "localhost", "127.0.0.1" };
|
|
||||||
|
|
||||||
// Primary 정보 (FTE Yellow/Green)
|
string derPath = Path.Combine(_baseDataPath, "own", "certs", $"{appName}.der");
|
||||||
if (!string.IsNullOrEmpty(model.PrimaryHostName))
|
string pfxPath = Path.Combine(_baseDataPath, "own", "private", $"{appName}.pfx");
|
||||||
{
|
|
||||||
subjectAlternativeNames.Add(model.PrimaryHostName);
|
|
||||||
subjectAlternativeNames.Add($"{model.PrimaryHostName}A");
|
|
||||||
}
|
|
||||||
if (!string.IsNullOrEmpty(model.PrimaryIpA)) subjectAlternativeNames.Add(model.PrimaryIpA);
|
|
||||||
if (!string.IsNullOrEmpty(model.PrimaryIpB)) subjectAlternativeNames.Add(model.PrimaryIpB);
|
|
||||||
|
|
||||||
// Secondary 정보 (Redundant 선택 시)
|
Directory.CreateDirectory(Path.GetDirectoryName(derPath)!);
|
||||||
if (model.IsRedundant)
|
Directory.CreateDirectory(Path.GetDirectoryName(pfxPath)!);
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(model.SecondaryHostName))
|
|
||||||
{
|
|
||||||
subjectAlternativeNames.Add(model.SecondaryHostName);
|
|
||||||
subjectAlternativeNames.Add($"{model.SecondaryHostName}B");
|
|
||||||
}
|
|
||||||
if (!string.IsNullOrEmpty(model.SecondaryIpA)) subjectAlternativeNames.Add(model.SecondaryIpA);
|
|
||||||
if (!string.IsNullOrEmpty(model.SecondaryIpB)) subjectAlternativeNames.Add(model.SecondaryIpB);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 인증서 생성 (KeySize 2048, 하니웰 규격)
|
using var rsa = RSA.Create(2048);
|
||||||
// var certificate = CertificateFactory.CreateCertificate(
|
// [교정] 오직 CN만 포함 (Configuration Error 방지를 위해 불필요한 필드 제거)
|
||||||
// model.ApplicationName,
|
var request = new CertificateRequest(
|
||||||
// model.ApplicationName,
|
$"CN={appName}",
|
||||||
// null,
|
rsa,
|
||||||
// subjectAlternativeNames,
|
HashAlgorithmName.SHA256,
|
||||||
// null,
|
RSASignaturePadding.Pkcs1);
|
||||||
// 2048,
|
|
||||||
// DateTime.UtcNow.AddDays(-1),
|
|
||||||
// model.ValidityYears * 12,
|
|
||||||
// null
|
|
||||||
// );
|
|
||||||
|
|
||||||
// 2. 인증서 생성 (로그에 표시된 15개 인자 서명 순서 엄격 준수)
|
// 1. Subject Key Identifier (필수 지문)
|
||||||
ushort keySize = 2048;
|
request.CertificateExtensions.Add(
|
||||||
ushort lifetimeInMonths = (ushort)(model.ValidityYears * 12);
|
new X509SubjectKeyIdentifierExtension(request.PublicKey, false));
|
||||||
|
|
||||||
var certificate = CertificateFactory.CreateCertificate(
|
// 2. SAN (URI와 DNS 단 하나씩만 - 하니웰의 해석 오류 방지)
|
||||||
"Directory", // 1. storeType (string)
|
var sanBuilder = new SubjectAlternativeNameBuilder();
|
||||||
Path.GetDirectoryName(_pfxPath), // 2. storePath (string)
|
sanBuilder.AddUri(new Uri(applicationUri));
|
||||||
null, // 3. password (string)
|
sanBuilder.AddDnsName(hostName);
|
||||||
$"urn:localhost:{model.ApplicationName}", // 4. applicationUri (string)
|
request.CertificateExtensions.Add(sanBuilder.Build());
|
||||||
model.ApplicationName, // 5. applicationName (string)
|
|
||||||
model.ApplicationName, // 6. subjectName (string)
|
|
||||||
subjectAlternativeNames, // 7. domainNames (IList<string>)
|
|
||||||
keySize, // 8. keySize (ushort)
|
|
||||||
DateTime.UtcNow.AddDays(-1), // 9. startTime (DateTime)
|
|
||||||
lifetimeInMonths, // 10. lifetimeInMonths (ushort)
|
|
||||||
(ushort)256, // 11. hashSizeInBits (ushort)
|
|
||||||
false, // 12. isCA (bool)
|
|
||||||
null, // 13. issuer (X509Certificate2)
|
|
||||||
null, // 14. privateKey (byte[])
|
|
||||||
0 // 15. hashAlgorithm (int)
|
|
||||||
);
|
|
||||||
|
|
||||||
|
// 3. Key Usage (가장 표준적인 3종으로 축소)
|
||||||
|
// NonRepudiation 플래그가 가끔 BadConfiguration을 유발하므로 뺍니다.
|
||||||
|
request.CertificateExtensions.Add(new X509KeyUsageExtension(
|
||||||
|
X509KeyUsageFlags.DigitalSignature |
|
||||||
|
X509KeyUsageFlags.KeyEncipherment |
|
||||||
|
X509KeyUsageFlags.DataEncipherment,
|
||||||
|
true));
|
||||||
|
|
||||||
// 3. 파일 저장
|
// 4. Enhanced Key Usage
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(_pfxPath)!);
|
request.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(_ownCertPath)!);
|
new OidCollection {
|
||||||
|
new Oid("1.3.6.1.5.5.7.3.1"), // Server Auth
|
||||||
|
new Oid("1.3.6.1.5.5.7.3.2") // Client Auth
|
||||||
|
}, false));
|
||||||
|
|
||||||
// PFX (개인키 포함) 저장
|
// 5. Basic Constraints (End Entity 명시)
|
||||||
byte[] pfxBytes = certificate.Export(X509ContentType.Pfx, "");
|
request.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, true));
|
||||||
await File.WriteAllBytesAsync(_pfxPath, pfxBytes);
|
|
||||||
|
|
||||||
// DER (공개키만) 저장 - 하니웰 서버에 수동으로 신뢰 등록할 때 필요할 수 있음
|
// [교정] 시작 시간을 1시간 전으로 설정하여 하니웰 서버와의 시간차 방어
|
||||||
byte[] derBytes = certificate.Export(X509ContentType.Cert);
|
using var certificate = request.CreateSelfSigned(
|
||||||
await File.WriteAllBytesAsync(_ownCertPath, derBytes);
|
DateTimeOffset.UtcNow.AddHours(-1),
|
||||||
|
DateTimeOffset.UtcNow.AddYears(5));
|
||||||
|
|
||||||
Console.WriteLine($"✅ 새 인증서 생성 완료 및 백업 성공.");
|
await File.WriteAllBytesAsync(pfxPath, certificate.Export(X509ContentType.Pfx));
|
||||||
return true;
|
await File.WriteAllBytesAsync(derPath, certificate.Export(X509ContentType.Cert));
|
||||||
|
|
||||||
|
Console.WriteLine($"\n--- [수동 작업 지시: mngr 계정 모드] ---");
|
||||||
|
Console.WriteLine($"1. 파일 생성 완료: {derPath}");
|
||||||
|
Console.WriteLine($"2. 하니웰 서버의 Trusted/certs 폴더에 기존 파일을 지우고 새 파일을 넣으세요.");
|
||||||
|
Console.WriteLine($"3. 'mngr' 계정의 권한으로 복사가 완료되면 엔터를 치세요.");
|
||||||
|
Console.ReadLine();
|
||||||
|
|
||||||
|
string primaryIp = model.PrimaryIpA?.Trim() ?? "";
|
||||||
|
// 바로 접속 테스트 진행
|
||||||
|
return await _sessionManager.SayHelloAsync(primaryIp);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"❌ 인증서 생성 실패: {ex.Message}");
|
Console.WriteLine($"❌ 작업 실패: {ex.Message}");
|
||||||
return false;
|
throw;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void BackupExistingCertificate()
|
|
||||||
{
|
|
||||||
if (File.Exists(_pfxPath))
|
|
||||||
{
|
|
||||||
string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
|
|
||||||
string backupPath = _pfxPath + "." + timestamp + ".bak";
|
|
||||||
File.Copy(_pfxPath, backupPath, true);
|
|
||||||
Console.WriteLine($"📦 기존 인증서 백업됨: {Path.GetFileName(backupPath)}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ namespace OpcPks.Core.Services
|
|||||||
{
|
{
|
||||||
await ProcessReferencesAsync(result.References, level);
|
await ProcessReferencesAsync(result.References, level);
|
||||||
|
|
||||||
byte[] cp = result.ContinuationPoint;
|
byte[]? cp = result.ContinuationPoint;
|
||||||
while (cp != null && cp.Length > 0)
|
while (cp != null && cp.Length > 0)
|
||||||
{
|
{
|
||||||
// ✅ ByteStringCollection을 사용하여 라이브러리 표준 인자 타입 일치화
|
// ✅ ByteStringCollection을 사용하여 라이브러리 표준 인자 타입 일치화
|
||||||
@@ -122,7 +122,7 @@ namespace OpcPks.Core.Services
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// 디렉토리가 없으면 생성
|
// 디렉토리가 없으면 생성
|
||||||
string dir = Path.GetDirectoryName(filePath);
|
string? dir = Path.GetDirectoryName(filePath);
|
||||||
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
|
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(dir);
|
Directory.CreateDirectory(dir);
|
||||||
|
|||||||
@@ -1,108 +1,108 @@
|
|||||||
using Opc.Ua;
|
using Opc.Ua;
|
||||||
using Opc.Ua.Client;
|
using Opc.Ua.Client;
|
||||||
using System.Security.Cryptography.X509Certificates;
|
using System;
|
||||||
using System.Net;
|
using System.Threading.Tasks;
|
||||||
|
using OpcPks.Core.Models;
|
||||||
|
|
||||||
namespace OpcPks.Core.Services
|
namespace OpcPks.Core.Services
|
||||||
{
|
{
|
||||||
public class OpcSessionManager
|
public class OpcSessionManager
|
||||||
{
|
{
|
||||||
private ISession? _session;
|
private readonly string _pkiPath = "/home/pacer/projects/OpcPksPlatform/OpcPks.Core/Data/pki";
|
||||||
public ISession? Session => _session;
|
|
||||||
|
|
||||||
public async Task<ISession> GetSessionAsync()
|
/// <summary>
|
||||||
|
/// [수정] 하니웰 인사용: 단순히 연결만 하는 게 아니라 서버 시간을 읽으려 시도하여
|
||||||
|
/// 하니웰 본체 엔진이 인증서를 Rejected로 던지도록 유도합니다.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> SayHelloAsync(string ip)
|
||||||
{
|
{
|
||||||
if (_session != null && _session.Connected) return _session;
|
try
|
||||||
|
{
|
||||||
|
// CertRequestModel은 최소 정보만 담아 전달
|
||||||
|
using (var session = await GetSessionAsync(ip, new CertRequestModel { PrimaryIpA = ip }))
|
||||||
|
{
|
||||||
|
if (session != null && session.Connected)
|
||||||
|
{
|
||||||
|
// 연결 성공 시 서버 시간 노드를 읽어 실제 통신이 가능한지 확인
|
||||||
|
var currentTime = session.ReadValue(Variables.Server_ServerStatus_CurrentTime);
|
||||||
|
Console.WriteLine($"✅ [SayHello] 하니웰 연결 및 데이터 읽기 성공: {currentTime}");
|
||||||
|
|
||||||
|
await session.CloseAsync();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// 여기서 에러가 나야 정상입니다. (인증서가 신뢰되지 않았을 때 하니웰이 거부하며 파일을 Rejected로 보냄)
|
||||||
|
Console.WriteLine($"⚠️ [SayHello] 하니웰 거부 반응 유도 성공: {ex.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
string serverHostName = "192.168.0.20";
|
/// <summary>
|
||||||
string endpointUrl = $"opc.tcp://{serverHostName}:4840";
|
/// [핵심] 크롤러 및 모든 서비스가 공통으로 사용하는 실제 세션 생성 로직
|
||||||
string applicationUri = "urn:dbsvr:OpcTestClient";
|
/// </summary>
|
||||||
string userName = "mngr";
|
public async Task<ISession?> GetSessionAsync(string ip, CertRequestModel model)
|
||||||
string password = "mngr";
|
{
|
||||||
string pfxPassword = ""; // openssl에서 확인한 비밀번호가 있다면 입력
|
if (string.IsNullOrEmpty(ip)) return null;
|
||||||
|
|
||||||
string baseDataPath = "/home/pacer/projects/OpcPksPlatform/OpcPks.Core/Data/pki";
|
string endpointUrl = $"opc.tcp://{ip}:4840";
|
||||||
string pfxFilePath = Path.Combine(baseDataPath, "own/private/OpcTestClient.pfx");
|
|
||||||
|
var config = new ApplicationConfiguration()
|
||||||
var config = new ApplicationConfiguration {
|
{
|
||||||
ApplicationName = "OpcTestClient",
|
ApplicationName = "OpcPksClient",
|
||||||
ApplicationType = ApplicationType.Client,
|
ApplicationType = ApplicationType.Client,
|
||||||
ApplicationUri = applicationUri,
|
SecurityConfiguration = new SecurityConfiguration
|
||||||
SecurityConfiguration = new SecurityConfiguration {
|
{
|
||||||
ApplicationCertificate = new CertificateIdentifier {
|
ApplicationCertificate = new CertificateIdentifier {
|
||||||
StoreType = "Directory",
|
StoreType = "Directory",
|
||||||
StorePath = Path.Combine(baseDataPath, "own"),
|
StorePath = $"{_pkiPath}/own"
|
||||||
SubjectName = "OpcTestClient"
|
|
||||||
},
|
},
|
||||||
TrustedPeerCertificates = new CertificateTrustList {
|
TrustedPeerCertificates = new CertificateTrustList {
|
||||||
StoreType = "Directory",
|
StoreType = "Directory",
|
||||||
StorePath = Path.Combine(baseDataPath, "trusted")
|
StorePath = $"{_pkiPath}/trusted"
|
||||||
},
|
},
|
||||||
AutoAcceptUntrustedCertificates = true,
|
RejectedCertificateStore = new CertificateTrustList {
|
||||||
AddAppCertToTrustedStore = true
|
StoreType = "Directory",
|
||||||
|
StorePath = $"{_pkiPath}/rejected"
|
||||||
|
},
|
||||||
|
AutoAcceptUntrustedCertificates = true
|
||||||
},
|
},
|
||||||
TransportQuotas = new TransportQuotas { OperationTimeout = 60000 },
|
TransportQuotas = new TransportQuotas { OperationTimeout = 10000 },
|
||||||
ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 60000 }
|
ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 10000 }
|
||||||
};
|
};
|
||||||
|
|
||||||
// 1. [인증서 강제 로드] Validate 호출 전에 인증서를 명시적으로 로드합니다.
|
|
||||||
if (!File.Exists(pfxFilePath)) {
|
|
||||||
throw new Exception($"❌ PFX 파일을 찾을 수 없습니다: {pfxFilePath}");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 리눅스 .NET 호환용 플래그
|
|
||||||
var cert = new X509Certificate2(
|
|
||||||
pfxFilePath,
|
|
||||||
pfxPassword,
|
|
||||||
X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.Exportable | X509KeyStorageFlags.PersistKeySet
|
|
||||||
);
|
|
||||||
|
|
||||||
// config에 인증서 직접 주입
|
|
||||||
config.SecurityConfiguration.ApplicationCertificate.Certificate = cert;
|
|
||||||
Console.WriteLine($"✅ [인증서 로드] {cert.Subject}");
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Console.WriteLine($"❌ [인증서 에러] PFX 로드 실패: {ex.Message}");
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 설정 검증 (인증서 주입 후 실행)
|
|
||||||
await config.Validate(ApplicationType.Client);
|
await config.Validate(ApplicationType.Client);
|
||||||
|
|
||||||
// Validate 이후 인증서가 풀렸을 경우를 대비해 다시 확인
|
try
|
||||||
if (config.SecurityConfiguration.ApplicationCertificate.Certificate == null) {
|
{
|
||||||
Console.WriteLine("⚠️ [경고] Validate 과정에서 인증서 설정이 유실되어 재할당합니다.");
|
// 하니웰 보안 설정에 맞는 엔드포인트 선택
|
||||||
config.SecurityConfiguration.ApplicationCertificate.Certificate = new X509Certificate2(pfxFilePath, pfxPassword, X509KeyStorageFlags.MachineKeySet);
|
var endpointDescription = CoreClientUtils.SelectEndpoint(endpointUrl, true, 5000);
|
||||||
|
var endpointConfiguration = EndpointConfiguration.Create(config);
|
||||||
|
var endpoint = new ConfiguredEndpoint(null, endpointDescription, endpointConfiguration);
|
||||||
|
|
||||||
|
// 세션 생성 시도: 이 과정 자체가 하니웰 본체에 대한 보안 컨택입니다.
|
||||||
|
return await Session.Create(
|
||||||
|
config,
|
||||||
|
endpoint,
|
||||||
|
false,
|
||||||
|
"OpcPksSession",
|
||||||
|
10000,
|
||||||
|
new UserIdentity(new AnonymousIdentityToken()),
|
||||||
|
null);
|
||||||
}
|
}
|
||||||
|
catch (ServiceResultException sx)
|
||||||
config.CertificateValidator.CertificateValidation += (s, e) => { e.Accept = true; };
|
{
|
||||||
|
Console.WriteLine($"[OPC_UA] 하니웰 보안 응답 코드: {sx.StatusCode}");
|
||||||
// 3. 엔드포인트 선택 및 보안 정책 로그
|
return null;
|
||||||
var endpointDescription = CoreClientUtils.SelectEndpoint(endpointUrl, true);
|
}
|
||||||
var endpointConfiguration = EndpointConfiguration.Create(config);
|
catch (Exception ex)
|
||||||
var configuredEndpoint = new ConfiguredEndpoint(null, endpointDescription, endpointConfiguration);
|
{
|
||||||
|
Console.WriteLine($"[OPC_UA] 세션 생성 중 오류: {ex.Message}");
|
||||||
Console.WriteLine($"📡 [세션] 연결 시도: {endpointUrl}");
|
return null;
|
||||||
Console.WriteLine($"🔐 [보안] {endpointDescription.SecurityPolicyUri} ({endpointDescription.SecurityMode})");
|
|
||||||
|
|
||||||
// 4. 세션 생성 시도
|
|
||||||
try {
|
|
||||||
_session = await Opc.Ua.Client.Session.Create(
|
|
||||||
config,
|
|
||||||
configuredEndpoint,
|
|
||||||
true,
|
|
||||||
"OpcPksSession",
|
|
||||||
60000,
|
|
||||||
new UserIdentity(userName, password),
|
|
||||||
null
|
|
||||||
);
|
|
||||||
Console.WriteLine("🚀 [성공] 하니웰 서버와 연결되었습니다!");
|
|
||||||
} catch (ServiceResultException srex) {
|
|
||||||
Console.WriteLine($"❌ [OPC 에러] {srex.StatusCode}: {srex.Message}");
|
|
||||||
throw;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return _session;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Binary file not shown.
Binary file not shown.
@@ -13,10 +13,10 @@ using System.Reflection;
|
|||||||
[assembly: System.Reflection.AssemblyCompanyAttribute("OpcPks.Core")]
|
[assembly: System.Reflection.AssemblyCompanyAttribute("OpcPks.Core")]
|
||||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+4e006a5a5f65f597fafaa3777b8a1073944eada2")]
|
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+4ea351946aa5ffb76c8c693b768cb7f460f0cb79")]
|
||||||
[assembly: System.Reflection.AssemblyProductAttribute("OpcPks.Core")]
|
[assembly: System.Reflection.AssemblyProductAttribute("OpcPks.Core")]
|
||||||
[assembly: System.Reflection.AssemblyTitleAttribute("OpcPks.Core")]
|
[assembly: System.Reflection.AssemblyTitleAttribute("OpcPks.Core")]
|
||||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||||
|
|
||||||
// MSBuild WriteCodeFragment 클래스에서 생성되었습니다.
|
// Generated by the MSBuild WriteCodeFragment class.
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
c75e210b6893d92e0b1636a6b8f564c5a308ff97701a72ccf681ea4473190030
|
a4e824e0dedb3502ec3664daca8c46528956a8b9400ccad8204c7aed55480ce3
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -1 +1 @@
|
|||||||
cea4cbfb0b4d3fd205bf727dfb75d0c31872eee74f74e79b0a11980153badb1b
|
bf8b9457af309b3d030af6437e28d995c9af82eaaa8c16419bd5b4f60d93749a
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -10,11 +10,11 @@
|
|||||||
"projectUniqueName": "/home/pacer/projects/OpcPksPlatform/OpcPks.Core/OpcPks.Core.csproj",
|
"projectUniqueName": "/home/pacer/projects/OpcPksPlatform/OpcPks.Core/OpcPks.Core.csproj",
|
||||||
"projectName": "OpcPks.Core",
|
"projectName": "OpcPks.Core",
|
||||||
"projectPath": "/home/pacer/projects/OpcPksPlatform/OpcPks.Core/OpcPks.Core.csproj",
|
"projectPath": "/home/pacer/projects/OpcPksPlatform/OpcPks.Core/OpcPks.Core.csproj",
|
||||||
"packagesPath": "/root/.nuget/packages/",
|
"packagesPath": "/home/pacer/.nuget/packages/",
|
||||||
"outputPath": "/home/pacer/projects/OpcPksPlatform/OpcPks.Core/obj/",
|
"outputPath": "/home/pacer/projects/OpcPksPlatform/OpcPks.Core/obj/",
|
||||||
"projectStyle": "PackageReference",
|
"projectStyle": "PackageReference",
|
||||||
"configFilePaths": [
|
"configFilePaths": [
|
||||||
"/root/.nuget/NuGet/NuGet.Config"
|
"/home/pacer/.nuget/NuGet/NuGet.Config"
|
||||||
],
|
],
|
||||||
"originalTargetFrameworks": [
|
"originalTargetFrameworks": [
|
||||||
"net8.0"
|
"net8.0"
|
||||||
|
|||||||
@@ -4,12 +4,12 @@
|
|||||||
<RestoreSuccess Condition=" '$(RestoreSuccess)' == '' ">True</RestoreSuccess>
|
<RestoreSuccess Condition=" '$(RestoreSuccess)' == '' ">True</RestoreSuccess>
|
||||||
<RestoreTool Condition=" '$(RestoreTool)' == '' ">NuGet</RestoreTool>
|
<RestoreTool Condition=" '$(RestoreTool)' == '' ">NuGet</RestoreTool>
|
||||||
<ProjectAssetsFile Condition=" '$(ProjectAssetsFile)' == '' ">$(MSBuildThisFileDirectory)project.assets.json</ProjectAssetsFile>
|
<ProjectAssetsFile Condition=" '$(ProjectAssetsFile)' == '' ">$(MSBuildThisFileDirectory)project.assets.json</ProjectAssetsFile>
|
||||||
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">/root/.nuget/packages/</NuGetPackageRoot>
|
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">/home/pacer/.nuget/packages/</NuGetPackageRoot>
|
||||||
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">/root/.nuget/packages/</NuGetPackageFolders>
|
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">/home/pacer/.nuget/packages/</NuGetPackageFolders>
|
||||||
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
|
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
|
||||||
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.8.1</NuGetToolVersion>
|
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.8.1</NuGetToolVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||||
<SourceRoot Include="/root/.nuget/packages/" />
|
<SourceRoot Include="/home/pacer/.nuget/packages/" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -555,7 +555,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"packageFolders": {
|
"packageFolders": {
|
||||||
"/root/.nuget/packages/": {}
|
"/home/pacer/.nuget/packages/": {}
|
||||||
},
|
},
|
||||||
"project": {
|
"project": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
@@ -563,11 +563,11 @@
|
|||||||
"projectUniqueName": "/home/pacer/projects/OpcPksPlatform/OpcPks.Core/OpcPks.Core.csproj",
|
"projectUniqueName": "/home/pacer/projects/OpcPksPlatform/OpcPks.Core/OpcPks.Core.csproj",
|
||||||
"projectName": "OpcPks.Core",
|
"projectName": "OpcPks.Core",
|
||||||
"projectPath": "/home/pacer/projects/OpcPksPlatform/OpcPks.Core/OpcPks.Core.csproj",
|
"projectPath": "/home/pacer/projects/OpcPksPlatform/OpcPks.Core/OpcPks.Core.csproj",
|
||||||
"packagesPath": "/root/.nuget/packages/",
|
"packagesPath": "/home/pacer/.nuget/packages/",
|
||||||
"outputPath": "/home/pacer/projects/OpcPksPlatform/OpcPks.Core/obj/",
|
"outputPath": "/home/pacer/projects/OpcPksPlatform/OpcPks.Core/obj/",
|
||||||
"projectStyle": "PackageReference",
|
"projectStyle": "PackageReference",
|
||||||
"configFilePaths": [
|
"configFilePaths": [
|
||||||
"/root/.nuget/NuGet/NuGet.Config"
|
"/home/pacer/.nuget/NuGet/NuGet.Config"
|
||||||
],
|
],
|
||||||
"originalTargetFrameworks": [
|
"originalTargetFrameworks": [
|
||||||
"net8.0"
|
"net8.0"
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
{
|
{
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"dgSpecHash": "Yr08mh1oXg2H9eup6pJDv3DAEJIeIlBI+QyHLkFeXakigkBdjFhrDkS0UWc3z6IMWBK5yeeRM5RBBASJtTIrbA==",
|
"dgSpecHash": "4hhhTNv5rdj9W8LEgtqRzvNnobbH+nWA4apshlgfd3j4OdsEpRlMXC2Q3X2ScEegniOPFgZFJJ9l+i9qtr7LrA==",
|
||||||
"success": true,
|
"success": true,
|
||||||
"projectFilePath": "/home/pacer/projects/OpcPksPlatform/OpcPks.Core/OpcPks.Core.csproj",
|
"projectFilePath": "/home/pacer/projects/OpcPksPlatform/OpcPks.Core/OpcPks.Core.csproj",
|
||||||
"expectedPackageFiles": [
|
"expectedPackageFiles": [
|
||||||
"/root/.nuget/packages/microsoft.extensions.dependencyinjection.abstractions/8.0.1/microsoft.extensions.dependencyinjection.abstractions.8.0.1.nupkg.sha512",
|
"/home/pacer/.nuget/packages/microsoft.extensions.dependencyinjection.abstractions/8.0.1/microsoft.extensions.dependencyinjection.abstractions.8.0.1.nupkg.sha512",
|
||||||
"/root/.nuget/packages/microsoft.extensions.logging.abstractions/8.0.1/microsoft.extensions.logging.abstractions.8.0.1.nupkg.sha512",
|
"/home/pacer/.nuget/packages/microsoft.extensions.logging.abstractions/8.0.1/microsoft.extensions.logging.abstractions.8.0.1.nupkg.sha512",
|
||||||
"/root/.nuget/packages/newtonsoft.json/13.0.3/newtonsoft.json.13.0.3.nupkg.sha512",
|
"/home/pacer/.nuget/packages/newtonsoft.json/13.0.3/newtonsoft.json.13.0.3.nupkg.sha512",
|
||||||
"/root/.nuget/packages/npgsql/8.0.4/npgsql.8.0.4.nupkg.sha512",
|
"/home/pacer/.nuget/packages/npgsql/8.0.4/npgsql.8.0.4.nupkg.sha512",
|
||||||
"/root/.nuget/packages/opcfoundation.netstandard.opc.ua.client/1.5.374.78/opcfoundation.netstandard.opc.ua.client.1.5.374.78.nupkg.sha512",
|
"/home/pacer/.nuget/packages/opcfoundation.netstandard.opc.ua.client/1.5.374.78/opcfoundation.netstandard.opc.ua.client.1.5.374.78.nupkg.sha512",
|
||||||
"/root/.nuget/packages/opcfoundation.netstandard.opc.ua.configuration/1.5.374.78/opcfoundation.netstandard.opc.ua.configuration.1.5.374.78.nupkg.sha512",
|
"/home/pacer/.nuget/packages/opcfoundation.netstandard.opc.ua.configuration/1.5.374.78/opcfoundation.netstandard.opc.ua.configuration.1.5.374.78.nupkg.sha512",
|
||||||
"/root/.nuget/packages/opcfoundation.netstandard.opc.ua.core/1.5.374.78/opcfoundation.netstandard.opc.ua.core.1.5.374.78.nupkg.sha512",
|
"/home/pacer/.nuget/packages/opcfoundation.netstandard.opc.ua.core/1.5.374.78/opcfoundation.netstandard.opc.ua.core.1.5.374.78.nupkg.sha512",
|
||||||
"/root/.nuget/packages/opcfoundation.netstandard.opc.ua.security.certificates/1.5.374.78/opcfoundation.netstandard.opc.ua.security.certificates.1.5.374.78.nupkg.sha512",
|
"/home/pacer/.nuget/packages/opcfoundation.netstandard.opc.ua.security.certificates/1.5.374.78/opcfoundation.netstandard.opc.ua.security.certificates.1.5.374.78.nupkg.sha512",
|
||||||
"/root/.nuget/packages/system.formats.asn1/8.0.1/system.formats.asn1.8.0.1.nupkg.sha512",
|
"/home/pacer/.nuget/packages/system.formats.asn1/8.0.1/system.formats.asn1.8.0.1.nupkg.sha512",
|
||||||
"/root/.nuget/packages/system.security.cryptography.cng/5.0.0/system.security.cryptography.cng.5.0.0.nupkg.sha512"
|
"/home/pacer/.nuget/packages/system.security.cryptography.cng/5.0.0/system.security.cryptography.cng.5.0.0.nupkg.sha512"
|
||||||
],
|
],
|
||||||
"logs": []
|
"logs": []
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using Npgsql;
|
using Npgsql;
|
||||||
using OpcPks.Core.Data;
|
using OpcPks.Core.Data;
|
||||||
using OpcPks.Core.Services;
|
using OpcPks.Core.Services;
|
||||||
using OpcPks.Core.Models; // CertRequestModel 참조 추가
|
using OpcPks.Core.Models;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -14,8 +14,19 @@ namespace OpcPks.Web.Controllers;
|
|||||||
[Route("Engineering")]
|
[Route("Engineering")]
|
||||||
public class EngineeringController : Controller
|
public class EngineeringController : Controller
|
||||||
{
|
{
|
||||||
// 하니웰 데이터 및 PKI 경로 고정
|
// [현장 설정] 하니웰 데이터 및 PKI 경로 고정
|
||||||
private readonly string _basePath = "/home/pacer/projects/OpcPksPlatform/OpcPks.Core/Data";
|
private readonly string _basePath = "/home/pacer/projects/OpcPksPlatform/OpcPks.Core/Data";
|
||||||
|
|
||||||
|
// [수정] 의존성 주입을 위한 필드 추가
|
||||||
|
private readonly CertificateGenerator _certGenerator;
|
||||||
|
private readonly OpcSessionManager _sessionManager;
|
||||||
|
|
||||||
|
// [수정] 생성자를 통해 Program.cs에서 등록된 서비스를 주입받음
|
||||||
|
public EngineeringController(CertificateGenerator certGenerator, OpcSessionManager sessionManager)
|
||||||
|
{
|
||||||
|
_certGenerator = certGenerator;
|
||||||
|
_sessionManager = sessionManager;
|
||||||
|
}
|
||||||
|
|
||||||
#region [기존 기능: 태그 탐사 및 관리]
|
#region [기존 기능: 태그 탐사 및 관리]
|
||||||
|
|
||||||
@@ -72,6 +83,8 @@ public class EngineeringController : Controller
|
|||||||
ON CONFLICT (full_node_id) DO NOTHING;";
|
ON CONFLICT (full_node_id) DO NOTHING;";
|
||||||
|
|
||||||
foreach (var tag in tags) {
|
foreach (var tag in tags) {
|
||||||
|
if (string.IsNullOrEmpty(tag.NodeId)) continue;
|
||||||
|
|
||||||
string sContent = tag.NodeId.Contains("s=") ? tag.NodeId.Split("s=")[1] : tag.NodeId;
|
string sContent = tag.NodeId.Contains("s=") ? tag.NodeId.Split("s=")[1] : tag.NodeId;
|
||||||
string[] parts = sContent.Split(':');
|
string[] parts = sContent.Split(':');
|
||||||
string server = parts[0];
|
string server = parts[0];
|
||||||
@@ -107,15 +120,14 @@ public class EngineeringController : Controller
|
|||||||
public async Task<IActionResult> RunCrawler()
|
public async Task<IActionResult> RunCrawler()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
var sessionManager = new OpcSessionManager();
|
// [수정] new 생성 대신 주입된 _sessionManager 사용
|
||||||
var session = await sessionManager.GetSessionAsync();
|
// GetSessionAsync에 필요한 인자(IP) 전달 (기본값 또는 모델 활용)
|
||||||
|
var session = await _sessionManager.GetSessionAsync("192.168.0.20", new CertRequestModel());
|
||||||
if (session == null || !session.Connected)
|
if (session == null || !session.Connected)
|
||||||
return BadRequest(new { message = "하니웰 서버 연결 실패." });
|
return BadRequest(new { message = "하니웰 서버 연결 실패." });
|
||||||
|
|
||||||
var crawler = new HoneywellCrawler(session);
|
var crawler = new HoneywellCrawler(session);
|
||||||
string csvPath = Path.Combine(_basePath, "Honeywell_FullMap.csv");
|
string csvPath = Path.Combine(_basePath, "Honeywell_FullMap.csv");
|
||||||
|
|
||||||
await crawler.RunAsync("ns=1;s=$assetmodel", csvPath);
|
await crawler.RunAsync("ns=1;s=$assetmodel", csvPath);
|
||||||
return Ok(new { message = "탐사 및 CSV 생성 완료!" });
|
return Ok(new { message = "탐사 및 CSV 생성 완료!" });
|
||||||
}
|
}
|
||||||
@@ -142,18 +154,18 @@ public class EngineeringController : Controller
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region [신규 기능: 하니웰 전용 인증서 관리]
|
#region [안전 강화 기능: 하니웰 전용 인증서 관리]
|
||||||
|
|
||||||
[HttpGet("CertManager")]
|
[HttpGet("CertManager")]
|
||||||
public IActionResult CertManager()
|
public IActionResult CertManager()
|
||||||
{
|
{
|
||||||
string pfxPath = Path.Combine(_basePath, "pki/own/private/OpcTestClient.pfx");
|
// [약속] 통일된 경로 확인 (CertificateGenerator 내부 경로와 일치시킴)
|
||||||
|
string pfxPath = Path.Combine(_basePath, "own/private/OpcPksClient.pfx");
|
||||||
|
|
||||||
// 파일 존재 여부를 ViewData에 담아 보냅니다.
|
|
||||||
ViewBag.IsCertExists = System.IO.File.Exists(pfxPath);
|
ViewBag.IsCertExists = System.IO.File.Exists(pfxPath);
|
||||||
|
|
||||||
ViewBag.SuccessMsg = TempData["Success"];
|
ViewBag.SuccessMsg = TempData["Success"];
|
||||||
ViewBag.ErrorMsg = TempData["Error"];
|
ViewBag.ErrorMsg = TempData["Error"];
|
||||||
|
|
||||||
return View(new CertRequestModel());
|
return View(new CertRequestModel());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,26 +174,38 @@ public class EngineeringController : Controller
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var generator = new CertificateGenerator(_basePath);
|
// [수정] 직접 new 생성하던 코드를 지우고 주입된 _certGenerator 사용
|
||||||
var success = await generator.CreateHoneywellCertificateAsync(model);
|
// 이제 이 호출은 내부적으로 하니웰 서버와 3회 밀당을 수행합니다.
|
||||||
|
var success = await _certGenerator.CreateHoneywellCertificateAsync(model);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
TempData["Success"] = "하니웰 FTE 대응 인증서가 생성 및 백업되었습니다.";
|
TempData["Success"] = "인증서 생성 및 하니웰 서버 최종 수용 확인 완료!";
|
||||||
return RedirectToAction("CertManager");
|
return RedirectToAction("CertManager");
|
||||||
}
|
}
|
||||||
|
|
||||||
TempData["Error"] = "인증서 생성 중 오류가 발생했습니다.";
|
TempData["Error"] = "인증서 전송 과정에서 오류가 발생했습니다.";
|
||||||
return RedirectToAction("CertManager");
|
return RedirectToAction("CertManager");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
TempData["Error"] = ex.Message;
|
Console.WriteLine($"[FATAL_CERT] {DateTime.Now}: {ex.Message}");
|
||||||
|
TempData["Error"] = $"시스템 오류: {ex.Message}";
|
||||||
return RedirectToAction("CertManager");
|
return RedirectToAction("CertManager");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
public class SearchRequest { public string TagTerm { get; set; } public List<string> Suffixes { get; set; } }
|
public class SearchRequest
|
||||||
public class TagRegistrationRequest { public string TagName { get; set; } public string NodeId { get; set; } public string DataType { get; set; } }
|
{
|
||||||
|
public string TagTerm { get; set; } = string.Empty;
|
||||||
|
public List<string> Suffixes { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TagRegistrationRequest
|
||||||
|
{
|
||||||
|
public string TagName { get; set; } = string.Empty;
|
||||||
|
public string NodeId { get; set; } = string.Empty;
|
||||||
|
public string DataType { get; set; } = "Double";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,27 +1,58 @@
|
|||||||
|
using OpcPks.Core.Services;
|
||||||
|
using OpcPks.Core.Models;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
// Add services to the container.
|
// -----------------------------------------------------------
|
||||||
|
// 1. 공통 경로 설정
|
||||||
|
// -----------------------------------------------------------
|
||||||
|
string baseDataPath = "/home/pacer/projects/OpcPksPlatform/OpcPks.Core/Data";
|
||||||
|
|
||||||
|
// -----------------------------------------------------------
|
||||||
|
// 2. 서비스 등록 (Dependency Injection)
|
||||||
|
// -----------------------------------------------------------
|
||||||
builder.Services.AddControllersWithViews();
|
builder.Services.AddControllersWithViews();
|
||||||
|
|
||||||
|
// [수정] OpcSessionManager를 먼저 등록해야 Generator에서 가져다 쓸 수 있습니다.
|
||||||
|
builder.Services.AddSingleton<OpcSessionManager>();
|
||||||
|
|
||||||
|
// [수정] 팩토리 메서드를 사용하여 OpcSessionManager를 주입(Injection)합니다.
|
||||||
|
builder.Services.AddSingleton<CertificateGenerator>(sp =>
|
||||||
|
{
|
||||||
|
var sessionManager = sp.GetRequiredService<OpcSessionManager>();
|
||||||
|
return new CertificateGenerator(baseDataPath, sessionManager);
|
||||||
|
});
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
// -----------------------------------------------------------
|
||||||
|
// 3. 실행 환경 설정
|
||||||
|
// -----------------------------------------------------------
|
||||||
if (!app.Environment.IsDevelopment())
|
if (!app.Environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
app.UseExceptionHandler("/Home/Error");
|
app.UseExceptionHandler("/Home/Error");
|
||||||
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
|
|
||||||
app.UseHsts();
|
app.UseHsts();
|
||||||
}
|
}
|
||||||
|
|
||||||
//app.UseHttpsRedirection();
|
// -----------------------------------------------------------
|
||||||
|
// 4. 미들웨어 및 라우팅 설정
|
||||||
|
// -----------------------------------------------------------
|
||||||
app.UseStaticFiles();
|
app.UseStaticFiles();
|
||||||
|
|
||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
|
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|
||||||
|
// 컨트롤러 루트 매핑
|
||||||
app.MapControllerRoute(
|
app.MapControllerRoute(
|
||||||
name: "default",
|
name: "default",
|
||||||
pattern: "{controller=Home}/{action=Index}/{id?}");
|
pattern: "{controller=Home}/{action=Index}/{id?}");
|
||||||
|
|
||||||
app.Run();
|
// [이미지: ASP.NET Core Dependency Injection Container flow]
|
||||||
|
|
||||||
|
|
||||||
|
// -----------------------------------------------------------
|
||||||
|
// 5. 서버 가동
|
||||||
|
// -----------------------------------------------------------
|
||||||
|
Console.WriteLine("🚀 OpcPksPlatform 서버 대기 중... (http://0.0.0.0:5000)");
|
||||||
|
Console.WriteLine("💡 사용자가 '인증서 생성' 버튼을 누를 때까지 대기합니다.");
|
||||||
|
|
||||||
|
app.Run();
|
||||||
@@ -1,89 +1,117 @@
|
|||||||
@model OpcPks.Core.Models.CertRequestModel
|
@model OpcPks.Core.Models.CertRequestModel
|
||||||
|
|
||||||
<div class="container mt-4">
|
<div class="container mt-4">
|
||||||
<div class="card shadow">
|
<div class="card shadow border-0">
|
||||||
<div class="card-header bg-primary text-white">
|
<div class="card-header bg-dark text-white d-flex justify-content-between align-items-center">
|
||||||
<h4>🛡️ 하니웰 Experion 전용 인증서 생성기</h4>
|
<h4 class="mb-0">🛡️ Honeywell Experion Certificate Manager</h4>
|
||||||
|
<span class="badge bg-success">v2.2 (Safe SMB Transfer)</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form asp-action="GenerateCertificate" method="post">
|
<form asp-action="GenerateCertificate" method="post" id="certForm">
|
||||||
|
|
||||||
|
<div class="card bg-light mb-4 border-info">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title text-info"><i class="bi bi-shield-lock-fill"></i> Windows Administrator Credentials</h5>
|
||||||
|
<p class="small text-muted">인증서 파일을 하니웰 서버의 Trusted 폴더로 안전하게 전송하기 위한 관리자 권한이 필요합니다.</p>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-2">
|
||||||
|
<label asp-for="AdminId" class="form-label small fw-bold">Admin ID</label>
|
||||||
|
<input asp-for="AdminId" class="form-control" placeholder="Administrator" required />
|
||||||
|
<span asp-validation-for="AdminId" class="text-danger small"></span>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-2">
|
||||||
|
<label asp-for="AdminPassword" class="form-label small fw-bold">Password</label>
|
||||||
|
<input asp-for="AdminPassword" type="password" class="form-control" placeholder="********" required />
|
||||||
|
<span asp-validation-for="AdminPassword" class="text-danger small"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label fw-bold">시스템 구성</label>
|
<label asp-for="IsRedundant" class="form-label fw-bold">시스템 구성 (FTE)</label>
|
||||||
<select asp-for="IsRedundant" class="form-select" id="systemType">
|
<select asp-for="IsRedundant" class="form-select" id="systemType">
|
||||||
<option value="false">Standalone (단일 서버)</option>
|
<option value="false">Standalone (단일 서버)</option>
|
||||||
<option value="true">Redundant (이중화 서버)</option>
|
<option value="true">Redundant (이중화 서버)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label fw-bold">어플리케이션 이름</label>
|
<label asp-for="ApplicationName" class="form-label fw-bold">어플리케이션 이름</label>
|
||||||
<input asp-for="ApplicationName" class="form-control" placeholder="OpcPksClient" />
|
<input asp-for="ApplicationName" class="form-control" readonly value="OpcPksClient" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr />
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="p-3 border rounded bg-white shadow-sm">
|
||||||
|
<h5 class="text-primary border-bottom pb-2">Primary Server</h5>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="small fw-bold">Host Name</label>
|
||||||
|
<input asp-for="PrimaryHostName" class="form-control form-control-sm" />
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="small text-warning fw-bold">FTE Yellow (A)</label>
|
||||||
|
<input asp-for="PrimaryIpA" class="form-control form-control-sm" placeholder="192.168.0.x" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="small text-success fw-bold">FTE Green (B)</label>
|
||||||
|
<input asp-for="PrimaryIpB" class="form-control form-control-sm" placeholder="192.168.1.x" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h5 class="text-secondary"><span class="badge bg-info">Primary</span> 서버 정보</h5>
|
<div class="col-md-6">
|
||||||
<div class="row mb-3">
|
<div class="p-3 border rounded bg-light shadow-sm" id="secondaryContainer">
|
||||||
<div class="col-md-4">
|
<h5 class="text-secondary border-bottom pb-2" id="secondaryTitle">Secondary Server</h5>
|
||||||
<label class="form-label">Host Name</label>
|
<div class="mb-2">
|
||||||
<input asp-for="PrimaryHostName" class="form-control" placeholder="예: HONPKS" />
|
<label class="small fw-bold">Host Name</label>
|
||||||
</div>
|
<input asp-for="SecondaryHostName" class="form-control form-control-sm sec-input" />
|
||||||
<div class="col-md-4">
|
</div>
|
||||||
<label class="form-label text-warning">IP Address (Yellow)</label>
|
<div class="mb-2">
|
||||||
<input asp-for="PrimaryIpA" class="form-control" placeholder="192.168.0.20" />
|
<label class="small text-warning fw-bold">FTE Yellow (A)</label>
|
||||||
</div>
|
<input asp-for="SecondaryIpA" class="form-control form-control-sm sec-input" />
|
||||||
<div class="col-md-4">
|
</div>
|
||||||
<label class="form-label text-success">IP Address (Green)</label>
|
<div>
|
||||||
<input asp-for="PrimaryIpB" class="form-control" placeholder="192.168.1.20" />
|
<label class="small text-success fw-bold">FTE Green (B)</label>
|
||||||
|
<input asp-for="SecondaryIpB" class="form-control form-control-sm sec-input" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h5 class="text-secondary mt-4"><span class="badge bg-secondary" id="secondaryBadge">Secondary</span> 서버 정보</h5>
|
<div class="card mt-4 border-primary">
|
||||||
<div class="row mb-3" id="secondaryFields">
|
<div class="card-body bg-light">
|
||||||
<div class="col-md-4">
|
<div class="alert alert-info mb-0">
|
||||||
<label class="form-label">Host Name</label>
|
<h6 class="fw-bold"><i class="bi bi-info-circle-fill"></i> 하니웰 인증서 안전 전송 안내</h6>
|
||||||
<input asp-for="SecondaryHostName" class="form-control sec-input" placeholder="예: HONPKS" />
|
<p class="small mb-2">이 작업은 하니웰 <code>HSCSERVER_Servicehost.exe</code> 프로세스를 <strong>종료하지 않습니다.</strong></p>
|
||||||
</div>
|
<ul class="small mb-0">
|
||||||
<div class="col-md-4">
|
<li>인증서 파일(.der)만 원격 서버의 Trusted 저장소로 복사됩니다.</li>
|
||||||
<label class="form-label text-warning">IP Address (Yellow)</label>
|
<li>실제 적용은 추후 엔지니어가 서버를 수동으로 재시작할 때 반영됩니다.</li>
|
||||||
<input asp-for="SecondaryIpA" class="form-control sec-input" placeholder="192.168.0.21" />
|
<li>현재 운영 중인 실시간 데이터 및 Station 연결에 영향을 주지 않습니다.</li>
|
||||||
</div>
|
</ul>
|
||||||
<div class="col-md-4">
|
</div>
|
||||||
<label class="form-label text-success">IP Address (Green)</label>
|
|
||||||
<input asp-for="SecondaryIpB" class="form-control sec-input" placeholder="192.168.1.21" />
|
<div class="mt-3">
|
||||||
</div>
|
@if (ViewBag.IsCertExists == true)
|
||||||
</div>
|
{
|
||||||
|
<div class="form-check mb-3">
|
||||||
<div class="card bg-light border-warning mt-5">
|
<input class="form-check-input" type="checkbox" id="chkUnlock" onchange="toggleCertBtn()">
|
||||||
<div class="card-body">
|
<label class="form-check-label text-primary fw-bold" for="chkUnlock">
|
||||||
@if (ViewBag.IsCertExists == true)
|
기존 인증서 갱신 및 파일 전송에 동의합니다.
|
||||||
{
|
</label>
|
||||||
<div class="alert alert-warning d-flex align-items-center">
|
|
||||||
<span class="fs-4 me-3">⚠️</span>
|
|
||||||
<div>
|
|
||||||
<strong>기존 인증서가 이미 존재합니다!</strong><br />
|
|
||||||
새로 생성 시 기존 서버와의 통신 신뢰관계(Trust)가 깨질 수 있습니다.
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<button type="button" id="btnGenerate" class="btn btn-secondary btn-lg w-100 shadow-sm" onclick="confirmTransfer()" disabled>
|
||||||
|
<i class="bi bi-lock-fill"></i> 기존 인증서 존재 (보호됨)
|
||||||
<div class="form-check mb-3">
|
</button>
|
||||||
<input class="form-check-input" type="checkbox" id="chkUnlock" onchange="toggleCertBtn()">
|
}
|
||||||
<label class="form-check-label text-danger fw-bold" for="chkUnlock">
|
else
|
||||||
[위험 인지] 기존 인증서를 무시하고 새로 생성하는 것에 동의합니다.
|
{
|
||||||
</label>
|
<button type="button" id="btnGenerate" class="btn btn-primary btn-lg w-100 shadow-sm" onclick="confirmTransfer()">
|
||||||
</div>
|
<i class="bi bi-send-check-fill"></i> 인증서 생성 및 원격 전송 시작
|
||||||
|
</button>
|
||||||
<button type="submit" id="btnGenerate" class="btn btn-danger btn-lg w-100" disabled>
|
}
|
||||||
🔒 인증서가 이미 존재하여 잠겨있습니다
|
</div>
|
||||||
</button>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<button type="submit" id="btnGenerate" class="btn btn-primary btn-lg w-100">
|
|
||||||
🚀 인증서 생성 및 자동 적용 시작
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -92,37 +120,48 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
@section Scripts {
|
@section Scripts {
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
// 1. 이중화 선택 로직
|
|
||||||
$('#systemType').change(function () {
|
$('#systemType').change(function () {
|
||||||
const isRedundant = $(this).val() === 'true';
|
const isRedundant = $(this).val() === 'true';
|
||||||
$('.sec-input').prop('disabled', !isRedundant);
|
$('.sec-input').prop('disabled', !isRedundant);
|
||||||
|
|
||||||
if(isRedundant) {
|
if(isRedundant) {
|
||||||
$('#secondaryBadge').removeClass('bg-secondary').addClass('bg-info');
|
$('#secondaryContainer').removeClass('bg-light').addClass('bg-white');
|
||||||
|
$('#secondaryTitle').removeClass('text-secondary').addClass('text-primary');
|
||||||
} else {
|
} else {
|
||||||
$('#secondaryBadge').removeClass('bg-info').addClass('bg-secondary');
|
$('#secondaryContainer').addClass('bg-light').removeClass('bg-white');
|
||||||
|
$('#secondaryTitle').addClass('text-secondary').removeClass('text-primary');
|
||||||
}
|
}
|
||||||
});
|
}).trigger('change');
|
||||||
|
|
||||||
$('#systemType').trigger('change');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. 버튼 잠금 해제 로직
|
|
||||||
function toggleCertBtn() {
|
function toggleCertBtn() {
|
||||||
const isChecked = document.getElementById('chkUnlock').checked;
|
const isChecked = $('#chkUnlock').is(':checked');
|
||||||
const btn = document.getElementById('btnGenerate');
|
const btn = $('#btnGenerate');
|
||||||
|
|
||||||
if(isChecked) {
|
if(isChecked) {
|
||||||
btn.disabled = false;
|
btn.prop('disabled', false).html('<i class="bi bi-arrow-repeat"></i> 인증서 갱신 및 전송 실행').removeClass('btn-secondary').addClass('btn-info text-white');
|
||||||
btn.innerText = "🔥 인증서 새로 생성 (강제 실행)";
|
|
||||||
btn.classList.replace('btn-danger', 'btn-warning');
|
|
||||||
} else {
|
} else {
|
||||||
btn.disabled = true;
|
btn.prop('disabled', true).html('<i class="bi bi-lock-fill"></i> 기존 인증서 존재 (보호됨)').removeClass('btn-info text-white').addClass('btn-secondary');
|
||||||
btn.innerText = "🔒 인증서가 이미 존재하여 잠겨있습니다";
|
|
||||||
btn.classList.replace('btn-warning', 'btn-danger');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function confirmTransfer() {
|
||||||
|
Swal.fire({
|
||||||
|
title: '인증서 전송',
|
||||||
|
text: "하니웰 서버의 파일만 교체됩니다. 서비스 연결은 끊기지 않습니다. 진행하시겠습니까?",
|
||||||
|
icon: 'question',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonColor: '#0d6efd',
|
||||||
|
cancelButtonColor: '#6c757d',
|
||||||
|
confirmButtonText: '네, 전송합니다',
|
||||||
|
cancelButtonText: '취소'
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
$('#certForm').submit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
}
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -13,10 +13,10 @@ using System.Reflection;
|
|||||||
[assembly: System.Reflection.AssemblyCompanyAttribute("OpcPks.Web")]
|
[assembly: System.Reflection.AssemblyCompanyAttribute("OpcPks.Web")]
|
||||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+4e006a5a5f65f597fafaa3777b8a1073944eada2")]
|
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+4ea351946aa5ffb76c8c693b768cb7f460f0cb79")]
|
||||||
[assembly: System.Reflection.AssemblyProductAttribute("OpcPks.Web")]
|
[assembly: System.Reflection.AssemblyProductAttribute("OpcPks.Web")]
|
||||||
[assembly: System.Reflection.AssemblyTitleAttribute("OpcPks.Web")]
|
[assembly: System.Reflection.AssemblyTitleAttribute("OpcPks.Web")]
|
||||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||||
|
|
||||||
// MSBuild WriteCodeFragment 클래스에서 생성되었습니다.
|
// Generated by the MSBuild WriteCodeFragment class.
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
e1494e87c92c732759b5281464380da6aa825fc12aba342324181898b9f0d33d
|
b82f0dd43a91c4b442cef90caf9521112866289d267678a4f9dd1c8eb3205e82
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -1 +1 @@
|
|||||||
da88b9ec6f30d3a67ed38dde28b0fbcd513cedae3a9742b74a5f06636cf1a15e
|
211409f9c66f4d86deef10e146a286bf43dcff595588425d60ad90ee2a7fd32f
|
||||||
|
|||||||
Binary file not shown.
@@ -1 +1 @@
|
|||||||
6467bb07a49b01c95068173444a1866c77670de41eed0f2a920478da9d4c65bc
|
ef34cdd3c41169ab6b411993059073056d84145d36a38b4aea950172aff7d741
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -10,11 +10,11 @@
|
|||||||
"projectUniqueName": "/home/pacer/projects/OpcPksPlatform/OpcPks.Core/OpcPks.Core.csproj",
|
"projectUniqueName": "/home/pacer/projects/OpcPksPlatform/OpcPks.Core/OpcPks.Core.csproj",
|
||||||
"projectName": "OpcPks.Core",
|
"projectName": "OpcPks.Core",
|
||||||
"projectPath": "/home/pacer/projects/OpcPksPlatform/OpcPks.Core/OpcPks.Core.csproj",
|
"projectPath": "/home/pacer/projects/OpcPksPlatform/OpcPks.Core/OpcPks.Core.csproj",
|
||||||
"packagesPath": "/root/.nuget/packages/",
|
"packagesPath": "/home/pacer/.nuget/packages/",
|
||||||
"outputPath": "/home/pacer/projects/OpcPksPlatform/OpcPks.Core/obj/",
|
"outputPath": "/home/pacer/projects/OpcPksPlatform/OpcPks.Core/obj/",
|
||||||
"projectStyle": "PackageReference",
|
"projectStyle": "PackageReference",
|
||||||
"configFilePaths": [
|
"configFilePaths": [
|
||||||
"/root/.nuget/NuGet/NuGet.Config"
|
"/home/pacer/.nuget/NuGet/NuGet.Config"
|
||||||
],
|
],
|
||||||
"originalTargetFrameworks": [
|
"originalTargetFrameworks": [
|
||||||
"net8.0"
|
"net8.0"
|
||||||
@@ -73,11 +73,11 @@
|
|||||||
"projectUniqueName": "/home/pacer/projects/OpcPksPlatform/OpcPks.Web/OpcPks.Web.csproj",
|
"projectUniqueName": "/home/pacer/projects/OpcPksPlatform/OpcPks.Web/OpcPks.Web.csproj",
|
||||||
"projectName": "OpcPks.Web",
|
"projectName": "OpcPks.Web",
|
||||||
"projectPath": "/home/pacer/projects/OpcPksPlatform/OpcPks.Web/OpcPks.Web.csproj",
|
"projectPath": "/home/pacer/projects/OpcPksPlatform/OpcPks.Web/OpcPks.Web.csproj",
|
||||||
"packagesPath": "/root/.nuget/packages/",
|
"packagesPath": "/home/pacer/.nuget/packages/",
|
||||||
"outputPath": "/home/pacer/projects/OpcPksPlatform/OpcPks.Web/obj/",
|
"outputPath": "/home/pacer/projects/OpcPksPlatform/OpcPks.Web/obj/",
|
||||||
"projectStyle": "PackageReference",
|
"projectStyle": "PackageReference",
|
||||||
"configFilePaths": [
|
"configFilePaths": [
|
||||||
"/root/.nuget/NuGet/NuGet.Config"
|
"/home/pacer/.nuget/NuGet/NuGet.Config"
|
||||||
],
|
],
|
||||||
"originalTargetFrameworks": [
|
"originalTargetFrameworks": [
|
||||||
"net8.0"
|
"net8.0"
|
||||||
|
|||||||
@@ -4,12 +4,12 @@
|
|||||||
<RestoreSuccess Condition=" '$(RestoreSuccess)' == '' ">True</RestoreSuccess>
|
<RestoreSuccess Condition=" '$(RestoreSuccess)' == '' ">True</RestoreSuccess>
|
||||||
<RestoreTool Condition=" '$(RestoreTool)' == '' ">NuGet</RestoreTool>
|
<RestoreTool Condition=" '$(RestoreTool)' == '' ">NuGet</RestoreTool>
|
||||||
<ProjectAssetsFile Condition=" '$(ProjectAssetsFile)' == '' ">$(MSBuildThisFileDirectory)project.assets.json</ProjectAssetsFile>
|
<ProjectAssetsFile Condition=" '$(ProjectAssetsFile)' == '' ">$(MSBuildThisFileDirectory)project.assets.json</ProjectAssetsFile>
|
||||||
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">/root/.nuget/packages/</NuGetPackageRoot>
|
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">/home/pacer/.nuget/packages/</NuGetPackageRoot>
|
||||||
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">/root/.nuget/packages/</NuGetPackageFolders>
|
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">/home/pacer/.nuget/packages/</NuGetPackageFolders>
|
||||||
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
|
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
|
||||||
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.8.1</NuGetToolVersion>
|
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.8.1</NuGetToolVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||||
<SourceRoot Include="/root/.nuget/packages/" />
|
<SourceRoot Include="/home/pacer/.nuget/packages/" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -574,7 +574,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"packageFolders": {
|
"packageFolders": {
|
||||||
"/root/.nuget/packages/": {}
|
"/home/pacer/.nuget/packages/": {}
|
||||||
},
|
},
|
||||||
"project": {
|
"project": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
@@ -582,11 +582,11 @@
|
|||||||
"projectUniqueName": "/home/pacer/projects/OpcPksPlatform/OpcPks.Web/OpcPks.Web.csproj",
|
"projectUniqueName": "/home/pacer/projects/OpcPksPlatform/OpcPks.Web/OpcPks.Web.csproj",
|
||||||
"projectName": "OpcPks.Web",
|
"projectName": "OpcPks.Web",
|
||||||
"projectPath": "/home/pacer/projects/OpcPksPlatform/OpcPks.Web/OpcPks.Web.csproj",
|
"projectPath": "/home/pacer/projects/OpcPksPlatform/OpcPks.Web/OpcPks.Web.csproj",
|
||||||
"packagesPath": "/root/.nuget/packages/",
|
"packagesPath": "/home/pacer/.nuget/packages/",
|
||||||
"outputPath": "/home/pacer/projects/OpcPksPlatform/OpcPks.Web/obj/",
|
"outputPath": "/home/pacer/projects/OpcPksPlatform/OpcPks.Web/obj/",
|
||||||
"projectStyle": "PackageReference",
|
"projectStyle": "PackageReference",
|
||||||
"configFilePaths": [
|
"configFilePaths": [
|
||||||
"/root/.nuget/NuGet/NuGet.Config"
|
"/home/pacer/.nuget/NuGet/NuGet.Config"
|
||||||
],
|
],
|
||||||
"originalTargetFrameworks": [
|
"originalTargetFrameworks": [
|
||||||
"net8.0"
|
"net8.0"
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
{
|
{
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"dgSpecHash": "1ODJAJibZeCWjDB9k93/7u2tzIkxUKCpt4LpJwtoyk9SPN+V9Z00ioG5BCZSRPJv0f/i3y9/JoZYPrLxXjCawQ==",
|
"dgSpecHash": "Lhk78ivUpJSYUca9AozBeOWL1MSXwG/J03crkR8ohPXr0Jr13fI6SjcPbbT0IMW8j7gHacAchV4I/6FtcYipBA==",
|
||||||
"success": true,
|
"success": true,
|
||||||
"projectFilePath": "/home/pacer/projects/OpcPksPlatform/OpcPks.Web/OpcPks.Web.csproj",
|
"projectFilePath": "/home/pacer/projects/OpcPksPlatform/OpcPks.Web/OpcPks.Web.csproj",
|
||||||
"expectedPackageFiles": [
|
"expectedPackageFiles": [
|
||||||
"/root/.nuget/packages/microsoft.extensions.dependencyinjection.abstractions/8.0.1/microsoft.extensions.dependencyinjection.abstractions.8.0.1.nupkg.sha512",
|
"/home/pacer/.nuget/packages/microsoft.extensions.dependencyinjection.abstractions/8.0.1/microsoft.extensions.dependencyinjection.abstractions.8.0.1.nupkg.sha512",
|
||||||
"/root/.nuget/packages/microsoft.extensions.logging.abstractions/8.0.1/microsoft.extensions.logging.abstractions.8.0.1.nupkg.sha512",
|
"/home/pacer/.nuget/packages/microsoft.extensions.logging.abstractions/8.0.1/microsoft.extensions.logging.abstractions.8.0.1.nupkg.sha512",
|
||||||
"/root/.nuget/packages/newtonsoft.json/13.0.3/newtonsoft.json.13.0.3.nupkg.sha512",
|
"/home/pacer/.nuget/packages/newtonsoft.json/13.0.3/newtonsoft.json.13.0.3.nupkg.sha512",
|
||||||
"/root/.nuget/packages/npgsql/8.0.4/npgsql.8.0.4.nupkg.sha512",
|
"/home/pacer/.nuget/packages/npgsql/8.0.4/npgsql.8.0.4.nupkg.sha512",
|
||||||
"/root/.nuget/packages/opcfoundation.netstandard.opc.ua.client/1.5.374.78/opcfoundation.netstandard.opc.ua.client.1.5.374.78.nupkg.sha512",
|
"/home/pacer/.nuget/packages/opcfoundation.netstandard.opc.ua.client/1.5.374.78/opcfoundation.netstandard.opc.ua.client.1.5.374.78.nupkg.sha512",
|
||||||
"/root/.nuget/packages/opcfoundation.netstandard.opc.ua.configuration/1.5.374.78/opcfoundation.netstandard.opc.ua.configuration.1.5.374.78.nupkg.sha512",
|
"/home/pacer/.nuget/packages/opcfoundation.netstandard.opc.ua.configuration/1.5.374.78/opcfoundation.netstandard.opc.ua.configuration.1.5.374.78.nupkg.sha512",
|
||||||
"/root/.nuget/packages/opcfoundation.netstandard.opc.ua.core/1.5.374.78/opcfoundation.netstandard.opc.ua.core.1.5.374.78.nupkg.sha512",
|
"/home/pacer/.nuget/packages/opcfoundation.netstandard.opc.ua.core/1.5.374.78/opcfoundation.netstandard.opc.ua.core.1.5.374.78.nupkg.sha512",
|
||||||
"/root/.nuget/packages/opcfoundation.netstandard.opc.ua.security.certificates/1.5.374.78/opcfoundation.netstandard.opc.ua.security.certificates.1.5.374.78.nupkg.sha512",
|
"/home/pacer/.nuget/packages/opcfoundation.netstandard.opc.ua.security.certificates/1.5.374.78/opcfoundation.netstandard.opc.ua.security.certificates.1.5.374.78.nupkg.sha512",
|
||||||
"/root/.nuget/packages/system.formats.asn1/8.0.1/system.formats.asn1.8.0.1.nupkg.sha512",
|
"/home/pacer/.nuget/packages/system.formats.asn1/8.0.1/system.formats.asn1.8.0.1.nupkg.sha512",
|
||||||
"/root/.nuget/packages/system.security.cryptography.cng/5.0.0/system.security.cryptography.cng.5.0.0.nupkg.sha512"
|
"/home/pacer/.nuget/packages/system.security.cryptography.cng/5.0.0/system.security.cryptography.cng.5.0.0.nupkg.sha512"
|
||||||
],
|
],
|
||||||
"logs": []
|
"logs": []
|
||||||
}
|
}
|
||||||
83
opcUaManager/Controllers/CertController.cs
Normal file
83
opcUaManager/Controllers/CertController.cs
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using OpcUaManager.Models;
|
||||||
|
using OpcUaManager.Services;
|
||||||
|
|
||||||
|
namespace OpcUaManager.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/cert")]
|
||||||
|
[Produces("application/json")]
|
||||||
|
public class CertController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly CertService _certService;
|
||||||
|
private readonly ILogger<CertController> _logger;
|
||||||
|
|
||||||
|
public CertController(CertService certService, ILogger<CertController> logger)
|
||||||
|
{
|
||||||
|
_certService = certService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// X.509 클라이언트 인증서를 생성하고 pki/own/certs/ 에 PFX로 저장합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 원본 Program.cs 의 인증서 체계(pki/ 폴더, Exportable|MachineKeySet 플래그)를 유지합니다.
|
||||||
|
///
|
||||||
|
/// Sample request:
|
||||||
|
///
|
||||||
|
/// POST /api/cert/generate
|
||||||
|
/// {
|
||||||
|
/// "clientHostName": "dbsvr",
|
||||||
|
/// "applicationName": "OpcTestClient",
|
||||||
|
/// "serverHostName": "opc-server-01",
|
||||||
|
/// "serverIp": "192.168.0.20",
|
||||||
|
/// "pfxPassword": "",
|
||||||
|
/// "validDays": 365
|
||||||
|
/// }
|
||||||
|
/// </remarks>
|
||||||
|
[HttpPost("generate")]
|
||||||
|
[ProducesResponseType(typeof(CertCreateResult), 200)]
|
||||||
|
[ProducesResponseType(typeof(ApiError), 400)]
|
||||||
|
public async Task<IActionResult> Generate([FromBody] CertCreateRequest req)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(req.ClientHostName) ||
|
||||||
|
string.IsNullOrWhiteSpace(req.ApplicationName))
|
||||||
|
return BadRequest(new ApiError
|
||||||
|
{
|
||||||
|
Error = "필수값 누락",
|
||||||
|
Detail = "ClientHostName, ApplicationName 은 필수입니다."
|
||||||
|
});
|
||||||
|
|
||||||
|
_logger.LogInformation("인증서 생성 요청: {Host}/{App}", req.ClientHostName, req.ApplicationName);
|
||||||
|
|
||||||
|
var result = await _certService.GenerateAsync(req);
|
||||||
|
|
||||||
|
if (!result.Success)
|
||||||
|
return BadRequest(new ApiError { Error = "인증서 생성 실패", Detail = result.Message });
|
||||||
|
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>pki/own/certs/ 폴더의 PFX 파일 목록을 반환합니다.</summary>
|
||||||
|
[HttpGet("list")]
|
||||||
|
[ProducesResponseType(typeof(IEnumerable<string>), 200)]
|
||||||
|
public IActionResult List()
|
||||||
|
=> Ok(_certService.ListCertificates());
|
||||||
|
|
||||||
|
/// <summary>PFX 파일을 다운로드합니다.</summary>
|
||||||
|
[HttpGet("download/{fileName}")]
|
||||||
|
public IActionResult Download(string fileName)
|
||||||
|
{
|
||||||
|
// 경로 traversal 방지
|
||||||
|
if (fileName.Contains("..") || fileName.Contains('/') || fileName.Contains('\\'))
|
||||||
|
return BadRequest(new ApiError { Error = "잘못된 파일 이름" });
|
||||||
|
|
||||||
|
string path = Path.Combine("pki", "own", "certs", fileName);
|
||||||
|
if (!System.IO.File.Exists(path))
|
||||||
|
return NotFound(new ApiError { Error = "파일 없음", Detail = path });
|
||||||
|
|
||||||
|
byte[] bytes = System.IO.File.ReadAllBytes(path);
|
||||||
|
return File(bytes, "application/x-pkcs12", fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
75
opcUaManager/Controllers/CrawlerController.cs
Normal file
75
opcUaManager/Controllers/CrawlerController.cs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using OpcUaManager.Models;
|
||||||
|
using OpcUaManager.Services;
|
||||||
|
|
||||||
|
namespace OpcUaManager.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/crawler")]
|
||||||
|
[Produces("application/json")]
|
||||||
|
public class CrawlerController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly OpcCrawlerService _crawlerSvc;
|
||||||
|
private readonly OpcSessionService _sessionSvc;
|
||||||
|
private readonly ILogger<CrawlerController> _logger;
|
||||||
|
|
||||||
|
public CrawlerController(
|
||||||
|
OpcCrawlerService crawlerSvc,
|
||||||
|
OpcSessionService sessionSvc,
|
||||||
|
ILogger<CrawlerController> logger)
|
||||||
|
{
|
||||||
|
_crawlerSvc = crawlerSvc;
|
||||||
|
_sessionSvc = sessionSvc;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 지정한 시작 노드부터 OPC UA 노드 트리를 재귀 탐색합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Sample request:
|
||||||
|
///
|
||||||
|
/// POST /api/crawler/start
|
||||||
|
/// {
|
||||||
|
/// "startNodeId": "ns=1;s=$assetmodel",
|
||||||
|
/// "maxDepth": 5
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// 탐색 결과는 응답 JSON 과 함께 서버 로컬에 Honeywell_FullMap.csv 로 저장됩니다.
|
||||||
|
/// </remarks>
|
||||||
|
[HttpPost("start")]
|
||||||
|
[ProducesResponseType(typeof(CrawlResult), 200)]
|
||||||
|
[ProducesResponseType(typeof(ApiError), 400)]
|
||||||
|
public async Task<IActionResult> Start([FromBody] CrawlRequest req)
|
||||||
|
{
|
||||||
|
if (!_sessionSvc.IsConnected)
|
||||||
|
return BadRequest(new ApiError
|
||||||
|
{
|
||||||
|
Error = "세션 없음",
|
||||||
|
Detail = "먼저 /api/session/connect 로 OPC 서버에 연결하세요."
|
||||||
|
});
|
||||||
|
|
||||||
|
_logger.LogInformation("Crawler 시작: {NodeId} (depth={Depth})",
|
||||||
|
req.StartNodeId, req.MaxDepth);
|
||||||
|
|
||||||
|
// 대규모 탐사는 시간이 오래 걸릴 수 있으므로 타임아웃을 늘려줍니다
|
||||||
|
HttpContext.RequestAborted.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var result = await _crawlerSvc.CrawlAsync(req);
|
||||||
|
|
||||||
|
return result.Success ? Ok(result)
|
||||||
|
: BadRequest(new ApiError { Error = "탐사 실패", Detail = result.Message });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>마지막 탐사로 생성된 CSV 파일을 다운로드합니다.</summary>
|
||||||
|
[HttpGet("csv")]
|
||||||
|
public IActionResult DownloadCsv()
|
||||||
|
{
|
||||||
|
string path = Path.GetFullPath("Honeywell_FullMap.csv");
|
||||||
|
if (!System.IO.File.Exists(path))
|
||||||
|
return NotFound(new ApiError { Error = "CSV 없음", Detail = "탐사를 먼저 실행하세요." });
|
||||||
|
|
||||||
|
byte[] bytes = System.IO.File.ReadAllBytes(path);
|
||||||
|
return File(bytes, "text/csv", "Honeywell_FullMap.csv");
|
||||||
|
}
|
||||||
|
}
|
||||||
98
opcUaManager/Controllers/DatabaseController.cs
Normal file
98
opcUaManager/Controllers/DatabaseController.cs
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using OpcUaManager.Models;
|
||||||
|
using OpcUaManager.Services;
|
||||||
|
|
||||||
|
namespace OpcUaManager.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/database")]
|
||||||
|
[Produces("application/json")]
|
||||||
|
public class DatabaseController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly DatabaseService _dbSvc;
|
||||||
|
private readonly ILogger<DatabaseController> _logger;
|
||||||
|
|
||||||
|
public DatabaseController(DatabaseService dbSvc, ILogger<DatabaseController> logger)
|
||||||
|
{
|
||||||
|
_dbSvc = dbSvc;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// DB 연결을 테스트합니다.
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("test")]
|
||||||
|
[ProducesResponseType(200)]
|
||||||
|
[ProducesResponseType(typeof(ApiError), 400)]
|
||||||
|
public async Task<IActionResult> Test([FromBody] DbWriteRequest req)
|
||||||
|
{
|
||||||
|
var (ok, msg) = await _dbSvc.TestConnectionAsync(req);
|
||||||
|
return ok ? Ok(new { Message = msg })
|
||||||
|
: BadRequest(new ApiError { Error = "연결 실패", Detail = msg });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// opc_history 테이블을 없으면 생성합니다.
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("init")]
|
||||||
|
[ProducesResponseType(200)]
|
||||||
|
[ProducesResponseType(typeof(ApiError), 400)]
|
||||||
|
public async Task<IActionResult> Init([FromBody] DbWriteRequest req)
|
||||||
|
{
|
||||||
|
var (ok, msg) = await _dbSvc.EnsureTableAsync(req);
|
||||||
|
return ok ? Ok(new { Message = msg })
|
||||||
|
: BadRequest(new ApiError { Error = "테이블 초기화 실패", Detail = msg });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// OPC 태그를 지정 횟수만큼 읽어 DB에 저장합니다 (원본 5회 루프).
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Sample request:
|
||||||
|
///
|
||||||
|
/// POST /api/database/write
|
||||||
|
/// {
|
||||||
|
/// "tagNodeId": "ns=1;s=shinam:p-6102.hzset.fieldvalue",
|
||||||
|
/// "tagName": "p-6102",
|
||||||
|
/// "count": 5,
|
||||||
|
/// "intervalMs": 2000,
|
||||||
|
/// "dbHost": "localhost",
|
||||||
|
/// "dbName": "opcdb",
|
||||||
|
/// "dbUser": "postgres",
|
||||||
|
/// "dbPassword": "postgres"
|
||||||
|
/// }
|
||||||
|
/// </remarks>
|
||||||
|
[HttpPost("write")]
|
||||||
|
[ProducesResponseType(typeof(DbWriteResult), 200)]
|
||||||
|
[ProducesResponseType(typeof(ApiError), 400)]
|
||||||
|
public async Task<IActionResult> Write([FromBody] DbWriteRequest req)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(req.TagNodeId) || string.IsNullOrWhiteSpace(req.TagName))
|
||||||
|
return BadRequest(new ApiError
|
||||||
|
{
|
||||||
|
Error = "필수값 누락",
|
||||||
|
Detail = "TagNodeId, TagName 은 필수입니다."
|
||||||
|
});
|
||||||
|
|
||||||
|
req.Count = Math.Clamp(req.Count, 1, 100);
|
||||||
|
|
||||||
|
_logger.LogInformation("DB 저장 시작: {Tag} × {Count}회", req.TagName, req.Count);
|
||||||
|
var result = await _dbSvc.WriteLoopAsync(req);
|
||||||
|
|
||||||
|
return result.Success ? Ok(result)
|
||||||
|
: BadRequest(new ApiError { Error = "저장 실패", Detail = result.Message });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// opc_history 테이블의 최근 레코드를 조회합니다.
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("query")]
|
||||||
|
[ProducesResponseType(typeof(DbQueryResult), 200)]
|
||||||
|
[ProducesResponseType(typeof(ApiError), 400)]
|
||||||
|
public async Task<IActionResult> Query([FromBody] DbWriteRequest req, [FromQuery] int limit = 100)
|
||||||
|
{
|
||||||
|
var result = await _dbSvc.QueryRecentAsync(req, limit);
|
||||||
|
return result.Success ? Ok(result)
|
||||||
|
: BadRequest(new ApiError { Error = "조회 실패", Detail = result.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
74
opcUaManager/Controllers/SessionController.cs
Normal file
74
opcUaManager/Controllers/SessionController.cs
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using OpcUaManager.Models;
|
||||||
|
using OpcUaManager.Services;
|
||||||
|
|
||||||
|
namespace OpcUaManager.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/session")]
|
||||||
|
[Produces("application/json")]
|
||||||
|
public class SessionController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly OpcSessionService _sessionSvc;
|
||||||
|
private readonly ILogger<SessionController> _logger;
|
||||||
|
|
||||||
|
public SessionController(OpcSessionService sessionSvc, ILogger<SessionController> logger)
|
||||||
|
{
|
||||||
|
_sessionSvc = sessionSvc;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// OPC UA 서버에 연결합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Sample request:
|
||||||
|
///
|
||||||
|
/// POST /api/session/connect
|
||||||
|
/// {
|
||||||
|
/// "serverIp": "192.168.0.20",
|
||||||
|
/// "port": 4840,
|
||||||
|
/// "userName": "mngr",
|
||||||
|
/// "password": "mngr",
|
||||||
|
/// "securityPolicy": "Basic256Sha256",
|
||||||
|
/// "sessionTimeoutMs": 60000,
|
||||||
|
/// "pfxPath": "pki/own/certs/OpcTestClient.pfx",
|
||||||
|
/// "pfxPassword": "",
|
||||||
|
/// "applicationName": "OpcTestClient",
|
||||||
|
/// "applicationUri": "urn:dbsvr:OpcTestClient"
|
||||||
|
/// }
|
||||||
|
/// </remarks>
|
||||||
|
[HttpPost("connect")]
|
||||||
|
[ProducesResponseType(typeof(ConnectResult), 200)]
|
||||||
|
[ProducesResponseType(typeof(ApiError), 400)]
|
||||||
|
public async Task<IActionResult> Connect([FromBody] ConnectRequest req)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(req.ServerIp))
|
||||||
|
return BadRequest(new ApiError { Error = "ServerIp 는 필수입니다." });
|
||||||
|
|
||||||
|
_logger.LogInformation("연결 요청: {Ip}:{Port}", req.ServerIp, req.Port);
|
||||||
|
var result = await _sessionSvc.ConnectAsync(req);
|
||||||
|
|
||||||
|
return result.Success ? Ok(result)
|
||||||
|
: BadRequest(new ApiError { Error = "연결 실패", Detail = result.Message });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>현재 OPC UA 세션을 종료합니다.</summary>
|
||||||
|
[HttpPost("disconnect")]
|
||||||
|
[ProducesResponseType(200)]
|
||||||
|
public async Task<IActionResult> Disconnect()
|
||||||
|
{
|
||||||
|
await _sessionSvc.DisconnectAsync();
|
||||||
|
return Ok(new { Message = "세션 종료 완료" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>현재 세션 상태를 반환합니다.</summary>
|
||||||
|
[HttpGet("status")]
|
||||||
|
[ProducesResponseType(200)]
|
||||||
|
public IActionResult Status()
|
||||||
|
=> Ok(new
|
||||||
|
{
|
||||||
|
IsConnected = _sessionSvc.IsConnected,
|
||||||
|
SessionId = _sessionSvc.SessionId
|
||||||
|
});
|
||||||
|
}
|
||||||
172
opcUaManager/Models/Models.cs
Normal file
172
opcUaManager/Models/Models.cs
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
namespace OpcUaManager.Models;
|
||||||
|
|
||||||
|
// ── 인증서 관련 ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>프론트엔드에서 전달받는 인증서 생성 요청</summary>
|
||||||
|
public class CertCreateRequest
|
||||||
|
{
|
||||||
|
/// <summary>내 컴퓨터(클라이언트) 호스트명 e.g. "dbsvr"</summary>
|
||||||
|
public string ClientHostName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>OPC Application 이름 e.g. "OpcTestClient"</summary>
|
||||||
|
public string ApplicationName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>상대 OPC 서버 호스트명 (SAN DNS) e.g. "opc-server-01"</summary>
|
||||||
|
public string ServerHostName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>상대 OPC 서버 IP (SAN IP) e.g. "192.168.0.20"</summary>
|
||||||
|
public string ServerIp { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>PFX 비밀번호 (없으면 빈 문자열)</summary>
|
||||||
|
public string PfxPassword { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>인증서 유효 기간(일)</summary>
|
||||||
|
public int ValidDays { get; set; } = 365;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>인증서 생성 결과</summary>
|
||||||
|
public class CertCreateResult
|
||||||
|
{
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>생성된 PFX 파일의 서버 내 절대 경로</summary>
|
||||||
|
public string PfxPath { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>적용된 Application URI</summary>
|
||||||
|
public string ApplicationUri { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>썸프린트(SHA-1 hex)</summary>
|
||||||
|
public string Thumbprint { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string SerialNumber { get; set; } = string.Empty;
|
||||||
|
public string NotBefore { get; set; } = string.Empty;
|
||||||
|
public string NotAfter { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── OPC 세션 관련 ────────────────────────────────────────────
|
||||||
|
|
||||||
|
public class ConnectRequest
|
||||||
|
{
|
||||||
|
public string ServerIp { get; set; } = string.Empty;
|
||||||
|
public int Port { get; set; } = 4840;
|
||||||
|
public string UserName { get; set; } = string.Empty;
|
||||||
|
public string Password { get; set; } = string.Empty;
|
||||||
|
public string SecurityPolicy { get; set; } = "Basic256Sha256";
|
||||||
|
public int SessionTimeoutMs { get; set; } = 60000;
|
||||||
|
|
||||||
|
/// <summary>사용할 PFX 경로 (cert 생성 후 전달)</summary>
|
||||||
|
public string PfxPath { get; set; } = string.Empty;
|
||||||
|
public string PfxPassword { get; set; } = string.Empty;
|
||||||
|
public string ApplicationName { get; set; } = "OpcTestClient";
|
||||||
|
public string ApplicationUri { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ConnectResult
|
||||||
|
{
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
public string SessionId { get; set; } = string.Empty;
|
||||||
|
public string EndpointUrl { get; set; } = string.Empty;
|
||||||
|
public string SecurityMode { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Crawler 관련 ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
public class CrawlRequest
|
||||||
|
{
|
||||||
|
public string StartNodeId { get; set; } = "ns=1;s=$assetmodel";
|
||||||
|
public int MaxDepth { get; set; } = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TagMaster
|
||||||
|
{
|
||||||
|
public string TagName { get; set; } = string.Empty;
|
||||||
|
public string FullNodeId { get; set; } = string.Empty;
|
||||||
|
public string NodeClass { get; set; } = string.Empty;
|
||||||
|
public string DataType { get; set; } = string.Empty;
|
||||||
|
public int Level { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CrawlResult
|
||||||
|
{
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
public int TotalNodes { get; set; }
|
||||||
|
public List<TagMaster> Tags { get; set; } = [];
|
||||||
|
public string CsvPath { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Database 관련 ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
public class DbWriteRequest
|
||||||
|
{
|
||||||
|
/// <summary>읽을 OPC 태그 Node ID</summary>
|
||||||
|
public string TagNodeId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>DB에 저장할 태그 이름</summary>
|
||||||
|
public string TagName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>반복 횟수</summary>
|
||||||
|
public int Count { get; set; } = 5;
|
||||||
|
|
||||||
|
/// <summary>읽기 간격 (ms)</summary>
|
||||||
|
public int IntervalMs { get; set; } = 2000;
|
||||||
|
|
||||||
|
// DB 접속 정보
|
||||||
|
public string DbHost { get; set; } = "localhost";
|
||||||
|
public string DbName { get; set; } = "opcdb";
|
||||||
|
public string DbUser { get; set; } = "postgres";
|
||||||
|
public string DbPassword { get; set; } = "postgres";
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DbWriteRecord
|
||||||
|
{
|
||||||
|
public int Seq { get; set; }
|
||||||
|
public DateTime Timestamp { get; set; }
|
||||||
|
public string TagName { get; set; } = string.Empty;
|
||||||
|
public double Value { get; set; }
|
||||||
|
public string StatusCode { get; set; } = string.Empty;
|
||||||
|
public bool DbSaved { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DbWriteResult
|
||||||
|
{
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
public int SavedCount { get; set; }
|
||||||
|
public List<DbWriteRecord> Records { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DbQueryResult
|
||||||
|
{
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
public int TotalCount { get; set; }
|
||||||
|
public List<OpcHistoryRow> Rows { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public class OpcHistoryRow
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public string TagName { get; set; } = string.Empty;
|
||||||
|
public double TagValue { get; set; }
|
||||||
|
public string StatusCode { get; set; } = string.Empty;
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 공통 ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public class StatusCodeInfo
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string Hex { get; set; } = string.Empty;
|
||||||
|
public ulong Decimal { get; set; }
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ApiError
|
||||||
|
{
|
||||||
|
public string Error { get; set; } = string.Empty;
|
||||||
|
public string Detail { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
16
opcUaManager/OpcUaManager.csproj
Normal file
16
opcUaManager/OpcUaManager.csproj
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<RootNamespace>OpcUaManager</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Client" Version="1.5.374.70" />
|
||||||
|
<PackageReference Include="Npgsql" Version="8.0.3" />
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
90
opcUaManager/Program.cs
Normal file
90
opcUaManager/Program.cs
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
using OpcUaManager.Services;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// ── DI 등록 ───────────────────────────────────────────────────────────
|
||||||
|
builder.Services.AddSingleton<OpcSessionService>();
|
||||||
|
builder.Services.AddSingleton<CertService>();
|
||||||
|
builder.Services.AddSingleton<OpcCrawlerService>();
|
||||||
|
builder.Services.AddSingleton<DatabaseService>();
|
||||||
|
|
||||||
|
builder.Services.AddControllers()
|
||||||
|
.AddJsonOptions(opt =>
|
||||||
|
{
|
||||||
|
opt.JsonSerializerOptions.Converters.Add(
|
||||||
|
new System.Text.Json.Serialization.JsonStringEnumConverter());
|
||||||
|
opt.JsonSerializerOptions.PropertyNamingPolicy =
|
||||||
|
System.Text.Json.JsonNamingPolicy.CamelCase;
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
|
builder.Services.AddSwaggerGen(opt =>
|
||||||
|
{
|
||||||
|
opt.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo
|
||||||
|
{
|
||||||
|
Title = "OPC UA Manager API",
|
||||||
|
Version = "v1",
|
||||||
|
Description = "OPC UA 클라이언트 인증서 생성 · 세션 관리 · 노드 탐색 · DB 저장"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddCors(opt =>
|
||||||
|
{
|
||||||
|
opt.AddDefaultPolicy(policy =>
|
||||||
|
policy.AllowAnyOrigin()
|
||||||
|
.AllowAnyHeader()
|
||||||
|
.AllowAnyMethod());
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── 앱 빌드 ───────────────────────────────────────────────────────────
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
// ── 정적 파일: wwwroot 경로를 명시적으로 지정 ─────────────────────────
|
||||||
|
// dotnet run 실행 위치와 무관하게 프로젝트 루트의 wwwroot 를 서빙
|
||||||
|
var wwwrootPath = Path.Combine(
|
||||||
|
builder.Environment.ContentRootPath, "wwwroot");
|
||||||
|
|
||||||
|
if (!Directory.Exists(wwwrootPath))
|
||||||
|
{
|
||||||
|
// dotnet run 이 프로젝트 루트가 아닌 곳에서 실행될 경우 대비
|
||||||
|
wwwrootPath = Path.Combine(
|
||||||
|
AppContext.BaseDirectory, "wwwroot");
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Logger.LogInformation("wwwroot 경로: {Path}", wwwrootPath);
|
||||||
|
|
||||||
|
app.UseDefaultFiles(new DefaultFilesOptions
|
||||||
|
{
|
||||||
|
FileProvider = new Microsoft.Extensions.FileProviders.PhysicalFileProvider(wwwrootPath),
|
||||||
|
RequestPath = ""
|
||||||
|
});
|
||||||
|
app.UseStaticFiles(new StaticFileOptions
|
||||||
|
{
|
||||||
|
FileProvider = new Microsoft.Extensions.FileProviders.PhysicalFileProvider(wwwrootPath),
|
||||||
|
RequestPath = ""
|
||||||
|
});
|
||||||
|
|
||||||
|
app.UseSwagger();
|
||||||
|
app.UseSwaggerUI(opt =>
|
||||||
|
{
|
||||||
|
opt.SwaggerEndpoint("/swagger/v1/swagger.json", "OPC UA Manager v1");
|
||||||
|
opt.RoutePrefix = "swagger";
|
||||||
|
});
|
||||||
|
|
||||||
|
app.UseCors();
|
||||||
|
app.UseAuthorization();
|
||||||
|
app.MapControllers();
|
||||||
|
|
||||||
|
// ── PKI 디렉토리 초기화 ───────────────────────────────────────────────
|
||||||
|
foreach (var dir in new[]
|
||||||
|
{
|
||||||
|
"pki/own/certs", "pki/trusted/certs",
|
||||||
|
"pki/issuers/certs", "pki/rejected/certs"
|
||||||
|
})
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
|
||||||
|
app.Logger.LogInformation("=== OPC UA Manager 시작 ===");
|
||||||
|
app.Logger.LogInformation("Frontend : http://localhost:5000/");
|
||||||
|
app.Logger.LogInformation("Swagger : http://localhost:5000/swagger");
|
||||||
|
|
||||||
|
app.Run();
|
||||||
99
opcUaManager/README.md
Normal file
99
opcUaManager/README.md
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
# OPC UA Manager
|
||||||
|
|
||||||
|
원본 `Program.cs` (단일 파일)를 **ASP.NET Core Web API + 프론트엔드** 프로젝트로 분리한 버전입니다.
|
||||||
|
|
||||||
|
## 프로젝트 구조
|
||||||
|
|
||||||
|
```
|
||||||
|
OpcUaManager/
|
||||||
|
├── OpcUaManager.csproj
|
||||||
|
├── Program.cs ← ASP.NET Core 진입점, DI 등록
|
||||||
|
├── appsettings.json
|
||||||
|
├── init_db.sql ← PostgreSQL 테이블 초기화
|
||||||
|
│
|
||||||
|
├── Models/
|
||||||
|
│ └── Models.cs ← DTO / 도메인 모델 전체
|
||||||
|
│
|
||||||
|
├── Services/
|
||||||
|
│ ├── CertService.cs ← X.509 인증서 생성 (pki/ 폴더 체계 유지)
|
||||||
|
│ ├── OpcSessionService.cs ← OPC UA 세션 관리 (Singleton)
|
||||||
|
│ ├── OpcCrawlerService.cs ← HoneywellCrawler 로직 → Service 분리
|
||||||
|
│ └── DatabaseService.cs ← PostgreSQL 저장/조회 (SaveToDatabase 패턴 유지)
|
||||||
|
│
|
||||||
|
├── Controllers/
|
||||||
|
│ ├── CertController.cs ← POST /api/cert/generate
|
||||||
|
│ ├── SessionController.cs ← POST /api/session/connect|disconnect
|
||||||
|
│ ├── CrawlerController.cs ← POST /api/crawler/start
|
||||||
|
│ └── DatabaseController.cs ← POST /api/database/write|query|test
|
||||||
|
│
|
||||||
|
└── wwwroot/
|
||||||
|
└── index.html ← 프론트엔드 (단일 HTML, 백엔드 API 연결)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 원본 코드와의 대응 관계
|
||||||
|
|
||||||
|
| 원본 코드 위치 | 분리된 위치 |
|
||||||
|
|---|---|
|
||||||
|
| `Directory.CreateDirectory("pki/...")` | `CertService.EnsurePkiDirectories()` |
|
||||||
|
| `new X509Certificate2(pfxPath, ...)` | `CertService.LoadPfx()` |
|
||||||
|
| `ApplicationConfiguration { ... }` | `OpcSessionService.ConnectAsync()` |
|
||||||
|
| `DiscoveryClient → Session.Create` | `OpcSessionService.ConnectAsync()` |
|
||||||
|
| `HoneywellCrawler.BrowseRecursiveAsync` | `OpcCrawlerService.CrawlAsync()` |
|
||||||
|
| `HoneywellCrawler.SaveToCsv` | `OpcCrawlerService.SaveToCsvAsync()` |
|
||||||
|
| `SaveToDatabase(tagName, val, status)` | `DatabaseService.SaveToDatabaseAsync()` |
|
||||||
|
| `for (int i = 0; i < 5; i++) { ... }` | `DatabaseService.WriteLoopAsync()` |
|
||||||
|
|
||||||
|
**원본 인증서 체계 유지 항목:**
|
||||||
|
- `pki/own/certs/`, `pki/trusted/certs/`, `pki/issuers/certs/`, `pki/rejected/certs/` 폴더 구조
|
||||||
|
- `X509KeyStorageFlags.Exportable | MachineKeySet`
|
||||||
|
- `AutoAcceptUntrustedCertificates = true`, `AddAppCertToTrustedStore = true`
|
||||||
|
- OPC UA SAN(Subject Alternative Name) 에 `ApplicationUri` 포함 (필수 요구사항)
|
||||||
|
|
||||||
|
## 실행 방법
|
||||||
|
|
||||||
|
### 1. PostgreSQL 초기화
|
||||||
|
```bash
|
||||||
|
createdb opcdb
|
||||||
|
psql -U postgres -d opcdb -f init_db.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 백엔드 실행
|
||||||
|
```bash
|
||||||
|
cd OpcUaManager
|
||||||
|
dotnet restore
|
||||||
|
dotnet run
|
||||||
|
# → http://localhost:5000
|
||||||
|
# → Swagger: http://localhost:5000/swagger
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 프론트엔드 접속
|
||||||
|
브라우저에서 `http://localhost:5000` 접속
|
||||||
|
(또는 `wwwroot/index.html` 을 별도 Live Server로 열고 API URL을 `http://localhost:5000` 으로 설정)
|
||||||
|
|
||||||
|
## API 엔드포인트 요약
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/cert/generate 인증서 생성
|
||||||
|
GET /api/cert/list PFX 목록 조회
|
||||||
|
GET /api/cert/download/{name} PFX 다운로드
|
||||||
|
|
||||||
|
POST /api/session/connect OPC 연결
|
||||||
|
POST /api/session/disconnect OPC 연결 해제
|
||||||
|
GET /api/session/status 세션 상태
|
||||||
|
|
||||||
|
POST /api/crawler/start 노드 탐사 시작
|
||||||
|
GET /api/crawler/csv CSV 다운로드
|
||||||
|
|
||||||
|
POST /api/database/test DB 연결 테스트
|
||||||
|
POST /api/database/init 테이블 초기화
|
||||||
|
POST /api/database/write OPC 읽기 + DB 저장 (N회)
|
||||||
|
POST /api/database/query 최근 레코드 조회
|
||||||
|
```
|
||||||
|
|
||||||
|
## 의존 패키지
|
||||||
|
|
||||||
|
```xml
|
||||||
|
OPCFoundation.NetStandard.Opc.Ua.Client 1.5.x
|
||||||
|
Npgsql 8.0.x
|
||||||
|
Swashbuckle.AspNetCore 6.6.x
|
||||||
|
```
|
||||||
158
opcUaManager/Services/CertService.cs
Normal file
158
opcUaManager/Services/CertService.cs
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using OpcUaManager.Models;
|
||||||
|
|
||||||
|
namespace OpcUaManager.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// OPC UA 클라이언트용 X.509 인증서를 생성하고 PFX로 저장합니다.
|
||||||
|
/// 원본 Program.cs 의 인증서 체계(pki/ 폴더 구조, X509KeyStorageFlags)를 유지합니다.
|
||||||
|
/// </summary>
|
||||||
|
public class CertService
|
||||||
|
{
|
||||||
|
private readonly ILogger<CertService> _logger;
|
||||||
|
private readonly string _pkiRoot;
|
||||||
|
|
||||||
|
public CertService(ILogger<CertService> logger, IWebHostEnvironment env)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_pkiRoot = Path.Combine(env.ContentRootPath, "pki");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsurePkiDirectories()
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(Path.Combine(_pkiRoot, "own", "certs"));
|
||||||
|
Directory.CreateDirectory(Path.Combine(_pkiRoot, "trusted", "certs"));
|
||||||
|
Directory.CreateDirectory(Path.Combine(_pkiRoot, "issuers", "certs"));
|
||||||
|
Directory.CreateDirectory(Path.Combine(_pkiRoot, "rejected", "certs"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<CertCreateResult> GenerateAsync(CertCreateRequest req)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
EnsurePkiDirectories();
|
||||||
|
|
||||||
|
string appUri = $"urn:{req.ClientHostName}:{req.ApplicationName}";
|
||||||
|
|
||||||
|
// ── 1. RSA 키 생성 ────────────────────────────────────────
|
||||||
|
using var rsa = RSA.Create(2048);
|
||||||
|
|
||||||
|
// ── 2. Distinguished Name ──────────────────────────────────
|
||||||
|
var dn = new X500DistinguishedName(
|
||||||
|
$"CN={req.ApplicationName}, O={req.ClientHostName}, C=KR");
|
||||||
|
|
||||||
|
// ── 3. Certificate Request ─────────────────────────────────
|
||||||
|
var certReq = new CertificateRequest(
|
||||||
|
dn, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||||
|
|
||||||
|
// ── 4. Extensions ──────────────────────────────────────────
|
||||||
|
certReq.CertificateExtensions.Add(
|
||||||
|
new X509BasicConstraintsExtension(false, false, 0, true));
|
||||||
|
|
||||||
|
certReq.CertificateExtensions.Add(
|
||||||
|
new X509KeyUsageExtension(
|
||||||
|
X509KeyUsageFlags.DigitalSignature |
|
||||||
|
X509KeyUsageFlags.NonRepudiation |
|
||||||
|
X509KeyUsageFlags.KeyEncipherment |
|
||||||
|
X509KeyUsageFlags.DataEncipherment,
|
||||||
|
critical: true));
|
||||||
|
|
||||||
|
certReq.CertificateExtensions.Add(
|
||||||
|
new X509EnhancedKeyUsageExtension(
|
||||||
|
new OidCollection
|
||||||
|
{
|
||||||
|
new Oid("1.3.6.1.5.5.7.3.2"), // clientAuth
|
||||||
|
new Oid("1.3.6.1.5.5.7.3.1") // serverAuth
|
||||||
|
}, false));
|
||||||
|
|
||||||
|
certReq.CertificateExtensions.Add(
|
||||||
|
new X509SubjectKeyIdentifierExtension(certReq.PublicKey, false));
|
||||||
|
|
||||||
|
// ── 5. Subject Alternative Names ──────────────────────────
|
||||||
|
var sanBuilder = new SubjectAlternativeNameBuilder();
|
||||||
|
sanBuilder.AddUri(new Uri(appUri));
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(req.ServerIp) &&
|
||||||
|
System.Net.IPAddress.TryParse(req.ServerIp, out var ip))
|
||||||
|
sanBuilder.AddIpAddress(ip);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(req.ServerHostName))
|
||||||
|
sanBuilder.AddDnsName(req.ServerHostName);
|
||||||
|
|
||||||
|
sanBuilder.AddDnsName(req.ClientHostName);
|
||||||
|
sanBuilder.AddDnsName("localhost");
|
||||||
|
|
||||||
|
certReq.CertificateExtensions.Add(sanBuilder.Build());
|
||||||
|
|
||||||
|
// ── 6. 자체 서명 인증서 생성 ──────────────────────────────
|
||||||
|
var notBefore = DateTimeOffset.UtcNow.AddMinutes(-5);
|
||||||
|
var notAfter = notBefore.AddDays(req.ValidDays);
|
||||||
|
|
||||||
|
// FIX: CreateSelfSigned 는 이미 RSA 키가 연결된 X509Certificate2 를 반환함
|
||||||
|
// → CopyWithPrivateKey() 를 추가로 호출하면
|
||||||
|
// "The certificate already has an associated private key" 에러 발생
|
||||||
|
// → CreateSelfSigned 결과를 직접 Export(Pfx) 하면 개인키 포함됨
|
||||||
|
using var certWithKey = certReq.CreateSelfSigned(notBefore, notAfter);
|
||||||
|
|
||||||
|
// ── 7. PFX 내보내기 ───────────────────────────────────────
|
||||||
|
string pfxPassword = string.IsNullOrEmpty(req.PfxPassword) ? "" : req.PfxPassword;
|
||||||
|
|
||||||
|
// .NET 9 이상: X509CertificateLoader 권장이지만 net8 에서는 Export 사용
|
||||||
|
byte[] pfxBytes = certWithKey.Export(X509ContentType.Pfx, pfxPassword);
|
||||||
|
|
||||||
|
string pfxFileName = $"{req.ApplicationName}.pfx";
|
||||||
|
string pfxPath = Path.Combine(_pkiRoot, "own", "certs", pfxFileName);
|
||||||
|
File.WriteAllBytes(pfxPath, pfxBytes);
|
||||||
|
|
||||||
|
// ── 8. Trusted 폴더에 DER(공개키) 복사 ───────────────────
|
||||||
|
byte[] derBytes = certWithKey.Export(X509ContentType.Cert);
|
||||||
|
string derPath = Path.Combine(_pkiRoot, "trusted", "certs",
|
||||||
|
$"{req.ApplicationName}[{certWithKey.Thumbprint}].der");
|
||||||
|
File.WriteAllBytes(derPath, derBytes);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"인증서 생성 완료 → {PfxPath} | Thumbprint: {Thumbprint}",
|
||||||
|
pfxPath, certWithKey.Thumbprint);
|
||||||
|
|
||||||
|
return Task.FromResult(new CertCreateResult
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = $"인증서 생성 완료: {pfxFileName}",
|
||||||
|
PfxPath = pfxPath,
|
||||||
|
ApplicationUri = appUri,
|
||||||
|
Thumbprint = certWithKey.Thumbprint ?? string.Empty,
|
||||||
|
SerialNumber = certWithKey.SerialNumber ?? string.Empty,
|
||||||
|
NotBefore = notBefore.ToString("yyyy-MM-dd HH:mm:ss UTC"),
|
||||||
|
NotAfter = notAfter.ToString("yyyy-MM-dd HH:mm:ss UTC"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "인증서 생성 실패");
|
||||||
|
return Task.FromResult(new CertCreateResult
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = $"인증서 생성 실패: {ex.Message}"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 기존 PFX를 로드합니다 (원본 코드와 동일한 플래그).
|
||||||
|
/// </summary>
|
||||||
|
public X509Certificate2 LoadPfx(string pfxPath, string pfxPassword)
|
||||||
|
{
|
||||||
|
return new X509Certificate2(
|
||||||
|
pfxPath,
|
||||||
|
pfxPassword,
|
||||||
|
X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<string> ListCertificates()
|
||||||
|
{
|
||||||
|
string dir = Path.Combine(_pkiRoot, "own", "certs");
|
||||||
|
if (!Directory.Exists(dir)) return [];
|
||||||
|
return Directory.GetFiles(dir, "*.pfx").Select(Path.GetFileName)!;
|
||||||
|
}
|
||||||
|
}
|
||||||
223
opcUaManager/Services/DatabaseService.cs
Normal file
223
opcUaManager/Services/DatabaseService.cs
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
using Npgsql;
|
||||||
|
using OpcUaManager.Models;
|
||||||
|
|
||||||
|
namespace OpcUaManager.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PostgreSQL opc_history 테이블에 대한 읽기/쓰기를 담당합니다.
|
||||||
|
/// 원본 SaveToDatabase 함수의 INSERT 패턴을 그대로 유지하며,
|
||||||
|
/// OPC 태그 읽기(OpcSessionService)와 DB 저장을 조율합니다.
|
||||||
|
/// </summary>
|
||||||
|
public class DatabaseService
|
||||||
|
{
|
||||||
|
private readonly ILogger<DatabaseService> _logger;
|
||||||
|
private readonly OpcSessionService _sessionSvc;
|
||||||
|
|
||||||
|
public DatabaseService(ILogger<DatabaseService> logger, OpcSessionService sessionSvc)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_sessionSvc = sessionSvc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 연결 문자열 빌더 ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static string BuildConnString(DbWriteRequest req)
|
||||||
|
=> $"Host={req.DbHost};Username={req.DbUser};Password={req.DbPassword};Database={req.DbName}";
|
||||||
|
|
||||||
|
// ── DB/테이블 초기화 (최초 한 번) ────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// opc_history 테이블이 없으면 자동 생성합니다.
|
||||||
|
/// 원본 코드에는 없었지만, 프로젝트화 시 처음 실행 환경을 위해 추가합니다.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<(bool Ok, string Msg)> EnsureTableAsync(DbWriteRequest req)
|
||||||
|
{
|
||||||
|
const string ddl = """
|
||||||
|
CREATE TABLE IF NOT EXISTS opc_history (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
tag_name TEXT NOT NULL,
|
||||||
|
tag_value DOUBLE PRECISION NOT NULL,
|
||||||
|
status_code TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
""";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var conn = new NpgsqlConnection(BuildConnString(req));
|
||||||
|
await conn.OpenAsync();
|
||||||
|
await using var cmd = new NpgsqlCommand(ddl, conn);
|
||||||
|
await cmd.ExecuteNonQueryAsync();
|
||||||
|
return (true, "테이블 확인/생성 완료");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "테이블 초기화 실패");
|
||||||
|
return (false, ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── OPC 읽기 + DB 저장 루프 ──────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// OPC 노드를 req.Count 회 읽고 각 결과를 DB에 저장합니다.
|
||||||
|
/// 원본 for 루프 + SaveToDatabase 패턴을 그대로 유지합니다.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<DbWriteResult> WriteLoopAsync(DbWriteRequest req)
|
||||||
|
{
|
||||||
|
if (!_sessionSvc.IsConnected)
|
||||||
|
return new DbWriteResult { Success = false, Message = "OPC 세션이 연결되어 있지 않습니다." };
|
||||||
|
|
||||||
|
var (tableOk, tableMsg) = await EnsureTableAsync(req);
|
||||||
|
if (!tableOk)
|
||||||
|
return new DbWriteResult { Success = false, Message = $"테이블 오류: {tableMsg}" };
|
||||||
|
|
||||||
|
var records = new List<DbWriteRecord>();
|
||||||
|
int saved = 0;
|
||||||
|
|
||||||
|
for (int i = 1; i <= req.Count; i++)
|
||||||
|
{
|
||||||
|
// ── 1. OPC 태그 읽기 ────────────────────────────────────
|
||||||
|
double val = 0;
|
||||||
|
string status = "Unknown";
|
||||||
|
bool dbSaved = false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var (rawVal, sc) = await _sessionSvc.ReadValueAsync(req.TagNodeId);
|
||||||
|
val = Convert.ToDouble(rawVal);
|
||||||
|
status = sc;
|
||||||
|
_logger.LogInformation("[{I}/{N}] {Tag} = {Val} ({Status})",
|
||||||
|
i, req.Count, req.TagName, val, status);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
status = $"ReadError: {ex.Message}";
|
||||||
|
_logger.LogWarning("[{I}/{N}] OPC 읽기 실패: {Msg}", i, req.Count, ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 2. DB 저장 (원본 SaveToDatabase 로직) ───────────────
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await SaveToDatabaseAsync(req, val, status);
|
||||||
|
dbSaved = true;
|
||||||
|
saved++;
|
||||||
|
_logger.LogInformation("[{I}/{N}] DB 저장 완료.", i, req.Count);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "[{I}/{N}] DB 저장 실패", i, req.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
records.Add(new DbWriteRecord
|
||||||
|
{
|
||||||
|
Seq = i,
|
||||||
|
Timestamp = DateTime.UtcNow,
|
||||||
|
TagName = req.TagName,
|
||||||
|
Value = val,
|
||||||
|
StatusCode = status,
|
||||||
|
DbSaved = dbSaved
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── 3. 인터벌 대기 (마지막 회차는 생략) ─────────────────
|
||||||
|
if (i < req.Count && req.IntervalMs > 0)
|
||||||
|
await Task.Delay(req.IntervalMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DbWriteResult
|
||||||
|
{
|
||||||
|
Success = saved > 0,
|
||||||
|
Message = $"{saved}/{req.Count}회 저장 완료",
|
||||||
|
SavedCount = saved,
|
||||||
|
Records = records
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 원본 SaveToDatabase (INSERT 패턴 동일) ────────────────────────
|
||||||
|
|
||||||
|
private async Task SaveToDatabaseAsync(DbWriteRequest req, double val, string status)
|
||||||
|
{
|
||||||
|
await using var conn = new NpgsqlConnection(BuildConnString(req));
|
||||||
|
await conn.OpenAsync();
|
||||||
|
|
||||||
|
// 원본 동일 INSERT
|
||||||
|
const string sql =
|
||||||
|
"INSERT INTO opc_history (tag_name, tag_value, status_code) " +
|
||||||
|
"VALUES (@tag, @val, @status)";
|
||||||
|
|
||||||
|
await using var cmd = new NpgsqlCommand(sql, conn);
|
||||||
|
cmd.Parameters.AddWithValue("tag", req.TagName);
|
||||||
|
cmd.Parameters.AddWithValue("val", val);
|
||||||
|
cmd.Parameters.AddWithValue("status", status);
|
||||||
|
|
||||||
|
await cmd.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── DB 조회 ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public async Task<DbQueryResult> QueryRecentAsync(DbWriteRequest req, int limit = 100)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var conn = new NpgsqlConnection(BuildConnString(req));
|
||||||
|
await conn.OpenAsync();
|
||||||
|
|
||||||
|
string sql = $"""
|
||||||
|
SELECT id, tag_name, tag_value, status_code, created_at
|
||||||
|
FROM opc_history
|
||||||
|
ORDER BY id DESC
|
||||||
|
LIMIT {limit}
|
||||||
|
""";
|
||||||
|
|
||||||
|
await using var cmd = new NpgsqlCommand(sql, conn);
|
||||||
|
await using var reader = await cmd.ExecuteReaderAsync();
|
||||||
|
|
||||||
|
var rows = new List<OpcHistoryRow>();
|
||||||
|
while (await reader.ReadAsync())
|
||||||
|
{
|
||||||
|
rows.Add(new OpcHistoryRow
|
||||||
|
{
|
||||||
|
Id = reader.GetInt64(0),
|
||||||
|
TagName = reader.GetString(1),
|
||||||
|
TagValue = reader.GetDouble(2),
|
||||||
|
StatusCode = reader.GetString(3),
|
||||||
|
CreatedAt = reader.GetDateTime(4)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 전체 카운트
|
||||||
|
await reader.CloseAsync();
|
||||||
|
await using var cntCmd = new NpgsqlCommand("SELECT COUNT(*) FROM opc_history", conn);
|
||||||
|
long total = (long)(await cntCmd.ExecuteScalarAsync() ?? 0L);
|
||||||
|
|
||||||
|
return new DbQueryResult
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
TotalCount = (int)total,
|
||||||
|
Rows = rows
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "DB 조회 실패");
|
||||||
|
return new DbQueryResult { Success = false, Message = ex.Message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 연결 테스트 ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public async Task<(bool Ok, string Msg)> TestConnectionAsync(DbWriteRequest req)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var conn = new NpgsqlConnection(BuildConnString(req));
|
||||||
|
await conn.OpenAsync();
|
||||||
|
await using var cmd = new NpgsqlCommand("SELECT version()", conn);
|
||||||
|
var ver = await cmd.ExecuteScalarAsync();
|
||||||
|
return (true, $"연결 성공: {ver}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return (false, ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
161
opcUaManager/Services/OpcCrawlerService.cs
Normal file
161
opcUaManager/Services/OpcCrawlerService.cs
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
using Opc.Ua;
|
||||||
|
using Opc.Ua.Client;
|
||||||
|
using OpcUaManager.Models;
|
||||||
|
|
||||||
|
namespace OpcUaManager.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 원본 HoneywellCrawler 를 Service 로 분리.
|
||||||
|
/// v1.5.374.70: BrowseNextAsync 가 튜플 반환이 아닌 BrowseNextResponse 객체 반환으로 변경됨.
|
||||||
|
/// </summary>
|
||||||
|
public class OpcCrawlerService
|
||||||
|
{
|
||||||
|
private readonly ILogger<OpcCrawlerService> _logger;
|
||||||
|
private readonly OpcSessionService _sessionSvc;
|
||||||
|
|
||||||
|
public OpcCrawlerService(ILogger<OpcCrawlerService> logger, OpcSessionService sessionSvc)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_sessionSvc = sessionSvc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CrawlResult> CrawlAsync(CrawlRequest req)
|
||||||
|
{
|
||||||
|
if (!_sessionSvc.IsConnected)
|
||||||
|
return new CrawlResult { Success = false, Message = "세션이 연결되어 있지 않습니다." };
|
||||||
|
|
||||||
|
var session = _sessionSvc.GetRawSession();
|
||||||
|
if (session == null)
|
||||||
|
return new CrawlResult { Success = false, Message = "Raw 세션을 가져올 수 없습니다." };
|
||||||
|
|
||||||
|
var tags = new List<TagMaster>();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogInformation("저인망 탐사 시작: {NodeId}", req.StartNodeId);
|
||||||
|
NodeId rootNode = NodeId.Parse(req.StartNodeId);
|
||||||
|
await BrowseRecursiveAsync(session, rootNode, 0, req.MaxDepth, tags);
|
||||||
|
|
||||||
|
string csvPath = await SaveToCsvAsync(tags);
|
||||||
|
|
||||||
|
_logger.LogInformation("탐사 완료: {Count}개 노드", tags.Count);
|
||||||
|
return new CrawlResult
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = $"탐사 완료: {tags.Count}개 노드 발견",
|
||||||
|
TotalNodes = tags.Count,
|
||||||
|
Tags = tags,
|
||||||
|
CsvPath = csvPath
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Crawler 실행 오류");
|
||||||
|
return new CrawlResult { Success = false, Message = ex.Message, Tags = tags };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task BrowseRecursiveAsync(
|
||||||
|
Session session, NodeId nodeId, int level, int maxDepth, List<TagMaster> tags)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 원본 동일: BrowseAsync 객체 방식
|
||||||
|
BrowseDescription description = new()
|
||||||
|
{
|
||||||
|
NodeId = nodeId,
|
||||||
|
BrowseDirection = BrowseDirection.Forward,
|
||||||
|
ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences,
|
||||||
|
IncludeSubtypes = true,
|
||||||
|
NodeClassMask = (uint)(NodeClass.Variable | NodeClass.Object),
|
||||||
|
ResultMask = (uint)BrowseResultMask.All
|
||||||
|
};
|
||||||
|
|
||||||
|
BrowseResponse response = await session.BrowseAsync(
|
||||||
|
null, null, 0, [description], default);
|
||||||
|
|
||||||
|
if (response?.Results == null || response.Results.Count == 0) return;
|
||||||
|
|
||||||
|
foreach (var result in response.Results)
|
||||||
|
{
|
||||||
|
await ProcessReferencesAsync(session, result.References, level, maxDepth, tags);
|
||||||
|
|
||||||
|
byte[] cp = result.ContinuationPoint;
|
||||||
|
while (cp != null && cp.Length > 0)
|
||||||
|
{
|
||||||
|
// FIX CS8130/CS1503: BrowseNextAsync 튜플 방식 → BrowseNextResponse 객체 방식
|
||||||
|
// v1.5.374.70 에서 반환 타입이 Task<BrowseNextResponse> 로 변경됨
|
||||||
|
BrowseNextResponse nextResponse = await session.BrowseNextAsync(
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
new ByteStringCollection { cp },
|
||||||
|
default);
|
||||||
|
|
||||||
|
if (nextResponse?.Results != null && nextResponse.Results.Count > 0)
|
||||||
|
{
|
||||||
|
var nextResult = nextResponse.Results[0];
|
||||||
|
await ProcessReferencesAsync(session, nextResult.References, level, maxDepth, tags);
|
||||||
|
cp = nextResult.ContinuationPoint;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// 원본과 동일: 특정 노드 권한 에러 무시
|
||||||
|
_logger.LogDebug("노드 탐색 건너뜀 [{Level}] {NodeId}: {Msg}", level, nodeId, ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessReferencesAsync(
|
||||||
|
Session session,
|
||||||
|
ReferenceDescriptionCollection? references,
|
||||||
|
int level,
|
||||||
|
int maxDepth,
|
||||||
|
List<TagMaster> tags)
|
||||||
|
{
|
||||||
|
if (references == null || references.Count == 0) return;
|
||||||
|
|
||||||
|
foreach (var rd in references)
|
||||||
|
{
|
||||||
|
NodeId childId = ExpandedNodeId.ToNodeId(rd.NodeId, session.NamespaceUris);
|
||||||
|
|
||||||
|
tags.Add(new TagMaster
|
||||||
|
{
|
||||||
|
TagName = rd.BrowseName.Name ?? "Unknown",
|
||||||
|
FullNodeId = childId.ToString(),
|
||||||
|
NodeClass = rd.NodeClass.ToString(),
|
||||||
|
Level = level
|
||||||
|
});
|
||||||
|
|
||||||
|
_logger.LogDebug("{Indent}[{Class}] {Name} ({Id})",
|
||||||
|
new string(' ', level * 2), rd.NodeClass, rd.BrowseName.Name, childId);
|
||||||
|
|
||||||
|
if (rd.NodeClass == NodeClass.Object && level < maxDepth)
|
||||||
|
await BrowseRecursiveAsync(session, childId, level + 1, maxDepth, tags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> SaveToCsvAsync(List<TagMaster> tags)
|
||||||
|
{
|
||||||
|
string path = Path.GetFullPath("Honeywell_FullMap.csv");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var sw = new StreamWriter(path);
|
||||||
|
await sw.WriteLineAsync("Level,Class,Name,NodeId");
|
||||||
|
foreach (var tag in tags)
|
||||||
|
await sw.WriteLineAsync($"{tag.Level},{tag.NodeClass},{tag.TagName},{tag.FullNodeId}");
|
||||||
|
|
||||||
|
_logger.LogInformation("CSV 저장 완료: {Path}", path);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "CSV 저장 실패");
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
180
opcUaManager/Services/OpcSessionService.cs
Normal file
180
opcUaManager/Services/OpcSessionService.cs
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
using System.Text;
|
||||||
|
using Opc.Ua;
|
||||||
|
using Opc.Ua.Client;
|
||||||
|
using OpcUaManager.Models;
|
||||||
|
|
||||||
|
// Microsoft.AspNetCore.Http.ISession 과 Opc.Ua.Client.ISession 충돌 해소
|
||||||
|
using OpcUaISession = Opc.Ua.Client.ISession;
|
||||||
|
|
||||||
|
// Microsoft.AspNetCore.Http.StatusCodes 와 Opc.Ua.StatusCodes 충돌 해소
|
||||||
|
using OpcStatusCodes = Opc.Ua.StatusCodes;
|
||||||
|
|
||||||
|
namespace OpcUaManager.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// OPC UA 세션을 싱글톤으로 관리합니다.
|
||||||
|
/// v1.5.374.70 API 기준으로 수정되었습니다.
|
||||||
|
/// </summary>
|
||||||
|
public class OpcSessionService : IAsyncDisposable
|
||||||
|
{
|
||||||
|
private readonly ILogger<OpcSessionService> _logger;
|
||||||
|
private readonly CertService _certService;
|
||||||
|
|
||||||
|
private OpcUaISession? _session;
|
||||||
|
private ApplicationConfiguration? _appConfig;
|
||||||
|
private readonly SemaphoreSlim _lock = new(1, 1);
|
||||||
|
|
||||||
|
public bool IsConnected => _session?.Connected == true;
|
||||||
|
public string SessionId => _session?.SessionId?.ToString() ?? string.Empty;
|
||||||
|
|
||||||
|
public OpcSessionService(ILogger<OpcSessionService> logger, CertService certService)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_certService = certService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ConnectResult> ConnectAsync(ConnectRequest req)
|
||||||
|
{
|
||||||
|
await _lock.WaitAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (IsConnected)
|
||||||
|
await InternalCloseAsync();
|
||||||
|
|
||||||
|
string endpointUrl = $"opc.tcp://{req.ServerIp}:{req.Port}";
|
||||||
|
|
||||||
|
// ── 1. 클라이언트 인증서 로드 ────────────────────────────
|
||||||
|
System.Security.Cryptography.X509Certificates.X509Certificate2? clientCert = null;
|
||||||
|
if (!string.IsNullOrWhiteSpace(req.PfxPath) && File.Exists(req.PfxPath))
|
||||||
|
clientCert = _certService.LoadPfx(req.PfxPath, req.PfxPassword);
|
||||||
|
|
||||||
|
string appUri = string.IsNullOrWhiteSpace(req.ApplicationUri)
|
||||||
|
? $"urn:{System.Net.Dns.GetHostName()}:{req.ApplicationName}"
|
||||||
|
: req.ApplicationUri;
|
||||||
|
|
||||||
|
// ── 2. ApplicationConfiguration ───────────────────────────
|
||||||
|
_appConfig = new ApplicationConfiguration
|
||||||
|
{
|
||||||
|
ApplicationName = req.ApplicationName,
|
||||||
|
ApplicationType = ApplicationType.Client,
|
||||||
|
ApplicationUri = appUri,
|
||||||
|
SecurityConfiguration = new SecurityConfiguration
|
||||||
|
{
|
||||||
|
ApplicationCertificate = clientCert != null
|
||||||
|
? new CertificateIdentifier { Certificate = clientCert }
|
||||||
|
: new CertificateIdentifier(),
|
||||||
|
TrustedPeerCertificates = new CertificateTrustList
|
||||||
|
{ StoreType = "Directory", StorePath = Path.GetFullPath("pki/trusted") },
|
||||||
|
TrustedIssuerCertificates = new CertificateTrustList
|
||||||
|
{ StoreType = "Directory", StorePath = Path.GetFullPath("pki/issuers") },
|
||||||
|
RejectedCertificateStore = new CertificateTrustList
|
||||||
|
{ StoreType = "Directory", StorePath = Path.GetFullPath("pki/rejected") },
|
||||||
|
AutoAcceptUntrustedCertificates = true,
|
||||||
|
AddAppCertToTrustedStore = true
|
||||||
|
},
|
||||||
|
TransportQuotas = new TransportQuotas { OperationTimeout = 15000 },
|
||||||
|
ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = req.SessionTimeoutMs }
|
||||||
|
};
|
||||||
|
|
||||||
|
// FIX CS1061: ValidateAsync → Validate (v1.5.374.70에서 동기 메서드)
|
||||||
|
_appConfig.Validate(ApplicationType.Client);
|
||||||
|
|
||||||
|
// FIX CS0104: StatusCodes → OpcStatusCodes 별칭 사용
|
||||||
|
_appConfig.CertificateValidator.CertificateValidation += (_, e) =>
|
||||||
|
{
|
||||||
|
if (e.Error.StatusCode != OpcStatusCodes.Good) e.Accept = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── 3. Endpoint Discovery ─────────────────────────────────
|
||||||
|
_logger.LogInformation("Endpoint Discovery: {Url}", endpointUrl);
|
||||||
|
var endpointConfig = EndpointConfiguration.Create(_appConfig);
|
||||||
|
|
||||||
|
// FIX CS0117: DiscoveryClient.CreateAsync 없음 → 동기 Create 사용
|
||||||
|
EndpointDescriptionCollection endpoints;
|
||||||
|
using (var discovery = DiscoveryClient.Create(new Uri(endpointUrl), endpointConfig))
|
||||||
|
{
|
||||||
|
endpoints = discovery.GetEndpoints(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
var selected = endpoints
|
||||||
|
.OrderByDescending(e => e.SecurityLevel)
|
||||||
|
.FirstOrDefault(e => e.SecurityPolicyUri.Contains(req.SecurityPolicy))
|
||||||
|
?? endpoints.OrderByDescending(e => e.SecurityLevel).First();
|
||||||
|
|
||||||
|
var endpoint = new ConfiguredEndpoint(null, selected, endpointConfig);
|
||||||
|
|
||||||
|
// FIX CS1503: UserIdentity(user, byte[]) → UserIdentity(user, string)
|
||||||
|
// v1.5.374.70 에서 password 파라미터가 string 으로 변경됨
|
||||||
|
var identity = new UserIdentity(req.UserName, req.Password);
|
||||||
|
|
||||||
|
// ── 5. Session.Create ─────────────────────────────────────
|
||||||
|
#pragma warning disable CS0618
|
||||||
|
_session = await Session.Create(
|
||||||
|
_appConfig,
|
||||||
|
endpoint,
|
||||||
|
false,
|
||||||
|
"OpcUaManagerSession",
|
||||||
|
(uint)req.SessionTimeoutMs,
|
||||||
|
identity,
|
||||||
|
null);
|
||||||
|
#pragma warning restore CS0618
|
||||||
|
|
||||||
|
string secMode = selected.SecurityMode.ToString();
|
||||||
|
_logger.LogInformation("OPC UA 연결 완료 | SessionId={Id} | Security={Sec}",
|
||||||
|
_session.SessionId, secMode);
|
||||||
|
|
||||||
|
return new ConnectResult
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = "연결 성공",
|
||||||
|
SessionId = _session.SessionId.ToString(),
|
||||||
|
EndpointUrl = endpointUrl,
|
||||||
|
SecurityMode = secMode
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "OPC UA 연결 실패");
|
||||||
|
return new ConnectResult { Success = false, Message = ex.Message };
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_lock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> DisconnectAsync()
|
||||||
|
{
|
||||||
|
await _lock.WaitAsync();
|
||||||
|
try { await InternalCloseAsync(); return true; }
|
||||||
|
finally { _lock.Release(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task InternalCloseAsync()
|
||||||
|
{
|
||||||
|
if (_session != null)
|
||||||
|
{
|
||||||
|
try { await _session.CloseAsync(); } catch { /* ignore */ }
|
||||||
|
_session.Dispose();
|
||||||
|
_session = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(object? Value, string StatusCode)> ReadValueAsync(string nodeId)
|
||||||
|
{
|
||||||
|
if (!IsConnected || _session == null)
|
||||||
|
throw new InvalidOperationException("세션이 연결되어 있지 않습니다.");
|
||||||
|
|
||||||
|
var result = await _session.ReadValueAsync(nodeId);
|
||||||
|
return (result.Value, result.StatusCode.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Session? GetRawSession() => _session as Session;
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
await InternalCloseAsync();
|
||||||
|
_lock.Dispose();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
opcUaManager/bin/Debug/net8.0/Microsoft.OpenApi.dll
Executable file
BIN
opcUaManager/bin/Debug/net8.0/Microsoft.OpenApi.dll
Executable file
Binary file not shown.
BIN
opcUaManager/bin/Debug/net8.0/Newtonsoft.Json.dll
Executable file
BIN
opcUaManager/bin/Debug/net8.0/Newtonsoft.Json.dll
Executable file
Binary file not shown.
BIN
opcUaManager/bin/Debug/net8.0/Npgsql.dll
Executable file
BIN
opcUaManager/bin/Debug/net8.0/Npgsql.dll
Executable file
Binary file not shown.
BIN
opcUaManager/bin/Debug/net8.0/Opc.Ua.Client.dll
Executable file
BIN
opcUaManager/bin/Debug/net8.0/Opc.Ua.Client.dll
Executable file
Binary file not shown.
BIN
opcUaManager/bin/Debug/net8.0/Opc.Ua.Configuration.dll
Executable file
BIN
opcUaManager/bin/Debug/net8.0/Opc.Ua.Configuration.dll
Executable file
Binary file not shown.
BIN
opcUaManager/bin/Debug/net8.0/Opc.Ua.Core.dll
Executable file
BIN
opcUaManager/bin/Debug/net8.0/Opc.Ua.Core.dll
Executable file
Binary file not shown.
BIN
opcUaManager/bin/Debug/net8.0/Opc.Ua.Security.Certificates.dll
Executable file
BIN
opcUaManager/bin/Debug/net8.0/Opc.Ua.Security.Certificates.dll
Executable file
Binary file not shown.
BIN
opcUaManager/bin/Debug/net8.0/OpcUaManager
Executable file
BIN
opcUaManager/bin/Debug/net8.0/OpcUaManager
Executable file
Binary file not shown.
266
opcUaManager/bin/Debug/net8.0/OpcUaManager.deps.json
Normal file
266
opcUaManager/bin/Debug/net8.0/OpcUaManager.deps.json
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
{
|
||||||
|
"runtimeTarget": {
|
||||||
|
"name": ".NETCoreApp,Version=v8.0",
|
||||||
|
"signature": ""
|
||||||
|
},
|
||||||
|
"compilationOptions": {},
|
||||||
|
"targets": {
|
||||||
|
".NETCoreApp,Version=v8.0": {
|
||||||
|
"OpcUaManager/1.0.0": {
|
||||||
|
"dependencies": {
|
||||||
|
"Npgsql": "8.0.3",
|
||||||
|
"OPCFoundation.NetStandard.Opc.Ua.Client": "1.5.374.70",
|
||||||
|
"Swashbuckle.AspNetCore": "6.6.2"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"OpcUaManager.dll": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.ApiDescription.Server/6.0.5": {},
|
||||||
|
"Microsoft.Extensions.DependencyInjection.Abstractions/8.0.0": {},
|
||||||
|
"Microsoft.Extensions.Logging.Abstractions/8.0.0": {
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.OpenApi/1.6.14": {
|
||||||
|
"runtime": {
|
||||||
|
"lib/netstandard2.0/Microsoft.OpenApi.dll": {
|
||||||
|
"assemblyVersion": "1.6.14.0",
|
||||||
|
"fileVersion": "1.6.14.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Newtonsoft.Json/13.0.3": {
|
||||||
|
"runtime": {
|
||||||
|
"lib/net6.0/Newtonsoft.Json.dll": {
|
||||||
|
"assemblyVersion": "13.0.0.0",
|
||||||
|
"fileVersion": "13.0.3.27908"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Npgsql/8.0.3": {
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.Logging.Abstractions": "8.0.0"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net8.0/Npgsql.dll": {
|
||||||
|
"assemblyVersion": "8.0.3.0",
|
||||||
|
"fileVersion": "8.0.3.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"OPCFoundation.NetStandard.Opc.Ua.Client/1.5.374.70": {
|
||||||
|
"dependencies": {
|
||||||
|
"OPCFoundation.NetStandard.Opc.Ua.Configuration": "1.5.374.70",
|
||||||
|
"OPCFoundation.NetStandard.Opc.Ua.Core": "1.5.374.70"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net8.0/Opc.Ua.Client.dll": {
|
||||||
|
"assemblyVersion": "1.5.374.0",
|
||||||
|
"fileVersion": "1.5.374.70"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"OPCFoundation.NetStandard.Opc.Ua.Configuration/1.5.374.70": {
|
||||||
|
"dependencies": {
|
||||||
|
"OPCFoundation.NetStandard.Opc.Ua.Core": "1.5.374.70"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net8.0/Opc.Ua.Configuration.dll": {
|
||||||
|
"assemblyVersion": "1.5.374.0",
|
||||||
|
"fileVersion": "1.5.374.70"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"OPCFoundation.NetStandard.Opc.Ua.Core/1.5.374.70": {
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.Logging.Abstractions": "8.0.0",
|
||||||
|
"Newtonsoft.Json": "13.0.3",
|
||||||
|
"OPCFoundation.NetStandard.Opc.Ua.Security.Certificates": "1.5.374.70"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net8.0/Opc.Ua.Core.dll": {
|
||||||
|
"assemblyVersion": "1.5.374.0",
|
||||||
|
"fileVersion": "1.5.374.70"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"OPCFoundation.NetStandard.Opc.Ua.Security.Certificates/1.5.374.70": {
|
||||||
|
"dependencies": {
|
||||||
|
"System.Formats.Asn1": "8.0.0",
|
||||||
|
"System.Security.Cryptography.Cng": "5.0.0"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net8.0/Opc.Ua.Security.Certificates.dll": {
|
||||||
|
"assemblyVersion": "1.5.374.0",
|
||||||
|
"fileVersion": "1.5.374.70"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Swashbuckle.AspNetCore/6.6.2": {
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.ApiDescription.Server": "6.0.5",
|
||||||
|
"Swashbuckle.AspNetCore.Swagger": "6.6.2",
|
||||||
|
"Swashbuckle.AspNetCore.SwaggerGen": "6.6.2",
|
||||||
|
"Swashbuckle.AspNetCore.SwaggerUI": "6.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Swashbuckle.AspNetCore.Swagger/6.6.2": {
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.OpenApi": "1.6.14"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net8.0/Swashbuckle.AspNetCore.Swagger.dll": {
|
||||||
|
"assemblyVersion": "6.6.2.0",
|
||||||
|
"fileVersion": "6.6.2.401"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Swashbuckle.AspNetCore.SwaggerGen/6.6.2": {
|
||||||
|
"dependencies": {
|
||||||
|
"Swashbuckle.AspNetCore.Swagger": "6.6.2"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net8.0/Swashbuckle.AspNetCore.SwaggerGen.dll": {
|
||||||
|
"assemblyVersion": "6.6.2.0",
|
||||||
|
"fileVersion": "6.6.2.401"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Swashbuckle.AspNetCore.SwaggerUI/6.6.2": {
|
||||||
|
"runtime": {
|
||||||
|
"lib/net8.0/Swashbuckle.AspNetCore.SwaggerUI.dll": {
|
||||||
|
"assemblyVersion": "6.6.2.0",
|
||||||
|
"fileVersion": "6.6.2.401"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"System.Formats.Asn1/8.0.0": {},
|
||||||
|
"System.Security.Cryptography.Cng/5.0.0": {
|
||||||
|
"dependencies": {
|
||||||
|
"System.Formats.Asn1": "8.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"libraries": {
|
||||||
|
"OpcUaManager/1.0.0": {
|
||||||
|
"type": "project",
|
||||||
|
"serviceable": false,
|
||||||
|
"sha512": ""
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.ApiDescription.Server/6.0.5": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-Ckb5EDBUNJdFWyajfXzUIMRkhf52fHZOQuuZg/oiu8y7zDCVwD0iHhew6MnThjHmevanpxL3f5ci2TtHQEN6bw==",
|
||||||
|
"path": "microsoft.extensions.apidescription.server/6.0.5",
|
||||||
|
"hashPath": "microsoft.extensions.apidescription.server.6.0.5.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.DependencyInjection.Abstractions/8.0.0": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-cjWrLkJXK0rs4zofsK4bSdg+jhDLTaxrkXu4gS6Y7MAlCvRyNNgwY/lJi5RDlQOnSZweHqoyvgvbdvQsRIW+hg==",
|
||||||
|
"path": "microsoft.extensions.dependencyinjection.abstractions/8.0.0",
|
||||||
|
"hashPath": "microsoft.extensions.dependencyinjection.abstractions.8.0.0.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.Logging.Abstractions/8.0.0": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==",
|
||||||
|
"path": "microsoft.extensions.logging.abstractions/8.0.0",
|
||||||
|
"hashPath": "microsoft.extensions.logging.abstractions.8.0.0.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.OpenApi/1.6.14": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-tTaBT8qjk3xINfESyOPE2rIellPvB7qpVqiWiyA/lACVvz+xOGiXhFUfohcx82NLbi5avzLW0lx+s6oAqQijfw==",
|
||||||
|
"path": "microsoft.openapi/1.6.14",
|
||||||
|
"hashPath": "microsoft.openapi.1.6.14.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Newtonsoft.Json/13.0.3": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==",
|
||||||
|
"path": "newtonsoft.json/13.0.3",
|
||||||
|
"hashPath": "newtonsoft.json.13.0.3.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Npgsql/8.0.3": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-6WEmzsQJCZAlUG1pThKg/RmeF6V+I0DmBBBE/8YzpRtEzhyZzKcK7ulMANDm5CkxrALBEC8H+5plxHWtIL7xnA==",
|
||||||
|
"path": "npgsql/8.0.3",
|
||||||
|
"hashPath": "npgsql.8.0.3.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"OPCFoundation.NetStandard.Opc.Ua.Client/1.5.374.70": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-TiguVbV6kANLKqV8KUPzf89re/ng53ebq3xaLUJJTfg0NAYxZTmUGYGAULR8c/C9aVi6xM5MGAuq+ggf1jEl+A==",
|
||||||
|
"path": "opcfoundation.netstandard.opc.ua.client/1.5.374.70",
|
||||||
|
"hashPath": "opcfoundation.netstandard.opc.ua.client.1.5.374.70.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"OPCFoundation.NetStandard.Opc.Ua.Configuration/1.5.374.70": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-Qai1Ieo+laEO6ARW/YNuAcS1W34KJYqcbg4gUEYMBvHUNvl1ACgp8lYbXkj34vJ9BlqHlT9nYM0W6ioynUbaAw==",
|
||||||
|
"path": "opcfoundation.netstandard.opc.ua.configuration/1.5.374.70",
|
||||||
|
"hashPath": "opcfoundation.netstandard.opc.ua.configuration.1.5.374.70.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"OPCFoundation.NetStandard.Opc.Ua.Core/1.5.374.70": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-zOrqHMkHm5Pf9oFbRgscr5smU0Jx54l3ARGJcBQugbb2r+O53PPR8C/y1a+8FtI4Lx/feDB2qgjwZ8xqzSfuXQ==",
|
||||||
|
"path": "opcfoundation.netstandard.opc.ua.core/1.5.374.70",
|
||||||
|
"hashPath": "opcfoundation.netstandard.opc.ua.core.1.5.374.70.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"OPCFoundation.NetStandard.Opc.Ua.Security.Certificates/1.5.374.70": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-hBCW2MAOX+BJmzVOekOCBLiSYNGz5hDJM/JEvGyaCqJ4dTv3D5/Fgi4y9NgquuY2pOd9bd9mOWjGgdW9ZQdJOg==",
|
||||||
|
"path": "opcfoundation.netstandard.opc.ua.security.certificates/1.5.374.70",
|
||||||
|
"hashPath": "opcfoundation.netstandard.opc.ua.security.certificates.1.5.374.70.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Swashbuckle.AspNetCore/6.6.2": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-+NB4UYVYN6AhDSjW0IJAd1AGD8V33gemFNLPaxKTtPkHB+HaKAKf9MGAEUPivEWvqeQfcKIw8lJaHq6LHljRuw==",
|
||||||
|
"path": "swashbuckle.aspnetcore/6.6.2",
|
||||||
|
"hashPath": "swashbuckle.aspnetcore.6.6.2.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Swashbuckle.AspNetCore.Swagger/6.6.2": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-ovgPTSYX83UrQUWiS5vzDcJ8TEX1MAxBgDFMK45rC24MorHEPQlZAHlaXj/yth4Zf6xcktpUgTEBvffRQVwDKA==",
|
||||||
|
"path": "swashbuckle.aspnetcore.swagger/6.6.2",
|
||||||
|
"hashPath": "swashbuckle.aspnetcore.swagger.6.6.2.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Swashbuckle.AspNetCore.SwaggerGen/6.6.2": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-zv4ikn4AT1VYuOsDCpktLq4QDq08e7Utzbir86M5/ZkRaLXbCPF11E1/vTmOiDzRTl0zTZINQU2qLKwTcHgfrA==",
|
||||||
|
"path": "swashbuckle.aspnetcore.swaggergen/6.6.2",
|
||||||
|
"hashPath": "swashbuckle.aspnetcore.swaggergen.6.6.2.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Swashbuckle.AspNetCore.SwaggerUI/6.6.2": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-mBBb+/8Hm2Q3Wygag+hu2jj69tZW5psuv0vMRXY07Wy+Rrj40vRP8ZTbKBhs91r45/HXT4aY4z0iSBYx1h6JvA==",
|
||||||
|
"path": "swashbuckle.aspnetcore.swaggerui/6.6.2",
|
||||||
|
"hashPath": "swashbuckle.aspnetcore.swaggerui.6.6.2.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"System.Formats.Asn1/8.0.0": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-AJukBuLoe3QeAF+mfaRKQb2dgyrvt340iMBHYv+VdBzCUM06IxGlvl0o/uPOS7lHnXPN6u8fFRHSHudx5aTi8w==",
|
||||||
|
"path": "system.formats.asn1/8.0.0",
|
||||||
|
"hashPath": "system.formats.asn1.8.0.0.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"System.Security.Cryptography.Cng/5.0.0": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-jIMXsKn94T9JY7PvPq/tMfqa6GAaHpElRDpmG+SuL+D3+sTw2M8VhnibKnN8Tq+4JqbPJ/f+BwtLeDMEnzAvRg==",
|
||||||
|
"path": "system.security.cryptography.cng/5.0.0",
|
||||||
|
"hashPath": "system.security.cryptography.cng.5.0.0.nupkg.sha512"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
opcUaManager/bin/Debug/net8.0/OpcUaManager.dll
Normal file
BIN
opcUaManager/bin/Debug/net8.0/OpcUaManager.dll
Normal file
Binary file not shown.
BIN
opcUaManager/bin/Debug/net8.0/OpcUaManager.pdb
Normal file
BIN
opcUaManager/bin/Debug/net8.0/OpcUaManager.pdb
Normal file
Binary file not shown.
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"runtimeOptions": {
|
||||||
|
"tfm": "net8.0",
|
||||||
|
"frameworks": [
|
||||||
|
{
|
||||||
|
"name": "Microsoft.NETCore.App",
|
||||||
|
"version": "8.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Microsoft.AspNetCore.App",
|
||||||
|
"version": "8.0.0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"configProperties": {
|
||||||
|
"System.GC.Server": true,
|
||||||
|
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user