Add project management

This commit is contained in:
David Stotijn
2020-10-11 17:09:39 +02:00
parent ca707d17ea
commit fedb425381
22 changed files with 2080 additions and 322 deletions

File diff suppressed because it is too large Load Diff

View File

@ -9,6 +9,14 @@ import (
"time"
)
type CloseProjectResult struct {
Success bool `json:"success"`
}
type DeleteProjectResult struct {
Success bool `json:"success"`
}
type HTTPHeader struct {
Key string `json:"key"`
Value string `json:"value"`
@ -34,6 +42,11 @@ type HTTPResponseLog struct {
Headers []HTTPHeader `json:"headers"`
}
type Project struct {
Name string `json:"name"`
IsActive bool `json:"isActive"`
}
type HTTPMethod string
const (

View File

@ -7,20 +7,35 @@ import (
"fmt"
"strings"
"github.com/99designs/gqlgen/graphql"
"github.com/dstotijn/hetty/pkg/proj"
"github.com/dstotijn/hetty/pkg/reqlog"
"github.com/vektah/gqlparser/v2/gqlerror"
)
type Resolver struct {
RequestLogService *reqlog.Service
ProjectService *proj.Service
}
type queryResolver struct{ *Resolver }
type mutationResolver struct{ *Resolver }
func (r *Resolver) Query() QueryResolver { return &queryResolver{r} }
func (r *Resolver) Query() QueryResolver { return &queryResolver{r} }
func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} }
func (r *queryResolver) HTTPRequestLogs(ctx context.Context) ([]HTTPRequestLog, error) {
opts := reqlog.FindRequestsOptions{OmitOutOfScope: false}
reqs, err := r.RequestLogService.FindRequests(ctx, opts)
if err == reqlog.ErrNoProject {
return nil, &gqlerror.Error{
Path: graphql.GetPath(ctx),
Message: "No active project.",
Extensions: map[string]interface{}{
"code": "no_active_project",
},
}
}
if err != nil {
return nil, fmt.Errorf("could not query repository for requests: %v", err)
}
@ -116,3 +131,65 @@ func parseRequestLog(req reqlog.Request) (HTTPRequestLog, error) {
return log, nil
}
func (r *mutationResolver) OpenProject(ctx context.Context, name string) (*Project, error) {
p, err := r.ProjectService.Open(name)
if err == proj.ErrInvalidName {
return nil, gqlerror.Errorf("Project name must only contain alphanumeric or space chars.")
}
if err != nil {
return nil, fmt.Errorf("could not open project: %v", err)
}
return &Project{
Name: p.Name,
IsActive: p.IsActive,
}, nil
}
func (r *queryResolver) ActiveProject(ctx context.Context) (*Project, error) {
p, err := r.ProjectService.ActiveProject()
if err == proj.ErrNoProject {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("could not open project: %v", err)
}
return &Project{
Name: p.Name,
IsActive: p.IsActive,
}, nil
}
func (r *queryResolver) Projects(ctx context.Context) ([]Project, error) {
p, err := r.ProjectService.Projects()
if err != nil {
return nil, fmt.Errorf("could not get projects: %v", err)
}
projects := make([]Project, len(p))
for i, proj := range p {
projects[i] = Project{
Name: proj.Name,
IsActive: proj.IsActive,
}
}
return projects, nil
}
func (r *mutationResolver) CloseProject(ctx context.Context) (*CloseProjectResult, error) {
if err := r.ProjectService.Close(); err != nil {
return nil, fmt.Errorf("could not close project: %v", err)
}
return &CloseProjectResult{true}, nil
}
func (r *mutationResolver) DeleteProject(ctx context.Context, name string) (*DeleteProjectResult, error) {
if err := r.ProjectService.Delete(name); err != nil {
return nil, fmt.Errorf("could not delete project: %v", err)
}
return &DeleteProjectResult{
Success: true,
}, nil
}

View File

@ -23,9 +23,30 @@ type HttpHeader {
value: String!
}
type Project {
name: String!
isActive: Boolean!
}
type CloseProjectResult {
success: Boolean!
}
type DeleteProjectResult {
success: Boolean!
}
type Query {
httpRequestLog(id: ID!): HttpRequestLog
httpRequestLogs: [HttpRequestLog!]!
activeProject: Project
projects: [Project!]!
}
type Mutation {
openProject(name: String!): Project
closeProject: CloseProjectResult!
deleteProject(name: String!): DeleteProjectResult!
}
enum HttpMethod {

View File

@ -3,11 +3,10 @@ package sqlite
import (
"context"
"database/sql"
"errors"
"fmt"
"net/http"
"net/url"
"os"
"path/filepath"
"time"
"github.com/dstotijn/hetty/pkg/reqlog"
@ -33,13 +32,10 @@ type httpRequestLogsQuery struct {
joinResponse bool
}
// New returns a new Client.
func New(filename string) (*Client, error) {
// Create directory for DB if it doesn't exist yet.
if dbDir, _ := filepath.Split(filename); dbDir != "" {
if _, err := os.Stat(dbDir); os.IsNotExist(err) {
os.Mkdir(dbDir, 0755)
}
// Open opens a database.
func (c *Client) Open(filename string) error {
if c.db != nil {
return errors.New("sqlite: database already open")
}
opts := make(url.Values)
@ -48,24 +44,24 @@ func New(filename string) (*Client, error) {
dsn := fmt.Sprintf("file:%v?%v", filename, opts.Encode())
db, err := sqlx.Open("sqlite3", dsn)
if err != nil {
return nil, err
return fmt.Errorf("sqlite: could not open database: %v", err)
}
if err := db.Ping(); err != nil {
return nil, fmt.Errorf("sqlite: could not ping database: %v", err)
return fmt.Errorf("sqlite: could not ping database: %v", err)
}
c := &Client{db: db}
if err := c.prepareSchema(); err != nil {
return nil, fmt.Errorf("sqlite: could not prepare schema: %v", err)
if err := prepareSchema(db); err != nil {
return fmt.Errorf("sqlite: could not prepare schema: %v", err)
}
return &Client{db: db}, nil
c.db = db
return nil
}
func (c Client) prepareSchema() error {
_, err := c.db.Exec(`CREATE TABLE IF NOT EXISTS http_requests (
func prepareSchema(db *sqlx.DB) error {
_, err := db.Exec(`CREATE TABLE IF NOT EXISTS http_requests (
id INTEGER PRIMARY KEY,
proto TEXT,
url TEXT,
@ -77,7 +73,7 @@ func (c Client) prepareSchema() error {
return fmt.Errorf("could not create http_requests table: %v", err)
}
_, err = c.db.Exec(`CREATE TABLE IF NOT EXISTS http_responses (
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS http_responses (
id INTEGER PRIMARY KEY,
req_id INTEGER REFERENCES http_requests(id) ON DELETE CASCADE,
proto TEXT,
@ -90,7 +86,7 @@ func (c Client) prepareSchema() error {
return fmt.Errorf("could not create http_responses table: %v", err)
}
_, err = c.db.Exec(`CREATE TABLE IF NOT EXISTS http_headers (
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS http_headers (
id INTEGER PRIMARY KEY,
req_id INTEGER REFERENCES http_requests(id) ON DELETE CASCADE,
res_id INTEGER REFERENCES http_responses(id) ON DELETE CASCADE,
@ -104,9 +100,16 @@ func (c Client) prepareSchema() error {
return nil
}
// Close uses the underlying database.
// Close uses the underlying database if it's open.
func (c *Client) Close() error {
return c.db.Close()
if c.db == nil {
return nil
}
if err := c.db.Close(); err != nil {
return fmt.Errorf("sqlite: could not close database: %v", err)
}
c.db = nil
return nil
}
var reqFieldToColumnMap = map[string]string{
@ -136,6 +139,10 @@ func (c *Client) FindRequestLogs(
opts reqlog.FindRequestsOptions,
scope *scope.Scope,
) (reqLogs []reqlog.Request, err error) {
if c.db == nil {
return nil, reqlog.ErrNoProject
}
httpReqLogsQuery := parseHTTPRequestLogsQuery(ctx)
reqQuery := sq.
@ -178,6 +185,9 @@ func (c *Client) FindRequestLogs(
}
func (c *Client) FindRequestLogByID(ctx context.Context, id int64) (reqlog.Request, error) {
if c.db == nil {
return reqlog.Request{}, reqlog.ErrNoProject
}
httpReqLogsQuery := parseHTTPRequestLogsQuery(ctx)
reqQuery := sq.
@ -218,6 +228,9 @@ func (c *Client) AddRequestLog(
body []byte,
timestamp time.Time,
) (*reqlog.Request, error) {
if c.db == nil {
return nil, reqlog.ErrNoProject
}
reqLog := &reqlog.Request{
Request: req,
@ -289,6 +302,10 @@ func (c *Client) AddResponseLog(
body []byte,
timestamp time.Time,
) (*reqlog.Response, error) {
if c.db == nil {
return nil, reqlog.ErrNoProject
}
resLog := &reqlog.Response{
RequestID: reqID,
Response: res,
@ -495,3 +512,7 @@ func (c *Client) queryHeaders(
return nil
}
func (c *Client) IsOpen() bool {
return c.db != nil
}

135
pkg/proj/proj.go Normal file
View File

@ -0,0 +1,135 @@
package proj
import (
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/dstotijn/hetty/pkg/db/sqlite"
"github.com/dstotijn/hetty/pkg/scope"
)
// Service is used for managing projects.
type Service struct {
dbPath string
db *sqlite.Client
name string
Scope *scope.Scope
}
type Project struct {
Name string
IsActive bool
}
var (
ErrNoProject = errors.New("proj: no open project")
ErrInvalidName = errors.New("proj: invalid name, must be alphanumeric or whitespace chars")
)
var nameRegexp = regexp.MustCompile(`^[\w\d\s]+$`)
// NewService returns a new Service.
func NewService(dbPath string) (*Service, error) {
// Create directory for DBs if it doesn't exist yet.
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
if err := os.MkdirAll(dbPath, 0755); err != nil {
return nil, fmt.Errorf("proj: could not create project directory: %v", err)
}
}
return &Service{
dbPath: dbPath,
db: &sqlite.Client{},
Scope: scope.New(nil),
}, nil
}
// Close closes the currently open project database (if there is one).
func (svc *Service) Close() error {
if err := svc.db.Close(); err != nil {
return fmt.Errorf("proj: could not close project: %v", err)
}
svc.name = ""
return nil
}
// Delete removes a project database file from disk (if there is one).
func (svc *Service) Delete(name string) error {
if name == "" {
return errors.New("proj: name cannot be empty")
}
if svc.name == name {
return fmt.Errorf("proj: project (%v) is active", name)
}
if err := os.Remove(filepath.Join(svc.dbPath, name+".db")); err != nil {
return fmt.Errorf("proj: could not remove database file: %v", err)
}
return nil
}
// Database returns the currently open database. If no database is open, it will
// return `nil`.
func (svc *Service) Database() *sqlite.Client {
return svc.db
}
// Open opens a database identified with `name`. If a database with this
// identifier doesn't exist yet, it will be automatically created.
func (svc *Service) Open(name string) (Project, error) {
if !nameRegexp.MatchString(name) {
return Project{}, ErrInvalidName
}
if err := svc.db.Close(); err != nil {
return Project{}, fmt.Errorf("proj: could not close previously open database: %v", err)
}
dbPath := filepath.Join(svc.dbPath, name+".db")
err := svc.db.Open(dbPath)
if err != nil {
return Project{}, fmt.Errorf("proj: could not open database: %v", err)
}
svc.name = name
return Project{
Name: name,
IsActive: true,
}, nil
}
func (svc *Service) ActiveProject() (Project, error) {
if !svc.db.IsOpen() {
return Project{}, ErrNoProject
}
return Project{
Name: svc.name,
}, nil
}
func (svc *Service) Projects() ([]Project, error) {
files, err := ioutil.ReadDir(svc.dbPath)
if err != nil {
return nil, fmt.Errorf("proj: could not read projects directory: %v", err)
}
projects := make([]Project, len(files))
for i, file := range files {
projName := strings.TrimSuffix(file.Name(), ".db")
projects[i] = Project{
Name: projName,
IsActive: svc.name == projName,
}
}
return projects, nil
}

View File

@ -83,13 +83,13 @@ func LoadOrCreateCA(caKeyFile, caCertFile string) (*x509.Certificate, *rsa.Priva
keyDir, _ := filepath.Split(caKeyFile)
if keyDir != "" {
if _, err := os.Stat(keyDir); os.IsNotExist(err) {
os.Mkdir(keyDir, 0755)
os.MkdirAll(keyDir, 0755)
}
}
keyDir, _ = filepath.Split(caCertFile)
if keyDir != "" {
if _, err := os.Stat("keyDir"); os.IsNotExist(err) {
os.Mkdir(keyDir, 0755)
os.MkdirAll(keyDir, 0755)
}
}

View File

@ -8,6 +8,10 @@ import (
"github.com/dstotijn/hetty/pkg/scope"
)
type RepositoryProvider interface {
Repository() Repository
}
type Repository interface {
FindRequestLogs(ctx context.Context, opts FindRequestsOptions, scope *scope.Scope) ([]Request, error)
FindRequestLogByID(ctx context.Context, id int64) (Request, error)

View File

@ -19,7 +19,10 @@ type contextKey int
const LogBypassedKey contextKey = 0
var ErrRequestNotFound = errors.New("reqlog: request not found")
var (
ErrRequestNotFound = errors.New("reqlog: request not found")
ErrNoProject = errors.New("reqlog: no project")
)
type Request struct {
ID int64
@ -133,6 +136,11 @@ func (svc *Service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestM
}
reqLog, err := svc.addRequest(req.Context(), *clone, body, now)
if err == ErrNoProject {
ctx := context.WithValue(req.Context(), LogBypassedKey, true)
*req = *req.WithContext(ctx)
return
}
if err != nil {
log.Printf("[ERROR] Could not store request log: %v", err)
return