삽질하다 도저히 문제 파악이 안돼서 opcUaManager로 분리 테스트 중

This commit is contained in:
2026-02-25 08:52:03 +09:00
parent 4ea351946a
commit e88ab87771
138 changed files with 1051971 additions and 351 deletions

View 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();
}
}
}

Binary file not shown.

View File

@@ -13,10 +13,10 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("OpcPks.Collector")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[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.AssemblyTitleAttribute("OpcPks.Collector")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
// MSBuild WriteCodeFragment 클래스에서 생성되었습니다.
// Generated by the MSBuild WriteCodeFragment class.

View File

@@ -1 +1 @@
cc5c3e28019345878790a222ce17c89ce44d1c1d74447a18859ea42c9a3adb70
54f6f1ad0709deb6cb2f3668e2c474e9539ddf45ae288d8f7bae089724875a72

View File

@@ -1,20 +1,28 @@
using System.ComponentModel.DataAnnotations;
namespace OpcPks.Core.Models
{
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 bool IsRedundant { get; set; } = false;
// 서버 호스트네임 (예: HONPKS)
public string PrimaryHostName { get; set; } = "";
public string SecondaryHostName { get; set; } = ""; // Redundant일 때 필수
public string PrimaryHostName { get; set; } = "192.168.0.20";
public string SecondaryHostName { get; set; } = "192.168.1.20";
// FTE 네트워크 구조 반영 (네트워크 대역 분리)
public string PrimaryIpA { get; set; } = ""; // 192.168.0.x 대역
public string PrimaryIpB { get; set; } = ""; // 192.168.1.x 대역 (FTE 이중화 시)
public string? PrimaryIpA { get; set; } = ""; // Yellow
public string? PrimaryIpB { get; set; } = ""; // Green
public string SecondaryIpA { get; set; } = ""; // 192.168.0.y 대역
public string SecondaryIpB { get; set; } = ""; // 192.168.1.y 대역
public string SecondaryIpA { get; set; } = "";
public string SecondaryIpB { get; set; } = "";
public int ValidityYears { get; set; } = 5;
}

View File

@@ -1,120 +1,96 @@
using Opc.Ua;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Net;
using System.Net.NetworkInformation;
using OpcPks.Core.Models;
using System.Diagnostics;
namespace OpcPks.Core.Services
{
public class CertificateGenerator
{
private readonly string _pfxPath;
private readonly string _ownCertPath;
private readonly string _baseDataPath;
private readonly OpcSessionManager _sessionManager;
public CertificateGenerator(string baseDataPath)
public CertificateGenerator(string baseDataPath, OpcSessionManager sessionManager)
{
// 경로 설정 (현재 tree 구조 반영)
_pfxPath = Path.Combine(baseDataPath, "pki/own/private/OpcTestClient.pfx");
_ownCertPath = Path.Combine(baseDataPath, "pki/own/certs/OpcTestClient.der");
_baseDataPath = baseDataPath;
_sessionManager = sessionManager;
}
public async Task<bool> CreateHoneywellCertificateAsync(CertRequestModel model)
{
try
{
// 1. [중요] 기존 인증서 백업
BackupExistingCertificate();
Console.WriteLine("🔐 하니웰 맞춤형 인증서 생성을 시작합니다...");
// SAN 리스트 구축 (localhost 기본 포함)
var subjectAlternativeNames = new List<string> { "localhost", "127.0.0.1" };
string appName = "OpcPksClient";
// [중요] 호스트네임은 대문자로 처리 (윈도우 기본값 준수)
string hostName = Environment.MachineName.ToUpper();
string applicationUri = $"urn:{hostName}:{appName}";
// Primary 정보 (FTE Yellow/Green)
if (!string.IsNullOrEmpty(model.PrimaryHostName))
{
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);
string derPath = Path.Combine(_baseDataPath, "own", "certs", $"{appName}.der");
string pfxPath = Path.Combine(_baseDataPath, "own", "private", $"{appName}.pfx");
// Secondary 정보 (Redundant 선택 시)
if (model.IsRedundant)
{
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);
}
Directory.CreateDirectory(Path.GetDirectoryName(derPath)!);
Directory.CreateDirectory(Path.GetDirectoryName(pfxPath)!);
// 2. 인증서 생성 (KeySize 2048, 하니웰 규격)
// var certificate = CertificateFactory.CreateCertificate(
// model.ApplicationName,
// model.ApplicationName,
// null,
// subjectAlternativeNames,
// null,
// 2048,
// DateTime.UtcNow.AddDays(-1),
// model.ValidityYears * 12,
// null
// );
using var rsa = RSA.Create(2048);
// [교정] 오직 CN만 포함 (Configuration Error 방지를 위해 불필요한 필드 제거)
var request = new CertificateRequest(
$"CN={appName}",
rsa,
HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1);
// 2. 인증서 생성 (로그에 표시된 15개 인자 서명 순서 엄격 준수)
ushort keySize = 2048;
ushort lifetimeInMonths = (ushort)(model.ValidityYears * 12);
// 1. Subject Key Identifier (필수 지문)
request.CertificateExtensions.Add(
new X509SubjectKeyIdentifierExtension(request.PublicKey, false));
var certificate = CertificateFactory.CreateCertificate(
"Directory", // 1. storeType (string)
Path.GetDirectoryName(_pfxPath), // 2. storePath (string)
null, // 3. password (string)
$"urn:localhost:{model.ApplicationName}", // 4. applicationUri (string)
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)
);
// 2. SAN (URI와 DNS 단 하나씩만 - 하니웰의 해석 오류 방지)
var sanBuilder = new SubjectAlternativeNameBuilder();
sanBuilder.AddUri(new Uri(applicationUri));
sanBuilder.AddDnsName(hostName);
request.CertificateExtensions.Add(sanBuilder.Build());
// 3. Key Usage (가장 표준적인 3종으로 축소)
// NonRepudiation 플래그가 가끔 BadConfiguration을 유발하므로 뺍니다.
request.CertificateExtensions.Add(new X509KeyUsageExtension(
X509KeyUsageFlags.DigitalSignature |
X509KeyUsageFlags.KeyEncipherment |
X509KeyUsageFlags.DataEncipherment,
true));
// 3. 파일 저장
Directory.CreateDirectory(Path.GetDirectoryName(_pfxPath)!);
Directory.CreateDirectory(Path.GetDirectoryName(_ownCertPath)!);
// 4. Enhanced Key Usage
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));
// PFX (개인키 포함) 저장
byte[] pfxBytes = certificate.Export(X509ContentType.Pfx, "");
await File.WriteAllBytesAsync(_pfxPath, pfxBytes);
// 5. Basic Constraints (End Entity 명시)
request.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, true));
// DER (공개키만) 저장 - 하니웰 서버에 수동으로 신뢰 등록할 때 필요할 수 있음
byte[] derBytes = certificate.Export(X509ContentType.Cert);
await File.WriteAllBytesAsync(_ownCertPath, derBytes);
// [교정] 시작 시간을 1시간 전으로 설정하여 하니웰 서버와의 시간차 방어
using var certificate = request.CreateSelfSigned(
DateTimeOffset.UtcNow.AddHours(-1),
DateTimeOffset.UtcNow.AddYears(5));
Console.WriteLine($"✅ 새 인증서 생성 완료 및 백업 성공.");
return true;
await File.WriteAllBytesAsync(pfxPath, certificate.Export(X509ContentType.Pfx));
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)
{
Console.WriteLine($"❌ 인증서 생성 실패: {ex.Message}");
return false;
}
}
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)}");
Console.WriteLine($"❌ 작업 실패: {ex.Message}");
throw;
}
}
}

View File

@@ -66,7 +66,7 @@ namespace OpcPks.Core.Services
{
await ProcessReferencesAsync(result.References, level);
byte[] cp = result.ContinuationPoint;
byte[]? cp = result.ContinuationPoint;
while (cp != null && cp.Length > 0)
{
// ✅ ByteStringCollection을 사용하여 라이브러리 표준 인자 타입 일치화
@@ -122,7 +122,7 @@ namespace OpcPks.Core.Services
try
{
// 디렉토리가 없으면 생성
string dir = Path.GetDirectoryName(filePath);
string? dir = Path.GetDirectoryName(filePath);
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
{
Directory.CreateDirectory(dir);

View File

@@ -1,108 +1,108 @@
using Opc.Ua;
using Opc.Ua.Client;
using System.Security.Cryptography.X509Certificates;
using System.Net;
using System;
using System.Threading.Tasks;
using OpcPks.Core.Models;
namespace OpcPks.Core.Services
{
public class OpcSessionManager
{
private ISession? _session;
public ISession? Session => _session;
private readonly string _pkiPath = "/home/pacer/projects/OpcPksPlatform/OpcPks.Core/Data/pki";
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";
string endpointUrl = $"opc.tcp://{serverHostName}:4840";
string applicationUri = "urn:dbsvr:OpcTestClient";
string userName = "mngr";
string password = "mngr";
string pfxPassword = ""; // openssl에서 확인한 비밀번호가 있다면 입력
/// <summary>
/// [핵심] 크롤러 및 모든 서비스가 공통으로 사용하는 실제 세션 생성 로직
/// </summary>
public async Task<ISession?> GetSessionAsync(string ip, CertRequestModel model)
{
if (string.IsNullOrEmpty(ip)) return null;
string baseDataPath = "/home/pacer/projects/OpcPksPlatform/OpcPks.Core/Data/pki";
string pfxFilePath = Path.Combine(baseDataPath, "own/private/OpcTestClient.pfx");
var config = new ApplicationConfiguration {
ApplicationName = "OpcTestClient",
string endpointUrl = $"opc.tcp://{ip}:4840";
var config = new ApplicationConfiguration()
{
ApplicationName = "OpcPksClient",
ApplicationType = ApplicationType.Client,
ApplicationUri = applicationUri,
SecurityConfiguration = new SecurityConfiguration {
SecurityConfiguration = new SecurityConfiguration
{
ApplicationCertificate = new CertificateIdentifier {
StoreType = "Directory",
StorePath = Path.Combine(baseDataPath, "own"),
SubjectName = "OpcTestClient"
StorePath = $"{_pkiPath}/own"
},
TrustedPeerCertificates = new CertificateTrustList {
StoreType = "Directory",
StorePath = Path.Combine(baseDataPath, "trusted")
StorePath = $"{_pkiPath}/trusted"
},
AutoAcceptUntrustedCertificates = true,
AddAppCertToTrustedStore = true
RejectedCertificateStore = new CertificateTrustList {
StoreType = "Directory",
StorePath = $"{_pkiPath}/rejected"
},
AutoAcceptUntrustedCertificates = true
},
TransportQuotas = new TransportQuotas { OperationTimeout = 60000 },
ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 60000 }
TransportQuotas = new TransportQuotas { OperationTimeout = 10000 },
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);
// Validate 이후 인증서가 풀렸을 경우를 대비해 다시 확인
if (config.SecurityConfiguration.ApplicationCertificate.Certificate == null) {
Console.WriteLine("⚠️ [경고] Validate 과정에서 인증서 설정이 유실되어 재할당합니다.");
config.SecurityConfiguration.ApplicationCertificate.Certificate = new X509Certificate2(pfxFilePath, pfxPassword, X509KeyStorageFlags.MachineKeySet);
try
{
// 하니웰 보안 설정에 맞는 엔드포인트 선택
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);
}
config.CertificateValidator.CertificateValidation += (s, e) => { e.Accept = true; };
// 3. 엔드포인트 선택 및 보안 정책 로그
var endpointDescription = CoreClientUtils.SelectEndpoint(endpointUrl, true);
var endpointConfiguration = EndpointConfiguration.Create(config);
var configuredEndpoint = new ConfiguredEndpoint(null, endpointDescription, endpointConfiguration);
Console.WriteLine($"📡 [세션] 연결 시도: {endpointUrl}");
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;
catch (ServiceResultException sx)
{
Console.WriteLine($"[OPC_UA] 하니웰 보안 응답 코드: {sx.StatusCode}");
return null;
}
catch (Exception ex)
{
Console.WriteLine($"[OPC_UA] 세션 생성 중 오류: {ex.Message}");
return null;
}
return _session;
}
}
}

View File

@@ -13,10 +13,10 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("OpcPks.Core")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[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.AssemblyTitleAttribute("OpcPks.Core")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
// MSBuild WriteCodeFragment 클래스에서 생성되었습니다.
// Generated by the MSBuild WriteCodeFragment class.

View File

@@ -1 +1 @@
c75e210b6893d92e0b1636a6b8f564c5a308ff97701a72ccf681ea4473190030
a4e824e0dedb3502ec3664daca8c46528956a8b9400ccad8204c7aed55480ce3

View File

@@ -1 +1 @@
cea4cbfb0b4d3fd205bf727dfb75d0c31872eee74f74e79b0a11980153badb1b
bf8b9457af309b3d030af6437e28d995c9af82eaaa8c16419bd5b4f60d93749a

View File

@@ -10,11 +10,11 @@
"projectUniqueName": "/home/pacer/projects/OpcPksPlatform/OpcPks.Core/OpcPks.Core.csproj",
"projectName": "OpcPks.Core",
"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/",
"projectStyle": "PackageReference",
"configFilePaths": [
"/root/.nuget/NuGet/NuGet.Config"
"/home/pacer/.nuget/NuGet/NuGet.Config"
],
"originalTargetFrameworks": [
"net8.0"

View File

@@ -4,12 +4,12 @@
<RestoreSuccess Condition=" '$(RestoreSuccess)' == '' ">True</RestoreSuccess>
<RestoreTool Condition=" '$(RestoreTool)' == '' ">NuGet</RestoreTool>
<ProjectAssetsFile Condition=" '$(ProjectAssetsFile)' == '' ">$(MSBuildThisFileDirectory)project.assets.json</ProjectAssetsFile>
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">/root/.nuget/packages/</NuGetPackageRoot>
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">/root/.nuget/packages/</NuGetPackageFolders>
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">/home/pacer/.nuget/packages/</NuGetPackageRoot>
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">/home/pacer/.nuget/packages/</NuGetPackageFolders>
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.8.1</NuGetToolVersion>
</PropertyGroup>
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<SourceRoot Include="/root/.nuget/packages/" />
<SourceRoot Include="/home/pacer/.nuget/packages/" />
</ItemGroup>
</Project>

View File

@@ -555,7 +555,7 @@
]
},
"packageFolders": {
"/root/.nuget/packages/": {}
"/home/pacer/.nuget/packages/": {}
},
"project": {
"version": "1.0.0",
@@ -563,11 +563,11 @@
"projectUniqueName": "/home/pacer/projects/OpcPksPlatform/OpcPks.Core/OpcPks.Core.csproj",
"projectName": "OpcPks.Core",
"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/",
"projectStyle": "PackageReference",
"configFilePaths": [
"/root/.nuget/NuGet/NuGet.Config"
"/home/pacer/.nuget/NuGet/NuGet.Config"
],
"originalTargetFrameworks": [
"net8.0"

View File

@@ -1,19 +1,19 @@
{
"version": 2,
"dgSpecHash": "Yr08mh1oXg2H9eup6pJDv3DAEJIeIlBI+QyHLkFeXakigkBdjFhrDkS0UWc3z6IMWBK5yeeRM5RBBASJtTIrbA==",
"dgSpecHash": "4hhhTNv5rdj9W8LEgtqRzvNnobbH+nWA4apshlgfd3j4OdsEpRlMXC2Q3X2ScEegniOPFgZFJJ9l+i9qtr7LrA==",
"success": true,
"projectFilePath": "/home/pacer/projects/OpcPksPlatform/OpcPks.Core/OpcPks.Core.csproj",
"expectedPackageFiles": [
"/root/.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",
"/root/.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",
"/root/.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",
"/root/.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",
"/root/.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/microsoft.extensions.dependencyinjection.abstractions/8.0.1/microsoft.extensions.dependencyinjection.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",
"/home/pacer/.nuget/packages/newtonsoft.json/13.0.3/newtonsoft.json.13.0.3.nupkg.sha512",
"/home/pacer/.nuget/packages/npgsql/8.0.4/npgsql.8.0.4.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",
"/home/pacer/.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.core/1.5.374.78/opcfoundation.netstandard.opc.ua.core.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",
"/home/pacer/.nuget/packages/system.formats.asn1/8.0.1/system.formats.asn1.8.0.1.nupkg.sha512",
"/home/pacer/.nuget/packages/system.security.cryptography.cng/5.0.0/system.security.cryptography.cng.5.0.0.nupkg.sha512"
],
"logs": []
}

View File

@@ -2,7 +2,7 @@ using Microsoft.AspNetCore.Mvc;
using Npgsql;
using OpcPks.Core.Data;
using OpcPks.Core.Services;
using OpcPks.Core.Models; // CertRequestModel 참조 추가
using OpcPks.Core.Models;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -14,8 +14,19 @@ namespace OpcPks.Web.Controllers;
[Route("Engineering")]
public class EngineeringController : Controller
{
// 하니웰 데이터 및 PKI 경로 고정
// [현장 설정] 하니웰 데이터 및 PKI 경로 고정
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 [ : ]
@@ -72,6 +83,8 @@ public class EngineeringController : Controller
ON CONFLICT (full_node_id) DO NOTHING;";
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[] parts = sContent.Split(':');
string server = parts[0];
@@ -107,15 +120,14 @@ public class EngineeringController : Controller
public async Task<IActionResult> RunCrawler()
{
try {
var sessionManager = new OpcSessionManager();
var session = await sessionManager.GetSessionAsync();
// [수정] new 생성 대신 주입된 _sessionManager 사용
// GetSessionAsync에 필요한 인자(IP) 전달 (기본값 또는 모델 활용)
var session = await _sessionManager.GetSessionAsync("192.168.0.20", new CertRequestModel());
if (session == null || !session.Connected)
return BadRequest(new { message = "하니웰 서버 연결 실패." });
var crawler = new HoneywellCrawler(session);
string csvPath = Path.Combine(_basePath, "Honeywell_FullMap.csv");
await crawler.RunAsync("ns=1;s=$assetmodel", csvPath);
return Ok(new { message = "탐사 및 CSV 생성 완료!" });
}
@@ -142,18 +154,18 @@ public class EngineeringController : Controller
#endregion
#region [ : ]
#region [ : ]
[HttpGet("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.SuccessMsg = TempData["Success"];
ViewBag.ErrorMsg = TempData["Error"];
return View(new CertRequestModel());
}
@@ -162,26 +174,38 @@ public class EngineeringController : Controller
{
try
{
var generator = new CertificateGenerator(_basePath);
var success = await generator.CreateHoneywellCertificateAsync(model);
// [수정] 직접 new 생성하던 코드를 지우고 주입된 _certGenerator 사용
// 이제 이 호출은 내부적으로 하니웰 서버와 3회 밀당을 수행합니다.
var success = await _certGenerator.CreateHoneywellCertificateAsync(model);
if (success) {
TempData["Success"] = "하니웰 FTE 대응 인증서 생성 및 백업되었습니다.";
TempData["Success"] = "인증서 생성 및 하니웰 서버 최종 수용 확인 완료!";
return RedirectToAction("CertManager");
}
TempData["Error"] = "인증서 생성 중 오류가 발생했습니다.";
TempData["Error"] = "인증서 전송 과정에서 오류가 발생했습니다.";
return RedirectToAction("CertManager");
}
catch (Exception ex)
{
TempData["Error"] = ex.Message;
Console.WriteLine($"[FATAL_CERT] {DateTime.Now}: {ex.Message}");
TempData["Error"] = $"시스템 오류: {ex.Message}";
return RedirectToAction("CertManager");
}
}
#endregion
public class SearchRequest { public string TagTerm { get; set; } public List<string> Suffixes { get; set; } }
public class TagRegistrationRequest { public string TagName { get; set; } public string NodeId { get; set; } public string DataType { get; set; } }
public class SearchRequest
{
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";
}
}

View File

@@ -1,27 +1,58 @@
using OpcPks.Core.Services;
using OpcPks.Core.Models;
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();
// [수정] 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();
// Configure the HTTP request pipeline.
// -----------------------------------------------------------
// 3. 실행 환경 설정
// -----------------------------------------------------------
if (!app.Environment.IsDevelopment())
{
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.UseHttpsRedirection();
// -----------------------------------------------------------
// 4. 미들웨어 및 라우팅 설정
// -----------------------------------------------------------
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
// 컨트롤러 루트 매핑
app.MapControllerRoute(
name: "default",
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();

View File

@@ -1,89 +1,117 @@
@model OpcPks.Core.Models.CertRequestModel
<div class="container mt-4">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h4>🛡️ 하니웰 Experion 전용 인증서 생성기</h4>
<div class="card shadow border-0">
<div class="card-header bg-dark text-white d-flex justify-content-between align-items-center">
<h4 class="mb-0">🛡️ Honeywell Experion Certificate Manager</h4>
<span class="badge bg-success">v2.2 (Safe SMB Transfer)</span>
</div>
<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="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">
<option value="false">Standalone (단일 서버)</option>
<option value="true">Redundant (이중화 서버)</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label fw-bold">어플리케이션 이름</label>
<input asp-for="ApplicationName" class="form-control" placeholder="OpcPksClient" />
<label asp-for="ApplicationName" class="form-label fw-bold">어플리케이션 이름</label>
<input asp-for="ApplicationName" class="form-control" readonly value="OpcPksClient" />
</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="row mb-3">
<div class="col-md-4">
<label class="form-label">Host Name</label>
<input asp-for="PrimaryHostName" class="form-control" placeholder="예: HONPKS" />
</div>
<div class="col-md-4">
<label class="form-label text-warning">IP Address (Yellow)</label>
<input asp-for="PrimaryIpA" class="form-control" placeholder="192.168.0.20" />
</div>
<div class="col-md-4">
<label class="form-label text-success">IP Address (Green)</label>
<input asp-for="PrimaryIpB" class="form-control" placeholder="192.168.1.20" />
<div class="col-md-6">
<div class="p-3 border rounded bg-light shadow-sm" id="secondaryContainer">
<h5 class="text-secondary border-bottom pb-2" id="secondaryTitle">Secondary Server</h5>
<div class="mb-2">
<label class="small fw-bold">Host Name</label>
<input asp-for="SecondaryHostName" class="form-control form-control-sm sec-input" />
</div>
<div class="mb-2">
<label class="small text-warning fw-bold">FTE Yellow (A)</label>
<input asp-for="SecondaryIpA" class="form-control form-control-sm sec-input" />
</div>
<div>
<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>
<h5 class="text-secondary mt-4"><span class="badge bg-secondary" id="secondaryBadge">Secondary</span> 서버 정보</h5>
<div class="row mb-3" id="secondaryFields">
<div class="col-md-4">
<label class="form-label">Host Name</label>
<input asp-for="SecondaryHostName" class="form-control sec-input" placeholder="예: HONPKS" />
</div>
<div class="col-md-4">
<label class="form-label text-warning">IP Address (Yellow)</label>
<input asp-for="SecondaryIpA" class="form-control sec-input" placeholder="192.168.0.21" />
</div>
<div class="col-md-4">
<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>
</div>
<div class="card bg-light border-warning mt-5">
<div class="card-body">
@if (ViewBag.IsCertExists == true)
{
<div class="alert alert-warning d-flex align-items-center">
<span class="fs-4 me-3">⚠️</span>
<div>
<strong>기존 인증서가 이미 존재합니다!</strong><br />
새로 생성 시 기존 서버와의 통신 신뢰관계(Trust)가 깨질 수 있습니다.
<div class="card mt-4 border-primary">
<div class="card-body bg-light">
<div class="alert alert-info mb-0">
<h6 class="fw-bold"><i class="bi bi-info-circle-fill"></i> 하니웰 인증서 안전 전송 안내</h6>
<p class="small mb-2">이 작업은 하니웰 <code>HSCSERVER_Servicehost.exe</code> 프로세스를 <strong>종료하지 않습니다.</strong></p>
<ul class="small mb-0">
<li>인증서 파일(.der)만 원격 서버의 Trusted 저장소로 복사됩니다.</li>
<li>실제 적용은 추후 엔지니어가 서버를 수동으로 재시작할 때 반영됩니다.</li>
<li>현재 운영 중인 실시간 데이터 및 Station 연결에 영향을 주지 않습니다.</li>
</ul>
</div>
<div class="mt-3">
@if (ViewBag.IsCertExists == true)
{
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="chkUnlock" onchange="toggleCertBtn()">
<label class="form-check-label text-primary fw-bold" for="chkUnlock">
기존 인증서 갱신 및 파일 전송에 동의합니다.
</label>
</div>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="chkUnlock" onchange="toggleCertBtn()">
<label class="form-check-label text-danger fw-bold" for="chkUnlock">
[위험 인지] 기존 인증서를 무시하고 새로 생성하는 것에 동의합니다.
</label>
</div>
<button type="submit" id="btnGenerate" class="btn btn-danger btn-lg w-100" disabled>
🔒 인증서가 이미 존재하여 잠겨있습니다
</button>
}
else
{
<button type="submit" id="btnGenerate" class="btn btn-primary btn-lg w-100">
🚀 인증서 생성 및 자동 적용 시작
</button>
}
<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> 기존 인증서 존재 (보호됨)
</button>
}
else
{
<button type="button" id="btnGenerate" class="btn btn-primary btn-lg w-100 shadow-sm" onclick="confirmTransfer()">
<i class="bi bi-send-check-fill"></i> 인증서 생성 및 원격 전송 시작
</button>
}
</div>
</div>
</div>
</form>
@@ -92,37 +120,48 @@
</div>
@section Scripts {
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
$(document).ready(function () {
// 1. 이중화 선택 로직
$('#systemType').change(function () {
const isRedundant = $(this).val() === 'true';
$('.sec-input').prop('disabled', !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 {
$('#secondaryBadge').removeClass('bg-info').addClass('bg-secondary');
$('#secondaryContainer').addClass('bg-light').removeClass('bg-white');
$('#secondaryTitle').addClass('text-secondary').removeClass('text-primary');
}
});
$('#systemType').trigger('change');
}).trigger('change');
});
// 2. 버튼 잠금 해제 로직
function toggleCertBtn() {
const isChecked = document.getElementById('chkUnlock').checked;
const btn = document.getElementById('btnGenerate');
const isChecked = $('#chkUnlock').is(':checked');
const btn = $('#btnGenerate');
if(isChecked) {
btn.disabled = false;
btn.innerText = "🔥 인증서 새로 생성 (강제 실행)";
btn.classList.replace('btn-danger', 'btn-warning');
btn.prop('disabled', false).html('<i class="bi bi-arrow-repeat"></i> 인증서 갱신 및 전송 실행').removeClass('btn-secondary').addClass('btn-info text-white');
} else {
btn.disabled = true;
btn.innerText = "🔒 인증서가 이미 존재하여 잠겨있습니다";
btn.classList.replace('btn-warning', 'btn-danger');
btn.prop('disabled', true).html('<i class="bi bi-lock-fill"></i> 기존 인증서 존재 (보호됨)').removeClass('btn-info text-white').addClass('btn-secondary');
}
}
function confirmTransfer() {
Swal.fire({
title: '인증서 전송',
text: "하니웰 서버의 파일만 교체됩니다. 서비스 연결은 끊기지 않습니다. 진행하시겠습니까?",
icon: 'question',
showCancelButton: true,
confirmButtonColor: '#0d6efd',
cancelButtonColor: '#6c757d',
confirmButtonText: '네, 전송합니다',
cancelButtonText: '취소'
}).then((result) => {
if (result.isConfirmed) {
$('#certForm').submit();
}
});
}
</script>
}

View File

@@ -13,10 +13,10 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("OpcPks.Web")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[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.AssemblyTitleAttribute("OpcPks.Web")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
// MSBuild WriteCodeFragment 클래스에서 생성되었습니다.
// Generated by the MSBuild WriteCodeFragment class.

View File

@@ -1 +1 @@
e1494e87c92c732759b5281464380da6aa825fc12aba342324181898b9f0d33d
b82f0dd43a91c4b442cef90caf9521112866289d267678a4f9dd1c8eb3205e82

View File

@@ -1 +1 @@
da88b9ec6f30d3a67ed38dde28b0fbcd513cedae3a9742b74a5f06636cf1a15e
211409f9c66f4d86deef10e146a286bf43dcff595588425d60ad90ee2a7fd32f

View File

@@ -1 +1 @@
6467bb07a49b01c95068173444a1866c77670de41eed0f2a920478da9d4c65bc
ef34cdd3c41169ab6b411993059073056d84145d36a38b4aea950172aff7d741

View File

@@ -10,11 +10,11 @@
"projectUniqueName": "/home/pacer/projects/OpcPksPlatform/OpcPks.Core/OpcPks.Core.csproj",
"projectName": "OpcPks.Core",
"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/",
"projectStyle": "PackageReference",
"configFilePaths": [
"/root/.nuget/NuGet/NuGet.Config"
"/home/pacer/.nuget/NuGet/NuGet.Config"
],
"originalTargetFrameworks": [
"net8.0"
@@ -73,11 +73,11 @@
"projectUniqueName": "/home/pacer/projects/OpcPksPlatform/OpcPks.Web/OpcPks.Web.csproj",
"projectName": "OpcPks.Web",
"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/",
"projectStyle": "PackageReference",
"configFilePaths": [
"/root/.nuget/NuGet/NuGet.Config"
"/home/pacer/.nuget/NuGet/NuGet.Config"
],
"originalTargetFrameworks": [
"net8.0"

View File

@@ -4,12 +4,12 @@
<RestoreSuccess Condition=" '$(RestoreSuccess)' == '' ">True</RestoreSuccess>
<RestoreTool Condition=" '$(RestoreTool)' == '' ">NuGet</RestoreTool>
<ProjectAssetsFile Condition=" '$(ProjectAssetsFile)' == '' ">$(MSBuildThisFileDirectory)project.assets.json</ProjectAssetsFile>
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">/root/.nuget/packages/</NuGetPackageRoot>
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">/root/.nuget/packages/</NuGetPackageFolders>
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">/home/pacer/.nuget/packages/</NuGetPackageRoot>
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">/home/pacer/.nuget/packages/</NuGetPackageFolders>
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.8.1</NuGetToolVersion>
</PropertyGroup>
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<SourceRoot Include="/root/.nuget/packages/" />
<SourceRoot Include="/home/pacer/.nuget/packages/" />
</ItemGroup>
</Project>

View File

@@ -574,7 +574,7 @@
]
},
"packageFolders": {
"/root/.nuget/packages/": {}
"/home/pacer/.nuget/packages/": {}
},
"project": {
"version": "1.0.0",
@@ -582,11 +582,11 @@
"projectUniqueName": "/home/pacer/projects/OpcPksPlatform/OpcPks.Web/OpcPks.Web.csproj",
"projectName": "OpcPks.Web",
"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/",
"projectStyle": "PackageReference",
"configFilePaths": [
"/root/.nuget/NuGet/NuGet.Config"
"/home/pacer/.nuget/NuGet/NuGet.Config"
],
"originalTargetFrameworks": [
"net8.0"

View File

@@ -1,19 +1,19 @@
{
"version": 2,
"dgSpecHash": "1ODJAJibZeCWjDB9k93/7u2tzIkxUKCpt4LpJwtoyk9SPN+V9Z00ioG5BCZSRPJv0f/i3y9/JoZYPrLxXjCawQ==",
"dgSpecHash": "Lhk78ivUpJSYUca9AozBeOWL1MSXwG/J03crkR8ohPXr0Jr13fI6SjcPbbT0IMW8j7gHacAchV4I/6FtcYipBA==",
"success": true,
"projectFilePath": "/home/pacer/projects/OpcPksPlatform/OpcPks.Web/OpcPks.Web.csproj",
"expectedPackageFiles": [
"/root/.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",
"/root/.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",
"/root/.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",
"/root/.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",
"/root/.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/microsoft.extensions.dependencyinjection.abstractions/8.0.1/microsoft.extensions.dependencyinjection.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",
"/home/pacer/.nuget/packages/newtonsoft.json/13.0.3/newtonsoft.json.13.0.3.nupkg.sha512",
"/home/pacer/.nuget/packages/npgsql/8.0.4/npgsql.8.0.4.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",
"/home/pacer/.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.core/1.5.374.78/opcfoundation.netstandard.opc.ua.core.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",
"/home/pacer/.nuget/packages/system.formats.asn1/8.0.1/system.formats.asn1.8.0.1.nupkg.sha512",
"/home/pacer/.nuget/packages/system.security.cryptography.cng/5.0.0/system.security.cryptography.cng.5.0.0.nupkg.sha512"
],
"logs": []
}