package main import ( "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "fmt" "go-speech/internal/logger" "html/template" "math/big" "net" "net/http" "os" "path/filepath" "slices" "strings" "time" ) // generateCertificate генерирует самоподписанный TLS сертификат // certFile - путь к файлу сертификата // keyFile - путь к файлу приватного ключа // caCertFile - путь к CA сертификату (опционально, если задан и существует - используется для подписи) // domains - список доменов через запятую для добавления в сертификат (всегда добавляются localhost и 127.0.0.1) func generateCertificate(certFile, keyFile, caCertFile, domains string) error { // Создаем директорию для сертификатов если не существует certDir := filepath.Dir(certFile) if err := os.MkdirAll(certDir, 0755); err != nil { return fmt.Errorf("не удалось создать директорию для сертификатов: %v", err) } // Парсим домены из переменной окружения domainList := []string{"localhost", "127.0.0.1"} if domains != "" { parts := strings.SplitSeq(domains, ",") for part := range parts { part = strings.TrimSpace(part) if part != "" { found := slices.Contains(domainList, part) if !found { domainList = append(domainList, part) } } } } logger.Debug("Генерация сертификата для доменов: %v", domainList) // Генерируем приватный ключ privateKey, err := rsa.GenerateKey(rand.Reader, 4096) if err != nil { return fmt.Errorf("ошибка генерации приватного ключа: %v", err) } // Создаем шаблон сертификата serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) if err != nil { return fmt.Errorf("ошибка генерации серийного номера: %v", err) } template := x509.Certificate{ SerialNumber: serialNumber, Subject: pkix.Name{ Country: []string{"RU"}, Organization: []string{"direct-dev.ru"}, OrganizationalUnit: []string{"TTS Service"}, CommonName: "localhost", }, NotBefore: time.Now(), NotAfter: time.Now().Add(365 * 3 * 24 * time.Hour), // 10 лет KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback}, DNSNames: domainList, } // Загружаем CA сертификат и ключ если указаны var caCert *x509.Certificate var caKey *rsa.PrivateKey var parent *x509.Certificate var signer any if caCertFile != "" { if _, err := os.Stat(caCertFile); err == nil { logger.Debug("Загрузка CA сертификата из %s", caCertFile) caCertData, err := os.ReadFile(caCertFile) if err != nil { return fmt.Errorf("ошибка чтения CA сертификата: %v", err) } block, _ := pem.Decode(caCertData) if block == nil { return fmt.Errorf("не удалось декодировать CA сертификат") } caCert, err = x509.ParseCertificate(block.Bytes) if err != nil { return fmt.Errorf("ошибка парсинга CA сертификата: %v", err) } // Ищем CA ключ (ожидается в том же каталоге с расширением .key) caKeyFile := strings.TrimSuffix(caCertFile, ".crt") + ".key" if strings.HasSuffix(caCertFile, ".pem") { caKeyFile = strings.TrimSuffix(caCertFile, ".pem") + ".key" } if _, err := os.Stat(caKeyFile); err == nil { logger.Debug("Загрузка CA ключа из %s", caKeyFile) caKeyData, err := os.ReadFile(caKeyFile) if err != nil { return fmt.Errorf("ошибка чтения CA ключа: %v", err) } block, _ := pem.Decode(caKeyData) if block == nil { return fmt.Errorf("не удалось декодировать CA ключ") } caKey, err = x509.ParsePKCS1PrivateKey(block.Bytes) if err != nil { // Пробуем PKCS8 формат key, err := x509.ParsePKCS8PrivateKey(block.Bytes) if err != nil { return fmt.Errorf("ошибка парсинга CA ключа: %v", err) } var ok bool caKey, ok = key.(*rsa.PrivateKey) if !ok { return fmt.Errorf("CA ключ не является RSA ключом") } } parent = caCert signer = caKey logger.Info("Используется CA сертификат для подписи") } else { logger.Warn("CA ключ не найден (%s), генерируем самоподписанный сертификат", caKeyFile) } } else { logger.Warn("CA сертификат не найден (%s), генерируем самоподписанный сертификат", caCertFile) } } // Если CA не используется - создаем самоподписанный сертификат if parent == nil { parent = &template signer = privateKey logger.Info("Генерация самоподписанного сертификата") } // Создаем сертификат certDER, err := x509.CreateCertificate(rand.Reader, &template, parent, &privateKey.PublicKey, signer) if err != nil { return fmt.Errorf("ошибка создания сертификата: %v", err) } // Сохраняем сертификат certOut, err := os.Create(certFile) if err != nil { return fmt.Errorf("ошибка создания файла сертификата: %v", err) } defer certOut.Close() if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: certDER}); err != nil { return fmt.Errorf("ошибка записи сертификата: %v", err) } // Сохраняем приватный ключ keyOut, err := os.OpenFile(keyFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) if err != nil { return fmt.Errorf("ошибка создания файла ключа: %v", err) } defer keyOut.Close() keyDER := x509.MarshalPKCS1PrivateKey(privateKey) if err := pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: keyDER}); err != nil { return fmt.Errorf("ошибка записи ключа: %v", err) } logger.Info("Сертификат успешно сгенерирован") return nil } // loggingMiddleware добавляет логирование запросов func loggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() logger.Debug("Входящий запрос: %s %s от %s", r.Method, r.URL.Path, r.RemoteAddr) logger.Debug(" User-Agent: %s", r.UserAgent()) logger.Debug(" Content-Type: %s", r.Header.Get("Content-Type")) logger.Debug(" Content-Length: %s", r.Header.Get("Content-Length")) next.ServeHTTP(w, r) duration := time.Since(start) logger.Info("%s %s %v", r.Method, r.URL.Path, duration) logger.Debug("Запрос обработан за %v", duration) }) } // getEnv получает переменную окружения или возвращает значение по умолчанию func getEnv(key, defaultValue string) string { if value := os.Getenv(key); value != "" { return value } return defaultValue } // GetModelPath формирует путь к модели на основе выбранного голоса func GetModelPath(modelDir, voice string) string { // Если указан полный путь через MODEL_PATH, используем его if modelPath := os.Getenv("MODEL_PATH"); modelPath != "" { return modelPath } // Формируем путь к модели на основе голоса modelFile := fmt.Sprintf("ru_RU-%s-medium.onnx", voice) return filepath.Join(modelDir, modelFile) } // handleHelp рендерит README.md в HTML func handleHelp(w http.ResponseWriter, r *http.Request) { logger.Debug("GET /go-speech/help - отображение документации") // Простой рендеринг markdown в HTML html := markdownToHTML(readmeContent) w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusOK) w.Write([]byte(html)) } // markdownToHTML конвертирует markdown в простой HTML func markdownToHTML(md string) string { html := `
" + template.HTMLEscapeString(codeBlock.String()) + "\n"
codeBlock.Reset()
inCodeBlock = false
} else {
// Открываем блок кода
inCodeBlock = true
}
if inList {
html += "\n"
inList = false
}
continue
}
if inCodeBlock {
codeBlock.WriteString(line + "\n")
continue
}
// Заголовки
if strings.HasPrefix(trimmedLine, "# ") {
if inList {
html += "\n"
inList = false
}
html += "" + content + "
\n" } // Закрываем открытые теги if inList { html += "\n" } html += ` ` return html } // processInlineCode обрабатывает inline код в markdown func processInlineCode(text string) string { // Простая обработка inline кода `code` parts := strings.Split(text, "`") result := strings.Builder{} for i, part := range parts { if i%2 == 0 { result.WriteString(part) } else { result.WriteString("")
result.WriteString(part)
result.WriteString("")
}
}
return result.String()
}