182 lines
5.3 KiB
Go
182 lines
5.3 KiB
Go
package extraction
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
domain "proxy-pool/internal/domain/extraction"
|
|
)
|
|
|
|
var (
|
|
ErrInvalidRequest = errors.New("invalid extraction request")
|
|
ErrCountExceeded = errors.New("extraction count exceeds policy")
|
|
ErrInvalidFulfillment = errors.New("invalid extraction fulfillment")
|
|
ErrInvalidServicePolicy = errors.New("invalid extraction service policy")
|
|
ErrAdmissionRejected = errors.New("extraction admission rejected")
|
|
ErrUnavailable = errors.New("extraction service unavailable")
|
|
)
|
|
|
|
type Policy struct {
|
|
MaxCountPerRequest int
|
|
DefaultFulfillment domain.Fulfillment
|
|
MinRemainingTTL time.Duration
|
|
MaxHealthCheckAge time.Duration
|
|
ReserveForGateway int
|
|
IdempotencyTTL time.Duration
|
|
}
|
|
|
|
type Filters struct {
|
|
Protocols []string
|
|
Regions []string
|
|
Carriers []string
|
|
Upstreams []string
|
|
}
|
|
|
|
type Request struct {
|
|
RequestID string
|
|
ClientID string
|
|
SourceIP string
|
|
IdempotencyKey string
|
|
Count int
|
|
Fulfillment domain.Fulfillment
|
|
Filters Filters
|
|
}
|
|
|
|
type ExtractedProxy struct {
|
|
ID string
|
|
Protocol string
|
|
Host string
|
|
Port uint16
|
|
Username string
|
|
Password string
|
|
URL string
|
|
Region string
|
|
Carrier string
|
|
Upstream string
|
|
ExpiresAt time.Time
|
|
RemainingTTLSeconds int64
|
|
ExtractedAt time.Time
|
|
}
|
|
|
|
type Response struct {
|
|
RequestID string
|
|
Requested int
|
|
Returned int
|
|
Proxies []ExtractedProxy
|
|
}
|
|
|
|
type Service struct {
|
|
store domain.Store
|
|
policy Policy
|
|
admission Admission
|
|
now func() time.Time
|
|
}
|
|
|
|
type Admission interface {
|
|
Admit(context.Context, string) error
|
|
}
|
|
|
|
func NewService(store domain.Store, policy Policy, admission Admission, now func() time.Time) (*Service, error) {
|
|
if admission == nil {
|
|
return nil, fmt.Errorf("%w: admission is required", ErrInvalidServicePolicy)
|
|
}
|
|
if store == nil {
|
|
return nil, fmt.Errorf("%w: store is required", ErrInvalidServicePolicy)
|
|
}
|
|
if policy.MaxCountPerRequest <= 0 || policy.MinRemainingTTL < 0 ||
|
|
policy.MaxHealthCheckAge < 0 || policy.ReserveForGateway < 0 || policy.IdempotencyTTL < 0 {
|
|
return nil, ErrInvalidServicePolicy
|
|
}
|
|
if policy.DefaultFulfillment != domain.Partial && policy.DefaultFulfillment != domain.AllOrNothing {
|
|
return nil, ErrInvalidServicePolicy
|
|
}
|
|
if now == nil {
|
|
now = time.Now
|
|
}
|
|
return &Service{store: store, policy: policy, admission: admission, now: now}, nil
|
|
}
|
|
|
|
func (s *Service) Extract(ctx context.Context, request Request) (Response, error) {
|
|
response := Response{RequestID: request.RequestID, Requested: request.Count}
|
|
if request.RequestID == "" || (request.ClientID == "" && request.SourceIP == "") || request.Count <= 0 {
|
|
return response, ErrInvalidRequest
|
|
}
|
|
if request.Count > s.policy.MaxCountPerRequest {
|
|
return response, ErrCountExceeded
|
|
}
|
|
fulfillment := request.Fulfillment
|
|
if fulfillment == "" {
|
|
fulfillment = s.policy.DefaultFulfillment
|
|
}
|
|
if fulfillment != domain.Partial && fulfillment != domain.AllOrNothing {
|
|
return response, ErrInvalidFulfillment
|
|
}
|
|
if err := s.admission.Admit(ctx, admissionKey(request)); err != nil {
|
|
return response, errors.Join(ErrAdmissionRejected, err)
|
|
}
|
|
|
|
now := s.now().UTC()
|
|
clientID := request.ClientID
|
|
if clientID == "" {
|
|
clientID = request.SourceIP
|
|
}
|
|
result, err := s.store.Extract(ctx, domain.Command{
|
|
RequestID: request.RequestID,
|
|
ClientID: clientID,
|
|
SourceIP: request.SourceIP,
|
|
IdempotencyKey: request.IdempotencyKey,
|
|
IdempotencyTTL: s.policy.IdempotencyTTL,
|
|
Requested: request.Count,
|
|
Fulfillment: fulfillment,
|
|
Now: now,
|
|
MinRemainingTTL: s.policy.MinRemainingTTL,
|
|
MaxHealthCheckAge: s.policy.MaxHealthCheckAge,
|
|
ReserveForGateway: s.policy.ReserveForGateway,
|
|
Protocols: append([]string(nil), request.Filters.Protocols...),
|
|
Regions: append([]string(nil), request.Filters.Regions...),
|
|
Carriers: append([]string(nil), request.Filters.Carriers...),
|
|
Upstreams: append([]string(nil), request.Filters.Upstreams...),
|
|
})
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
|
|
response.Proxies = make([]ExtractedProxy, 0, len(result.Items))
|
|
extractedAt := result.ExtractedAt
|
|
if extractedAt.IsZero() {
|
|
extractedAt = now
|
|
}
|
|
for _, candidate := range result.Items {
|
|
remaining := int64(0)
|
|
if !candidate.ExpiresAt.IsZero() && candidate.ExpiresAt.After(extractedAt) {
|
|
remaining = int64(candidate.ExpiresAt.Sub(extractedAt) / time.Second)
|
|
}
|
|
response.Proxies = append(response.Proxies, ExtractedProxy{
|
|
ID: candidate.ID,
|
|
Protocol: candidate.Protocol,
|
|
Host: candidate.Host,
|
|
Port: candidate.Port,
|
|
Username: candidate.Username,
|
|
Password: candidate.Password,
|
|
URL: candidate.URL,
|
|
Region: candidate.Region,
|
|
Carrier: candidate.Carrier,
|
|
Upstream: candidate.Upstream,
|
|
ExpiresAt: candidate.ExpiresAt,
|
|
RemainingTTLSeconds: remaining,
|
|
ExtractedAt: extractedAt,
|
|
})
|
|
}
|
|
response.Returned = len(response.Proxies)
|
|
return response, nil
|
|
}
|
|
|
|
func admissionKey(request Request) string {
|
|
if request.ClientID != "" {
|
|
return "client:" + request.ClientID
|
|
}
|
|
return "source:" + request.SourceIP
|
|
}
|