-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.cpp
More file actions
3162 lines (2799 loc) · 125 KB
/
Copy pathmain.cpp
File metadata and controls
3162 lines (2799 loc) · 125 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#define UNICODE
#define _UNICODE
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <windows.h>
#include <commctrl.h>
#include <shellapi.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cstdint>
#include <filesystem>
#include <map>
#include <memory>
#include <set>
#include <sstream>
#include <string>
#include <thread>
#include <utility>
#include <vector>
#include "resource.h"
#pragma comment(lib, "comctl32.lib")
#pragma comment(lib, "shell32.lib")
#pragma comment(lib, "advapi32.lib")
#pragma comment(lib, "ws2_32.lib")
namespace {
constexpr wchar_t kWindowClassName[] = L"LANNameResolverWindowClass";
constexpr wchar_t kAppTitle[] = L"LAN Name Resolver";
constexpr wchar_t kConfigFileName[] = L"lan_name_resolver_entries.txt";
constexpr wchar_t kSettingsFileName[] = L"lan_name_resolver_settings.ini";
constexpr wchar_t kStartupValueName[] = L"LANNameResolver";
constexpr wchar_t kHostsManagedBlockBegin[] = L"# BEGIN LAN Name Resolver";
constexpr wchar_t kHostsManagedBlockEnd[] = L"# END LAN Name Resolver";
constexpr wchar_t kAboutGithubUrl[] = L"https://github.com/Terence0816/LAN-Name-Resolver";
constexpr UINT WM_TRAYICON = WM_APP + 1;
constexpr UINT WM_APP_LOG = WM_APP + 2;
constexpr UINT WM_APP_STATUS = WM_APP + 3;
constexpr UINT kTrayIconId = 1;
constexpr int kWindowWidth = 1120;
constexpr int kWindowHeight = 820;
constexpr int IDC_GROUP_LEFT = 100;
constexpr int IDC_GROUP_SERVICE = 101;
constexpr int IDC_GROUP_STARTUP = 102;
constexpr int IDC_GROUP_LOG = 103;
constexpr int IDC_LEFT_INTRO = 104;
constexpr int IDC_NAME_HEADER = 105;
constexpr int IDC_IP_HEADER = 106;
constexpr int IDC_NAME_INPUT = 107;
constexpr int IDC_IP_INPUT = 108;
constexpr int IDC_ADD_ROW = 109;
constexpr int IDC_REMOVE_ROW = 110;
constexpr int IDC_SAVE_SETTINGS = 111;
constexpr int IDC_ENTRY_LIST = 112;
constexpr int IDC_INFO_TEXT = 113;
constexpr int IDC_START_SERVER = 114;
constexpr int IDC_STOP_SERVER = 115;
constexpr int IDC_STATUS_TEXT = 116;
constexpr int IDC_SERVICE_DETAIL = 117;
constexpr int IDC_ENABLE_STARTUP = 118;
constexpr int IDC_DISABLE_STARTUP = 119;
constexpr int IDC_STARTUP_STATUS = 120;
constexpr int IDC_STARTUP_DETAIL = 121;
constexpr int IDC_HINT_TEXT = 122;
constexpr int IDC_LOG_EDIT = 123;
constexpr int IDC_TAB_CONTROL = 124;
constexpr int IDC_HOSTS_GROUP = 130;
constexpr int IDC_HOSTS_INTRO = 131;
constexpr int IDC_HOSTS_EDIT = 132;
constexpr int IDC_WRITE_HOSTS = 134;
constexpr int IDC_OPEN_HOSTS = 135;
constexpr int IDC_HOSTS_FORMAT = 136;
constexpr int IDC_HOSTS_NOTE = 137;
constexpr int IDC_SETTINGS_APP_GROUP = 140;
constexpr int IDC_MINIMIZE_TO_TRAY = 141;
constexpr int IDC_SETTINGS_LANGUAGE_GROUP = 142;
constexpr int IDC_LANGUAGE_LABEL = 143;
constexpr int IDC_LANGUAGE_COMBO = 144;
constexpr int IDC_CUSTOM_LANGUAGE_HINT = 147;
constexpr int IDC_SETTINGS_ABOUT_GROUP = 148;
constexpr int IDC_ABOUT_TITLE = 149;
constexpr int IDC_ABOUT_BODY = 150;
constexpr int IDC_ABOUT_GITHUB_LINK = 151;
constexpr UINT ID_TRAY_SHOW = 40001;
constexpr UINT ID_TRAY_EXIT = 40002;
constexpr unsigned short kNbnsPort = 137;
constexpr unsigned short kLlmnrPort = 5355;
constexpr unsigned short kNbType = 0x0020;
constexpr unsigned short kNbStatType = 0x0021;
constexpr unsigned short kInClass = 0x0001;
constexpr unsigned short kDnsAType = 0x0001;
constexpr unsigned short kDnsAnyType = 0x00FF;
constexpr uint32_t kDefaultTtlSeconds = 300;
constexpr uint32_t kLlmnrTtlSeconds = 30;
constexpr int kAnnounceIntervalSeconds = 30;
constexpr COLORREF kStartFill = RGB(233, 247, 239);
constexpr COLORREF kStartBorder = RGB(35, 119, 67);
constexpr COLORREF kStartText = RGB(18, 94, 47);
constexpr COLORREF kStopFill = RGB(255, 242, 242);
constexpr COLORREF kStopBorder = RGB(188, 70, 63);
constexpr COLORREF kStopText = RGB(155, 44, 44);
constexpr COLORREF kSecondaryFill = RGB(239, 245, 251);
constexpr COLORREF kSecondaryBorder = RGB(98, 127, 160);
constexpr COLORREF kSecondaryText = RGB(53, 84, 120);
constexpr COLORREF kNeutralFill = RGB(248, 248, 248);
constexpr COLORREF kNeutralBorder = RGB(160, 160, 160);
constexpr COLORREF kNeutralText = RGB(90, 90, 90);
struct RowControl {
HWND nameEdit = nullptr;
HWND ipEdit = nullptr;
};
struct ResolverEntry {
std::wstring displayName;
std::string upperName;
std::wstring ipText;
uint32_t ipAddress = 0;
};
struct NameWireResult {
std::vector<std::vector<uint8_t>> labels;
std::vector<uint8_t> wireName;
size_t endOffset = 0;
bool ok = false;
};
struct AppState {
HINSTANCE instance = nullptr;
HWND window = nullptr;
HFONT font = nullptr;
HFONT tabFont = nullptr;
HFONT sectionFont = nullptr;
HFONT bannerFont = nullptr;
HFONT detailFont = nullptr;
HFONT monoFont = nullptr;
HFONT linkFont = nullptr;
HWND tabControl = nullptr;
HWND leftGroup = nullptr;
HWND serviceGroup = nullptr;
HWND startupGroup = nullptr;
HWND logGroup = nullptr;
HWND leftIntro = nullptr;
HWND nameHeader = nullptr;
HWND ipHeader = nullptr;
HWND nameInput = nullptr;
HWND ipInput = nullptr;
HWND addButton = nullptr;
HWND removeButton = nullptr;
HWND saveButton = nullptr;
HWND entryList = nullptr;
HWND infoText = nullptr;
HWND startButton = nullptr;
HWND stopButton = nullptr;
HWND statusText = nullptr;
HWND serviceDetailText = nullptr;
HWND hintText = nullptr;
HWND enableStartupButton = nullptr;
HWND disableStartupButton = nullptr;
HWND startupStatusText = nullptr;
HWND startupDetailText = nullptr;
HWND logEdit = nullptr;
HWND hostsGroup = nullptr;
HWND hostsIntro = nullptr;
HWND hostsEdit = nullptr;
HWND writeHostsButton = nullptr;
HWND openHostsButton = nullptr;
HWND hostsFormatText = nullptr;
HWND hostsNoteText = nullptr;
HWND settingsAppGroup = nullptr;
HWND minimizeToTrayCheck = nullptr;
HWND settingsLanguageGroup = nullptr;
HWND languageLabel = nullptr;
HWND languageCombo = nullptr;
HWND customLanguageHintText = nullptr;
HWND settingsAboutGroup = nullptr;
HWND aboutTitleText = nullptr;
HWND aboutBodyText = nullptr;
HWND aboutGithubLink = nullptr;
std::vector<RowControl> rows;
std::unique_ptr<class ResolverService> service;
std::filesystem::path configPath;
std::filesystem::path settingsPath;
std::map<std::wstring, std::wstring> strings;
std::vector<std::wstring> languageCodes;
std::wstring languageCode = L"tw";
int activeTab = 0;
bool trayAdded = false;
bool exiting = false;
bool startMinimized = false;
bool autoStartServer = false;
bool serviceRunning = false;
bool startupEnabled = false;
bool minimizeToTray = true;
bool isElevated = false;
};
AppState* g_app = nullptr;
std::wstring Trim(const std::wstring& input) {
const auto begin = input.find_first_not_of(L" \t\r\n");
if (begin == std::wstring::npos) {
return L"";
}
const auto end = input.find_last_not_of(L" \t\r\n");
return input.substr(begin, end - begin + 1);
}
std::wstring ToUpperAscii(const std::wstring& input) {
std::wstring output = input;
for (auto& ch : output) {
if (ch >= L'a' && ch <= L'z') {
ch = static_cast<wchar_t>(ch - L'a' + L'A');
}
}
return output;
}
std::wstring ToLowerAscii(const std::wstring& input) {
std::wstring output = input;
for (auto& ch : output) {
if (ch >= L'A' && ch <= L'Z') {
ch = static_cast<wchar_t>(ch - L'A' + L'a');
}
}
return output;
}
std::string WideToUtf8(const std::wstring& input) {
if (input.empty()) {
return {};
}
const int needed = WideCharToMultiByte(CP_UTF8, 0, input.c_str(), -1, nullptr, 0, nullptr, nullptr);
if (needed <= 1) {
return {};
}
std::string output(static_cast<size_t>(needed), '\0');
WideCharToMultiByte(CP_UTF8, 0, input.c_str(), -1, output.data(), needed, nullptr, nullptr);
output.resize(static_cast<size_t>(needed - 1));
return output;
}
std::wstring Utf8ToWide(const std::string& input) {
if (input.empty()) {
return {};
}
const int needed = MultiByteToWideChar(CP_UTF8, 0, input.c_str(), -1, nullptr, 0);
if (needed <= 1) {
return {};
}
std::wstring output(static_cast<size_t>(needed), L'\0');
MultiByteToWideChar(CP_UTF8, 0, input.c_str(), -1, output.data(), needed);
output.resize(static_cast<size_t>(needed - 1));
return output;
}
std::wstring FormatTimeNow() {
SYSTEMTIME localTime{};
GetLocalTime(&localTime);
wchar_t buffer[32] = {};
wsprintfW(buffer, L"%02u:%02u:%02u", localTime.wHour, localTime.wMinute, localTime.wSecond);
return buffer;
}
std::wstring FormatErrorMessage(DWORD code) {
LPWSTR raw = nullptr;
const DWORD flags = FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS;
FormatMessageW(flags, nullptr, code, 0, reinterpret_cast<LPWSTR>(&raw), 0, nullptr);
std::wstring message = raw ? Trim(raw) : L"未知錯誤";
if (raw) {
LocalFree(raw);
}
return message;
}
std::wstring QuoteForCommandLine(const std::wstring& path) {
if (path.find(L' ') == std::wstring::npos) {
return path;
}
return L"\"" + path + L"\"";
}
std::filesystem::path GetExePath() {
std::wstring buffer(MAX_PATH, L'\0');
while (true) {
const DWORD length = GetModuleFileNameW(nullptr, buffer.data(), static_cast<DWORD>(buffer.size()));
if (length == 0) {
return {};
}
if (length < buffer.size() - 1) {
buffer.resize(length);
return std::filesystem::path(buffer);
}
buffer.resize(buffer.size() * 2);
}
}
std::filesystem::path GetConfigPath() {
return GetExePath().parent_path() / kConfigFileName;
}
std::filesystem::path GetSettingsPath() {
return GetExePath().parent_path() / kSettingsFileName;
}
std::filesystem::path GetLanguageFilePath(const std::wstring& selection) {
if (selection.empty()) {
return GetExePath().parent_path() / L"en.txt";
}
std::wstring fileName = selection;
if (fileName == L"tw" || fileName == L"en") {
fileName += L".txt";
}
return GetExePath().parent_path() / fileName;
}
std::filesystem::path GetSystemHostsPath() {
std::wstring buffer(MAX_PATH, L'\0');
const UINT length = GetSystemWindowsDirectoryW(buffer.data(), static_cast<UINT>(buffer.size()));
if (length == 0 || length >= buffer.size()) {
return L"C:\\Windows\\System32\\drivers\\etc\\hosts";
}
buffer.resize(length);
return std::filesystem::path(buffer) / L"System32" / L"drivers" / L"etc" / L"hosts";
}
bool IsProcessElevated() {
HANDLE token = nullptr;
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) {
return false;
}
TOKEN_ELEVATION elevation{};
DWORD size = 0;
const BOOL ok = GetTokenInformation(token, TokenElevation, &elevation, sizeof(elevation), &size);
CloseHandle(token);
return ok == TRUE && elevation.TokenIsElevated != 0;
}
bool ReadWholeFileUtf8(const std::filesystem::path& path, std::string& content) {
content.clear();
const HANDLE file = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (file == INVALID_HANDLE_VALUE) {
return false;
}
LARGE_INTEGER size{};
if (!GetFileSizeEx(file, &size) || size.QuadPart < 0 || size.QuadPart > 16 * 1024 * 1024) {
CloseHandle(file);
return false;
}
content.resize(static_cast<size_t>(size.QuadPart));
DWORD bytesRead = 0;
const BOOL ok = content.empty() ? TRUE : ReadFile(file, content.data(), static_cast<DWORD>(content.size()), &bytesRead, nullptr);
CloseHandle(file);
if (!ok) {
content.clear();
return false;
}
content.resize(bytesRead);
if (content.size() >= 3 &&
static_cast<unsigned char>(content[0]) == 0xEF &&
static_cast<unsigned char>(content[1]) == 0xBB &&
static_cast<unsigned char>(content[2]) == 0xBF) {
content.erase(0, 3);
}
return true;
}
bool WriteWholeFileUtf8(const std::filesystem::path& path, const std::string& content) {
const HANDLE file = CreateFileW(path.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
if (file == INVALID_HANDLE_VALUE) {
return false;
}
DWORD bytesWritten = 0;
const BOOL ok = content.empty() ? TRUE : WriteFile(file, content.data(), static_cast<DWORD>(content.size()), &bytesWritten, nullptr);
CloseHandle(file);
return ok && bytesWritten == content.size();
}
using StringMap = std::map<std::wstring, std::wstring>;
std::wstring EscapeConfigValue(const std::wstring& value) {
std::wstring output;
output.reserve(value.size());
for (const wchar_t ch : value) {
switch (ch) {
case L'\\':
output += L"\\\\";
break;
case L'\r':
break;
case L'\n':
output += L"\\n";
break;
default:
output.push_back(ch);
break;
}
}
return output;
}
std::wstring UnescapeConfigValue(const std::wstring& value) {
std::wstring output;
output.reserve(value.size());
bool escaping = false;
for (const wchar_t ch : value) {
if (escaping) {
if (ch == L'n') {
output += L'\n';
} else {
output.push_back(ch);
}
escaping = false;
continue;
}
if (ch == L'\\') {
escaping = true;
continue;
}
output.push_back(ch);
}
if (escaping) {
output.push_back(L'\\');
}
return output;
}
bool LoadKeyValueFile(const std::filesystem::path& path, StringMap& values) {
values.clear();
std::string content;
if (!ReadWholeFileUtf8(path, content)) {
return false;
}
std::istringstream stream(content);
std::string line;
while (std::getline(stream, line)) {
if (!line.empty() && line.back() == '\r') {
line.pop_back();
}
if (line.empty() || line[0] == '#') {
continue;
}
const auto eqPos = line.find('=');
if (eqPos == std::string::npos) {
continue;
}
const std::wstring key = Trim(Utf8ToWide(line.substr(0, eqPos)));
const std::wstring value = UnescapeConfigValue(Utf8ToWide(line.substr(eqPos + 1)));
if (!key.empty()) {
values[key] = value;
}
}
return true;
}
bool SaveKeyValueFile(const std::filesystem::path& path, const StringMap& values) {
std::ostringstream stream;
for (const auto& [key, value] : values) {
stream << WideToUtf8(key) << '=' << WideToUtf8(EscapeConfigValue(value)) << "\n";
}
return WriteWholeFileUtf8(path, stream.str());
}
StringMap BuildTraditionalChineseStrings() {
return {
{L"window_title", L"LAN Name Resolver"},
{L"tab_server", L"伺服器管理"},
{L"tab_hosts", L"Hosts 修改"},
{L"tab_settings", L"設定"},
{L"tab_about", L"關於"},
{L"group_name_ip", L"名稱 / IP 設定"},
{L"group_service", L"服務控制"},
{L"group_startup", L"開機自動啟動"},
{L"group_log", L"執行紀錄"},
{L"server_intro", L"先輸入名稱與 IP,再按「新增一組」加入下方列表,啟動時會以列表內容為主。"},
{L"label_name", L"名稱"},
{L"label_ip", L"IPv4"},
{L"button_add", L"新增一組"},
{L"button_remove", L"刪除一組"},
{L"button_save", L"儲存設定"},
{L"server_info", L"支援 NBNS / LLMNR,同網段可用名稱直接解析到指定 IP。"},
{L"status_running", L"[OK] 解析服務已啟動"},
{L"status_stopped", L"[STOP] 解析服務未啟動"},
{L"button_start", L"啟動服務器"},
{L"button_stop", L"停止服務器"},
{L"service_count_prefix", L"已設定名稱: "},
{L"service_mode", L"解析方式: 依下方狀態顯示"},
{L"service_nbns_on", L"NBNS / NetBIOS: 已啟用"},
{L"service_nbns_waiting", L"NBNS / NetBIOS: 等待啟動"},
{L"service_nbns_failed", L"NBNS / NetBIOS: 未啟用,137 無法綁定"},
{L"service_llmnr_on", L"LLMNR: 已啟用"},
{L"service_llmnr_waiting", L"LLMNR: 等待啟動"},
{L"service_llmnr_blocked", L"LLMNR: 未啟用,5355 可能被占用"},
{L"service_llmnr_failed", L"LLMNR: 未啟用,5355 無法綁定"},
{L"service_purpose", L"用途: 同網段名稱解析,減少改 hosts"},
{L"service_hint", L"說明:\r\n1. 監聽 UDP 137 / 5355\r\n2. 回應 NBNS 與 LLMNR 查詢"},
{L"startup_enabled", L"[OK] 已加入開機自動啟動"},
{L"startup_disabled", L"[STOP] 未啟用開機自動啟動"},
{L"startup_detail_enabled", L"開機後會自動縮到托盤並啟動解析服務"},
{L"startup_detail_disabled", L"目前未加入 Windows 開機啟動"},
{L"button_enable_startup", L"加入開機自動啟動"},
{L"button_disable_startup", L"移除開機自動啟動"},
{L"hosts_group", L"Hosts 修改"},
{L"hosts_intro", L"可直接手動編輯下方內容,每行格式為「IP 名稱」。"},
{L"hosts_format", L"範例:\r\n192.168.11.22 pc1\r\n192.168.11.33 pc2\r\n192.168.11.44 pc3"},
{L"button_import_server", L"匯入伺服器設定"},
{L"button_write_hosts", L"寫入 hosts"},
{L"button_open_hosts", L"開啟 hosts"},
{L"hosts_note_admin", L"本功能需以管理員身分執行本程式,才能直接寫入 hosts。"},
{L"hosts_note_admin_ok", L"目前已使用管理員身分執行,可直接寫入 hosts。"},
{L"group_app_settings", L"應用程式設定"},
{L"check_minimize_to_tray", L"關閉視窗時縮小到系統區"},
{L"group_language", L"語系"},
{L"label_language", L"介面語言"},
{L"button_apply_language", L"套用語言"},
{L"button_open_language_file", L"開啟語言檔"},
{L"custom_language_hint", L"會自動讀取程式同目錄的 *.txt 語言檔。"},
{L"group_about", L"關於"},
{L"about_title", L"LAN Name Resolver"},
{L"about_description",
L"這是一個適用於 Windows 7 /Windows 10 / 11 的區域網路名稱解析工具。\r\n\r\n"
L"主要功能:\r\n"
L"- 回應 NBNS / NetBIOS 名稱查詢\r\n"
L"- 回應 LLMNR 名稱查詢\r\n"
L"- 將自訂主機名稱對應到指定 IPv4 位址\r\n"
L"- 提供選用的 hosts 修改工具\r\n"
L"- 支援系統托盤與開機自動啟動\r\n\r\n"
L"適用情境:\r\n"
L"- VPN 後需要使用 \\\\pcname 連線內部電腦\r\n"
L"- 小型區域網路沒有 DNS / WINS Server\r\n"
L"- 多台電腦不想逐台手動修改 hosts\r\n"
L"- 臨時維修、部署、測試或故障排除環境\r\n\r\n"
L"安全性說明:\r\n"
L"本工具僅適合在自己的 LAN、VPN、維護環境或已授權的網路中使用。\r\n"
L"hosts 檔案只會在使用者手動操作時修改。"},
{L"about_version", L"版本 1.0"},
{L"about_copyright", L"Copyright \u00A9 2026 Terence0816"},
{L"about_github_label", L"Project:"},
{L"language_option_tw", L"繁體中文"},
{L"language_option_en", L"English"},
{L"language_option_custom", L"自定義語言"},
{L"title_error", L"錯誤"},
{L"title_info", L"提示"},
{L"title_warning", L"警告"},
{L"title_config_error", L"設定錯誤"},
{L"title_start_failed", L"啟動失敗"},
{L"title_duplicate_name", L"名稱重複"},
{L"message_save_failed", L"儲存設定失敗,請確認程式目錄可寫入。"},
{L"message_select_remove_entry", L"請先選取要刪除的那一組名稱 / IP。"},
{L"message_name_empty", L"名稱不能是空的。"},
{L"message_name_too_long_prefix", L"名稱「"},
{L"message_name_too_long_suffix", L"」超過 15 個字元,NetBIOS 名稱最多 15 個字元。"},
{L"message_name_ascii_prefix", L"名稱「"},
{L"message_name_ascii_suffix", L"」只能使用 ASCII 字元。"},
{L"message_invalid_ip_prefix", L"IP「"},
{L"message_invalid_ip_suffix", L"」不是有效的 IPv4 位址。"},
{L"message_duplicate_unique_prefix", L"名稱「"},
{L"message_duplicate_unique_suffix", L"」重複,請改成唯一名稱。"},
{L"message_duplicate_exists_prefix", L"名稱「"},
{L"message_duplicate_exists_suffix", L"」已存在,請先刪除舊項目或改用其他名稱。"},
{L"message_need_entry_list", L"請先新增至少一組名稱與 IP 到下方列表。"},
{L"message_hosts_line_prefix", L"第 "},
{L"message_hosts_line_invalid_format_suffix", L" 行格式錯誤。"},
{L"message_hosts_line_missing_ip_suffix", L" 行缺少有效的 IPv4 位址。"},
{L"message_hosts_line_missing_name_suffix", L" 行缺少主機名稱。"},
{L"message_winsock_init_failed", L"無法初始化 Winsock。"},
{L"message_custom_language_missing", L"找不到自定義語言檔,已替你建立範本後再開啟。"},
{L"message_custom_language_open_failed", L"無法開啟自定義語言檔。"},
{L"message_hosts_open_failed", L"無法開啟 hosts 檔案。"},
{L"message_admin_required", L"此功能需要以管理員身分執行本程式。"},
{L"message_no_server_entries", L"目前伺服器管理列表沒有可匯入的名稱 / IP。"},
{L"message_hosts_written", L"已將內容寫入 hosts。"},
{L"message_language_applied", L"介面語言已重新載入。"},
{L"log_program_ready", L"程式已開啟,可在上方輸入名稱 / IP 後加入列表。"},
{L"log_entries_saved", L"已儲存名稱 / IP 設定。"},
{L"log_startup_enabled", L"已啟用開機自動啟動。"},
{L"log_startup_disabled", L"已停用開機自動啟動。"},
{L"log_hide_to_tray", L"視窗已縮到右下角系統托盤。"},
{L"log_hosts_imported", L"已將伺服器管理頁的名稱 / IP 匯入 hosts 編輯區。"},
{L"log_hosts_written", L"已將編輯區內容寫入 hosts。"},
{L"log_language_applied", L"已重新載入介面語言。"},
{L"log_service_started", L"已啟動 LAN Name Resolver 服務。"},
{L"log_nbns_enabled", L"NBNS UDP 137 已啟用。"},
{L"log_nbns_failed_prefix", L"NBNS UDP 137 啟動失敗,將改以其他可用解析方式繼續服務。原因: "},
{L"log_llmnr_enabled", L"LLMNR UDP 5355 已啟用。"},
{L"log_llmnr_failed_prefix", L"LLMNR UDP 5355 啟動失敗,將改以其他可用解析方式繼續服務。原因: "},
{L"log_service_stopped", L"服務已停止。"},
{L"log_periodic_prefix", L"週期宣告完成,共宣告 "},
{L"log_periodic_suffix", L" 組名稱/IP。"},
{L"log_nbns_query_prefix", L"已回應 NBNS 查詢: "},
{L"log_llmnr_query_prefix", L"已回應 LLMNR 查詢: "},
{L"log_node_status_prefix", L"已回應 Node Status 查詢,來源 "},
{L"log_source_label", L",來源 "},
{L"tray_show", L"顯示畫面"},
{L"tray_exit", L"退出"}
};
}
StringMap BuildEnglishStrings() {
return {
{L"window_title", L"LAN Name Resolver"},
{L"tab_server", L"Server"},
{L"tab_hosts", L"Hosts"},
{L"tab_settings", L"Settings"},
{L"tab_about", L"About"},
{L"group_name_ip", L"Name / IP Entries"},
{L"group_service", L"Service Control"},
{L"group_startup", L"Startup"},
{L"group_log", L"Log"},
{L"server_intro", L"Enter name and IP, click Add, then start with the list below."},
{L"label_name", L"Name"},
{L"label_ip", L"IPv4"},
{L"button_add", L"Add"},
{L"button_remove", L"Delete"},
{L"button_save", L"Save"},
{L"server_info", L"Supports NBNS / LLMNR so names can resolve to the target IP on the same LAN."},
{L"status_running", L"[OK] Resolver Active"},
{L"status_stopped", L"[STOP] Resolver Stopped"},
{L"button_start", L"Start Server"},
{L"button_stop", L"Stop Server"},
{L"service_count_prefix", L"Configured names: "},
{L"service_mode", L"Resolver mode: shown below"},
{L"service_nbns_on", L"NBNS / NetBIOS: enabled"},
{L"service_nbns_waiting", L"NBNS / NetBIOS: waiting to start"},
{L"service_nbns_failed", L"NBNS / NetBIOS: unavailable, 137 could not be bound"},
{L"service_llmnr_on", L"LLMNR: enabled"},
{L"service_llmnr_waiting", L"LLMNR: waiting to start"},
{L"service_llmnr_blocked", L"LLMNR: unavailable, 5355 is likely already in use"},
{L"service_llmnr_failed", L"LLMNR: unavailable, 5355 could not be bound"},
{L"service_purpose", L"Purpose: LAN name resolution without editing hosts everywhere"},
{L"service_hint", L"Notes:\r\n1. Listen on UDP 137 / 5355\r\n2. Reply to NBNS and LLMNR queries"},
{L"startup_enabled", L"[OK] Startup Enabled"},
{L"startup_disabled", L"[STOP] Startup Disabled"},
{L"startup_detail_enabled", L"The app will start minimized to tray and launch the resolver automatically."},
{L"startup_detail_disabled", L"Not added to Windows startup."},
{L"button_enable_startup", L"Enable Startup"},
{L"button_disable_startup", L"Disable Startup"},
{L"hosts_group", L"Hosts Editor"},
{L"hosts_intro", L"Edit the lines below manually. Each line should use the format \"IP name\"."},
{L"hosts_format", L"Example:\r\n192.168.11.22 pc1\r\n192.168.11.33 pc2\r\n192.168.11.44 pc3"},
{L"button_import_server", L"Import Server Entries"},
{L"button_write_hosts", L"Write to hosts"},
{L"button_open_hosts", L"Open hosts"},
{L"hosts_note_admin", L"This feature requires the application to run as administrator before it can write to hosts directly."},
{L"hosts_note_admin_ok", L"The application is already running as administrator and can write to hosts directly."},
{L"group_app_settings", L"Application Settings"},
{L"check_minimize_to_tray", L"Minimize to tray when closing the window"},
{L"group_language", L"Language"},
{L"label_language", L"Interface language"},
{L"button_apply_language", L"Apply"},
{L"button_open_language_file", L"Open Language File"},
{L"custom_language_hint", L"The program automatically reads *.txt language files from its folder."},
{L"group_about", L"About"},
{L"about_title", L"LAN Name Resolver"},
{L"about_description",
L"This is a LAN name resolution tool for Windows 7 / Windows 10 / 11.\r\n\r\n"
L"Main features:\r\n"
L"- Reply to NBNS / NetBIOS name queries\r\n"
L"- Reply to LLMNR name queries\r\n"
L"- Map custom host names to target IPv4 addresses\r\n"
L"- Provide an optional hosts editing helper\r\n"
L"- Support system tray and Windows startup\r\n\r\n"
L"Recommended scenarios:\r\n"
L"- Access internal computers by \\\\pcname after connecting through VPN\r\n"
L"- Small LAN environments without a DNS / WINS server\r\n"
L"- Multiple PCs where editing hosts one by one is inconvenient\r\n"
L"- Temporary maintenance, deployment, testing, or troubleshooting environments\r\n\r\n"
L"Security note:\r\n"
L"This tool should only be used in your own LAN, VPN, maintenance environment, or other authorized networks.\r\n"
L"The hosts file is modified only when the user performs the action manually."},
{L"about_version", L"Version 1.0"},
{L"about_copyright", L"Copyright \u00A9 2026 Terence0816"},
{L"about_github_label", L"Project:"},
{L"language_option_tw", L"Traditional Chinese"},
{L"language_option_en", L"English"},
{L"language_option_custom", L"Custom Language"},
{L"title_error", L"Error"},
{L"title_info", L"Information"},
{L"title_warning", L"Warning"},
{L"title_config_error", L"Invalid Settings"},
{L"title_start_failed", L"Start Failed"},
{L"title_duplicate_name", L"Duplicate Name"},
{L"message_save_failed", L"Failed to save settings. Please make sure the program folder is writable."},
{L"message_select_remove_entry", L"Select the entry you want to remove first."},
{L"message_name_empty", L"Name cannot be empty."},
{L"message_name_too_long_prefix", L"Name \""},
{L"message_name_too_long_suffix", L"\" is longer than 15 characters. NetBIOS names allow up to 15 characters."},
{L"message_name_ascii_prefix", L"Name \""},
{L"message_name_ascii_suffix", L"\" must use ASCII characters only."},
{L"message_invalid_ip_prefix", L"IP \""},
{L"message_invalid_ip_suffix", L"\" is not a valid IPv4 address."},
{L"message_duplicate_unique_prefix", L"Name \""},
{L"message_duplicate_unique_suffix", L"\" is duplicated. Please make it unique."},
{L"message_duplicate_exists_prefix", L"Name \""},
{L"message_duplicate_exists_suffix", L"\" already exists. Remove the old entry or choose another name."},
{L"message_need_entry_list", L"Add at least one name / IP entry to the list first."},
{L"message_hosts_line_prefix", L"Line "},
{L"message_hosts_line_invalid_format_suffix", L" format is invalid."},
{L"message_hosts_line_missing_ip_suffix", L" is missing a valid IPv4 address."},
{L"message_hosts_line_missing_name_suffix", L" is missing a host name."},
{L"message_winsock_init_failed", L"Unable to initialize Winsock."},
{L"message_custom_language_missing", L"The custom language file was not found. A template will be created before opening it."},
{L"message_custom_language_open_failed", L"Unable to open the custom language file."},
{L"message_hosts_open_failed", L"Unable to open the hosts file."},
{L"message_admin_required", L"This feature requires the program to run as administrator."},
{L"message_no_server_entries", L"There are no server entries available to import."},
{L"message_hosts_written", L"The hosts file has been updated."},
{L"message_language_applied", L"The interface language has been reloaded."},
{L"log_program_ready", L"The program is ready. Enter a name and IP above, then add it to the list."},
{L"log_entries_saved", L"Name / IP settings were saved."},
{L"log_startup_enabled", L"Startup was enabled."},
{L"log_startup_disabled", L"Startup was disabled."},
{L"log_hide_to_tray", L"The window was minimized to the system tray."},
{L"log_hosts_imported", L"Server entries were imported into the hosts editor."},
{L"log_hosts_written", L"The hosts editor content was written to the hosts file."},
{L"log_language_applied", L"The interface language was reloaded."},
{L"log_service_started", L"LAN Name Resolver service started."},
{L"log_nbns_enabled", L"NBNS UDP 137 is enabled."},
{L"log_nbns_failed_prefix", L"NBNS UDP 137 failed to start. The service will continue with other available resolver modes. Reason: "},
{L"log_llmnr_enabled", L"LLMNR UDP 5355 is enabled."},
{L"log_llmnr_failed_prefix", L"LLMNR UDP 5355 failed to start. The service will continue with other available resolver modes. Reason: "},
{L"log_service_stopped", L"The service was stopped."},
{L"log_periodic_prefix", L"Periodic announcement finished. Total name/IP entries announced: "},
{L"log_periodic_suffix", L"."},
{L"log_nbns_query_prefix", L"Replied to NBNS query: "},
{L"log_llmnr_query_prefix", L"Replied to LLMNR query: "},
{L"log_node_status_prefix", L"Replied to Node Status query, source "},
{L"log_source_label", L", source "},
{L"tray_show", L"Show Window"},
{L"tray_exit", L"Exit"}
};
}
const StringMap& GetBuiltInStrings(const std::wstring& languageCode) {
static const StringMap kTraditionalChinese = BuildTraditionalChineseStrings();
static const StringMap kEnglish = BuildEnglishStrings();
return languageCode == L"en" ? kEnglish : kTraditionalChinese;
}
std::wstring NormalizeLanguageCode(const std::wstring& value) {
std::wstring code = ToLowerAscii(Trim(value));
std::replace(code.begin(), code.end(), L'_', L'-');
if (code.empty()) {
return L"";
}
if (code == L"tw" || code == L"tw.txt" || code == L"zh-tw" || code == L"zh-hk" || code == L"zh-mo" || code == L"zh-hant") {
return L"tw";
}
if (code.rfind(L"zh-", 0) == 0 && code.find(L"hant") != std::wstring::npos) {
return L"tw";
}
if (code == L"en" || code == L"en.txt" || code == L"en-us" || code == L"en-gb") {
return L"en";
}
if (code.size() < 4 || code.substr(code.size() - 4) != L".txt") {
code += L".txt";
}
return code;
}
std::wstring DetectDefaultLanguage() {
wchar_t localeName[LOCALE_NAME_MAX_LENGTH] = {};
if (GetUserDefaultLocaleName(localeName, LOCALE_NAME_MAX_LENGTH) > 0) {
if (NormalizeLanguageCode(localeName) == L"tw") {
return L"tw";
}
}
const LANGID languageId = GetUserDefaultUILanguage();
if (PRIMARYLANGID(languageId) == LANG_CHINESE) {
switch (SUBLANGID(languageId)) {
case SUBLANG_CHINESE_TRADITIONAL:
case SUBLANG_CHINESE_HONGKONG:
case SUBLANG_CHINESE_MACAU:
return L"tw";
default:
break;
}
}
return L"en";
}
std::wstring GetText(const AppState& app, const wchar_t* key) {
const auto found = app.strings.find(key);
if (found != app.strings.end()) {
return found->second;
}
return key;
}
bool LoadCustomLanguageOverrides(const std::filesystem::path& path, StringMap& values) {
StringMap overrides;
if (!LoadKeyValueFile(path, overrides)) {
return false;
}
for (const auto& [key, value] : overrides) {
values[key] = value;
}
return true;
}
std::string SerializeLanguageTemplate(const StringMap& values);
bool EnsureBuiltInLanguageFiles() {
for (const std::wstring& code : {std::wstring(L"tw"), std::wstring(L"en")}) {
StringMap merged = GetBuiltInStrings(code);
LoadCustomLanguageOverrides(GetLanguageFilePath(code), merged);
if (!WriteWholeFileUtf8(GetLanguageFilePath(code), SerializeLanguageTemplate(merged))) {
return false;
}
}
return true;
}
bool IsLanguageFileCandidate(const std::filesystem::path& path) {
StringMap values;
if (!LoadKeyValueFile(path, values)) {
return false;
}
return values.find(L"window_title") != values.end() ||
values.find(L"tab_server") != values.end() ||
values.find(L"label_language") != values.end();
}
std::vector<std::wstring> GetAvailableLanguageSelections() {
EnsureBuiltInLanguageFiles();
std::vector<std::wstring> result = {L"tw", L"en"};
WIN32_FIND_DATAW data{};
const std::wstring pattern = (GetExePath().parent_path() / L"*.txt").wstring();
HANDLE handle = FindFirstFileW(pattern.c_str(), &data);
if (handle == INVALID_HANDLE_VALUE) {
return result;
}
do {
if ((data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) {
continue;
}
const std::wstring fileName = data.cFileName;
const std::wstring normalized = NormalizeLanguageCode(fileName);
if (normalized == L"tw" || normalized == L"en") {
continue;
}
if (!IsLanguageFileCandidate(GetExePath().parent_path() / fileName)) {
continue;
}
result.push_back(fileName);
} while (FindNextFileW(handle, &data));
FindClose(handle);
if (result.size() > 2) {
std::sort(result.begin() + 2, result.end(), [](const std::wstring& left, const std::wstring& right) {
return ToLowerAscii(left) < ToLowerAscii(right);
});
}
return result;
}
bool LoadLanguageBundle(AppState& app) {
EnsureBuiltInLanguageFiles();
app.languageCode = NormalizeLanguageCode(app.languageCode);
if (app.languageCode.empty()) {
app.languageCode = DetectDefaultLanguage();
}
const std::wstring baseLanguage = (app.languageCode == L"tw") ? L"tw" : L"en";
app.strings = GetBuiltInStrings(baseLanguage);
const bool loadedFile = LoadCustomLanguageOverrides(GetLanguageFilePath(app.languageCode), app.strings);
if (app.languageCode != L"tw" && app.languageCode != L"en" && !loadedFile) {
app.languageCode = L"en";
}
return true;
}
bool SaveAppSettings(const AppState& app) {
StringMap values;
values[L"language"] = app.languageCode;
values[L"minimize_to_tray"] = app.minimizeToTray ? L"1" : L"0";
return SaveKeyValueFile(app.settingsPath, values);
}
void LoadAppSettings(AppState& app) {
StringMap values;
if (!LoadKeyValueFile(app.settingsPath, values)) {
app.languageCode = DetectDefaultLanguage();
app.minimizeToTray = true;
return;
}
const auto language = values.find(L"language");
app.languageCode = language == values.end() ? DetectDefaultLanguage() : NormalizeLanguageCode(language->second);
if (app.languageCode.empty()) {
app.languageCode = DetectDefaultLanguage();
}
const auto minimize = values.find(L"minimize_to_tray");
app.minimizeToTray = minimize == values.end() ? true : (minimize->second != L"0");
}
std::string SerializeLanguageTemplate(const StringMap& values) {
std::ostringstream stream;
stream << "# Language file for LAN Name Resolver\n";
stream << "# Edit the values on the right side of '=' and save the file.\n";
for (const auto& [key, value] : values) {
stream << WideToUtf8(key) << '=' << WideToUtf8(EscapeConfigValue(value)) << "\n";
}
return stream.str();
}
std::vector<uint8_t> EncodeNetbiosName(const std::string& upperName, uint8_t suffix) {
std::string padded = upperName;
padded.resize(15, ' ');
padded.push_back(static_cast<char>(suffix));
std::vector<uint8_t> encoded;
encoded.reserve(34);
encoded.push_back(32);
for (const unsigned char byte : padded) {
encoded.push_back(static_cast<uint8_t>('A' + ((byte >> 4) & 0x0F)));
encoded.push_back(static_cast<uint8_t>('A' + (byte & 0x0F)));
}
encoded.push_back(0x00);
return encoded;
}
bool DecodeFirstLevelLabel(const std::vector<uint8_t>& label, std::string& nameOut, uint8_t& suffixOut) {
if (label.size() != 32) {
return false;
}
std::string raw;
raw.reserve(16);
for (size_t index = 0; index < 32; index += 2) {
const int high = static_cast<int>(label[index]) - 'A';
const int low = static_cast<int>(label[index + 1]) - 'A';
if (high < 0 || high > 0x0F || low < 0 || low > 0x0F) {
return false;
}
raw.push_back(static_cast<char>((high << 4) | low));
}
suffixOut = static_cast<uint8_t>(raw[15]);
raw.resize(15);
while (!raw.empty() && raw.back() == ' ') {
raw.pop_back();
}
nameOut = raw;
return true;
}
std::vector<uint8_t> LabelsToWire(const std::vector<std::vector<uint8_t>>& labels) {
std::vector<uint8_t> wire;
for (const auto& label : labels) {
wire.push_back(static_cast<uint8_t>(label.size()));
wire.insert(wire.end(), label.begin(), label.end());
}
wire.push_back(0x00);
return wire;
}