Files
go-speech/main-funcs.go
2025-11-28 17:43:00 +06:00

386 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 := `<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Go Speech - Документация</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
max-width: 900px;
margin: 0 auto;
padding: 40px 20px;
line-height: 1.6;
color: #333;
background: #f5f5f5;
}
h1, h2, h3 { color: #667eea; margin-top: 30px; }
code { background: #f4f4f4; padding: 2px 6px; border-radius: 3px; font-family: 'Courier New', monospace; }
pre { background: #f4f4f4; padding: 15px; border-radius: 8px; overflow-x: auto; }
pre code { background: none; padding: 0; }
a { color: #667eea; text-decoration: none; }
a:hover { text-decoration: underline; }
blockquote { border-left: 4px solid #667eea; padding-left: 20px; margin-left: 0; color: #666; }
table { border-collapse: collapse; width: 100%; margin: 20px 0; }
th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
th { background: #667eea; color: white; }
</style>
</head>
<body>
`
// Простая конвертация markdown в HTML
lines := strings.Split(md, "\n")
inCodeBlock := false
inList := false
var codeBlock strings.Builder
for i, line := range lines {
line = strings.TrimRight(line, "\r")
trimmedLine := strings.TrimSpace(line)
// Обработка блоков кода
if strings.HasPrefix(trimmedLine, "```") {
if inCodeBlock {
// Закрываем блок кода
html += "<pre><code>" + template.HTMLEscapeString(codeBlock.String()) + "</code></pre>\n"
codeBlock.Reset()
inCodeBlock = false
} else {
// Открываем блок кода
inCodeBlock = true
}
if inList {
html += "</ul>\n"
inList = false
}
continue
}
if inCodeBlock {
codeBlock.WriteString(line + "\n")
continue
}
// Заголовки
if strings.HasPrefix(trimmedLine, "# ") {
if inList {
html += "</ul>\n"
inList = false
}
html += "<h1>" + template.HTMLEscapeString(strings.TrimPrefix(trimmedLine, "# ")) + "</h1>\n"
continue
}
if strings.HasPrefix(trimmedLine, "## ") {
if inList {
html += "</ul>\n"
inList = false
}
html += "<h2>" + template.HTMLEscapeString(strings.TrimPrefix(trimmedLine, "## ")) + "</h2>\n"
continue
}
if strings.HasPrefix(trimmedLine, "### ") {
if inList {
html += "</ul>\n"
inList = false
}
html += "<h3>" + template.HTMLEscapeString(strings.TrimPrefix(trimmedLine, "### ")) + "</h3>\n"
continue
}
// Списки
if strings.HasPrefix(trimmedLine, "- ") {
if !inList {
html += "<ul>\n"
inList = true
}
content := strings.TrimPrefix(trimmedLine, "- ")
// Обработка inline кода в списках
content = processInlineCode(content)
html += "<li>" + content + "</li>\n"
continue
}
// Закрываем список если он был открыт
if inList && trimmedLine == "" {
html += "</ul>\n"
inList = false
}
// Пустые строки
if trimmedLine == "" {
if i < len(lines)-1 {
html += "<br>\n"
}
continue
}
// Обычный текст
content := processInlineCode(template.HTMLEscapeString(trimmedLine))
html += "<p>" + content + "</p>\n"
}
// Закрываем открытые теги
if inList {
html += "</ul>\n"
}
html += `</body>
</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("<code>")
result.WriteString(part)
result.WriteString("</code>")
}
}
return result.String()
}