diff --git a/resolver.go b/resolver.go index e9103a1..90d7ce9 100644 --- a/resolver.go +++ b/resolver.go @@ -2,85 +2,123 @@ package sam3 import ( "bufio" - "bytes" - "errors" + "context" "fmt" "strings" + "time" "github.com/go-i2p/i2pkeys" ) +// SAMResolver handles name resolution for I2P addresses type SAMResolver struct { - *SAM + sam *SAM } +// ResolveResult represents the possible outcomes of name resolution +type ResolveResult struct { + Address i2pkeys.I2PAddr + Error error +} + +const ( + defaultTimeout = 30 * time.Second + samReplyPrefix = "NAMING REPLY " +) + +// NewSAMResolver creates a resolver from an existing SAM instance func NewSAMResolver(parent *SAM) (*SAMResolver, error) { - log.Debug("Creating new SAMResolver from existing SAM instance") - var s SAMResolver - s.SAM = parent - return &s, nil + if parent == nil { + return nil, fmt.Errorf("parent SAM instance required") + } + return &SAMResolver{sam: parent}, nil } +// NewFullSAMResolver creates a new resolver with its own SAM connection func NewFullSAMResolver(address string) (*SAMResolver, error) { - log.WithField("address", address).Debug("Creating new full SAMResolver") - var s SAMResolver - var err error - s.SAM, err = NewSAM(address) - if err != nil { - log.WithError(err).Error("Failed to create new SAM instance") - return nil, err - } - return &s, nil + sam, err := NewSAM(address) + if err != nil { + return nil, fmt.Errorf("creating SAM connection: %w", err) + } + return &SAMResolver{sam: sam}, nil } -// Performs a lookup, probably this order: 1) routers known addresses, cached -// addresses, 3) by asking peers in the I2P network. -func (sam *SAMResolver) Resolve(name string) (i2pkeys.I2PAddr, error) { - log.WithField("name", name).Debug("Resolving name") - query := []byte(fmt.Sprintf("NAMING LOOKUP NAME=%s\n", name)) - if _, err := sam.conn.Write(query); err != nil { - log.WithError(err).Error("Failed to write to SAM connection") - sam.Close() - return i2pkeys.I2PAddr(""), err - } - buf := make([]byte, 4096) - n, err := sam.conn.Read(buf) - if err != nil { - log.WithError(err).Error("Failed to read from SAM connection") - sam.Close() - return i2pkeys.I2PAddr(""), err - } - if n <= 13 || !strings.HasPrefix(string(buf[:n]), "NAMING REPLY ") { - log.Error("Failed to parse SAM response") - return i2pkeys.I2PAddr(""), errors.New("Failed to parse.") - } - s := bufio.NewScanner(bytes.NewReader(buf[13:n])) - s.Split(bufio.ScanWords) - - for s.Scan() { - text := s.Text() - log.WithField("text", text).Debug("Parsing SAM response token") - // log.Println("SAM3", text) - if text == "RESULT=OK" { - continue - } else if text == "RESULT=INVALID_KEY" { - log.Error("Invalid key in resolver") - return i2pkeys.I2PAddr(""), fmt.Errorf("Invalid key - resolver") - } else if text == "RESULT=KEY_NOT_FOUND" { - log.WithField("name", name).Error("Unable to resolve name") - return i2pkeys.I2PAddr(""), fmt.Errorf("Unable to resolve %s", name) - } else if text == "NAME="+name { - continue - } else if strings.HasPrefix(text, "VALUE=") { - addr := i2pkeys.I2PAddr(text[6:]) - log.WithField("addr", addr).Debug("Name resolved successfully") - return i2pkeys.I2PAddr(text[6:]), nil - } else if strings.HasPrefix(text, "MESSAGE=") { - log.WithField("message", text[8:]).Warn("Received message from SAM") - return i2pkeys.I2PAddr(""), fmt.Errorf("Received message from SAM: %s", text[8:]) - } else { - continue - } - } - return i2pkeys.I2PAddr(""), fmt.Errorf("Unable to resolve %s", name) +func (r *SAMResolver) Resolve(name string) (i2pkeys.I2PAddr, error) { + return r.ResolveWithContext(context.Background(), name) } + +// Resolve looks up an I2P address by name with context support +func (r *SAMResolver) ResolveWithContext(ctx context.Context, name string) (i2pkeys.I2PAddr, error) { + if name == "" { + return "", fmt.Errorf("name cannot be empty") + } + + // Create query + query := fmt.Sprintf("NAMING LOOKUP NAME=%s\n", name) + + // Set up timeout if context doesn't have one + if _, hasTimeout := ctx.Deadline(); !hasTimeout { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, defaultTimeout) + defer cancel() + } + + // Write query with context awareness + if err := r.writeWithContext(ctx, query); err != nil { + return "", fmt.Errorf("writing query: %w", err) + } + + // Read and parse response + return r.readResponse(ctx, name) +} + +func (r *SAMResolver) writeWithContext(ctx context.Context, query string) error { + done := make(chan error, 1) + + go func() { + _, err := r.sam.conn.Write([]byte(query)) + done <- err + }() + + select { + case err := <-done: + return err + case <-ctx.Done(): + return ctx.Err() + } +} + +func (r *SAMResolver) readResponse(ctx context.Context, name string) (i2pkeys.I2PAddr, error) { + reader := bufio.NewReader(r.sam.conn) + + // Read first line + line, err := reader.ReadString('\n') + if err != nil { + return "", fmt.Errorf("reading response: %w", err) + } + + if !strings.HasPrefix(line, samReplyPrefix) { + return "", fmt.Errorf("invalid response format") + } + + // Parse response + fields := strings.Fields(strings.TrimPrefix(line, samReplyPrefix)) + for _, field := range fields { + switch { + case field == "RESULT=OK": + continue + case field == "RESULT=INVALID_KEY": + return "", fmt.Errorf("invalid key") + case field == "RESULT=KEY_NOT_FOUND": + return "", fmt.Errorf("name not found: %s", name) + case field == "NAME="+name: + continue + case strings.HasPrefix(field, "VALUE="): + return i2pkeys.I2PAddr(strings.TrimPrefix(field, "VALUE=")), nil + case strings.HasPrefix(field, "MESSAGE="): + return "", fmt.Errorf("SAM error: %s", strings.TrimPrefix(field, "MESSAGE=")) + } + } + + return "", fmt.Errorf("unable to resolve %s", name) +} \ No newline at end of file