// Command bj-license-server — сервер лицензирования Bridge&Join (https://lic.zetit.ru). // // Выдаёт и продлевает лицензии установкам bj по коду активации (контракт — // docs/license-server-api.md) и даёт веб-кабинет администратора: клиенты, лицензии, // установки, журнал, администраторы. Вход в кабинет — логин и пароль. // // Хранилище — один JSON-файл с атомарной записью и ежедневными копиями рядом: клиентов // десятки, записей немного, отдельная СУБД не нужна. // // bj-license-server serve -addr 127.0.0.1:8091 -data /var/lib/bj-license/state.json -key /etc/bj-license/license.priv // bj-license-server user-set -data /var/lib/bj-license/state.json -login admin // (пароль из stdin; пусто — сгенерировать; сервер должен быть остановлен) // bj-license-server fw-apply -dir /var/lib/bj-license [-off] (root, из systemd: защита, см. firewall.go) package main import ( "bufio" "crypto/ed25519" "flag" "fmt" "log" "net/http" "os" "strings" "time" "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/license" ) func main() { if len(os.Args) < 2 { usage() } switch os.Args[1] { case "serve": serve(os.Args[2:]) case "user-set": userSet(os.Args[2:]) case "fw-apply": fwApply(os.Args[2:]) case "fw-ignore": fwIgnore(os.Args[2:]) case "admin-info": adminInfo(os.Args[2:]) case "admin-config": adminConfig(os.Args[2:]) default: usage() } } func usage() { fmt.Fprintln(os.Stderr, "bj-license-server serve [-addr 127.0.0.1:8091] [-data state.json] [-key license.priv] [-keyid main]") fmt.Fprintln(os.Stderr, "bj-license-server user-set -login admin [-name Имя] [-data state.json] (пароль из stdin)") fmt.Fprintln(os.Stderr, "bj-license-server fw-apply [-dir /var/lib/bj-license] [-off] (root: применить защиту; -off — аварийно выключить оборону)") fmt.Fprintln(os.Stderr, "bj-license-server fw-ignore [-dir /var/lib/bj-license] api|admin (ignorecommand fail2ban)") fmt.Fprintln(os.Stderr, "bj-license-server admin-info [-data state.json] (адрес кабинета, белый список, второй фактор)") fmt.Fprintln(os.Stderr, "bj-license-server admin-config [-data state.json] [-allow IP=комментарий ...] [-path свой|random|admin] [-allow-only on|off] [-reset-2fa логин] (сервер должен быть остановлен)") os.Exit(2) } func serve(args []string) { fs := flag.NewFlagSet("serve", flag.ExitOnError) addr := fs.String("addr", "127.0.0.1:8091", "адрес прослушивания (снаружи — через nginx)") data := fs.String("data", "/var/lib/bj-license/state.json", "файл состояния") keyPath := fs.String("key", "/etc/bj-license/license.priv", "закрытый ключ подписи лицензий (base64 seed Ed25519)") keyID := fs.String("keyid", "main", "идентификатор ключа в токене") _ = fs.Parse(args) priv, err := license.LoadPrivateKey(*keyPath) if err != nil { log.Fatalf("ключ подписи: %v", err) } st, err := openStore(*data) if err != nil { log.Fatalf("хранилище: %v", err) } srv := &server{ st: st, priv: priv, pub: priv.Public().(ed25519.PublicKey), keyID: *keyID, sessions: newSessions(), fails: newLimiter(8, 15*time.Minute), apiRate: newLimiter(60, time.Minute), } st.read(func(s *State) { srv.fails.set(loginLimitOf(s)) }) if err := srv.loadTemplates(); err != nil { log.Fatalf("шаблоны: %v", err) } mux := http.NewServeMux() srv.routes(mux) go st.backupLoop() st.read(func(s *State) { log.Printf("bj-license-server: слушаю %s; клиентов %d, лицензий %d, установок %d, администраторов %d", *addr, len(s.Tenants), len(s.Licenses), len(s.Installs), len(s.Users)) if len(s.Users) == 0 { log.Printf("bj-license-server: администраторов нет — создайте: bj-license-server user-set -login admin") } }) hs := &http.Server{ Addr: *addr, Handler: securityHeaders(srv.adminGate(mux)), ReadHeaderTimeout: 10 * time.Second, ReadTimeout: 30 * time.Second, WriteTimeout: 60 * time.Second, } log.Fatal(hs.ListenAndServe()) } func userSet(args []string) { fs := flag.NewFlagSet("user-set", flag.ExitOnError) data := fs.String("data", "/var/lib/bj-license/state.json", "файл состояния") login := fs.String("login", "", "логин администратора") name := fs.String("name", "", "имя (для журнала)") _ = fs.Parse(args) if !loginRe.MatchString(*login) { log.Fatal("логин: латиница, цифры, точка, дефис, подчёркивание") } st, err := openStore(*data) if err != nil { log.Fatal(err) } pw, _ := bufio.NewReader(os.Stdin).ReadString('\n') pw = strings.TrimRight(pw, "\r\n") generated := false if pw == "" { pw, generated = randomPassword(16), true } if len(pw) < 10 { log.Fatal("пароль короче 10 символов") } if err := st.write(func(s *State) error { return s.setUserPassword(*login, *name, pw, "cli") }); err != nil { log.Fatal(err) } if generated { fmt.Printf("Администратор %s: пароль %s\n", *login, pw) } else { fmt.Printf("Администратор %s: пароль задан\n", *login) } }