diff --git a/internal/dnsdb/dnsdb.go b/internal/dnsdb/dnsdb.go index 32f3bc2a..b3f6bf3a 100644 --- a/internal/dnsdb/dnsdb.go +++ b/internal/dnsdb/dnsdb.go @@ -4,14 +4,13 @@ import ( "context" "errors" "fmt" + "slices" "strings" "time" "github.com/miekg/dns" "github.com/nt0xa/sonar/internal/database" - "github.com/nt0xa/sonar/internal/utils/pointer" - "github.com/nt0xa/sonar/internal/utils/slice" "github.com/nt0xa/sonar/pkg/dnsx" ) @@ -110,7 +109,7 @@ func (h *Records) Get(ctx context.Context, name string, qtype uint16) ([]dns.RR, record.LastAccessedAt != nil && len(record.LastAnswer) > 0 && time.Since(*record.LastAccessedAt) < time.Second*3 { - i := slice.FindIndex(record.Values, record.LastAnswer[0]) + i := slices.Index(record.Values, record.LastAnswer[0]) res = []dns.RR{rrs[min(i+1, len(rrs)-1)]} } else { // Fallback to first record. @@ -120,7 +119,7 @@ func (h *Records) Get(ctx context.Context, name string, qtype uint16) ([]dns.RR, // Update last answer and last answer time. lastAnswer := dnsx.RRsToStrings(res) - lastAccessedAt := pointer.Time(time.Now()) + lastAccessedAt := time.Now() if _, err := h.DB.DNSRecordsUpdate(ctx, database.DNSRecordsUpdateParams{ ID: record.ID, @@ -131,7 +130,7 @@ func (h *Records) Get(ctx context.Context, name string, qtype uint16) ([]dns.RR, Values: record.Values, Strategy: record.Strategy, LastAnswer: lastAnswer, - LastAccessedAt: lastAccessedAt, + LastAccessedAt: &lastAccessedAt, }); err != nil { return nil, err } diff --git a/internal/modules/lark/context.go b/internal/modules/lark/context.go deleted file mode 100644 index dd966381..00000000 --- a/internal/modules/lark/context.go +++ /dev/null @@ -1,25 +0,0 @@ -package lark - -import ( - "context" - - "github.com/nt0xa/sonar/internal/utils/errors" -) - -type contextKey string - -const ( - messageIDKey contextKey = "lark.messageID" -) - -func GetMessageID(ctx context.Context) (*string, errors.Error) { - id, ok := ctx.Value(messageIDKey).(string) - if !ok { - return nil, errors.Internalf("no %q key in context", messageIDKey) - } - return &id, nil -} - -func SetMessageID(ctx context.Context, msgID string) context.Context { - return context.WithValue(ctx, messageIDKey, msgID) -} diff --git a/internal/modules/lark/lark.go b/internal/modules/lark/lark.go index 1c56cb08..e3c71809 100644 --- a/internal/modules/lark/lark.go +++ b/internal/modules/lark/lark.go @@ -261,8 +261,6 @@ func (lrk *Lark) makeDispatcher(verificationToken, eventEncryptKey string, dedup return nil } - ctx = SetMessageID(ctx, *msgID) - text := msg.Text // remove @mention from the text diff --git a/internal/modules/telegram/telegram.go b/internal/modules/telegram/telegram.go index 2cc064bc..a3058741 100644 --- a/internal/modules/telegram/telegram.go +++ b/internal/modules/telegram/telegram.go @@ -16,7 +16,6 @@ import ( "github.com/nt0xa/sonar/internal/database" "github.com/nt0xa/sonar/internal/service" "github.com/nt0xa/sonar/internal/templates" - "github.com/nt0xa/sonar/internal/utils/errors" "github.com/nt0xa/sonar/pkg/telemetry" ) @@ -117,7 +116,7 @@ func (tg *Telegram) preExec(root *cobra.Command) { RunE: func(cmd *cobra.Command, args []string) error { mi, err := getMsgInfo(cmd.Context()) if err != nil { - return errors.Internal(err) + return err } tg.htmlMessage(cmd.Context(), mi.chatID, &mi.msgID, fmt.Sprintf("%d", mi.chatID)) diff --git a/internal/utils/errors/errors.go b/internal/utils/errors/errors.go deleted file mode 100644 index 69eeeaa5..00000000 --- a/internal/utils/errors/errors.go +++ /dev/null @@ -1,219 +0,0 @@ -package errors - -import ( - "fmt" -) - -// -// Error -// - -type Error interface { - Message() string - Error() string -} - -// -// BaseError -// - -type BaseError struct { - Msg string `json:"message"` - Det string `json:"details,omitempty"` -} - -func (e *BaseError) Error() string { - if e.Det == "" { - return e.Msg - } - return fmt.Sprintf("%s: %s", e.Msg, e.Det) -} - -func (e *BaseError) Message() string { - return e.Msg -} - -func (e *BaseError) Details() string { - return e.Det -} - -// -// Internal -// - -type InternalError struct { - BaseError - Cause error `json:"-"` -} - -func Internal(err error) Error { - return &InternalError{ - BaseError: BaseError{ - Msg: "internal error", - }, - Cause: err, - } -} - -func (e *InternalError) Error() string { - return fmt.Sprintf("%s: %s", e.Msg, e.Cause) -} - -func Internalf(format string, args ...interface{}) Error { - return Internal(fmt.Errorf(format, args...)) -} - -// -// Bad format -// - -type BadFormatError struct { - BaseError -} - -func BadFormat(err error) Error { - return &BadFormatError{ - BaseError: BaseError{ - Msg: "bad format", - Det: err.Error(), - }, - } -} - -func BadFormatf(format string, args ...interface{}) Error { - return &BadFormatError{ - BaseError: BaseError{ - Msg: "bad format", - Det: fmt.Sprintf(format, args...), - }, - } -} - -// -// Validation -// - -type Errors map[string]error - -func (e Errors) Error() string { - s := "" - for field, err := range e { - s += fmt.Sprintf("%s: %+v;", field, err) - } - return s -} - -type ValidationError struct { - BaseError - Errors error `json:"errors,omitempty"` -} - -func (e *ValidationError) Error() string { - if e.Errors != nil { - return fmt.Sprintf("%s: %s", e.Msg, e.Errors) - } - - return e.BaseError.Error() -} - -func Validation(errs error) Error { - return &ValidationError{ - BaseError: BaseError{ - Msg: "validation failed", - }, - Errors: errs, - } -} - -func Validationf(format string, args ...interface{}) Error { - return &ValidationError{ - BaseError: BaseError{ - Msg: "validation failed", - Det: fmt.Sprintf(format, args...), - }, - } -} - -// -// Conflict -// - -type ConflictError struct { - BaseError -} - -func Conflictf(format string, args ...interface{}) Error { - return &ConflictError{ - BaseError: BaseError{ - Msg: "conflict", - Det: fmt.Sprintf(format, args...), - }, - } -} - -// -// NotFound -// - -type NotFoundError struct { - BaseError -} - -func NotFoundf(format string, args ...interface{}) Error { - return &NotFoundError{ - BaseError: BaseError{ - Msg: "not found", - Det: fmt.Sprintf(format, args...), - }, - } -} - -// -// Unauthorized -// - -type UnauthorizedError struct { - BaseError -} - -func Unauthorized() Error { - return &UnauthorizedError{ - BaseError: BaseError{ - Msg: "unauthorized", - }, - } -} - -func Unauthorizedf(format string, args ...interface{}) Error { - return &UnauthorizedError{ - BaseError: BaseError{ - Msg: "unauthorized", - Det: fmt.Sprintf(format, args...), - }, - } -} - -// -// Forbidden -// - -type ForbiddenError struct { - BaseError -} - -func Forbidden() Error { - return &ForbiddenError{ - BaseError: BaseError{ - Msg: "forbidden", - }, - } -} - -func Forbiddenf(format string, args ...interface{}) Error { - return &ForbiddenError{ - BaseError: BaseError{ - Msg: "forbidden", - Det: fmt.Sprintf(format, args...), - }, - } -} diff --git a/internal/utils/parse/json.go b/internal/utils/parse/json.go deleted file mode 100644 index 6b85039f..00000000 --- a/internal/utils/parse/json.go +++ /dev/null @@ -1,50 +0,0 @@ -package parse - -import ( - "encoding/json" - "errors" - "fmt" - "io" - "strings" -) - -func JSON(src io.Reader, dst interface{}) error { - dec := json.NewDecoder(src) - dec.DisallowUnknownFields() - - err := dec.Decode(&dst) - if err != nil { - var syntaxError *json.SyntaxError - var unmarshalTypeError *json.UnmarshalTypeError - - switch { - case errors.As(err, &syntaxError): - return fmt.Errorf("badly-formed json (at position %d)", syntaxError.Offset) - - case errors.Is(err, io.ErrUnexpectedEOF): - return errors.New("badly-formed json") - - case errors.As(err, &unmarshalTypeError): - return fmt.Errorf("invalid value for the %q field (at position %d)", unmarshalTypeError.Field, unmarshalTypeError.Offset) - - case strings.HasPrefix(err.Error(), "json: unknown field "): - fieldName := strings.TrimPrefix(err.Error(), "json: unknown field ") - return fmt.Errorf("unknown field %s", fieldName) - - case errors.Is(err, io.EOF): - return errors.New("empty") - - case err.Error() == "http: request body too large": - return errors.New("too large") - - default: - return errors.New("unknown error") - } - } - - if dec.More() { - return errors.New("multple json objects") - } - - return nil -} diff --git a/internal/utils/pointer/pointer.go b/internal/utils/pointer/pointer.go deleted file mode 100644 index f102658b..00000000 --- a/internal/utils/pointer/pointer.go +++ /dev/null @@ -1,19 +0,0 @@ -package pointer - -import "time" - -func Bool(v bool) *bool { - return &v -} - -func Int64(v int64) *int64 { - return &v -} - -func String(v string) *string { - return &v -} - -func Time(v time.Time) *time.Time { - return &v -} diff --git a/internal/utils/slice/slice.go b/internal/utils/slice/slice.go deleted file mode 100644 index 3d671f34..00000000 --- a/internal/utils/slice/slice.go +++ /dev/null @@ -1,44 +0,0 @@ -package slice - -import "sort" - -// StringsDedup returns slice of strings without duplicates. -func StringsDedup(items []string) []string { - if len(items) == 0 { - return []string{} - } - - sort.Strings(items) - - j := 0 - for i := 1; i < len(items); i++ { - if items[j] == items[i] { - continue - } - - j++ - - items[j] = items[i] - } - - return items[:j+1] -} - -func StringsContains(items []string, item string) bool { - for _, s := range items { - if s == item { - return true - } - } - return false -} - -func FindIndex(values []string, value string) int { - for i, v := range values { - if value == v { - return i - } - } - - return -1 -} diff --git a/internal/utils/struct.go b/internal/utils/struct.go index 953d11e3..aa00d140 100644 --- a/internal/utils/struct.go +++ b/internal/utils/struct.go @@ -33,87 +33,3 @@ func StructKeys(s any, tagName string) []string { return keys } - -// StructToMap serializes a struct (or pointer to struct) into a map, -// including only fields marked with the `audit` tag. -func StructToMap(input any) map[string]any { - v := reflect.ValueOf(input) - for v.Kind() == reflect.Pointer { - if v.IsNil() { - return map[string]any{} - } - v = v.Elem() - } - if v.Kind() != reflect.Struct { - return map[string]any{} - } - - t := v.Type() - out := map[string]any{} - for i := 0; i < t.NumField(); i++ { - sf := t.Field(i) - if sf.PkgPath != "" { - continue - } - - tag, ok := sf.Tag.Lookup("audit") - if !ok || tag == "" || tag == "-" { - continue - } - key, options, _ := strings.Cut(tag, ",") - if key == "" || key == "-" { - continue - } - - fv := v.Field(i) - if hasTagOption(options, "omitempty") && isEmptyValue(fv) { - continue - } - - if fv.Kind() == reflect.Pointer { - if fv.IsNil() { - continue - } - out[key] = fv.Elem().Interface() - continue - } - - out[key] = fv.Interface() - } - - return out -} - -func hasTagOption(options string, want string) bool { - if options == "" { - return false - } - for _, opt := range strings.Split(options, ",") { - if strings.TrimSpace(opt) == want { - return true - } - } - return false -} - -// isEmptyValue mirrors encoding/json emptiness checks for omitempty. -func isEmptyValue(v reflect.Value) bool { - switch v.Kind() { - case reflect.Array, reflect.Map, reflect.Slice, reflect.String: - return v.Len() == 0 - case reflect.Bool: - return !v.Bool() - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return v.Int() == 0 - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: - return v.Uint() == 0 - case reflect.Float32, reflect.Float64: - return v.Float() == 0 - case reflect.Interface, reflect.Pointer: - return v.IsNil() - case reflect.Struct: - return v.IsZero() - } - - return false -} diff --git a/internal/utils/struct_test.go b/internal/utils/struct_test.go deleted file mode 100644 index 215df49a..00000000 --- a/internal/utils/struct_test.go +++ /dev/null @@ -1,95 +0,0 @@ -package utils_test - -import ( - "testing" - - "github.com/nt0xa/sonar/internal/utils" - "github.com/stretchr/testify/assert" -) - -type Inner struct { - B string `audit:"b"` - C int `audit:"c"` -} - -type Outer struct { - A string `audit:"a"` - D *Inner `audit:"d"` - E []int `audit:"e"` - F map[string]string `audit:"f"` - G Inner `audit:"g"` - H []Inner - O int `audit:"o,omitempty"` - P string `audit:"p,omitempty"` - Q bool `audit:"q,omitempty"` -} - -// Test for StructToMap -func TestStructToMap(t *testing.T) { - tests := []struct { - name string - input Outer - expect map[string]any - }{ - { - name: "All fields non-empty", - input: Outer{ - A: "hello", - D: &Inner{B: "test", C: 42}, - E: []int{1, 2, 3}, - F: map[string]string{"foo": "bar"}, - G: Inner{B: "test", C: 42}, - O: 7, - P: "x", - Q: true, - }, - expect: map[string]any{ - "a": "hello", - "d": Inner{B: "test", C: 42}, - "e": []int{1, 2, 3}, - "f": map[string]string{"foo": "bar"}, - "g": Inner{B: "test", C: 42}, - "o": 7, - "p": "x", - "q": true, - }, - }, - { - name: "Empty nested struct, nil pointer, empty slice/map", - input: Outer{ - A: "", - D: &Inner{B: "", C: 0}, - E: nil, - F: nil, - G: Inner{}, - }, - expect: map[string]any{ - "a": "", - "d": Inner{}, - "e": []int(nil), - "f": map[string]string(nil), - "g": Inner{}, - }, - }, - { - name: "Some fields empty", - input: Outer{ - A: "world", - E: []int{}, - }, - expect: map[string]any{ - "a": "world", - "e": []int{}, - "f": map[string]string(nil), - "g": Inner{}, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := utils.StructToMap(tt.input) - assert.EqualValues(t, tt.expect, got) - }) - } -} diff --git a/internal/utils/utils.go b/internal/utils/utils.go index 36488268..d023c3d5 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -3,8 +3,6 @@ package utils import ( "crypto/rand" "encoding/hex" - "fmt" - "unicode" ) func GenerateRandomString(n int) (string, error) { @@ -16,58 +14,3 @@ func GenerateRandomString(n int) (string, error) { return hex.EncodeToString(b), nil } - -func HexDump(by []byte) string { - s := "" - n := len(by) - rowcount := 0 - stop := (n / 8) * 8 - k := 0 - for i := 0; i <= stop; i += 8 { - k++ - if i+8 < n { - rowcount = 8 - } else { - rowcount = Min(k*8, n) % 8 - } - - s += fmt.Sprintf("%02d: ", i) - for j := 0; j < rowcount; j++ { - s += fmt.Sprintf("%02x ", by[i+j]) - } - for j := rowcount; j < 8; j++ { - s += " " - } - s += fmt.Sprintf(" %s\n", ViewString(by[i:(i+rowcount)])) - } - - return s -} - -func Min(a, b int) int { - if a < b { - return a - } - return b -} - -func ViewString(b []byte) string { - r := []rune(string(b)) - for i := range r { - if r[i] > unicode.MaxASCII || !unicode.IsPrint(r[i]) { - r[i] = '.' - } - } - return string(r) -} - -func StringPrintable(s string) bool { - r := []rune(s) - for i := range r { - if r[i] > unicode.MaxASCII || - !unicode.IsPrint(r[i]) && !unicode.IsSpace(r[i]) { - return false - } - } - return true -} diff --git a/internal/utils/valid/valid.go b/internal/utils/valid/valid.go index ed06f9f6..a99c1806 100644 --- a/internal/utils/valid/valid.go +++ b/internal/utils/valid/valid.go @@ -1,26 +1,11 @@ package valid import ( - "encoding/base64" "errors" - "fmt" "os" - "reflect" - "regexp" - "strconv" - "strings" - - validation "github.com/go-ozzo/ozzo-validation/v4" - "github.com/go-ozzo/ozzo-validation/v4/is" -) - -var ( - namePattern = `[a-z0-9]{1}([a-z0-9-]*[a-z0-9]{1})?` - subdomainRegexp = regexp.MustCompile(fmt.Sprintf(`^(\*|%[1]s)(\.%[1]s)*$`, namePattern)) - fqdnRegexp = regexp.MustCompile(fmt.Sprintf(`^(%s\.)+$`, namePattern)) ) -func File(value interface{}) error { +func File(value any) error { path, _ := value.(string) if _, err := os.Stat(path); os.IsNotExist(err) { @@ -30,7 +15,7 @@ func File(value interface{}) error { return nil } -func Directory(value interface{}) error { +func Directory(value any) error { path, _ := value.(string) if fi, err := os.Stat(path); os.IsNotExist(err) { @@ -41,124 +26,3 @@ func Directory(value interface{}) error { return nil } - -func Subdomain(value interface{}) error { - val, _ := value.(string) - - if !subdomainRegexp.MatchString(val) { - return errors.New("invalid subdomain") - } - - return nil -} - -func FQDN(value interface{}) error { - val, _ := value.(string) - - if !fqdnRegexp.MatchString(val) { - return errors.New("invalid fqdn") - } - - return nil -} - -func MX(value interface{}) error { - val, _ := value.(string) - - parts := strings.Split(val, " ") - - _, err := strconv.Atoi(parts[0]) - - if len(parts) == 2 && - err == nil && - fqdnRegexp.MatchString(parts[1]) { - return nil - } - - return errors.New("invalid mx record") -} - -func CAA(value interface{}) error { - v, _ := value.(string) - - var ( - flag uint8 - tag string - val string - ) - _, err := fmt.Sscanf(v, "%d %s %q", &flag, &tag, &val) - if err != nil { - return fmt.Errorf("invalid caa record: %w", err) - } - - return nil -} - -func DNSRecord(typ string) validation.Rule { - switch typ { - case "A": - return is.IPv4 - - case "AAAA": - return is.IPv6 - - case "MX": - return validation.By(MX) - - case "TXT": - return validation.Required - - case "CNAME": - return validation.By(FQDN) - - case "CAA": - return validation.By(CAA) - } - - return validation.Required -} - -func Base64(value interface{}) error { - val, _ := value.(string) - - _, err := base64.StdEncoding.DecodeString(val) - - if err != nil { - return fmt.Errorf("invalid base64 data") - } - - return nil -} - -type OneOfRule struct { - values []string - caseSensetive bool -} - -func (r *OneOfRule) Validate(value interface{}) error { - if value == nil { - return fmt.Errorf("invalid nil value") - } - - v := reflect.ValueOf(value) - if v.Kind() == reflect.Pointer { - v = v.Elem() - } - val, _ := v.Interface().(string) - - if !r.caseSensetive { - val = strings.ToLower(val) - } - - for _, v := range r.values { - if val == strings.ToLower(v) { - return nil - } - } - - return fmt.Errorf("invalid value, expected one of %s", strings.Join(r.values, ",")) -} - -func OneOf(values []string, caseSensetive bool) validation.Rule { - return &OneOfRule{values, caseSensetive} -}