before stt adding
This commit is contained in:
385
main-funcs.go
Normal file
385
main-funcs.go
Normal 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()
|
||||
}
|
||||
Reference in New Issue
Block a user