mirror of
https://github.com/dstotijn/hetty.git
synced 2025-07-01 18:47:29 -04:00
399 lines
12 KiB
Go
399 lines
12 KiB
Go
package proj
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"regexp"
|
|
"sync"
|
|
|
|
connect "connectrpc.com/connect"
|
|
"github.com/oklog/ulid/v2"
|
|
|
|
"github.com/dstotijn/hetty/pkg/filter"
|
|
"github.com/dstotijn/hetty/pkg/proxy/intercept"
|
|
"github.com/dstotijn/hetty/pkg/reqlog"
|
|
"github.com/dstotijn/hetty/pkg/scope"
|
|
"github.com/dstotijn/hetty/pkg/sender"
|
|
)
|
|
|
|
type Service struct {
|
|
repo Repository
|
|
interceptSvc *intercept.Service
|
|
reqLogSvc *reqlog.Service
|
|
senderSvc *sender.Service
|
|
scope *scope.Scope
|
|
activeProjectID string
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
type Settings struct {
|
|
// Request log settings
|
|
ReqLogBypassOutOfScope bool
|
|
ReqLogOnlyFindInScope bool
|
|
ReqLogSearchExpr filter.Expression
|
|
|
|
// Intercept settings
|
|
InterceptRequests bool
|
|
InterceptResponses bool
|
|
InterceptRequestFilter filter.Expression
|
|
InterceptResponseFilter filter.Expression
|
|
|
|
// Sender settings
|
|
SenderOnlyFindInScope bool
|
|
SenderSearchExpr filter.Expression
|
|
|
|
// Scope settings
|
|
ScopeRules []scope.Rule
|
|
}
|
|
|
|
var (
|
|
ErrProjectNotFound = errors.New("proj: project not found")
|
|
ErrNoProject = errors.New("proj: no open project")
|
|
ErrNoSettings = errors.New("proj: settings not found")
|
|
ErrInvalidName = errors.New("proj: invalid name, must be alphanumeric or whitespace chars")
|
|
)
|
|
|
|
var nameRegexp = regexp.MustCompile(`^[\w\d\s]+$`)
|
|
|
|
type Config struct {
|
|
Repository Repository
|
|
InterceptService *intercept.Service
|
|
ReqLogService *reqlog.Service
|
|
SenderService *sender.Service
|
|
Scope *scope.Scope
|
|
}
|
|
|
|
// NewService returns a new Service.
|
|
func NewService(cfg Config) (*Service, error) {
|
|
return &Service{
|
|
repo: cfg.Repository,
|
|
interceptSvc: cfg.InterceptService,
|
|
reqLogSvc: cfg.ReqLogService,
|
|
senderSvc: cfg.SenderService,
|
|
scope: cfg.Scope,
|
|
}, nil
|
|
}
|
|
|
|
func (svc *Service) CreateProject(ctx context.Context, req *connect.Request[CreateProjectRequest]) (*connect.Response[CreateProjectResponse], error) {
|
|
if !nameRegexp.MatchString(req.Msg.Name) {
|
|
return nil, ErrInvalidName
|
|
}
|
|
|
|
project := &Project{
|
|
Id: ulid.Make().String(),
|
|
Name: req.Msg.Name,
|
|
}
|
|
|
|
err := svc.repo.UpsertProject(ctx, project)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("proj: could not create project: %w", err)
|
|
}
|
|
|
|
return &connect.Response[CreateProjectResponse]{
|
|
Msg: &CreateProjectResponse{
|
|
Project: project,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// CloseProject closes the currently open project (if there is one).
|
|
func (svc *Service) CloseProject(ctx context.Context, _ *connect.Request[CloseProjectRequest]) (*connect.Response[CloseProjectResponse], error) {
|
|
svc.mu.Lock()
|
|
defer svc.mu.Unlock()
|
|
|
|
if svc.activeProjectID == "" {
|
|
return nil, connect.NewError(connect.CodeFailedPrecondition, ErrNoProject)
|
|
}
|
|
|
|
svc.activeProjectID = ""
|
|
svc.reqLogSvc.SetActiveProjectID("")
|
|
svc.reqLogSvc.SetBypassOutOfScopeRequests(false)
|
|
svc.reqLogSvc.SetRequestLogsFilter(nil)
|
|
svc.interceptSvc.UpdateSettings(intercept.Settings{
|
|
RequestsEnabled: false,
|
|
ResponsesEnabled: false,
|
|
RequestFilter: nil,
|
|
ResponseFilter: nil,
|
|
})
|
|
svc.senderSvc.SetActiveProjectID("")
|
|
svc.scope.SetRules(nil)
|
|
|
|
return &connect.Response[CloseProjectResponse]{}, nil
|
|
}
|
|
|
|
// DeleteProject removes a project from the repository.
|
|
func (svc *Service) DeleteProject(ctx context.Context, req *connect.Request[DeleteProjectRequest]) (*connect.Response[DeleteProjectResponse], error) {
|
|
if svc.activeProjectID == "" {
|
|
return nil, connect.NewError(connect.CodeFailedPrecondition, ErrNoProject)
|
|
}
|
|
|
|
if err := svc.repo.DeleteProject(ctx, req.Msg.ProjectId); err != nil {
|
|
return nil, fmt.Errorf("proj: could not delete project: %w", err)
|
|
}
|
|
|
|
return &connect.Response[DeleteProjectResponse]{}, nil
|
|
}
|
|
|
|
// OpenProject sets a project as the currently active project.
|
|
func (svc *Service) OpenProject(ctx context.Context, req *connect.Request[OpenProjectRequest]) (*connect.Response[OpenProjectResponse], error) {
|
|
svc.mu.Lock()
|
|
defer svc.mu.Unlock()
|
|
|
|
p, err := svc.repo.FindProjectByID(ctx, req.Msg.ProjectId)
|
|
if errors.Is(err, ErrProjectNotFound) {
|
|
return nil, connect.NewError(connect.CodeNotFound, ErrProjectNotFound)
|
|
}
|
|
if err != nil {
|
|
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("proj: failed to find project: %w", err))
|
|
}
|
|
|
|
svc.activeProjectID = p.Id
|
|
|
|
interceptSettings := intercept.Settings{
|
|
RequestsEnabled: p.InterceptRequests,
|
|
ResponsesEnabled: p.InterceptResponses,
|
|
}
|
|
|
|
if p.InterceptRequestFilterExpr != "" {
|
|
expr, err := filter.ParseQuery(p.InterceptRequestFilterExpr)
|
|
if err != nil {
|
|
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("proj: failed to parse intercept request filter: %w", err))
|
|
}
|
|
interceptSettings.RequestFilter = expr
|
|
}
|
|
|
|
if p.InterceptResponseFilterExpr != "" {
|
|
expr, err := filter.ParseQuery(p.InterceptResponseFilterExpr)
|
|
if err != nil {
|
|
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("proj: failed to parse intercept response filter: %w", err))
|
|
}
|
|
interceptSettings.ResponseFilter = expr
|
|
}
|
|
|
|
// Request log settings.
|
|
svc.reqLogSvc.SetActiveProjectID(p.Id)
|
|
svc.reqLogSvc.SetBypassOutOfScopeRequests(p.ReqLogBypassOutOfScope)
|
|
svc.reqLogSvc.SetRequestLogsFilter(p.ReqLogFilter)
|
|
|
|
// Intercept settings.
|
|
svc.interceptSvc.UpdateSettings(interceptSettings)
|
|
|
|
// Sender settings.
|
|
svc.senderSvc.SetActiveProjectID(p.Id)
|
|
svc.senderSvc.SetRequestsFilter(&sender.RequestsFilter{
|
|
OnlyInScope: p.SenderOnlyFindInScope,
|
|
SearchExpr: p.SenderSearchExpr,
|
|
})
|
|
|
|
// Scope settings.
|
|
scopeRules, err := p.ParseScopeRules()
|
|
if err != nil {
|
|
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("proj: failed to parse scope rules: %w", err))
|
|
}
|
|
svc.scope.SetRules(scopeRules)
|
|
|
|
p.IsActive = true
|
|
|
|
return &connect.Response[OpenProjectResponse]{
|
|
Msg: &OpenProjectResponse{
|
|
Project: p,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (svc *Service) GetActiveProject(ctx context.Context, _ *connect.Request[GetActiveProjectRequest]) (*connect.Response[GetActiveProjectResponse], error) {
|
|
project, err := svc.activeProject(ctx)
|
|
if errors.Is(err, ErrNoProject) {
|
|
return nil, connect.NewError(connect.CodeNotFound, err)
|
|
}
|
|
if err != nil {
|
|
return nil, connect.NewError(connect.CodeInternal, err)
|
|
}
|
|
|
|
project.IsActive = true
|
|
|
|
return &connect.Response[GetActiveProjectResponse]{
|
|
Msg: &GetActiveProjectResponse{
|
|
Project: project,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (svc *Service) activeProject(ctx context.Context) (*Project, error) {
|
|
if svc.activeProjectID == "" {
|
|
return nil, ErrNoProject
|
|
}
|
|
|
|
project, err := svc.repo.FindProjectByID(ctx, svc.activeProjectID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("proj: failed to get active project: %w", err)
|
|
}
|
|
|
|
return project, nil
|
|
}
|
|
|
|
func (svc *Service) ListProjects(ctx context.Context, _ *connect.Request[ListProjectsRequest]) (*connect.Response[ListProjectsResponse], error) {
|
|
projects, err := svc.repo.Projects(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("proj: could not get projects: %w", err)
|
|
}
|
|
|
|
for _, project := range projects {
|
|
if svc.IsProjectActive(project.Id) {
|
|
project.IsActive = true
|
|
}
|
|
}
|
|
|
|
return &connect.Response[ListProjectsResponse]{
|
|
Msg: &ListProjectsResponse{
|
|
Projects: projects,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (svc *Service) Scope() *scope.Scope {
|
|
return svc.scope
|
|
}
|
|
|
|
func (svc *Service) SetScopeRules(ctx context.Context, req *connect.Request[SetScopeRulesRequest]) (*connect.Response[SetScopeRulesResponse], error) {
|
|
p, err := svc.activeProject(ctx)
|
|
if errors.Is(err, ErrNoProject) {
|
|
return nil, connect.NewError(connect.CodeFailedPrecondition, err)
|
|
}
|
|
if err != nil {
|
|
return nil, connect.NewError(connect.CodeInternal, err)
|
|
}
|
|
|
|
p.ScopeRules = req.Msg.Rules
|
|
|
|
err = svc.repo.UpsertProject(ctx, p)
|
|
if err != nil {
|
|
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("proj: failed to update project: %w", err))
|
|
}
|
|
|
|
scopeRules, err := p.ParseScopeRules()
|
|
if err != nil {
|
|
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("proj: failed to parse scope rules: %w", err))
|
|
}
|
|
|
|
svc.scope.SetRules(scopeRules)
|
|
|
|
return &connect.Response[SetScopeRulesResponse]{}, nil
|
|
}
|
|
|
|
func (p *Project) ParseScopeRules() ([]scope.Rule, error) {
|
|
var err error
|
|
scopeRules := make([]scope.Rule, len(p.ScopeRules))
|
|
|
|
for i, rule := range p.ScopeRules {
|
|
scopeRules[i] = scope.Rule{}
|
|
if rule.UrlRegexp != "" {
|
|
scopeRules[i].URL, err = regexp.Compile(rule.UrlRegexp)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse scope rule's URL field: %w", err)
|
|
}
|
|
}
|
|
if rule.HeaderKeyRegexp != "" {
|
|
scopeRules[i].Header.Key, err = regexp.Compile(rule.HeaderKeyRegexp)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse scope rule's header key field: %w", err)
|
|
}
|
|
}
|
|
if rule.HeaderValueRegexp != "" {
|
|
scopeRules[i].Header.Value, err = regexp.Compile(rule.HeaderValueRegexp)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse scope rule's header value field: %w", err)
|
|
}
|
|
}
|
|
if rule.BodyRegexp != "" {
|
|
scopeRules[i].Body, err = regexp.Compile(rule.BodyRegexp)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse scope rule's body field: %w", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return scopeRules, nil
|
|
}
|
|
|
|
func (svc *Service) SetRequestLogsFilter(ctx context.Context, req *connect.Request[SetRequestLogsFilterRequest]) (*connect.Response[SetRequestLogsFilterResponse], error) {
|
|
project, err := svc.activeProject(ctx)
|
|
if errors.Is(err, ErrNoProject) {
|
|
return nil, connect.NewError(connect.CodeFailedPrecondition, err)
|
|
}
|
|
if err != nil {
|
|
return nil, connect.NewError(connect.CodeInternal, err)
|
|
}
|
|
|
|
project.ReqLogFilter = req.Msg.Filter
|
|
|
|
err = svc.repo.UpsertProject(ctx, project)
|
|
if err != nil {
|
|
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("proj: failed to update project: %w", err))
|
|
}
|
|
|
|
svc.reqLogSvc.SetRequestLogsFilter(req.Msg.Filter)
|
|
|
|
return &connect.Response[SetRequestLogsFilterResponse]{}, nil
|
|
}
|
|
|
|
func (svc *Service) SetSenderRequestFindFilter(ctx context.Context, filter *sender.RequestsFilter) error {
|
|
project, err := svc.activeProject(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
project.SenderOnlyFindInScope = filter.OnlyInScope
|
|
project.SenderSearchExpr = filter.SearchExpr
|
|
|
|
err = svc.repo.UpsertProject(ctx, project)
|
|
if err != nil {
|
|
return fmt.Errorf("proj: failed to update project: %w", err)
|
|
}
|
|
|
|
svc.senderSvc.SetRequestsFilter(filter)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (svc *Service) IsProjectActive(projectID string) bool {
|
|
return projectID == svc.activeProjectID
|
|
}
|
|
|
|
func (svc *Service) UpdateInterceptSettings(ctx context.Context, req *connect.Request[UpdateInterceptSettingsRequest]) (*connect.Response[UpdateInterceptSettingsResponse], error) {
|
|
project, err := svc.activeProject(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
project.InterceptRequests = req.Msg.RequestsEnabled
|
|
project.InterceptResponses = req.Msg.ResponsesEnabled
|
|
project.InterceptRequestFilterExpr = req.Msg.RequestFilterExpr
|
|
project.InterceptResponseFilterExpr = req.Msg.ResponseFilterExpr
|
|
|
|
err = svc.repo.UpsertProject(ctx, project)
|
|
if err != nil {
|
|
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("proj: failed to update project: %w", err))
|
|
}
|
|
|
|
reqFilterExpr, err := filter.ParseQuery(req.Msg.RequestFilterExpr)
|
|
if err != nil {
|
|
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("proj: failed to parse intercept request filter: %w", err))
|
|
}
|
|
|
|
respFilterExpr, err := filter.ParseQuery(req.Msg.ResponseFilterExpr)
|
|
if err != nil {
|
|
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("proj: failed to parse intercept response filter: %w", err))
|
|
}
|
|
|
|
svc.interceptSvc.UpdateSettings(intercept.Settings{
|
|
RequestsEnabled: req.Msg.RequestsEnabled,
|
|
ResponsesEnabled: req.Msg.ResponsesEnabled,
|
|
RequestFilter: reqFilterExpr,
|
|
ResponseFilter: respFilterExpr,
|
|
})
|
|
|
|
return &connect.Response[UpdateInterceptSettingsResponse]{}, nil
|
|
}
|