Skip to content
Closed
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Usage
-basic-auth="" : setup HTTP Basic Authentication ("user_name:password"), can be used several times
-timeout=N : set timeout for execute shell command (in seconds)
-no-log-timestamp : log output without timestamps
-avoid-log-for=url: don't log requests for specified paths ("path1,path2,...")
-version
-help

Expand Down
34 changes: 34 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ type Config struct {
showErrors bool // returns the standard output even if the command exits with a non-zero exit code
includeStderr bool // also returns output written to stderr (default is stdout only)
intServerErr bool // return 500 error if shell status code != 0
avoidLogFor string // don't log requests for specified paths (comma separated: "path1,path2,...")
avoidLogPaths map[string]bool // parsed set of paths to avoid logging
formCheckRe *regexp.Regexp // regexp for check form fields
}

Expand Down Expand Up @@ -104,6 +106,7 @@ func getConfig() (*Config, error) {
flag.BoolVar(&cfg.showErrors, "show-errors", false, "show the standard output even if the command exits with a non-zero exit code")
flag.BoolVar(&cfg.includeStderr, "include-stderr", false, "include stderr to output (default is stdout only)")
flag.BoolVar(&cfg.intServerErr, "500", false, "return 500 error if shell exit code != 0")
flag.StringVar(&cfg.avoidLogFor, "avoid-log-for", "", "don't log requests for specified paths (\"path1,path2,...\")")
flag.StringVar(&cfg.cert, "cert", "", "SSL certificate `path` (if specified -cert/-key options - run https server)")
flag.StringVar(&cfg.key, "key", "", "SSL private key `/path/...`")
flag.Var(&cfg.auth, "basic-auth", "setup HTTP Basic Authentication (\"user_name:password\"), can be used several times")
Expand Down Expand Up @@ -163,9 +166,40 @@ func getConfig() (*Config, error) {
cfg.formCheckRe = re
}

if len(cfg.avoidLogFor) > 0 {
cfg.avoidLogPaths = make(map[string]bool)
for _, rawPath := range strings.Split(cfg.avoidLogFor, ",") {
p := strings.TrimSpace(rawPath)
if p == "" {
continue
}
if !strings.HasPrefix(p, "/") {
p = "/" + p
}
cfg.avoidLogPaths[p] = true
if len(p) > 1 && strings.HasSuffix(p, "/") {
cfg.avoidLogPaths[strings.TrimSuffix(p, "/")] = true
}
}
}

return &cfg, nil
}

// isAvoidLog - check if path is in avoid-log list
func (cfg Config) isAvoidLog(path string) bool {
if len(cfg.avoidLogPaths) == 0 {
return false
}
if cfg.avoidLogPaths[path] {
return true
}
if len(path) > 1 && strings.HasSuffix(path, "/") {
return cfg.avoidLogPaths[strings.TrimSuffix(path, "/")]
}
return cfg.avoidLogPaths[path+"/"]
}

// readableURL - get readable URL for logging
func (cfg Config) readableURL(addr fmt.Stringer) string {
prefix := "http"
Expand Down
1 change: 1 addition & 0 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ Usage:
-basic-auth="" : setup HTTP Basic Authentication ("user_name:password"), can be used several times
-timeout=N : set timeout for execute shell command (in seconds)
-no-log-timestamp : log output without timestamps
-avoid-log-for=url: don't log requests for specified paths ("path1,path2,...")
-version
-help

Expand Down
6 changes: 5 additions & 1 deletion http_middlewares.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,12 @@ func mwBasicAuth(handler http.HandlerFunc, users authUsers) http.HandlerFunc {
}

// mwLogging - add logging for handler
func mwLogging(handler http.HandlerFunc) http.HandlerFunc {
func mwLogging(handler http.HandlerFunc, isAvoidLog ...func(path string) bool) http.HandlerFunc {
return func(rw http.ResponseWriter, req *http.Request) {
if len(isAvoidLog) > 0 && isAvoidLog[0] != nil && isAvoidLog[0](req.URL.Path) {
handler.ServeHTTP(rw, req)
return
}
remoteAddr := req.RemoteAddr
if realIP, ok := req.Header["X-Real-Ip"]; ok && len(realIP) > 0 {
remoteAddr = realIP[0] + ", " + remoteAddr
Expand Down
1 change: 1 addition & 0 deletions shell2http.1
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ options:
\-basic\-auth="" : setup HTTP Basic Authentication ("user_name:password"), can be used several times
\-timeout=N : set timeout for execute shell command (in seconds)
\-no\-log\-timestamp : log output without timestamps
\-avoid\-log\-for=url: don\'t log requests for specified paths ("path1,path2,\.\.\.")
\-version
\-help
.
Expand Down
9 changes: 7 additions & 2 deletions shell2http.go
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,9 @@ func setupHandlers(cmdHandlers []command, appConfig Config, cacheTTL raphanus.DB
cmd: "index page",
handler: func(rw http.ResponseWriter, req *http.Request) {
if req.URL.Path != "/" {
log.Printf("%s - 404", req.URL.Path)
if !appConfig.isAvoidLog(req.URL.Path) {
log.Printf("%s - 404", req.URL.Path)
}
http.NotFound(rw, req)
return
}
Expand Down Expand Up @@ -612,7 +614,10 @@ func main() {
if appConfig.oneThread {
handlerFunc = mwOneThread(handlerFunc)
}
handlerFunc = mwLogging(mwCommonHeaders(handlerFunc))
handlerFunc = mwCommonHeaders(handlerFunc)
if !appConfig.isAvoidLog(handler.path) {
handlerFunc = mwLogging(handlerFunc, appConfig.isAvoidLog)
}

http.HandleFunc(handler.path, handlerFunc)
log.Printf("register: %s (%s)\n", handler.path, handler.cmd)
Expand Down
218 changes: 218 additions & 0 deletions shell2http_test.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
package main

import (
"bytes"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"net/http"
"net/http/httptest"
"os"
"reflect"
"strings"
"testing"
"time"

"github.com/msoap/raphanus"
)

func Test_parseCGIHeaders(t *testing.T) {
Expand Down Expand Up @@ -409,3 +414,216 @@ func Test_parsePathAndCommands(t *testing.T) {
})
}
}

func Test_isAvoidLog(t *testing.T) {
tests := []struct {
name string
avoidFor string
checkPaths map[string]bool
}{
{
name: "empty config",
avoidFor: "",
checkPaths: map[string]bool{
"/health": false,
"/": false,
"/date": false,
},
},
{
name: "single path without slash",
avoidFor: "health",
checkPaths: map[string]bool{
"/health": true,
"/health/": true,
"/date": false,
},
},
{
name: "single path with slash",
avoidFor: "/health",
checkPaths: map[string]bool{
"/health": true,
"/health/": true,
"/date": false,
},
},
{
name: "single path with trailing slash",
avoidFor: "/health/",
checkPaths: map[string]bool{
"/health": true,
"/health/": true,
"/date": false,
},
},
{
name: "multiple paths with spaces and slashes",
avoidFor: " health, /metrics , /status/ ",
checkPaths: map[string]bool{
"/health": true,
"/health/": true,
"/metrics": true,
"/metrics/": true,
"/status": true,
"/status/": true,
"/date": false,
},
},
{
name: "root path",
avoidFor: "/",
checkPaths: map[string]bool{
"/": true,
"/health": false,
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := Config{avoidLogFor: tt.avoidFor}
if len(cfg.avoidLogFor) > 0 {
cfg.avoidLogPaths = make(map[string]bool)
for _, rawPath := range strings.Split(cfg.avoidLogFor, ",") {
p := strings.TrimSpace(rawPath)
if p == "" {
continue
}
if !strings.HasPrefix(p, "/") {
p = "/" + p
}
cfg.avoidLogPaths[p] = true
if len(p) > 1 && strings.HasSuffix(p, "/") {
cfg.avoidLogPaths[strings.TrimSuffix(p, "/")] = true
}
}
}

for path, expected := range tt.checkPaths {
if got := cfg.isAvoidLog(path); got != expected {
t.Errorf("isAvoidLog(%q) = %v, expected %v", path, got, expected)
}
}
})
}
}

func Test_mwLogging_avoidLog(t *testing.T) {
cfg := Config{
avoidLogPaths: map[string]bool{
"/health": true,
"/health/": true,
},
}

dummyHandler := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusOK)
_, _ = rw.Write([]byte("ok"))
})

mw := mwLogging(dummyHandler, cfg.isAvoidLog)

var logBuf bytes.Buffer
origOutput := log.Writer()
log.SetOutput(&logBuf)
defer log.SetOutput(origOutput)

// 1. Avoided path should not produce any log
logBuf.Reset()
reqHealth := httptest.NewRequest("GET", "/health", nil)
rwHealth := httptest.NewRecorder()
mw.ServeHTTP(rwHealth, reqHealth)
if rwHealth.Code != http.StatusOK {
t.Errorf("expected 200, got %d", rwHealth.Code)
}
if logBuf.Len() != 0 {
t.Errorf("expected no log for /health, got: %s", logBuf.String())
}

// 2. Avoided path with query string should not produce any log
logBuf.Reset()
reqHealthQuery := httptest.NewRequest("GET", "/health?probe=1", nil)
rwHealthQuery := httptest.NewRecorder()
mw.ServeHTTP(rwHealthQuery, reqHealthQuery)
if rwHealthQuery.Code != http.StatusOK {
t.Errorf("expected 200, got %d", rwHealthQuery.Code)
}
if logBuf.Len() != 0 {
t.Errorf("expected no log for /health?probe=1, got: %s", logBuf.String())
}

// 3. Non-avoided path should produce log
logBuf.Reset()
reqDate := httptest.NewRequest("GET", "/date", nil)
rwDate := httptest.NewRecorder()
mw.ServeHTTP(rwDate, reqDate)
if rwDate.Code != http.StatusOK {
t.Errorf("expected 200, got %d", rwDate.Code)
}
if !strings.Contains(logBuf.String(), `GET /date`) {
t.Errorf("expected log for /date, got: %s", logBuf.String())
}

// 4. Backward compatibility: mwLogging without isAvoidLog
mwDefault := mwLogging(dummyHandler)
logBuf.Reset()
mwDefault.ServeHTTP(rwHealth, reqHealth)
if !strings.Contains(logBuf.String(), `GET /health`) {
t.Errorf("expected log for default mwLogging, got: %s", logBuf.String())
}
}

func Test_index404_avoidLog(t *testing.T) {
cfg := Config{
avoidLogPaths: map[string]bool{
"/health": true,
"/health/": true,
},
}
handlers, err := setupHandlers([]command{{path: "/date", cmd: "date"}}, cfg, raphanus.DB{})
if err != nil {
t.Fatalf("setupHandlers failed: %s", err)
}

var rootHandler http.HandlerFunc
for _, h := range handlers {
if h.path == "/" {
rootHandler = h.handler
break
}
}
if rootHandler == nil {
t.Fatal("root handler not found")
}

var logBuf bytes.Buffer
origOutput := log.Writer()
log.SetOutput(&logBuf)
defer log.SetOutput(origOutput)

// 404 for avoided path /health
logBuf.Reset()
reqHealth := httptest.NewRequest("GET", "http://localhost/health", nil)
rwHealth := httptest.NewRecorder()
rootHandler.ServeHTTP(rwHealth, reqHealth)
if rwHealth.Code != http.StatusNotFound {
t.Errorf("expected 404, got %d", rwHealth.Code)
}
if logBuf.Len() != 0 {
t.Errorf("expected no 404 log for /health, got: %s", logBuf.String())
}

// 404 for non-avoided path /unknown
logBuf.Reset()
reqUnknown := httptest.NewRequest("GET", "http://localhost/unknown", nil)
rwUnknown := httptest.NewRecorder()
rootHandler.ServeHTTP(rwUnknown, reqUnknown)
if rwUnknown.Code != http.StatusNotFound {
t.Errorf("expected 404, got %d", rwUnknown.Code)
}
if !strings.Contains(logBuf.String(), "/unknown - 404") {
t.Errorf("expected 404 log for /unknown, got: %s", logBuf.String())
}
}