before stt adding

This commit is contained in:
2025-11-28 17:43:00 +06:00
parent fccafad6de
commit f933c315e8
17 changed files with 10002 additions and 672 deletions

385
main-funcs.go Normal file
View File

@@ -0,0 +1,385 @@
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()
}