package main import ( "crypto/subtle" "embed" "encoding/json" "flag" "fmt" "io/fs" "log" "net/http" "os" "regexp" "sync" "github.com/hpcloud/tail" ) //go:embed ../frontend/dist/* var staticFiles embed.FS // Variáveis para os argumentos de linha de comando var ( port string password string ) // init() é executado antes de main(). É o lugar ideal para configurar flags. func init() { flag.StringVar(&port, "port", ":8080", "Porta para o servidor web (ex: :8080, 9090)") flag.StringVar(&password, "password", "", "Senha para autenticar o acesso à interface web") } // LogEntry representa uma linha de log type LogEntry struct { Filename string `json:"filename"` Line string `json:"line"` Timestamp string `json:"timestamp"` } // Broker gerencia os clientes SSE e broadcasting type Broker struct { clients map[chan LogEntry]bool mu sync.Mutex } var broker = &Broker{ clients: make(map[chan LogEntry]bool), } func (b *Broker) Subscribe() chan LogEntry { b.mu.Lock() defer b.mu.Unlock() ch := make(chan LogEntry, 100) b.clients[ch] = true return ch } func (b *Broker) Unsubscribe(ch chan LogEntry) { b.mu.Lock() defer b.mu.Unlock() if _, ok := b.clients[ch]; ok { delete(b.clients, ch) close(ch) } } func (b *Broker) Broadcast(entry LogEntry) { b.mu.Lock() defer b.mu.Unlock() for clientCh := range b.clients { select { case clientCh <- entry: default: delete(b.clients, clientCh) close(clientCh) } } } // handleSSE envia logs em tempo real via SSE func handleSSE(w http.ResponseWriter, r *http.Request) { flusher, ok := w.(http.Flusher) if !ok { http.Error(w, "Streaming unsupported!", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") w.Header().Set("Access-Control-Allow-Origin", "*") clientChan := broker.Subscribe() defer broker.Unsubscribe(clientChan) for { select { case entry := <-clientChan: data, _ := json.Marshal(entry) fmt.Fprintf(w, "data: %s\n\n", data) flusher.Flush() case <-r.Context().Done(): return } } } // handleFiles lista os arquivos de log disponíveis func handleFiles(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(os.Args[1:]) } // startTailing inicia o monitoramento dos arquivos func startTailing(filenames []string) { for _, filename := range filenames { tailConfig := tail.Config{ Follow: true, ReOpen: true, Location: &tail.SeekInfo{Offset: 0, Whence: os.SEEK_END}, } t, err := tail.TailFile(filename, tailConfig) if err != nil { log.Printf("Erro ao fazer tail do arquivo %s: %v", filename, err) continue } log.Printf("Iniciando tail no arquivo: %s", filename) go func(t *tail.Tail, fn string) { for line := range t.Lines { if line.Err != nil { log.Printf("Erro lendo linha de %s: %v", fn, line.Err) continue } timestamp := "" if match := regexp.MustCompile(`\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}`).FindString(line.Text); match != "" { timestamp = match } else if match := regexp.MustCompile(`\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}`).FindString(line.Text); match != "" { timestamp = match } broker.Broadcast(LogEntry{Filename: fn, Line: line.Text, Timestamp: timestamp}) } }(t, filename) } } // authMiddleware é um middleware que protege as rotas com HTTP Basic Auth func authMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if password == "" { next.ServeHTTP(w, r) return } user, pass, ok := r.BasicAuth() if !ok { requestAuth(w) return } usernameMatch := subtle.ConstantTimeCompare([]byte(user), []byte("admin")) == 1 passwordMatch := subtle.ConstantTimeCompare([]byte(pass), []byte(password)) == 1 if usernameMatch && passwordMatch { next.ServeHTTP(w, r) } else { requestAuth(w) } }) } // requestAuth envia o header 401 Unauthorized func requestAuth(w http.ResponseWriter) { w.Header().Set("WWW-Authenticate", `Basic realm="Web Tail Pro - Acesso Restrito"`) w.WriteHeader(http.StatusUnauthorized) w.Write([]byte("Não autorizado.\n")) } func main() { flag.Parse() logFiles := flag.Args() if len(logFiles) < 1 { fmt.Println("Uso: web-tail-pro [opções] ...") fmt.Println("\nOpções:") flag.PrintDefaults() os.Exit(1) } startTailing(logFiles) distFS, err := fs.Sub(staticFiles, "frontend/dist") if err != nil { log.Fatal("Erro ao criar sub-filesystem para arquivos estáticos:", err) } mux := http.NewServeMux() mux.Handle("/", http.FileServer(http.FS(distFS))) mux.HandleFunc("/logs", handleSSE) mux.HandleFunc("/api/files", handleFiles) protectedHandler := authMiddleware(mux) log.Printf("Servidor iniciado em http://localhost%s", port) if password != "" { log.Println("🔐 Autenticação por senha está ATIVADA.") } if err := http.ListenAndServe(port, protectedHandler); err != nil { log.Fatal("Erro ao iniciar o servidor:", err) } }