Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions src/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package client
import (
"bytes"
"fmt"
"io"
"log/slog"
"net/http"
"os"
Expand Down Expand Up @@ -70,7 +69,7 @@ func uploadPlaylistArtwork(hc *util.HttpClient, endpoint, localPath string, head
}
req, err := http.NewRequest("POST", endpoint, bytes.NewReader(data))
if err != nil {
return fmt.Errorf("build request: %w", err)
return fmt.Errorf("build artwork upload request")
}
req.Header.Set("Content-Type", "image/jpeg")
req.ContentLength = int64(len(data))
Expand All @@ -79,16 +78,15 @@ func uploadPlaylistArtwork(hc *util.HttpClient, endpoint, localPath string, head
}
resp, err := hc.Client.Do(req)
if err != nil {
return fmt.Errorf("post: %w", err)
return fmt.Errorf("artwork upload request failed")
}
defer func() {
if cerr := resp.Body.Close(); cerr != nil {
slog.Warn("artwork upload: response body close failed", "err", cerr.Error())
}
}()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("upload artwork: status %d, body: %s", resp.StatusCode, string(body))
return fmt.Errorf("upload artwork: status %d", resp.StatusCode)
}
return nil
}
Expand Down Expand Up @@ -325,4 +323,4 @@ func rankResult(track NormalisedTrack, r SearchResult) int {
}
}
return score
}
}
6 changes: 4 additions & 2 deletions src/logging/logging.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (


func Init(cfg *config.Config) {
configureRedaction(cfg)
baseHandler := slog.NewTextHandler( os.Stdout, &slog.HandlerOptions{
Level: getLogLevel(cfg.LogLevel),
})
Expand All @@ -19,10 +20,11 @@ func Init(cfg *config.Config) {
}


handler := &notifyHandler{
notifyHandler := &notifyHandler{
handler: baseHandler,
notify: notifyClient,
}
handler := newRedactingHandler(notifyHandler)
logger := slog.New(handler)
slog.SetDefault(logger)
}
Expand Down Expand Up @@ -54,4 +56,4 @@ func getLogLevel(level string) slog.Level {
default:
return slog.LevelWarn
}
}
}
148 changes: 148 additions & 0 deletions src/logging/redact_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package logging

import (
"context"
"fmt"
"io"
"log/slog"
"sort"
"strings"
"sync/atomic"

"explo/src/config"
)

const redactedValue = "[REDACTED]"

var secretValues atomic.Value

func init() {
secretValues.Store([]string(nil))
}

type redactingHandler struct {
handler slog.Handler
}

func newRedactingHandler(handler slog.Handler) slog.Handler {
return &redactingHandler{handler: handler}
}

// NewTextHandler creates a text handler that omits credential fields and
// redacts configured credential values from messages and attributes.
func NewTextHandler(w io.Writer, opts *slog.HandlerOptions) slog.Handler {
return newRedactingHandler(slog.NewTextHandler(w, opts))
}

func (h *redactingHandler) Enabled(ctx context.Context, level slog.Level) bool {
return h.handler.Enabled(ctx, level)
}

func (h *redactingHandler) Handle(ctx context.Context, record slog.Record) error {
clean := slog.NewRecord(record.Time, record.Level, redactText(record.Message), record.PC)
record.Attrs(func(attr slog.Attr) bool {
if attr = redactAttr(attr); attr.Key != "" {
clean.AddAttrs(attr)
}
return true
})
return h.handler.Handle(ctx, clean)
}

func (h *redactingHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
clean := make([]slog.Attr, 0, len(attrs))
for _, attr := range attrs {
if attr = redactAttr(attr); attr.Key != "" {
clean = append(clean, attr)
}
}
return &redactingHandler{handler: h.handler.WithAttrs(clean)}
}

func (h *redactingHandler) WithGroup(name string) slog.Handler {
return &redactingHandler{handler: h.handler.WithGroup(name)}
}

func redactAttr(attr slog.Attr) slog.Attr {
if isCredentialKey(attr.Key) {
return slog.Attr{}
}

value := attr.Value.Resolve()
switch value.Kind() {
case slog.KindString:
attr.Value = slog.StringValue(redactText(value.String()))
case slog.KindGroup:
group := value.Group()
clean := make([]slog.Attr, 0, len(group))
for _, child := range group {
if child = redactAttr(child); child.Key != "" {
clean = append(clean, child)
}
}
attr.Value = slog.GroupValue(clean...)
case slog.KindAny:
plain := fmt.Sprint(value.Any())
if redacted := redactText(plain); redacted != plain {
attr.Value = slog.StringValue(redacted)
}
}
return attr
}

func isCredentialKey(key string) bool {
key = strings.ToLower(key)
key = strings.NewReplacer("_", "", "-", "", " ", "").Replace(key)
for _, marker := range []string{"password", "passwd", "apikey", "authorization", "credential", "secret", "token"} {
if strings.Contains(key, marker) {
return true
}
}
return false
}

func redactText(value string) string {
for _, secret := range secretValues.Load().([]string) {
value = strings.ReplaceAll(value, secret, redactedValue)
}
return value
}

func configureRedaction(cfg *config.Config) {
values := []string{
cfg.ServerCfg.Password,
cfg.ClientCfg.Creds.APIKey,
cfg.ClientCfg.Creds.Password,
cfg.ClientCfg.Creds.Token,
cfg.ClientCfg.Creds.Salt,
cfg.ClientCfg.AdminCreds.APIKey,
cfg.ClientCfg.AdminCreds.Password,
cfg.DownloadCfg.Youtube.APIKey,
cfg.DownloadCfg.Slskd.APIKey,
cfg.DownloadCfg.Lidarr.APIKey,
cfg.DiscoveryCfg.Listenbrainz.UserToken,
cfg.NotifyCfg.Matrix.AccessToken,
cfg.NotifyCfg.Discord.BotToken,
}
values = append(values, cfg.NotifyCfg.Http.ReceiverURLs...)
for key, value := range cfg.ClientCfg.Creds.Headers {
if isCredentialKey(key) {
values = append(values, value)
}
}

seen := make(map[string]struct{}, len(values))
secrets := make([]string, 0, len(values))
for _, value := range values {
if value == "" {
continue
}
if _, ok := seen[value]; ok {
continue
}
seen[value] = struct{}{}
secrets = append(secrets, value)
}
sort.Slice(secrets, func(i, j int) bool { return len(secrets[i]) > len(secrets[j]) })
secretValues.Store(secrets)
}
26 changes: 18 additions & 8 deletions src/util/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import (
"io"
"log/slog"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"time"

"explo/src/config"
"explo/src/logging"
)

type HttpClientConfig struct {
Expand All @@ -36,7 +36,7 @@ func NewHttp(cfg HttpClientConfig) *HttpClient {
func (c *HttpClient) MakeRequest(method, url string, payload io.Reader, headers map[string]string) ([]byte, error) {
req, err := http.NewRequest(method, url, payload)
if err != nil {
return nil, fmt.Errorf("failed to initialize request: %s", err.Error())
return nil, fmt.Errorf("failed to initialize %s request to %s", method, requestEndpoint(url))
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Accept", "application/json")
Expand All @@ -48,7 +48,7 @@ func (c *HttpClient) MakeRequest(method, url string, payload io.Reader, headers

resp, err := c.Client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to make request: %s", err.Error())
return nil, fmt.Errorf("failed to make %s request to %s", method, requestEndpoint(url))
}

defer func() {
Expand All @@ -63,17 +63,27 @@ func (c *HttpClient) MakeRequest(method, url string, payload io.Reader, headers
}

if resp.StatusCode < 200 || resp.StatusCode >= 300 {
slog.Debug("response info", logging.RuntimeAttr(string(body)))
return nil, fmt.Errorf("got %d from %s", resp.StatusCode, url)
return nil, fmt.Errorf("got %d from %s", resp.StatusCode, requestEndpoint(url))
}

return body, nil
}

func requestEndpoint(rawURL string) string {
u, err := url.Parse(rawURL)
if err != nil {
return "remote endpoint"
}
u.User = nil
u.RawQuery = ""
u.ForceQuery = false
u.Fragment = ""
return u.String()
}

func ParseResp[T any](body []byte, target *T) error {

if err := json.Unmarshal(body, target); err != nil {
slog.Debug("response info", logging.RuntimeAttr(string(body)))
return fmt.Errorf("error unmarshaling response body: %s", err.Error())
}
return nil
Expand All @@ -93,15 +103,15 @@ func DownloadFile(url, destPath string) (string, error) {
}
resp, err := http.Get(url) //nolint:noctx
if err != nil {
return "", fmt.Errorf("get: %w", err)
return "", fmt.Errorf("failed to download from %s", requestEndpoint(url))
}
defer func() {
if cerr := resp.Body.Close(); cerr != nil {
slog.Warn("DownloadFile: close failed", "err", cerr.Error())
}
}()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("status %d from %s", resp.StatusCode, url)
return "", fmt.Errorf("status %d from %s", resp.StatusCode, requestEndpoint(url))
}
data, err := io.ReadAll(resp.Body)
if err != nil {
Expand Down
13 changes: 5 additions & 8 deletions src/web/backend/playlist/spotify.go
Original file line number Diff line number Diff line change
Expand Up @@ -331,13 +331,12 @@ func (s *spotifySession) fetchAccessToken() error {

resp, err := s.client.Do(req)
if err != nil {
return err
return fmt.Errorf("token endpoint request failed")
}
defer func() { _ = resp.Body.Close() }()

if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
return fmt.Errorf("token endpoint returned %d: %s", resp.StatusCode, string(b))
return fmt.Errorf("token endpoint returned %d", resp.StatusCode)
}

var tok struct {
Expand Down Expand Up @@ -391,8 +390,7 @@ func (s *spotifySession) fetchClientToken() error {
defer func() { _ = resp.Body.Close() }()

if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
return fmt.Errorf("client token endpoint returned %d: %s", resp.StatusCode, string(b))
return fmt.Errorf("client token endpoint returned %d", resp.StatusCode)
}

var result struct {
Expand Down Expand Up @@ -661,8 +659,7 @@ func queryPartnerAPI(playlistID string, offset, limit int) (*partnerPlaylistResp
defer func() { _ = resp.Body.Close() }()

if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
return nil, fmt.Errorf("partner API returned %d: %s", resp.StatusCode, string(b))
return nil, fmt.Errorf("partner API returned %d", resp.StatusCode)
}

var result partnerPlaylistResp
Expand Down Expand Up @@ -808,4 +805,4 @@ func findPartHash(jsContent, name string) string {
return ""
}
return rest[:end]
}
}
12 changes: 7 additions & 5 deletions src/web/backend/run/events.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package run

import (
"bufio"
"fmt"
"io"
"log/slog"
"os"
"io"
"path/filepath"
"bufio"
"os/exec"
"path/filepath"

"explo/src/logging"
)


Expand Down Expand Up @@ -90,7 +92,7 @@ func (mr *ManualRun) InitServerLog() {
return
}
w := io.MultiWriter(os.Stderr, lf)
slog.SetDefault(slog.New(slog.NewTextHandler(w, nil)))
slog.SetDefault(slog.New(logging.NewTextHandler(w, nil)))
}

// openRunLog opens the single rolling log file in append mode.
Expand All @@ -100,4 +102,4 @@ func (mr *ManualRun) openRunLog() (*os.File, error) {
return nil, err
}
return os.OpenFile(p, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
}
}
Loading