mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 00:39:13 +00:00

* initial commit * handle tleech urls * improved and simplified regex * add sanitization status & loading anim for log dl * removed unused imports * improved regex * fixed regex and added tests * regex improvements and tests * added unicode matching to saslRegex * added missing baseurl * swapped the css animator for a react component the css version froze when served through a reverse proxy * optimized regex compilation --------- Co-authored-by: soup <soup@r4tio.cat>
199 lines
5.3 KiB
Go
199 lines
5.3 KiB
Go
package http
|
|
|
|
import (
|
|
"io/fs"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/autobrr/autobrr/internal/config"
|
|
|
|
"github.com/dustin/go-humanize"
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/go-chi/render"
|
|
)
|
|
|
|
type logsHandler struct {
|
|
cfg *config.AppConfig
|
|
}
|
|
|
|
func newLogsHandler(cfg *config.AppConfig) *logsHandler {
|
|
return &logsHandler{cfg: cfg}
|
|
}
|
|
|
|
func (h logsHandler) Routes(r chi.Router) {
|
|
r.Get("/files", h.files)
|
|
r.Get("/files/{logFile}", h.downloadFile)
|
|
}
|
|
|
|
func (h logsHandler) files(w http.ResponseWriter, r *http.Request) {
|
|
response := LogfilesResponse{
|
|
Files: []logFile{},
|
|
Count: 0,
|
|
}
|
|
|
|
if h.cfg.Config.LogPath == "" {
|
|
render.JSON(w, r, response)
|
|
return
|
|
}
|
|
|
|
logsDir := path.Dir(h.cfg.Config.LogPath)
|
|
|
|
// check if dir exists before walkDir
|
|
if _, err := os.Stat(logsDir); os.IsNotExist(err) {
|
|
render.JSON(w, r, response)
|
|
return
|
|
}
|
|
|
|
var walk = func(path string, d fs.DirEntry, err error) error {
|
|
if d.IsDir() {
|
|
return nil
|
|
}
|
|
|
|
if filepath.Ext(path) == ".log" {
|
|
i, err := d.Info()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
response.Files = append(response.Files, logFile{
|
|
Name: d.Name(),
|
|
SizeBytes: i.Size(),
|
|
Size: humanize.Bytes(uint64(i.Size())),
|
|
UpdatedAt: i.ModTime(),
|
|
})
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
if err := filepath.WalkDir(logsDir, walk); err != nil {
|
|
render.Status(r, http.StatusInternalServerError)
|
|
render.JSON(w, r, errorResponse{
|
|
Message: err.Error(),
|
|
Status: http.StatusInternalServerError,
|
|
})
|
|
return
|
|
}
|
|
|
|
response.Count = len(response.Files)
|
|
|
|
render.JSON(w, r, response)
|
|
}
|
|
|
|
var ( // regexes for sanitizing log files
|
|
keyValueRegex = regexp.MustCompile(`(torrent_pass|passkey|authkey|secret_key|apikey)=([a-zA-Z0-9]+)`)
|
|
combinedRegex = regexp.MustCompile(`(https?://[^\s]+/((rss/download/[a-zA-Z0-9]+/)|torrent/download/((auto\.[a-zA-Z0-9]+\.|[a-zA-Z0-9]+\.))))([a-zA-Z0-9]+)`)
|
|
inviteRegex = regexp.MustCompile(`(Voyager autobot [\p{L}0-9]+ |Satsuki enter #announce [\p{L}0-9]+ |Millie announce |DBBot announce |ENDOR !invite [\p{L}0-9]+ |Vertigo ENTER #GGn-Announce [\p{L}0-9]+ |midgards announce |HeBoT !invite |NBOT !invite |Muffit bot #nbl-announce [\p{L}0-9]+ |hermes enter #announce [\p{L}0-9]+ |LiMEY_ !invite |PS-Info pass |PT-BOT invite |Hummingbird ENTER [\p{L}0-9]+ |Drone enter #red-announce [\p{L}0-9]+ |SceneHD \.invite |erica letmeinannounce [\p{L}0-9]+ |Synd1c4t3 invite |UHDBot invite |Sauron bot #ant-announce [\p{L}0-9]+ |RevoTT !invite [\p{L}0-9]+ |Cerberus identify [\p{L}0-9]+ )([\p{L}0-9]+)`)
|
|
nickservRegex = regexp.MustCompile(`(NickServ IDENTIFY )([\p{L}0-9!#%&*+/:;<=>?@^_` + "`" + `{|}~]+)`)
|
|
saslRegex = regexp.MustCompile(`(--> AUTHENTICATE )([\p{L}0-9!#%&*+/:;<=>?@^_` + "`" + `{|}~]+)`)
|
|
)
|
|
|
|
func SanitizeLogFile(filePath string) (string, error) {
|
|
data, err := ioutil.ReadFile(filePath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
sanitizedData := string(data)
|
|
|
|
// torrent_pass, passkey, authkey, secret_key, apikey, rsskey
|
|
sanitizedData = keyValueRegex.ReplaceAllString(sanitizedData, "${1}=REDACTED")
|
|
sanitizedData = combinedRegex.ReplaceAllString(sanitizedData, "${1}REDACTED")
|
|
|
|
// irc related
|
|
sanitizedData = inviteRegex.ReplaceAllString(sanitizedData, "${1}REDACTED")
|
|
sanitizedData = nickservRegex.ReplaceAllString(sanitizedData, "${1}REDACTED")
|
|
sanitizedData = saslRegex.ReplaceAllString(sanitizedData, "${1}REDACTED")
|
|
|
|
tmpFile, err := ioutil.TempFile("", "sanitized-log-*.log")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
_, err = tmpFile.WriteString(sanitizedData)
|
|
if err != nil {
|
|
tmpFile.Close()
|
|
return "", err
|
|
}
|
|
|
|
err = tmpFile.Close()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return tmpFile.Name(), nil
|
|
}
|
|
|
|
func (h logsHandler) downloadFile(w http.ResponseWriter, r *http.Request) {
|
|
if h.cfg.Config.LogPath == "" {
|
|
render.Status(r, http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
logsDir := path.Dir(h.cfg.Config.LogPath)
|
|
|
|
// check if dir exists before walkDir
|
|
if _, err := os.Stat(logsDir); os.IsNotExist(err) {
|
|
render.Status(r, http.StatusNotFound)
|
|
render.JSON(w, r, errorResponse{
|
|
Message: "log directory not found or inaccessible",
|
|
Status: http.StatusNotFound,
|
|
})
|
|
return
|
|
}
|
|
|
|
logFile := chi.URLParam(r, "logFile")
|
|
if logFile == "" {
|
|
render.Status(r, http.StatusBadRequest)
|
|
render.JSON(w, r, errorResponse{
|
|
Message: "empty log file",
|
|
Status: http.StatusBadRequest,
|
|
})
|
|
return
|
|
} else if !strings.Contains(logFile, ".log") {
|
|
render.Status(r, http.StatusBadRequest)
|
|
render.JSON(w, r, errorResponse{
|
|
Message: "invalid file",
|
|
Status: http.StatusBadRequest,
|
|
})
|
|
return
|
|
}
|
|
|
|
filePath := filepath.Join(logsDir, logFile)
|
|
|
|
// Sanitize the log file
|
|
sanitizedFilePath, err := SanitizeLogFile(filePath)
|
|
if err != nil {
|
|
render.Status(r, http.StatusInternalServerError)
|
|
render.JSON(w, r, errorResponse{
|
|
Message: err.Error(),
|
|
Status: http.StatusInternalServerError,
|
|
})
|
|
return
|
|
}
|
|
defer os.Remove(sanitizedFilePath)
|
|
|
|
w.Header().Set("Content-Disposition", "attachment; filename="+strconv.Quote(logFile))
|
|
w.Header().Set("Content-Type", "application/octet-stream")
|
|
|
|
http.ServeFile(w, r, sanitizedFilePath)
|
|
}
|
|
|
|
type logFile struct {
|
|
Name string `json:"filename"`
|
|
SizeBytes int64 `json:"size_bytes"`
|
|
Size string `json:"size"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
type LogfilesResponse struct {
|
|
Files []logFile `json:"files"`
|
|
Count int `json:"count"`
|
|
}
|