diff --git a/client.go b/client.go index ccee839..9b750af 100644 --- a/client.go +++ b/client.go @@ -728,6 +728,82 @@ func CapturePanicAndWait(f func(), tags map[string]string, interfaces ...Interfa return DefaultClient.CapturePanicAndWait(f, tags, interfaces...) } +// ReportPanic reports a panic to the Sentry server if it occurs and allows that panic to continue. +func (client *Client) ReportPanic(err interface{}, tags map[string]string, interfaces ...Interface) { + // Note: This doesn't need to check for client, because we still want to go through the defer/recover path + // Down the line, Capture will be noop'd, so while this does a _tiny_ bit of overhead constructing the + // *Packet just to be thrown away, this should not be the normal case. Could be refactored to + // be completely noop though if we cared. + var packet *Packet + switch rval := err.(type) { + case nil: + return + case error: + if client.shouldExcludeErr(rval.Error()) { + return + } + packet = NewPacket(rval.Error(), append(append(interfaces, client.context.interfaces()...), NewException(rval, NewStacktrace(2, 3, client.includePaths)))...) + default: + rvalStr := fmt.Sprint(rval) + if client.shouldExcludeErr(rvalStr) { + return + } + packet = NewPacket(rvalStr, append(append(interfaces, client.context.interfaces()...), NewException(errors.New(rvalStr), NewStacktrace(2, 3, client.includePaths)))...) + } + + client.Capture(packet, tags) + // send the panic up the stack + panic(err) +} + +// ReportPanic reports a panic to the Sentry server if it occurs and allows that panic to continue. +func ReportPanic(tags map[string]string, interfaces ...Interface) { + err := recover() + if err == nil { + return + } + DefaultClient.ReportPanic(err, tags, interfaces...) +} + +// ReportPanicAndWait is identical to ReportPanic, except it blocks and assures that the event was sent. +func (client *Client) ReportPanicAndWait(err interface{}, tags map[string]string, interfaces ...Interface) { + // Note: This doesn't need to check for client, because we still want to go through the defer/recover path + // Down the line, Capture will be noop'd, so while this does a _tiny_ bit of overhead constructing the + // *Packet just to be thrown away, this should not be the normal case. Could be refactored to + // be completely noop though if we cared. + + var packet *Packet + switch rval := err.(type) { + case nil: + return + case error: + if client.shouldExcludeErr(rval.Error()) { + return + } + packet = NewPacket(rval.Error(), append(append(interfaces, client.context.interfaces()...), NewException(rval, NewStacktrace(2, 3, client.includePaths)))...) + default: + rvalStr := fmt.Sprint(rval) + if client.shouldExcludeErr(rvalStr) { + return + } + packet = NewPacket(rvalStr, append(append(interfaces, client.context.interfaces()...), NewException(errors.New(rvalStr), NewStacktrace(2, 3, client.includePaths)))...) + } + _, ch := client.Capture(packet, tags) + // block to make sure the report is sent + <-ch + // send the panic up the stack + panic(err) +} + +// ReportPanicAndWait is identical to ReportPanic, except it blocks and assures that the event was sent. +func ReportPanicAndWait(tags map[string]string, interfaces ...Interface) { + err := recover() + if err == nil { + return + } + DefaultClient.ReportPanicAndWait(err, tags, interfaces...) +} + func (client *Client) Close() { close(client.queue) } diff --git a/http.go b/http.go index 32107b8..eaad679 100644 --- a/http.go +++ b/http.go @@ -18,14 +18,15 @@ func NewHttp(req *http.Request) *Http { h := &Http{ Method: req.Method, Cookies: req.Header.Get("Cookie"), - Query: sanitizeQuery(req.URL.Query()).Encode(), + Query: url.Values(sanitizeValues(req.URL.Query())).Encode(), URL: proto + "://" + req.Host + req.URL.Path, Headers: make(map[string]string, len(req.Header)), } if addr, port, err := net.SplitHostPort(req.RemoteAddr); err == nil { h.Env = map[string]string{"REMOTE_ADDR": addr, "REMOTE_PORT": port} } - for k, v := range req.Header { + + for k, v := range http.Header(sanitizeValues(req.Header)) { h.Headers[k] = strings.Join(v, ",") } return h @@ -33,10 +34,10 @@ func NewHttp(req *http.Request) *Http { var querySecretFields = []string{"password", "passphrase", "passwd", "secret"} -func sanitizeQuery(query url.Values) url.Values { +func sanitizeValues(query map[string][]string) map[string][]string { for _, keyword := range querySecretFields { for field := range query { - if strings.Contains(field, keyword) { + if strings.Contains(strings.ToLower(field), strings.ToLower(keyword)) { query[field] = []string{"********"} } } @@ -44,6 +45,13 @@ func sanitizeQuery(query url.Values) url.Values { return query } +// AddSanitizewField adds a custom sanitize field to the array of fields to +// search for and sanitize. This allows you to hide sensitive information in +// both the query string and headers. +func AddSanitizeField(field string) { + querySecretFields = append(querySecretFields, field) +} + // https://docs.getsentry.com/hosted/clientdev/interfaces/#context-interfaces type Http struct { // Required @@ -62,9 +70,12 @@ type Http struct { func (h *Http) Class() string { return "request" } -// Recovery handler to wrap the stdlib net/http Mux. +// Recovery handler to wrap the stdlib net/http Mux. This function will detect a +// panic, report it, and recover from the panic, preventing it from continuing +// further. +// // Example: -// http.HandleFunc("/", raven.RecoveryHandler(func(w http.ResponseWriter, r *http.Request) { +// http.HandleFunc("/", raven.ReportHandler(func(w http.ResponseWriter, r *http.Request) { // ... // })) func RecoveryHandler(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { @@ -82,3 +93,27 @@ func RecoveryHandler(handler func(http.ResponseWriter, *http.Request)) func(http handler(w, r) } } + +// Report handler to wrap the stdlib net/http Mux. This function will detect a +// panic, report it, and allow the panic to contune. +// +// Example: +// http.HandleFunc("/", raven.ReportHandler(func(w http.ResponseWriter, r *http.Request) { +// ... +// })) +func ReportHandler(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + defer func() { + if rval := recover(); rval != nil { + debug.PrintStack() + rvalStr := fmt.Sprint(rval) + packet := NewPacket(rvalStr, NewException(errors.New(rvalStr), NewStacktrace(2, 3, nil)), NewHttp(r)) + Capture(packet, nil) + w.WriteHeader(http.StatusInternalServerError) + panic(rval) + } + }() + + handler(w, r) + } +} diff --git a/http_test.go b/http_test.go index 7d611b1..ba73c09 100644 --- a/http_test.go +++ b/http_test.go @@ -140,10 +140,38 @@ func parseQuery(q string) url.Values { func TestSanitizeQuery(t *testing.T) { for _, test := range sanitizeQueryTests { - actual := sanitizeQuery(parseQuery(test.input)) + actual := url.Values(sanitizeValues(parseQuery(test.input))) expected := parseQuery(test.output) if !reflect.DeepEqual(actual, expected) { t.Errorf("incorrect sanitization: got %+v, want %+v", actual, expected) } } } + +var sanitizeHeadersTest = []struct { + input, output string +}{ + {"foo=bar", "foo=bar"}, + {"password=foo", "password=********"}, + {"passphrase=foo", "passphrase=********"}, + {"passwd=foo", "passwd=********"}, + {"secret=foo", "secret=********"}, + {"secretstuff=foo", "secretstuff=********"}, + {"foo=bar&secret=foo", "foo=bar&secret=********"}, + {"secret=foo&secret=bar", "secret=********"}, +} + +func parseHeaders(q string) http.Header { + r, _ := url.ParseQuery(q) + return http.Header(r) +} + +func TestSanitizeHeaders(t *testing.T) { + for _, test := range sanitizeHeadersTest { + actual := http.Header(sanitizeValues(parseQuery(test.input))) + expected := parseHeaders(test.output) + if !reflect.DeepEqual(actual, expected) { + t.Errorf("incorrect sanitization: got %+v, want %+v", actual, expected) + } + } +}