v0.36.0 doc sweep update
Package / arch (push) Canceled after 0s
Package / user (push) Canceled after 0s
Package / deb (push) Canceled after 0s
Package / freebsd (push) Canceled after 0s
Package / image (push) Canceled after 0s
Test / test (push) Canceled after 0s
Test / e2e (push) Canceled after 0s
Package / arch (push) Canceled after 0s
Package / user (push) Canceled after 0s
Package / deb (push) Canceled after 0s
Package / freebsd (push) Canceled after 0s
Package / image (push) Canceled after 0s
Test / test (push) Canceled after 0s
Test / e2e (push) Canceled after 0s
This commit is contained in:
1 parent
de87b9a008
commit
a63b512733
51 files changed
+641
-420
No files matched your search
@@ -573,7 +573,8 @@ func (p *Policy) Rejected() uint64 {
|
||||
// Stats reports policy state for a plugin's stats details map. Merge it in
|
||||
// with maps.Copy so rejections surface in the status reporter and in the
|
||||
// http sink's status endpoint. allowed counts admissions (certificate checks,
|
||||
// SCRAM logins); rejected counts every refusal.
|
||||
// SCRAM logins), rejected refusals; a throttled or busy login counts in
|
||||
// auth_throttled or auth_busy, a forwarded client the acl refuses in acl_denied.
|
||||
func (p *Policy) Stats() map[string]any {
|
||||
if p == nil {
|
||||
return map[string]any{"auth": MethodNone}
|
||||
|
||||
@@ -196,7 +196,8 @@ type asked struct {
|
||||
tty bool
|
||||
}
|
||||
|
||||
// startupOver is set once lw's first service is built
|
||||
// startupOver is set once lw builds the service it runs, after any run on the
|
||||
// --tui screen
|
||||
var startupOver atomic.Bool
|
||||
|
||||
// EndStartup stops reading descriptors, pipes, FIFOs and terminals: at a
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Package chain is the protocol between logwisp nodes: the hello a dialer
|
||||
// opens with (ProtocolVersion, node, SCRAM), the NDJSON entries after it, the
|
||||
// headers and /auth path of its HTTP mapping, and the reconnect backoff the
|
||||
// chain sinks share.
|
||||
package chain
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// Package compose is the configuration engine lw --tui and the website share:
|
||||
// pipelines started empty, from a preset, a configuration or a command line,
|
||||
// edited, validated, and written as a command line, environment or file. It
|
||||
// reads no file and links no network or terminal code, so it builds as WebAssembly.
|
||||
// pipelines started empty, from a preset, a configuration or a pasted form,
|
||||
// edited, validated, and written as a command line, environment or file that
|
||||
// reads back to the same pipelines; Schema is lw --schema. It reads no file and
|
||||
// links no TLS, HTTP or terminal code, so it builds as WebAssembly.
|
||||
package compose
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
// Package config is lw's configuration: the settings and pipeline tables, the
|
||||
// plugin catalogue whose option structs' tags are the schema, the pipeline
|
||||
// flags, LOGWISP_ variables and presets that build pipelines, and the rules
|
||||
// Decode and ValidateConfig apply. Load's Manager merges the file, environment
|
||||
// and command line, and rereads the file on reload.
|
||||
package config
|
||||
|
||||
// --- LogWisp Configuration Options ---
|
||||
@@ -31,7 +36,7 @@ type LogConfig struct {
|
||||
// File output settings (when Output includes "file" or "all")
|
||||
File *LogFileConfig `toml:"file"`
|
||||
|
||||
// Console output settings
|
||||
// Accepted for older files: logging.output chooses the console
|
||||
Console *LogConsoleConfig `toml:"console"`
|
||||
}
|
||||
|
||||
@@ -44,7 +49,8 @@ type LogFileConfig struct {
|
||||
RetentionHours float64 `toml:"retention_hours" help:"hours a .log file is kept; 0 keeps them"`
|
||||
}
|
||||
|
||||
// LogConsoleConfig defines settings for console-based application logging
|
||||
// LogConsoleConfig is logging.console, whose target has no effect:
|
||||
// logging.output chooses the console stream
|
||||
type LogConsoleConfig struct {
|
||||
Target string `toml:"target" lw:"enum=stdout|stderr|split" help:"accepted without effect: logging.output chooses the console"`
|
||||
}
|
||||
|
||||
@@ -231,9 +231,9 @@ func settingKeys(v reflect.Value, prefix string) []Key {
|
||||
}
|
||||
|
||||
// Decode reads a plugin's config map into its options: unknown keys, kinds,
|
||||
// the defaults, the tag rules, the tls, auth and acl rules of its side, then the
|
||||
// options' own Check; a leading ~ in a path is the home directory. It reads no
|
||||
// file.
|
||||
// the defaults, the tag rules and the options' own Check, then the host, tls,
|
||||
// acl and auth rules of its side; a leading ~ in a path is the home directory.
|
||||
// It reads no file.
|
||||
func Decode[T any](role, typ string, m map[string]any) (*T, error) {
|
||||
v, err := decode(role, typ, m)
|
||||
if err != nil {
|
||||
@@ -272,7 +272,8 @@ func decode(role, typ string, m map[string]any) (any, error) {
|
||||
}
|
||||
|
||||
// Settle gives a decoded table its defaults, applies the tag rules, then its
|
||||
// own Check: Decode's last steps, for the flow stages a file decodes typed
|
||||
// own Check, as Decode does before its side's rules, for the flow stages a
|
||||
// file decodes typed
|
||||
func Settle(v any) error {
|
||||
Fill(v)
|
||||
if err := checkTags(reflect.ValueOf(v).Elem(), ""); err != nil {
|
||||
|
||||
@@ -238,7 +238,8 @@ func SpecPipelines(specs []Spec, isDir IsDir) ([]PipelineConfig, error) {
|
||||
}
|
||||
|
||||
// buildPipelines turns specs, in order, into pipelines; specs before the first
|
||||
// --pipeline go to one named "cli". Every call returns fresh maps.
|
||||
// --pipeline go to one named after the preset that starts it, or else "cli".
|
||||
// Every call returns fresh maps.
|
||||
func buildPipelines(specs []pipelineSpec, isDir IsDir) ([]PipelineConfig, error) {
|
||||
var pipelines []PipelineConfig
|
||||
var seen map[*specKind]bool
|
||||
|
||||
@@ -25,7 +25,8 @@ import (
|
||||
// ValidateConfig checks a configuration without reading a file it names:
|
||||
// the settings, then each pipeline's flow stages, and its sources and sinks
|
||||
// decoded in full. Its error joins every problem, one a part, each a
|
||||
// *KeyError naming the path of the refused key. lw --check opens the files.
|
||||
// *KeyError naming the refused key's path, save "no pipelines configured".
|
||||
// lw --check opens the files.
|
||||
func ValidateConfig(cfg *Config) error {
|
||||
if cfg == nil {
|
||||
return fmt.Errorf("config is nil")
|
||||
@@ -521,9 +522,11 @@ func (c *FilterConfig) Check() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Scan decodes a flow stage's command-line map into target, rejecting keys
|
||||
// target does not declare: a misspelled key must fail, not be dropped. Its
|
||||
// values take their kinds as a plugin's do, a wrong one refused at its key.
|
||||
// Scan decodes a table into target: a flow stage from a spec or an edit, a
|
||||
// file's root or the website's pipelines. It refuses a key target does not
|
||||
// declare, nested tables' too: a misspelled key must fail, not be dropped.
|
||||
// Values outside a list of tables take their kinds as a plugin's do, a wrong
|
||||
// one refused at its key.
|
||||
func Scan(configMap map[string]any, target any) error {
|
||||
if err := checkKeys(configMap, reflect.TypeOf(target), ""); err != nil {
|
||||
return err
|
||||
@@ -580,7 +583,7 @@ func checkKeys(m map[string]any, t reflect.Type, prefix string) error {
|
||||
|
||||
// checkFileKeys rejects keys the configuration file declares that Config does
|
||||
// not: a misspelled table path such as plugin_sinks.confg.tls would otherwise
|
||||
// drop the whole table. Plugin config maps are checked by Scan instead.
|
||||
// drop the whole table. Plugin config maps are checked by Decode instead.
|
||||
func checkFileKeys(path string) error {
|
||||
data, err := os.ReadFile(path)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Package core holds what the parts of a pipeline share: the LogEntry and the
|
||||
// TransportEvent sinks get, the Tap a viewer reads a flow's decisions from,
|
||||
// the level names, plugin capabilities, size limits and timings, and Network
|
||||
// and Listen, which keep a listener or dialer to its host's address family.
|
||||
package core
|
||||
|
||||
import (
|
||||
@@ -29,7 +33,8 @@ type TransportEvent struct {
|
||||
|
||||
// Tapped is what a pipeline's flow decided about one entry: Stage names the
|
||||
// stage that dropped Event.Entry (rate_limit, filters at Index, format), or is
|
||||
// "" when it passed and Event.Payload is what the sinks got.
|
||||
// "" when it passed and Event.Payload is what dispatch offered the sinks; a
|
||||
// full sink input drops it for that sink only.
|
||||
type Tapped struct {
|
||||
Pipeline string
|
||||
Source string // the source's id; "" for a heartbeat
|
||||
|
||||
@@ -37,7 +37,6 @@ func NewFilter(cfg config.FilterConfig, logger *log.Logger) (*Filter, error) {
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
// Compile patterns
|
||||
for i, pattern := range cfg.Patterns {
|
||||
re, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
@@ -82,8 +81,8 @@ func (f *Filter) Apply(entry core.LogEntry) bool {
|
||||
"logic", f.config.Logic,
|
||||
"entry_level", entry.Level,
|
||||
"entry_source", entry.Source,
|
||||
"entry_message", entry.Message[:min(100, len(entry.Message))], // First 100 chars
|
||||
"text_to_match", text[:min(150, len(text))], // First 150 chars
|
||||
"entry_message", entry.Message[:min(100, len(entry.Message))],
|
||||
"text_to_match", text[:min(150, len(text))],
|
||||
"patterns", f.config.Patterns)
|
||||
|
||||
for i, pattern := range f.config.Patterns {
|
||||
@@ -103,7 +102,6 @@ func (f *Filter) Apply(entry core.LogEntry) bool {
|
||||
"component", "filter",
|
||||
"matched", matched)
|
||||
|
||||
// Determine if we should pass or drop
|
||||
shouldPass := false
|
||||
switch f.config.Type {
|
||||
case config.FilterTypeInclude:
|
||||
@@ -137,11 +135,12 @@ func (f *Filter) GetStats() map[string]any {
|
||||
}
|
||||
}
|
||||
|
||||
// UpdatePatterns allows for dynamic, thread-safe updates to the filter's regex patterns
|
||||
// UpdatePatterns replaces the patterns under f.mu, which Apply does not take:
|
||||
// it is safe only before entries flow
|
||||
func (f *Filter) UpdatePatterns(patterns []string) error {
|
||||
compiled := make([]*regexp.Regexp, 0, len(patterns))
|
||||
|
||||
// Compile all patterns first
|
||||
// Compiled before the swap, so a bad pattern leaves the filter as it was
|
||||
for i, pattern := range patterns {
|
||||
re, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
@@ -150,7 +149,6 @@ func (f *Filter) UpdatePatterns(patterns []string) error {
|
||||
compiled = append(compiled, re)
|
||||
}
|
||||
|
||||
// Update atomically
|
||||
f.mu.Lock()
|
||||
f.patterns = compiled
|
||||
f.config.Patterns = patterns
|
||||
@@ -166,7 +164,6 @@ func (f *Filter) UpdatePatterns(patterns []string) error {
|
||||
func (f *Filter) matches(text string) bool {
|
||||
switch f.config.Logic {
|
||||
case config.FilterLogicOr:
|
||||
// Match any pattern
|
||||
for _, re := range f.patterns {
|
||||
if re.MatchString(text) {
|
||||
return true
|
||||
@@ -175,7 +172,6 @@ func (f *Filter) matches(text string) bool {
|
||||
return false
|
||||
|
||||
case config.FilterLogicAnd:
|
||||
// Must match all patterns
|
||||
for _, re := range f.patterns {
|
||||
if !re.MatchString(text) {
|
||||
return false
|
||||
|
||||
@@ -83,7 +83,6 @@ func (hg *HeartbeatGenerator) generateHeartbeat(t time.Time) core.TransportEvent
|
||||
Message: "heartbeat",
|
||||
}
|
||||
|
||||
// Add stats if configured
|
||||
if hg.config.IncludeStats {
|
||||
fields := map[string]any{
|
||||
"type": "heartbeat",
|
||||
@@ -98,13 +97,11 @@ func (hg *HeartbeatGenerator) generateHeartbeat(t time.Time) core.TransportEvent
|
||||
entry.Fields = fieldsJSON
|
||||
}
|
||||
|
||||
// Use formatter to generate payload
|
||||
var payload []byte
|
||||
var err error
|
||||
|
||||
// Check if we need special formatting for heartbeat
|
||||
// Unreachable: heartbeat.format's enum (txt, json, raw) refuses "comment"
|
||||
if hg.config.Format == "comment" {
|
||||
// SSE comment format - bypass formatter for this special case
|
||||
if hg.config.IncludeStats {
|
||||
beatNum := hg.beatCount.Load()
|
||||
payload = []byte(": heartbeat " + t.Format(time.RFC3339) +
|
||||
@@ -115,7 +112,6 @@ func (hg *HeartbeatGenerator) generateHeartbeat(t time.Time) core.TransportEvent
|
||||
} else {
|
||||
// Use flow's formatter for consistent formatting
|
||||
if adapter, ok := hg.formatter.(*format.FormatterAdapter); ok {
|
||||
// Customize flags for heartbeat if needed
|
||||
// The formatter shows a timestamp unless told otherwise
|
||||
customFlags := formatter.FlagDefault
|
||||
if !hg.config.IncludeTimestamp {
|
||||
@@ -123,14 +119,12 @@ func (hg *HeartbeatGenerator) generateHeartbeat(t time.Time) core.TransportEvent
|
||||
}
|
||||
payload, err = adapter.FormatWithFlags(entry, customFlags)
|
||||
} else {
|
||||
// Fallback to standard format
|
||||
payload, err = hg.formatter.Format(entry)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
hg.logger.Error("msg", "Failed to format heartbeat",
|
||||
"error", err)
|
||||
// Fallback to simple text
|
||||
payload = []byte("heartbeat: " + t.Format(time.RFC3339) + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Package netacl is the address seam of the network listeners, beside tlsx
|
||||
// and authz: a Policy reads the PROXY header of the proxies it trusts, then
|
||||
// refuses a peer by its client's address, or past its client's limits, before
|
||||
// TLS or a byte of the protocol is read. New returns nil when nothing is set,
|
||||
// and every method tolerates a nil receiver.
|
||||
// and authz: a Policy reads its trusted proxies' PROXY headers, refuses a peer
|
||||
// by address or connection count before TLS, and an HTTP request past its rate
|
||||
// or from a forwarded client the rules refuse. Its Table also throttles SCRAM.
|
||||
// New returns nil when nothing is set; every method tolerates a nil receiver.
|
||||
package netacl
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
// Package pipeline runs one pipeline: its sources through the flow to its
|
||||
// sinks, each plugin made by the factory its type registered. Sinks start
|
||||
// before the sources and stop after them, so entries in flight arrive; a full
|
||||
// sink drops its event unless it declares core.CapBackpressure. A tap gives a
|
||||
// viewer a copy of what the flow decides.
|
||||
package pipeline
|
||||
|
||||
import (
|
||||
@@ -39,7 +44,7 @@ type Pipeline struct {
|
||||
cancel context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
running atomic.Bool
|
||||
halt chan struct{} // closed by Stop: backpressure sinks drop again
|
||||
halt chan struct{} // closed by Stop: backpressure events may then drop, room or not
|
||||
finished chan struct{} // closed when every source has ended
|
||||
finishedOnce sync.Once
|
||||
}
|
||||
|
||||
@@ -143,8 +143,8 @@ func TestStopReleasesStalledBackpressureSink(t *testing.T) {
|
||||
}
|
||||
|
||||
// The tap gets each entry's verdict under its source's id: the stage that
|
||||
// dropped it, or what the sinks got. Drops come only when All is set, and
|
||||
// heartbeats come as passed, from no source.
|
||||
// dropped it, or what dispatch offered the sinks. Drops come only when All is
|
||||
// set, and heartbeats come as passed, from no source.
|
||||
func TestTapSeesEachVerdict(t *testing.T) {
|
||||
const n = 1000
|
||||
verdict := func(msg string) (string, int) {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// Package plugin maps each source and sink type to the constructor its
|
||||
// package registers in init; a type without a row in config's catalogue is
|
||||
// refused.
|
||||
package plugin
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// Package service holds the pipelines of one configuration: it builds all of
|
||||
// them or none, starts and stops them together, gives a viewer's tap to each,
|
||||
// and reports when every one has finished.
|
||||
package service
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Package session tracks the sessions of a pipeline's plugins: a client
|
||||
// connection, or a plugin's own input or output. A Manager per pipeline sweeps
|
||||
// the idle ones; each plugin reaches it through a Proxy that sees only its
|
||||
// instance's sessions.
|
||||
package session
|
||||
|
||||
import (
|
||||
@@ -11,7 +15,7 @@ import (
|
||||
"github.com/lixenwraith/logwisp/internal/core"
|
||||
)
|
||||
|
||||
// Session represents a connection session
|
||||
// Session is a client connection, or a plugin's own input or output
|
||||
type Session struct {
|
||||
InstanceID string // Plugin instance identifier
|
||||
ID string // Unique session identifier
|
||||
@@ -20,8 +24,7 @@ type Session struct {
|
||||
LastActivity time.Time // Last activity timestamp
|
||||
Metadata map[string]any // Optional metadata (e.g., TLS info)
|
||||
|
||||
// Connection context
|
||||
Source string // Source type: "tcp_source", "http_source", "tcp_sink", etc.
|
||||
Source string // the owning plugin's instance ID, which Proxy sets
|
||||
}
|
||||
|
||||
// Manager handles the lifecycle of sessions
|
||||
@@ -34,7 +37,7 @@ type Manager struct {
|
||||
cleanupTicker *time.Ticker
|
||||
done chan struct{}
|
||||
|
||||
// Expiry callbacks by source type
|
||||
// Expiry callbacks by plugin instance
|
||||
expiryCallbacks map[string]func(sessionID, remoteAddr string)
|
||||
callbacksMu sync.RWMutex
|
||||
}
|
||||
@@ -52,7 +55,6 @@ func NewManager(maxIdleTime time.Duration) *Manager {
|
||||
expiryCallbacks: make(map[string]func(sessionID, remoteAddr string)),
|
||||
}
|
||||
|
||||
// Start cleanup routine
|
||||
m.startCleanup()
|
||||
|
||||
return m
|
||||
@@ -145,7 +147,7 @@ func (m *Manager) GetSessionCount() int {
|
||||
return len(m.sessions)
|
||||
}
|
||||
|
||||
// GetSessionsBySource returns snapshots of sessions matching a specific source type.
|
||||
// GetSessionsBySource returns snapshots of sessions matching a specific plugin instance.
|
||||
func (m *Manager) GetSessionsBySource(source string) []*Session {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
@@ -159,7 +161,7 @@ func (m *Manager) GetSessionsBySource(source string) []*Session {
|
||||
return sessions
|
||||
}
|
||||
|
||||
// GetActiveSessionsBySource returns snapshots of active sessions for a given source.
|
||||
// GetActiveSessionsBySource returns snapshots of active sessions for a given plugin instance.
|
||||
func (m *Manager) GetActiveSessionsBySource(source string) []*Session {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
@@ -240,7 +242,7 @@ func (m *Manager) RegisterExpiryCallback(source string, callback func(sessionID,
|
||||
m.expiryCallbacks[source] = callback
|
||||
}
|
||||
|
||||
// UnregisterExpiryCallback removes an expiry callback for a given source type
|
||||
// UnregisterExpiryCallback removes an expiry callback for a given plugin instance
|
||||
func (m *Manager) UnregisterExpiryCallback(source string) {
|
||||
m.callbacksMu.Lock()
|
||||
defer m.callbacksMu.Unlock()
|
||||
@@ -290,7 +292,6 @@ func (m *Manager) cleanupIdleSessions() {
|
||||
|
||||
for _, session := range expiredSessions {
|
||||
if callback, exists := callbacks[session.Source]; exists {
|
||||
// Call callback to notify owner
|
||||
go callback(session.ID, session.RemoteAddr)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,8 +82,8 @@ func NewConsoleSinkPlugin(
|
||||
|
||||
output := Stream(opts.Target)
|
||||
|
||||
// inline decides auto: a terminal, NO_COLOR unset, TERM not dumb. Paint
|
||||
// returns its text unchanged exactly when color is off.
|
||||
// inline decides auto: a terminal, NO_COLOR empty or unset, TERM not
|
||||
// dumb. Paint returns its text unchanged exactly when color is off.
|
||||
printer := inline.New(output)
|
||||
if opts.Color != "auto" {
|
||||
printer.SetColor(opts.Color == "always")
|
||||
|
||||
@@ -56,7 +56,6 @@ func NewFileSinkPlugin(
|
||||
logger *log.Logger,
|
||||
proxy *session.Proxy,
|
||||
) (sink.Sink, error) {
|
||||
// Create empty config struct
|
||||
opts, err := config.Decode[config.FileSinkOptions]("sink", "file", configMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -71,7 +70,7 @@ func NewFileSinkPlugin(
|
||||
writerConfig.RetentionPeriodHrs = opts.RetentionHours
|
||||
writerConfig.BufferSize = opts.BufferSize
|
||||
writerConfig.FlushIntervalMs = opts.FlushIntervalMs
|
||||
// Sink logic
|
||||
// The payload is already formatted: the writer adds nothing and writes the file only
|
||||
writerConfig.EnableConsole = false
|
||||
writerConfig.EnableFile = true
|
||||
writerConfig.ShowTimestamp = false
|
||||
@@ -97,7 +96,6 @@ func NewFileSinkPlugin(
|
||||
}
|
||||
fs.lastProcessed.Store(time.Time{})
|
||||
|
||||
// Create session for file output
|
||||
fs.session = proxy.CreateSession(
|
||||
fmt.Sprintf("file:///%s/%s", opts.Directory, opts.Name),
|
||||
map[string]any{
|
||||
@@ -173,12 +171,10 @@ func (fs *FileSink) Stop() {
|
||||
<-fs.exited
|
||||
}
|
||||
|
||||
// Remove session
|
||||
if fs.session != nil {
|
||||
fs.proxy.RemoveSession(fs.session.ID)
|
||||
}
|
||||
|
||||
// Shutdown the writer with timeout
|
||||
if err := fs.writer.Shutdown(core.LoggerShutdownTimeout); err != nil {
|
||||
fs.logger.Error("msg", "Error shutting down file writer",
|
||||
"component", "file_sink",
|
||||
|
||||
@@ -452,7 +452,6 @@ func (h *HTTPSink) handleStream(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("X-Accel-Buffering", "no")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
// Connected event with metadata, parity with fasthttp sink
|
||||
info, _ := json.Marshal(map[string]any{
|
||||
"client_id": strconv.FormatUint(id, 10),
|
||||
"session_id": sess.ID,
|
||||
@@ -621,10 +620,11 @@ func (h *HTTPSink) GetStats() sink.SinkStats {
|
||||
// handlers; absent when auth is disabled
|
||||
type identityKey struct{}
|
||||
|
||||
// authMiddleware gates every endpoint on the policy: a client certificate
|
||||
// or a bearer token. The rejection carries no detail: the status endpoint
|
||||
// already exposes host, port, and throughput counters, so a refusal should
|
||||
// not add the shape of the policy on top of that.
|
||||
// authMiddleware gates stream and status on the policy: a client
|
||||
// certificate, a bearer token or, behind a proxy, the session cookie. The
|
||||
// rejection carries no detail: the status endpoint already exposes host,
|
||||
// port, and throughput counters, so a refusal should not add the shape of
|
||||
// the policy on top of that.
|
||||
func (h *HTTPSink) authMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ident, status, err := h.auth.AuthorizeRequest(r)
|
||||
|
||||
@@ -373,7 +373,7 @@ func TestProxiedStreamSessionKeepsTheProxy(t *testing.T) {
|
||||
|
||||
// The constructor refuses what it cannot serve: the login pages outside
|
||||
// scram's proxy mode, a viewer without the login page, a path under /auth, and
|
||||
// a path ServeMux or a URL would not take literally, which panicked at Start.
|
||||
// a path ServeMux or a URL would not take literally.
|
||||
func TestUnservableOptionsAreRefused(t *testing.T) {
|
||||
scram := map[string]any{"type": "scram", "credentials_file": scramCredentials(t)}
|
||||
pki := testutil.NewPKI(t, "viewer-01")
|
||||
|
||||
@@ -53,7 +53,7 @@ type HTTPChainSink struct {
|
||||
tlsEnabled bool
|
||||
mtls bool
|
||||
|
||||
// Authorization: pins the downstream server's identity
|
||||
// Authorization: mtls pins the downstream server's identity; scram logs this sink in
|
||||
auth *authz.Policy
|
||||
|
||||
client *http.Client
|
||||
@@ -189,7 +189,7 @@ func (t *HTTPChainSink) Capabilities() []core.Capability {
|
||||
caps = append(caps, core.CapTLS)
|
||||
}
|
||||
if t.auth.Enabled() {
|
||||
caps = append(caps, core.CapAuth) // pins the server identity
|
||||
caps = append(caps, core.CapAuth) // pins the server, or logs in under scram
|
||||
}
|
||||
return caps
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
// Sink represents an output data stream.
|
||||
type Sink interface {
|
||||
// Capabilities returns a slice of supported Source capabilities
|
||||
// Capabilities returns a slice of supported Sink capabilities
|
||||
Capabilities() []core.Capability
|
||||
|
||||
// Input returns the channel for sending transport events to this sink.
|
||||
|
||||
@@ -221,8 +221,8 @@ func (t *TCPSink) Start(ctx context.Context) error {
|
||||
go t.acceptLoop()
|
||||
go t.broadcastLoop()
|
||||
|
||||
// Pipeline context cancellation mirrors gnet engine stop: cease accepting
|
||||
// and tear down existing connections
|
||||
// Pipeline context cancellation stops accepting and tears down the
|
||||
// connections, as Stop does
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -299,7 +299,8 @@ func (t *TCPSink) acceptLoop() {
|
||||
}
|
||||
|
||||
if t.config.MaxConnections > 0 && t.activeConns.Load() >= t.config.MaxConnections {
|
||||
// Load/admit race can over-admit by a conn under burst; acceptable
|
||||
// The count rises only in handleConn, so a burst can over-admit by
|
||||
// the conns accepted before their handleConn runs; acceptable
|
||||
t.rejectedConns.Add(1)
|
||||
conn.Close()
|
||||
continue
|
||||
@@ -328,7 +329,7 @@ func (t *TCPSink) handleConn(conn net.Conn) {
|
||||
defer t.wg.Done()
|
||||
remote := conn.RemoteAddr().String()
|
||||
|
||||
// Counted from accept: max_connections bounds concurrent handshakes too
|
||||
// Counted before the handshake: max_connections bounds concurrent handshakes too
|
||||
count := t.activeConns.Add(1)
|
||||
defer func() {
|
||||
conn.Close()
|
||||
|
||||
@@ -44,7 +44,7 @@ type TCPChainSink struct {
|
||||
network string
|
||||
tlsConfig *tls.Config
|
||||
|
||||
// Authorization: pins the downstream server's identity
|
||||
// Authorization: mtls pins the downstream server's identity; scram logs this sink in
|
||||
auth *authz.Policy
|
||||
|
||||
input chan core.TransportEvent
|
||||
@@ -158,7 +158,7 @@ func (t *TCPChainSink) Capabilities() []core.Capability {
|
||||
caps = append(caps, core.CapTLS)
|
||||
}
|
||||
if t.auth.Enabled() {
|
||||
caps = append(caps, core.CapAuth) // pins the server identity
|
||||
caps = append(caps, core.CapAuth) // pins the server, or logs in under scram
|
||||
}
|
||||
return caps
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ func TestRemoveWatcherPreservesReplacement(t *testing.T) {
|
||||
}
|
||||
|
||||
// A line longer than core.MaxLogEntryBytes continues in the next entry, and the
|
||||
// lines around it are read once: the watcher used to stop at it for good
|
||||
// lines around it are read once
|
||||
func TestLongLineContinuesInTheNextEntry(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "app.log")
|
||||
long := strings.Repeat("x", core.MaxLogEntryBytes+10)
|
||||
|
||||
@@ -20,7 +20,7 @@ type Source interface {
|
||||
// Stop gracefully shuts down the source
|
||||
Stop()
|
||||
|
||||
// SourceStats contains statistics about a source
|
||||
// GetStats returns the source's statistics
|
||||
GetStats() SourceStats
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Package tlsx builds crypto/tls configurations from config.TLSOptions.
|
||||
// It is the single seam between declarative TLS config and the stdlib;
|
||||
// each network plugin calls exactly one constructor.
|
||||
// Package tlsx is the TLS seam of the network plugins, beside authz and
|
||||
// netacl: Server and Client build a plugin's crypto/tls configuration from
|
||||
// config.TLSOptions, a listener without files gets a certificate issued on the
|
||||
// process key, lw tls makes its CA and certificates here, and LogWarnings
|
||||
// reports settings that work but are likely mistakes.
|
||||
package tlsx
|
||||
|
||||
import (
|
||||
|
||||
+4
-3
@@ -1,7 +1,8 @@
|
||||
// Package tui is lw --tui: pipelines composed on a full-screen canvas drawn as
|
||||
// the website draws them, then run or written as a command line, environment
|
||||
// or file. The pipelines are a compose.Composition; this package only draws
|
||||
// them and turns keys into the engine's edits.
|
||||
// the website draws them, run in place with a live view of what their stages
|
||||
// pass and drop, then left to run or written as a command line, environment
|
||||
// or file. The pipelines are a compose.Composition; this package draws them,
|
||||
// turns keys into the engine's edits and runs them through a Runner from cmd/lw.
|
||||
package tui
|
||||
|
||||
import (
|
||||
|
||||
@@ -187,8 +187,8 @@ func TestARowCountsAllItHolds(t *testing.T) {
|
||||
|
||||
// The canvas cursor picks the point and v the verdicts at it: a source's
|
||||
// entries, what reaches a stage or a filter, passes it or it drops, the
|
||||
// heartbeats, what the sinks got and what the flow dropped on the way; lw's
|
||||
// own log shows from warn up at every point, and whole at its own
|
||||
// heartbeats, what the flow offered the sinks and what it dropped on the way;
|
||||
// lw's own log shows from warn up at every point, and whole at its own
|
||||
func TestThePointAndModePickTheRows(t *testing.T) {
|
||||
c := pipeline(t, []string{"null", "null"}, []string{"null"})
|
||||
for _, typ := range []string{"include", "exclude"} {
|
||||
|
||||
Reference in new issue
Block a user