Files
Bridge-and-Join-s/internal/lkgateway/addiscover.go
T
zuevav 773fa8e0bf Мастер: шапка на месте; домен — только логин и пароль; модуль «1С: база мидл-офиса» (тест)
- Шапка мастера: правый блок (шлюз НРД, пользователь, тема) снова справа — после правила [hidden] скрытое меню перестало его распирать.
- Домен в шаге «Учётные записи»: bj сам находит домен и контроллеры (логин с @домен, домены поиска DNS, rootDSE DNS-серверов, SRV _ldap._tcp.dc._msdcs); адрес и домен — в расширенных настройках, открываются сами, если не нашлось. Контроллеры по очереди, LDAPS → LDAP+StartTLS.
- Неудачная попытка ничего не теряет: введённое (и пароль) — черновиком, форма заполнена; причины словами (неизвестный ЦС, имя в сертификате, IP вместо имени, порт закрыт, нет в DNS).
- Сертификат неизвестного центра: карточка «кем выдан, отпечаток (SHA-1 как в Windows и SHA-256)» и «Доверять и подключить»; после входа bj берёт из домена сертификат центра (AIA / Certification Authorities), подписавший сертификат контроллера, — подходят все контроллеры. То же в Настройка → Пользователи при «Проверить».
- Лицензия: модуль onec-middle «1С: база мидл-офиса (тест)»; раздел настроек — адрес публикации, пользователь, пароль, проверка через OData $metadata; в мастере — в «Модулях».
- ЛК (callback, API, готовность) — только при модуле «ЕСИА Финанс»; карточка режима QUIK и стили настроек как в мастере.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
2026-10-07 01:03:16 +03:00

176 lines
6.4 KiB
Go

package lkgateway
// Поиск домена без вопросов (07.10.2026): человеку в мастере достаточно ввести учётную запись и пароль — домен и
// его контроллеры bj находит сам. Откуда: суффикс в логине (ivanov@corp.example.ru), домены поиска DNS и имя
// сервера bj; если их нет — DNS-серверы сервера bj (в сети Active Directory это обычно сами контроллеры): у
// контроллера без пароля читается rootDSE — имя домена и самого контроллера. Список контроллеров — по DNS
// (_ldap._tcp.dc._msdcs.<домен>). Не нашлось — мастер открывает расширенные настройки.
import (
"context"
"net"
"net/http"
"os"
"sort"
"strings"
"time"
"github.com/go-ldap/ldap/v3"
)
// adFound — найденный домен.
type adFound struct {
Domain string `json:"domain"`
DCs []string `json:"dcs"` // контроллеры (имена из DNS)
From string `json:"from"` // откуда узнали домен — для подписи в мастере
}
// adDiscover — домен и контроллеры. domain — если уже известен (из логина или расширенных настроек).
func adDiscover(ctx context.Context, domain string) *adFound {
if domain = strings.ToLower(strings.Trim(strings.TrimSpace(domain), ".")); domain != "" {
dcs := adLookupDCs(ctx, domain)
return &adFound{Domain: domain, DCs: dcs, From: "указан"}
}
for _, c := range adDomainCandidates() {
if dcs := adLookupDCs(ctx, c.name); len(dcs) > 0 {
return &adFound{Domain: c.name, DCs: dcs, From: c.from}
}
}
for _, ns := range adNameservers() {
dom, host := adRootDSEDomain(ctx, ns)
if dom == "" {
continue
}
dcs := adLookupDCs(ctx, dom)
if len(dcs) == 0 && host != "" {
dcs = []string{host}
}
return &adFound{Domain: dom, DCs: dcs, From: "DNS-сервер " + ns + " — контроллер этого домена"}
}
return nil
}
type adCand struct{ name, from string }
// adDomainCandidates — домены поиска DNS и домен в имени сервера (с родительскими, но не короче двух уровней).
func adDomainCandidates() []adCand {
var out []adCand
seen := map[string]bool{}
add := func(name, from string) {
name = strings.ToLower(strings.Trim(name, "."))
for name != "" && strings.Count(name, ".") >= 1 {
if !seen[name] {
seen[name] = true
out = append(out, adCand{name, from})
}
_, name, _ = strings.Cut(name, ".")
}
}
for _, f := range []string{"/run/systemd/resolve/resolv.conf", "/etc/resolv.conf"} {
b, _ := os.ReadFile(f)
for _, ln := range strings.Split(string(b), "\n") {
fs := strings.Fields(ln)
if len(fs) > 1 && (fs[0] == "search" || fs[0] == "domain") {
for _, d := range fs[1:] {
add(d, "домен поиска DNS сервера bj")
}
}
}
}
if h, err := os.Hostname(); err == nil {
if _, d, ok := strings.Cut(h, "."); ok {
add(d, "имя сервера bj")
}
}
return out
}
// adNameservers — DNS-серверы сервера bj (настоящие, не локальная заглушка systemd-resolved).
func adNameservers() []string {
var out []string
seen := map[string]bool{}
for _, f := range []string{"/run/systemd/resolve/resolv.conf", "/etc/resolv.conf"} {
b, _ := os.ReadFile(f)
for _, ln := range strings.Split(string(b), "\n") {
fs := strings.Fields(ln)
if len(fs) < 2 || fs[0] != "nameserver" {
continue
}
ip := net.ParseIP(fs[1])
if ip == nil || ip.IsLoopback() || seen[fs[1]] {
continue
}
seen[fs[1]] = true
out = append(out, fs[1])
}
}
return out
}
// adLookupDCs — контроллеры домена по записям DNS SRV (по приоритету).
func adLookupDCs(ctx context.Context, domain string) []string {
ctx, cancel := context.WithTimeout(ctx, 4*time.Second)
defer cancel()
var srvs []*net.SRV
for _, name := range []string{"dc._msdcs." + domain, domain} {
if _, s, err := net.DefaultResolver.LookupSRV(ctx, "ldap", "tcp", name); err == nil && len(s) > 0 {
srvs = s
break
}
}
sort.SliceStable(srvs, func(i, j int) bool { return srvs[i].Priority < srvs[j].Priority })
var out []string
seen := map[string]bool{}
for _, s := range srvs {
h := strings.ToLower(strings.TrimSuffix(s.Target, "."))
if h != "" && !seen[h] {
seen[h] = true
out = append(out, h)
}
}
return out
}
// adRootDSEDomain — спросить у сервера (порт 389, без пароля) rootDSE: контроллер AD отвечает именем домена
// (defaultNamingContext) и своим именем (dnsHostName). Не контроллер — пусто.
func adRootDSEDomain(ctx context.Context, host string) (domain, dnsHost string) {
d := &net.Dialer{Timeout: 3 * time.Second}
conn, err := ldap.DialURL("ldap://"+net.JoinHostPort(host, "389"), ldap.DialWithDialer(d))
if err != nil {
return "", ""
}
defer conn.Close()
conn.SetTimeout(4 * time.Second)
sr, err := conn.Search(ldap.NewSearchRequest("", ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 4, false,
"(objectClass=*)", []string{"defaultNamingContext", "dnsHostName"}, nil))
if err != nil || len(sr.Entries) == 0 {
return "", ""
}
e := sr.Entries[0]
return adDNToDomain(e.GetAttributeValue("defaultNamingContext")), strings.ToLower(e.GetAttributeValue("dnsHostName"))
}
// adDNToDomain — DC=corp,DC=example,DC=ru → corp.example.ru.
func adDNToDomain(dn string) string {
var parts []string
for _, p := range strings.Split(dn, ",") {
k, v, ok := strings.Cut(strings.TrimSpace(p), "=")
if ok && strings.EqualFold(k, "DC") && v != "" {
parts = append(parts, strings.ToLower(v))
}
}
return strings.Join(parts, ".")
}
// adDiscoverHTTP — GET /admin/setup/users/ad-discover: что bj нашёл сам (для подписи в мастере).
func (h *setupHandlers) adDiscoverHTTP(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
defer cancel()
f := adDiscover(ctx, r.URL.Query().Get("domain"))
if f == nil || len(f.DCs) == 0 {
jsonOut(w, map[string]any{"ok": false, "found": f})
return
}
jsonOut(w, map[string]any{"ok": true, "found": f})
}