Compare commits

..

No commits in common. "main" and "v0.0.13" have entirely different histories.

50 changed files with 483 additions and 1321 deletions

View File

@ -4,7 +4,7 @@ jobs:
test: test:
strategy: strategy:
matrix: matrix:
go-version: [1.22.x] go-version: [1.21.x]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/setup-go@v3 - uses: actions/setup-go@v3

View File

@ -4,11 +4,11 @@ import (
"crypto/tls" "crypto/tls"
"crypto/x509/pkix" "crypto/x509/pkix"
"fmt" "fmt"
"github.com/1f349/violet/logger"
"github.com/1f349/violet/utils" "github.com/1f349/violet/utils"
"github.com/mrmelon54/certgen" "github.com/MrMelon54/certgen"
"github.com/mrmelon54/rescheduler" "github.com/MrMelon54/rescheduler"
"io/fs" "io/fs"
"log"
"math/big" "math/big"
"os" "os"
"strings" "strings"
@ -17,8 +17,6 @@ import (
"time" "time"
) )
var Logger = logger.Logger.WithPrefix("Violet Certs")
// Certs is the certificate loader and management system. // Certs is the certificate loader and management system.
type Certs struct { type Certs struct {
cDir fs.FS cDir fs.FS
@ -71,7 +69,7 @@ func New(certDir fs.FS, keyDir fs.FS, selfCert bool) *Certs {
return now.AddDate(10, 0, 0) return now.AddDate(10, 0, 0)
}) })
if err != nil { if err != nil {
logger.Logger.Fatal("Failed to generate CA cert for self-signed mode", "err", err) log.Fatalln("Failed to generate CA cert for self-signed mode:", err)
} }
c.ca = ca c.ca = ca
} }
@ -147,7 +145,7 @@ func (c *Certs) threadCompile() {
// compile map and check errors // compile map and check errors
err := c.internalCompile(certMap) err := c.internalCompile(certMap)
if err != nil { if err != nil {
Logger.Infof("Compile failed: %s\n", err) log.Printf("[Certs] Compile failed: %s\n", err)
return return
} }
@ -170,7 +168,7 @@ func (c *Certs) internalCompile(m map[string]*tls.Certificate) error {
return fmt.Errorf("failed to read cert dir: %w", err) return fmt.Errorf("failed to read cert dir: %w", err)
} }
Logger.Infof("Compiling lookup table for %d certificates\n", len(files)) log.Printf("[Certs] Compiling lookup table for %d certificates\n", len(files))
// find and parse certs // find and parse certs
for _, i := range files { for _, i := range files {

View File

@ -3,7 +3,7 @@ package certs
import ( import (
"crypto/x509/pkix" "crypto/x509/pkix"
"fmt" "fmt"
"github.com/mrmelon54/certgen" "github.com/MrMelon54/certgen"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"math/big" "math/big"
"testing" "testing"

View File

@ -2,15 +2,14 @@ package main
import ( import (
"context" "context"
"database/sql"
"encoding/json" "encoding/json"
"flag" "flag"
"github.com/1f349/mjwt" "github.com/1f349/mjwt"
"github.com/1f349/violet"
"github.com/1f349/violet/certs" "github.com/1f349/violet/certs"
"github.com/1f349/violet/domains" "github.com/1f349/violet/domains"
errorPages "github.com/1f349/violet/error-pages" errorPages "github.com/1f349/violet/error-pages"
"github.com/1f349/violet/favicons" "github.com/1f349/violet/favicons"
"github.com/1f349/violet/logger"
"github.com/1f349/violet/proxy" "github.com/1f349/violet/proxy"
"github.com/1f349/violet/proxy/websocket" "github.com/1f349/violet/proxy/websocket"
"github.com/1f349/violet/router" "github.com/1f349/violet/router"
@ -18,64 +17,59 @@ import (
"github.com/1f349/violet/servers/api" "github.com/1f349/violet/servers/api"
"github.com/1f349/violet/servers/conf" "github.com/1f349/violet/servers/conf"
"github.com/1f349/violet/utils" "github.com/1f349/violet/utils"
"github.com/charmbracelet/log" "github.com/MrMelon54/exit-reload"
"github.com/cloudflare/tableflip"
"github.com/google/subcommands" "github.com/google/subcommands"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors"
"io/fs" "io/fs"
"log"
"net/http" "net/http"
_ "net/http/pprof"
"os" "os"
"os/signal"
"path/filepath" "path/filepath"
"syscall" "runtime/pprof"
"time"
) )
type serveCmd struct { type serveCmd struct {
configPath string configPath string
debugLog bool cpuprofile string
pidFile string
} }
func (s *serveCmd) Name() string { return "serve" } func (s *serveCmd) Name() string { return "serve" }
func (s *serveCmd) Synopsis() string { return "Serve reverse proxy server" } func (s *serveCmd) Synopsis() string { return "Serve reverse proxy server" }
func (s *serveCmd) SetFlags(f *flag.FlagSet) { func (s *serveCmd) SetFlags(f *flag.FlagSet) {
f.StringVar(&s.configPath, "conf", "", "/path/to/config.json : path to the config file") f.StringVar(&s.configPath, "conf", "", "/path/to/config.json : path to the config file")
f.BoolVar(&s.debugLog, "debug", false, "enable debug logging") f.StringVar(&s.cpuprofile, "cpuprofile", "", "write cpu profile to file")
f.StringVar(&s.pidFile, "pid-file", "", "path to pid file")
} }
func (s *serveCmd) Usage() string { func (s *serveCmd) Usage() string {
return `serve [-conf <config file>] [-debug] [-pid-file <pid file>] return `serve [-conf <config file>] [-cpuprofile <profile file>]
Serve reverse proxy server using information from config file Serve reverse proxy server using information from config file
` `
} }
func (s *serveCmd) Execute(_ context.Context, _ *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { func (s *serveCmd) Execute(_ context.Context, _ *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
if s.debugLog { log.Println("[Violet] Starting...")
logger.Logger.SetLevel(log.DebugLevel)
}
logger.Logger.Info("Starting...")
upg, err := tableflip.New(tableflip.Options{ // Enable cpu profiling
PIDFile: s.pidFile, if s.cpuprofile != "" {
}) f, err := os.Create(s.cpuprofile)
if err != nil { if err != nil {
panic(err) log.Fatal(err)
}
log.Printf("[Violet] CPU profiling enabled, writing to '%s'\n", s.cpuprofile)
_ = pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
} }
defer upg.Stop()
if s.configPath == "" { if s.configPath == "" {
logger.Logger.Info("Error: config flag is missing") log.Println("[Violet] Error: config flag is missing")
return subcommands.ExitUsageError return subcommands.ExitUsageError
} }
openConf, err := os.Open(s.configPath) openConf, err := os.Open(s.configPath)
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
logger.Logger.Info("Error: missing config file") log.Println("[Violet] Error: missing config file")
} else { } else {
logger.Logger.Info("Error: open config file: ", err) log.Println("[Violet] Error: open config file: ", err)
} }
return subcommands.ExitFailure return subcommands.ExitFailure
} }
@ -83,167 +77,127 @@ func (s *serveCmd) Execute(_ context.Context, _ *flag.FlagSet, _ ...interface{})
var config startUpConfig var config startUpConfig
err = json.NewDecoder(openConf).Decode(&config) err = json.NewDecoder(openConf).Decode(&config)
if err != nil { if err != nil {
logger.Logger.Info("Error: invalid config file: ", err) log.Println("[Violet] Error: invalid config file: ", err)
return subcommands.ExitFailure return subcommands.ExitFailure
} }
// working directory is the parent of the config file // working directory is the parent of the config file
wd := filepath.Dir(s.configPath) wd := filepath.Dir(s.configPath)
normalLoad(config, wd)
return subcommands.ExitSuccess
}
func normalLoad(startUp startUpConfig, wd string) {
// the cert and key paths are useless in self-signed mode // the cert and key paths are useless in self-signed mode
if !config.SelfSigned { if !startUp.SelfSigned {
// create path to cert dir // create path to cert dir
err := os.MkdirAll(filepath.Join(wd, "certs"), os.ModePerm) err := os.MkdirAll(filepath.Join(wd, "certs"), os.ModePerm)
if err != nil { if err != nil {
logger.Logger.Fatal("Failed to create certificate path") log.Fatal("[Violet] Failed to create certificate path")
} }
// create path to key dir // create path to key dir
err = os.MkdirAll(filepath.Join(wd, "keys"), os.ModePerm) err = os.MkdirAll(filepath.Join(wd, "keys"), os.ModePerm)
if err != nil { if err != nil {
logger.Logger.Fatal("Failed to create certificate key path") log.Fatal("[Violet] Failed to create certificate key path")
} }
} }
// errorPageDir stores an FS interface for accessing the error page directory // errorPageDir stores an FS interface for accessing the error page directory
var errorPageDir fs.FS var errorPageDir fs.FS
if config.ErrorPagePath != "" { if startUp.ErrorPagePath != "" {
errorPageDir = os.DirFS(config.ErrorPagePath) errorPageDir = os.DirFS(startUp.ErrorPagePath)
err := os.MkdirAll(config.ErrorPagePath, os.ModePerm) err := os.MkdirAll(startUp.ErrorPagePath, os.ModePerm)
if err != nil { if err != nil {
logger.Logger.Fatal("Failed to create error page", "path", config.ErrorPagePath) log.Fatalf("[Violet] Failed to create error page path '%s'", startUp.ErrorPagePath)
} }
} }
// load the MJWT RSA public key from a pem encoded file // load the MJWT RSA public key from a pem encoded file
mJwtVerify, err := mjwt.NewMJwtVerifierFromFile(filepath.Join(wd, "signer.public.pem")) mJwtVerify, err := mjwt.NewMJwtVerifierFromFile(filepath.Join(wd, "signer.public.pem"))
if err != nil { if err != nil {
logger.Logger.Fatal("Failed to load MJWT verifier public key", "file", filepath.Join(wd, "signer.public.pem"), "err", err) log.Fatalf("[Violet] Failed to load MJWT verifier public key from file '%s': %s", filepath.Join(wd, "signer.public.pem"), err)
} }
// open sqlite database // open sqlite database
db, err := violet.InitDB(filepath.Join(wd, "violet.db.sqlite")) db, err := sql.Open("sqlite3", filepath.Join(wd, "violet.db.sqlite"))
if err != nil { if err != nil {
logger.Logger.Fatal("Failed to open database", "err", err) log.Fatal("[Violet] Failed to open database")
} }
certDir := os.DirFS(filepath.Join(wd, "certs")) certDir := os.DirFS(filepath.Join(wd, "certs"))
keyDir := os.DirFS(filepath.Join(wd, "keys")) keyDir := os.DirFS(filepath.Join(wd, "keys"))
// setup registry for metrics
promRegistry := prometheus.NewRegistry()
promRegistry.MustRegister(
collectors.NewGoCollector(),
collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
)
ws := websocket.NewServer() ws := websocket.NewServer()
allowedDomains := domains.New(db) // load allowed domains allowedDomains := domains.New(db) // load allowed domains
acmeChallenges := utils.NewAcmeChallenge() // load acme challenge store acmeChallenges := utils.NewAcmeChallenge() // load acme challenge store
allowedCerts := certs.New(certDir, keyDir, config.SelfSigned) // load certificate manager allowedCerts := certs.New(certDir, keyDir, startUp.SelfSigned) // load certificate manager
hybridTransport := proxy.NewHybridTransport(ws) // load reverse proxy hybridTransport := proxy.NewHybridTransport(ws) // load reverse proxy
dynamicFavicons := favicons.New(db, config.InkscapeCmd) // load dynamic favicon provider dynamicFavicons := favicons.New(db, startUp.InkscapeCmd) // load dynamic favicon provider
dynamicErrorPages := errorPages.New(errorPageDir) // load dynamic error page provider dynamicErrorPages := errorPages.New(errorPageDir) // load dynamic error page provider
dynamicRouter := router.NewManager(db, hybridTransport) // load dynamic router manager dynamicRouter := router.NewManager(db, hybridTransport) // load dynamic router manager
// struct containing config for the http servers // struct containing config for the http servers
srvConf := &conf.Conf{ srvConf := &conf.Conf{
RateLimit: config.RateLimit, ApiListen: startUp.Listen.Api,
DB: db, HttpListen: startUp.Listen.Http,
Domains: allowedDomains, HttpsListen: startUp.Listen.Https,
Acme: acmeChallenges, RateLimit: startUp.RateLimit,
Certs: allowedCerts, DB: db,
Favicons: dynamicFavicons, Domains: allowedDomains,
Signer: mJwtVerify, Acme: acmeChallenges,
ErrorPages: dynamicErrorPages, Certs: allowedCerts,
Router: dynamicRouter, Favicons: dynamicFavicons,
Signer: mJwtVerify,
ErrorPages: dynamicErrorPages,
Router: dynamicRouter,
} }
// create the compilable list and run a first time compile // create the compilable list and run a first time compile
allCompilables := utils.MultiCompilable{allowedDomains, allowedCerts, dynamicFavicons, dynamicErrorPages, dynamicRouter} allCompilables := utils.MultiCompilable{allowedDomains, allowedCerts, dynamicFavicons, dynamicErrorPages, dynamicRouter}
allCompilables.Compile() allCompilables.Compile()
_, httpsPort, ok := utils.SplitDomainPort(config.Listen.Https, 443)
if !ok {
httpsPort = 443
}
var srvApi, srvHttp, srvHttps *http.Server var srvApi, srvHttp, srvHttps *http.Server
if config.Listen.Api != "" { if srvConf.ApiListen != "" {
// Listen must be called before Ready srvApi = api.NewApiServer(srvConf, allCompilables)
lnApi, err := upg.Listen("tcp", config.Listen.Api)
if err != nil {
logger.Logger.Fatal("Listen failed", "err", err)
}
srvApi = api.NewApiServer(srvConf, allCompilables, promRegistry)
srvApi.SetKeepAlivesEnabled(false) srvApi.SetKeepAlivesEnabled(false)
l := logger.Logger.With("server", "API") log.Printf("[API] Starting API server on: '%s'\n", srvApi.Addr)
l.Info("Starting server", "addr", config.Listen.Api) go utils.RunBackgroundHttp("API", srvApi)
go utils.RunBackgroundHttp(l, srvApi, lnApi)
} }
if config.Listen.Http != "" { if srvConf.HttpListen != "" {
// Listen must be called before Ready srvHttp = servers.NewHttpServer(srvConf)
lnHttp, err := upg.Listen("tcp", config.Listen.Http)
if err != nil {
logger.Logger.Fatal("Listen failed", "err", err)
}
srvHttp = servers.NewHttpServer(uint16(httpsPort), srvConf, promRegistry)
srvHttp.SetKeepAlivesEnabled(false) srvHttp.SetKeepAlivesEnabled(false)
l := logger.Logger.With("server", "HTTP") log.Printf("[HTTP] Starting HTTP server on: '%s'\n", srvHttp.Addr)
l.Info("Starting server", "addr", config.Listen.Http) go utils.RunBackgroundHttp("HTTP", srvHttp)
go utils.RunBackgroundHttp(l, srvHttp, lnHttp)
} }
if config.Listen.Https != "" { if srvConf.HttpsListen != "" {
// Listen must be called before Ready srvHttps = servers.NewHttpsServer(srvConf)
lnHttps, err := upg.Listen("tcp", config.Listen.Https)
if err != nil {
logger.Logger.Fatal("Listen failed", "err", err)
}
srvHttps = servers.NewHttpsServer(srvConf, promRegistry)
srvHttps.SetKeepAlivesEnabled(false) srvHttps.SetKeepAlivesEnabled(false)
l := logger.Logger.With("server", "HTTPS") log.Printf("[HTTPS] Starting HTTPS server on: '%s'\n", srvHttps.Addr)
l.Info("Starting server", "addr", config.Listen.Https) go utils.RunBackgroundHttps("HTTPS", srvHttps)
go utils.RunBackgroundHttps(l, srvHttps, lnHttps)
} }
// Do an upgrade on SIGHUP
go func() { go func() {
sig := make(chan os.Signal, 1) log.Println(http.ListenAndServe("localhost:6600", nil))
signal.Notify(sig, syscall.SIGHUP)
for range sig {
err := upg.Upgrade()
if err != nil {
logger.Logger.Error("Failed upgrade", "err", err)
}
}
}() }()
logger.Logger.Info("Ready") exit_reload.ExitReload("Violet", func() {
if err := upg.Ready(); err != nil { allCompilables.Compile()
panic(err) }, func() {
} // stop updating certificates
<-upg.Exit() allowedCerts.Stop()
time.AfterFunc(30*time.Second, func() { // close websockets first
logger.Logger.Warn("Graceful shutdown timed out") ws.Shutdown()
os.Exit(1)
// close http servers
if srvApi != nil {
_ = srvApi.Close()
}
if srvHttp != nil {
_ = srvHttp.Close()
}
if srvHttps != nil {
_ = srvHttps.Close()
}
}) })
// stop updating certificates
allowedCerts.Stop()
// close websockets first
ws.Shutdown()
// close http servers
if srvApi != nil {
_ = srvApi.Close()
}
if srvHttp != nil {
_ = srvHttp.Close()
}
if srvHttps != nil {
_ = srvHttps.Close()
}
return subcommands.ExitSuccess
} }

View File

@ -2,18 +2,18 @@ package main
import ( import (
"context" "context"
"database/sql"
"encoding/json" "encoding/json"
"flag" "flag"
"fmt" "fmt"
"github.com/1f349/violet"
"github.com/1f349/violet/domains" "github.com/1f349/violet/domains"
"github.com/1f349/violet/logger"
"github.com/1f349/violet/proxy" "github.com/1f349/violet/proxy"
"github.com/1f349/violet/proxy/websocket" "github.com/1f349/violet/proxy/websocket"
"github.com/1f349/violet/router" "github.com/1f349/violet/router"
"github.com/1f349/violet/target" "github.com/1f349/violet/target"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/google/subcommands" "github.com/google/subcommands"
"log"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
@ -42,7 +42,7 @@ func (s *setupCmd) Execute(_ context.Context, _ *flag.FlagSet, _ ...interface{})
// get absolute path to specify files // get absolute path to specify files
wdAbs, err := filepath.Abs(s.wdPath) wdAbs, err := filepath.Abs(s.wdPath)
if err != nil { if err != nil {
fmt.Println("Failed to get full directory path: ", err) fmt.Println("[Violet] Failed to get full directory path: ", err)
return subcommands.ExitFailure return subcommands.ExitFailure
} }
@ -50,11 +50,11 @@ func (s *setupCmd) Execute(_ context.Context, _ *flag.FlagSet, _ ...interface{})
createFile := false createFile := false
err = survey.AskOne(&survey.Confirm{Message: fmt.Sprintf("Create Violet config files in this directory: '%s'?", wdAbs)}, &createFile) err = survey.AskOne(&survey.Confirm{Message: fmt.Sprintf("Create Violet config files in this directory: '%s'?", wdAbs)}, &createFile)
if err != nil { if err != nil {
fmt.Println("Error: ", err) fmt.Println("[Violet] Error: ", err)
return subcommands.ExitFailure return subcommands.ExitFailure
} }
if !createFile { if !createFile {
fmt.Println("Goodbye") fmt.Println("[Violet] Goodbye")
return subcommands.ExitSuccess return subcommands.ExitSuccess
} }
@ -111,7 +111,7 @@ func (s *setupCmd) Execute(_ context.Context, _ *flag.FlagSet, _ ...interface{})
}, },
}, &answers) }, &answers)
if err != nil { if err != nil {
fmt.Println("Error: ", err) fmt.Println("[Violet] Error: ", err)
return subcommands.ExitFailure return subcommands.ExitFailure
} }
@ -142,14 +142,14 @@ func (s *setupCmd) Execute(_ context.Context, _ *flag.FlagSet, _ ...interface{})
RateLimit: answers.RateLimit, RateLimit: answers.RateLimit,
}) })
if err != nil { if err != nil {
fmt.Println("Failed to write config file: ", err) fmt.Println("[Violet] Failed to write config file: ", err)
return subcommands.ExitFailure return subcommands.ExitFailure
} }
// open sqlite database // open sqlite database
db, err := violet.InitDB(databaseFile) db, err := sql.Open("sqlite3", databaseFile)
if err != nil { if err != nil {
logger.Logger.Fatal("Failed to open database", "err", err) log.Fatalf("[Violet] Failed to open database '%s'...", databaseFile)
} }
// domain manager to add a domain, no need to compile here as the program needs // domain manager to add a domain, no need to compile here as the program needs
@ -168,14 +168,14 @@ func (s *setupCmd) Execute(_ context.Context, _ *flag.FlagSet, _ ...interface{})
return nil return nil
})) }))
if err != nil { if err != nil {
fmt.Println("Error: ", err) fmt.Println("[Violet] Error: ", err)
return subcommands.ExitFailure return subcommands.ExitFailure
} }
// parse the api url // parse the api url
apiUrl, err := url.Parse(answers.ApiUrl) apiUrl, err := url.Parse(answers.ApiUrl)
if err != nil { if err != nil {
fmt.Println("Failed to parse API URL: ", err) fmt.Println("[Violet] Failed to parse API URL: ", err)
return subcommands.ExitFailure return subcommands.ExitFailure
} }
@ -191,13 +191,13 @@ func (s *setupCmd) Execute(_ context.Context, _ *flag.FlagSet, _ ...interface{})
Active: true, Active: true,
}) })
if err != nil { if err != nil {
fmt.Println("Failed to insert api route into database: ", err) fmt.Println("[Violet] Failed to insert api route into database: ", err)
return subcommands.ExitFailure return subcommands.ExitFailure
} }
} }
fmt.Println("Setup complete") fmt.Println("[Violet] Setup complete")
fmt.Printf("Run the reverse proxy with `violet serve -conf %s`\n", confFile) fmt.Printf("[Violet] Run the reverse proxy with `violet serve -conf %s`\n", confFile)
return subcommands.ExitSuccess return subcommands.ExitSuccess
} }

View File

@ -1,31 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
package database
import (
"context"
"database/sql"
)
type DBTX interface {
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
PrepareContext(context.Context, string) (*sql.Stmt, error)
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
return &Queries{
db: tx,
}
}

View File

@ -1,68 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
// source: domain.sql
package database
import (
"context"
)
const addDomain = `-- name: AddDomain :exec
INSERT OR
REPLACE
INTO domains (domain, active)
VALUES (?, ?)
`
type AddDomainParams struct {
Domain string `json:"domain"`
Active bool `json:"active"`
}
func (q *Queries) AddDomain(ctx context.Context, arg AddDomainParams) error {
_, err := q.db.ExecContext(ctx, addDomain, arg.Domain, arg.Active)
return err
}
const deleteDomain = `-- name: DeleteDomain :exec
INSERT OR
REPLACE
INTO domains(domain, active)
VALUES (?, false)
`
func (q *Queries) DeleteDomain(ctx context.Context, domain string) error {
_, err := q.db.ExecContext(ctx, deleteDomain, domain)
return err
}
const getActiveDomains = `-- name: GetActiveDomains :many
SELECT domain
FROM domains
WHERE active = 1
`
func (q *Queries) GetActiveDomains(ctx context.Context) ([]string, error) {
rows, err := q.db.QueryContext(ctx, getActiveDomains)
if err != nil {
return nil, err
}
defer rows.Close()
var items []string
for rows.Next() {
var domain string
if err := rows.Scan(&domain); err != nil {
return nil, err
}
items = append(items, domain)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

View File

@ -1,74 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
// source: favicon.sql
package database
import (
"context"
"database/sql"
)
const getFavicons = `-- name: GetFavicons :many
SELECT host, svg, png, ico
FROM favicons
`
type GetFaviconsRow struct {
Host string `json:"host"`
Svg sql.NullString `json:"svg"`
Png sql.NullString `json:"png"`
Ico sql.NullString `json:"ico"`
}
func (q *Queries) GetFavicons(ctx context.Context) ([]GetFaviconsRow, error) {
rows, err := q.db.QueryContext(ctx, getFavicons)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetFaviconsRow
for rows.Next() {
var i GetFaviconsRow
if err := rows.Scan(
&i.Host,
&i.Svg,
&i.Png,
&i.Ico,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateFaviconCache = `-- name: UpdateFaviconCache :exec
INSERT OR
REPLACE INTO favicons (host, svg, png, ico)
VALUES (?, ?, ?, ?)
`
type UpdateFaviconCacheParams struct {
Host string `json:"host"`
Svg sql.NullString `json:"svg"`
Png sql.NullString `json:"png"`
Ico sql.NullString `json:"ico"`
}
func (q *Queries) UpdateFaviconCache(ctx context.Context, arg UpdateFaviconCacheParams) error {
_, err := q.db.ExecContext(ctx, updateFaviconCache,
arg.Host,
arg.Svg,
arg.Png,
arg.Ico,
)
return err
}

View File

@ -1,4 +0,0 @@
DROP TABLE domains;
DROP TABLE favicons;
DROP TABLE routes;
DROP TABLE redirects;

View File

@ -1,36 +0,0 @@
CREATE TABLE IF NOT EXISTS domains
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
domain TEXT UNIQUE NOT NULL,
active BOOLEAN NOT NULL DEFAULT 1
);
CREATE TABLE IF NOT EXISTS favicons
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
host VARCHAR NOT NULL,
svg VARCHAR,
png VARCHAR,
ico VARCHAR
);
CREATE TABLE IF NOT EXISTS routes
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
source TEXT UNIQUE NOT NULL,
destination TEXT NOT NULL,
description TEXT NOT NULL,
flags INTEGER NOT NULL DEFAULT 0,
active BOOLEAN NOT NULL DEFAULT 1
);
CREATE TABLE IF NOT EXISTS redirects
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
source TEXT UNIQUE NOT NULL,
destination TEXT NOT NULL,
description TEXT NOT NULL,
flags INTEGER NOT NULL DEFAULT 0,
code INTEGER NOT NULL DEFAULT 0,
active BOOLEAN NOT NULL DEFAULT 1
);

View File

@ -1,44 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
package database
import (
"database/sql"
"github.com/1f349/violet/target"
)
type Domain struct {
ID int64 `json:"id"`
Domain string `json:"domain"`
Active bool `json:"active"`
}
type Favicon struct {
ID int64 `json:"id"`
Host string `json:"host"`
Svg sql.NullString `json:"svg"`
Png sql.NullString `json:"png"`
Ico sql.NullString `json:"ico"`
}
type Redirect struct {
ID int64 `json:"id"`
Source string `json:"source"`
Destination string `json:"destination"`
Description string `json:"description"`
Flags target.Flags `json:"flags"`
Code int64 `json:"code"`
Active bool `json:"active"`
}
type Route struct {
ID int64 `json:"id"`
Source string `json:"source"`
Destination string `json:"destination"`
Description string `json:"description"`
Flags target.Flags `json:"flags"`
Active bool `json:"active"`
}

View File

@ -1,16 +0,0 @@
-- name: GetActiveDomains :many
SELECT domain
FROM domains
WHERE active = 1;
-- name: AddDomain :exec
INSERT OR
REPLACE
INTO domains (domain, active)
VALUES (?, ?);
-- name: DeleteDomain :exec
INSERT OR
REPLACE
INTO domains(domain, active)
VALUES (?, false);

View File

@ -1,8 +0,0 @@
-- name: GetFavicons :many
SELECT host, svg, png, ico
FROM favicons;
-- name: UpdateFaviconCache :exec
INSERT OR
REPLACE INTO favicons (host, svg, png, ico)
VALUES (?, ?, ?, ?);

View File

@ -1,39 +0,0 @@
-- name: GetActiveRoutes :many
SELECT source, destination, flags
FROM routes
WHERE active = 1;
-- name: GetActiveRedirects :many
SELECT source, destination, flags, code
FROM redirects
WHERE active = 1;
-- name: GetAllRoutes :many
SELECT source, destination, description, flags, active
FROM routes;
-- name: GetAllRedirects :many
SELECT source, destination, description, flags, code, active
FROM redirects;
-- name: AddRoute :exec
INSERT OR
REPLACE
INTO routes (source, destination, description, flags, active)
VALUES (?, ?, ?, ?, ?);
-- name: AddRedirect :exec
INSERT OR
REPLACE
INTO redirects (source, destination, description, flags, code, active)
VALUES (?, ?, ?, ?, ?, ?);
-- name: RemoveRoute :exec
DELETE
FROM routes
WHERE source = ?;
-- name: RemoveRedirect :exec
DELETE
FROM redirects
WHERE source = ?;

View File

@ -1,250 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
// source: routing.sql
package database
import (
"context"
"github.com/1f349/violet/target"
)
const addRedirect = `-- name: AddRedirect :exec
INSERT OR
REPLACE
INTO redirects (source, destination, description, flags, code, active)
VALUES (?, ?, ?, ?, ?, ?)
`
type AddRedirectParams struct {
Source string `json:"source"`
Destination string `json:"destination"`
Description string `json:"description"`
Flags target.Flags `json:"flags"`
Code int64 `json:"code"`
Active bool `json:"active"`
}
func (q *Queries) AddRedirect(ctx context.Context, arg AddRedirectParams) error {
_, err := q.db.ExecContext(ctx, addRedirect,
arg.Source,
arg.Destination,
arg.Description,
arg.Flags,
arg.Code,
arg.Active,
)
return err
}
const addRoute = `-- name: AddRoute :exec
INSERT OR
REPLACE
INTO routes (source, destination, description, flags, active)
VALUES (?, ?, ?, ?, ?)
`
type AddRouteParams struct {
Source string `json:"source"`
Destination string `json:"destination"`
Description string `json:"description"`
Flags target.Flags `json:"flags"`
Active bool `json:"active"`
}
func (q *Queries) AddRoute(ctx context.Context, arg AddRouteParams) error {
_, err := q.db.ExecContext(ctx, addRoute,
arg.Source,
arg.Destination,
arg.Description,
arg.Flags,
arg.Active,
)
return err
}
const getActiveRedirects = `-- name: GetActiveRedirects :many
SELECT source, destination, flags, code
FROM redirects
WHERE active = 1
`
type GetActiveRedirectsRow struct {
Source string `json:"source"`
Destination string `json:"destination"`
Flags target.Flags `json:"flags"`
Code int64 `json:"code"`
}
func (q *Queries) GetActiveRedirects(ctx context.Context) ([]GetActiveRedirectsRow, error) {
rows, err := q.db.QueryContext(ctx, getActiveRedirects)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetActiveRedirectsRow
for rows.Next() {
var i GetActiveRedirectsRow
if err := rows.Scan(
&i.Source,
&i.Destination,
&i.Flags,
&i.Code,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getActiveRoutes = `-- name: GetActiveRoutes :many
SELECT source, destination, flags
FROM routes
WHERE active = 1
`
type GetActiveRoutesRow struct {
Source string `json:"source"`
Destination string `json:"destination"`
Flags target.Flags `json:"flags"`
}
func (q *Queries) GetActiveRoutes(ctx context.Context) ([]GetActiveRoutesRow, error) {
rows, err := q.db.QueryContext(ctx, getActiveRoutes)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetActiveRoutesRow
for rows.Next() {
var i GetActiveRoutesRow
if err := rows.Scan(&i.Source, &i.Destination, &i.Flags); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getAllRedirects = `-- name: GetAllRedirects :many
SELECT source, destination, description, flags, code, active
FROM redirects
`
type GetAllRedirectsRow struct {
Source string `json:"source"`
Destination string `json:"destination"`
Description string `json:"description"`
Flags target.Flags `json:"flags"`
Code int64 `json:"code"`
Active bool `json:"active"`
}
func (q *Queries) GetAllRedirects(ctx context.Context) ([]GetAllRedirectsRow, error) {
rows, err := q.db.QueryContext(ctx, getAllRedirects)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetAllRedirectsRow
for rows.Next() {
var i GetAllRedirectsRow
if err := rows.Scan(
&i.Source,
&i.Destination,
&i.Description,
&i.Flags,
&i.Code,
&i.Active,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getAllRoutes = `-- name: GetAllRoutes :many
SELECT source, destination, description, flags, active
FROM routes
`
type GetAllRoutesRow struct {
Source string `json:"source"`
Destination string `json:"destination"`
Description string `json:"description"`
Flags target.Flags `json:"flags"`
Active bool `json:"active"`
}
func (q *Queries) GetAllRoutes(ctx context.Context) ([]GetAllRoutesRow, error) {
rows, err := q.db.QueryContext(ctx, getAllRoutes)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetAllRoutesRow
for rows.Next() {
var i GetAllRoutesRow
if err := rows.Scan(
&i.Source,
&i.Destination,
&i.Description,
&i.Flags,
&i.Active,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const removeRedirect = `-- name: RemoveRedirect :exec
DELETE
FROM redirects
WHERE source = ?
`
func (q *Queries) RemoveRedirect(ctx context.Context, source string) error {
_, err := q.db.ExecContext(ctx, removeRedirect, source)
return err
}
const removeRoute = `-- name: RemoveRoute :exec
DELETE
FROM routes
WHERE source = ?
`
func (q *Queries) RemoveRoute(ctx context.Context, source string) error {
_, err := q.db.ExecContext(ctx, removeRoute, source)
return err
}

View File

@ -0,0 +1,6 @@
CREATE TABLE IF NOT EXISTS domains
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
domain TEXT UNIQUE,
active INTEGER DEFAULT 1
);

View File

@ -1,34 +1,41 @@
package domains package domains
import ( import (
"context" "database/sql"
_ "embed" _ "embed"
"github.com/1f349/violet/database"
"github.com/1f349/violet/logger"
"github.com/1f349/violet/utils" "github.com/1f349/violet/utils"
"github.com/mrmelon54/rescheduler" "github.com/MrMelon54/rescheduler"
"log"
"strings" "strings"
"sync" "sync"
) )
var Logger = logger.Logger.WithPrefix("Violet Domains") //go:embed create-table-domains.sql
var createTableDomains string
// Domains is the domain list and management system. // Domains is the domain list and management system.
type Domains struct { type Domains struct {
db *database.Queries db *sql.DB
s *sync.RWMutex s *sync.RWMutex
m map[string]struct{} m map[string]struct{}
r *rescheduler.Rescheduler r *rescheduler.Rescheduler
} }
// New creates a new domain list // New creates a new domain list
func New(db *database.Queries) *Domains { func New(db *sql.DB) *Domains {
a := &Domains{ a := &Domains{
db: db, db: db,
s: &sync.RWMutex{}, s: &sync.RWMutex{},
m: make(map[string]struct{}), m: make(map[string]struct{}),
} }
a.r = rescheduler.NewRescheduler(a.threadCompile) a.r = rescheduler.NewRescheduler(a.threadCompile)
// init domains table
_, err := a.db.Exec(createTableDomains)
if err != nil {
log.Printf("[WARN] Failed to generate 'domains' table\n")
return nil
}
return a return a
} }
@ -70,7 +77,7 @@ func (d *Domains) threadCompile() {
// compile map and check errors // compile map and check errors
err := d.internalCompile(domainMap) err := d.internalCompile(domainMap)
if err != nil { if err != nil {
Logger.Info("Compile faile", "err", err) log.Printf("[Domains] Compile failed: %s\n", err)
return return
} }
@ -83,39 +90,43 @@ func (d *Domains) threadCompile() {
// internalCompile is a hidden internal method for querying the database during // internalCompile is a hidden internal method for querying the database during
// the Compile() method. // the Compile() method.
func (d *Domains) internalCompile(m map[string]struct{}) error { func (d *Domains) internalCompile(m map[string]struct{}) error {
Logger.Info("Updating domains from database") log.Println("[Domains] Updating domains from database")
// sql or something? // sql or something?
rows, err := d.db.GetActiveDomains(context.Background()) rows, err := d.db.Query(`select domain from domains where active = 1`)
if err != nil { if err != nil {
return err return err
} }
defer rows.Close()
for _, i := range rows { // loop through rows and scan the allowed domain names
m[i] = struct{}{} for rows.Next() {
var name string
err = rows.Scan(&name)
if err != nil {
return err
}
m[name] = struct{}{}
} }
// check for errors // check for errors
return nil return rows.Err()
} }
func (d *Domains) Put(domain string, active bool) { func (d *Domains) Put(domain string, active bool) {
d.s.Lock() d.s.Lock()
defer d.s.Unlock() defer d.s.Unlock()
err := d.db.AddDomain(context.Background(), database.AddDomainParams{ _, err := d.db.Exec("INSERT OR REPLACE INTO domains (domain, active) VALUES (?, ?)", domain, active)
Domain: domain,
Active: active,
})
if err != nil { if err != nil {
logger.Logger.Infof("Database error: %s\n", err) log.Printf("[Violet] Database error: %s\n", err)
} }
} }
func (d *Domains) Delete(domain string) { func (d *Domains) Delete(domain string) {
d.s.Lock() d.s.Lock()
defer d.s.Unlock() defer d.s.Unlock()
err := d.db.DeleteDomain(context.Background(), domain) _, err := d.db.Exec("INSERT OR REPLACE INTO domains (domain, active) VALUES (?, ?)", domain, false)
if err != nil { if err != nil {
logger.Logger.Infof("Database error: %s\n", err) log.Printf("[Violet] Database error: %s\n", err)
} }
} }

View File

@ -1,20 +1,18 @@
package domains package domains
import ( import (
"context" "database/sql"
"github.com/1f349/violet"
"github.com/1f349/violet/database"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"testing" "testing"
) )
func TestDomainsNew(t *testing.T) { func TestDomainsNew(t *testing.T) {
db, err := violet.InitDB("file:TestDomainsNew?mode=memory&cache=shared") db, err := sql.Open("sqlite3", "file::memory:?cache=shared")
assert.NoError(t, err) assert.NoError(t, err)
domains := New(db) domains := New(db)
err = db.AddDomain(context.Background(), database.AddDomainParams{Domain: "example.com", Active: true}) _, err = db.Exec("INSERT OR IGNORE INTO domains (domain, active) VALUES (?, ?)", "example.com", 1)
assert.NoError(t, err) assert.NoError(t, err)
domains.Compile() domains.Compile()
@ -29,11 +27,11 @@ func TestDomainsNew(t *testing.T) {
func TestDomains_IsValid(t *testing.T) { func TestDomains_IsValid(t *testing.T) {
// open sqlite database // open sqlite database
db, err := violet.InitDB("file:TestDomains_IsValid?mode=memory&cache=shared") db, err := sql.Open("sqlite3", "file::memory:?cache=shared")
assert.NoError(t, err) assert.NoError(t, err)
domains := New(db) domains := New(db)
err = db.AddDomain(context.Background(), database.AddDomainParams{Domain: "example.com", Active: true}) _, err = domains.db.Exec("INSERT OR IGNORE INTO domains (domain, active) VALUES (?, ?)", "example.com", 1)
assert.NoError(t, err) assert.NoError(t, err)
domains.s.Lock() domains.s.Lock()

View File

@ -2,9 +2,9 @@ package error_pages
import ( import (
"fmt" "fmt"
"github.com/1f349/violet/logger" "github.com/MrMelon54/rescheduler"
"github.com/mrmelon54/rescheduler"
"io/fs" "io/fs"
"log"
"net/http" "net/http"
"path/filepath" "path/filepath"
"strconv" "strconv"
@ -12,8 +12,6 @@ import (
"sync" "sync"
) )
var Logger = logger.Logger.WithPrefix("Violet Error Pages")
// ErrorPages stores the custom error pages and is called by the servers to // ErrorPages stores the custom error pages and is called by the servers to
// output meaningful pages for HTTP error codes // output meaningful pages for HTTP error codes
type ErrorPages struct { type ErrorPages struct {
@ -80,7 +78,7 @@ func (e *ErrorPages) threadCompile() {
if e.dir != nil { if e.dir != nil {
err := e.internalCompile(errorPageMap) err := e.internalCompile(errorPageMap)
if err != nil { if err != nil {
Logger.Info("Compile failed", "err", err) log.Printf("[ErrorPages] Compile failed: %s\n", err)
return return
} }
} }
@ -98,7 +96,7 @@ func (e *ErrorPages) internalCompile(m map[int]func(rw http.ResponseWriter)) err
return fmt.Errorf("failed to read error pages dir: %w", err) return fmt.Errorf("failed to read error pages dir: %w", err)
} }
Logger.Info("Compiling lookup table", "page count", len(files)) log.Printf("[ErrorPages] Compiling lookup table for %d error pages\n", len(files))
// find and load error pages // find and load error pages
for _, i := range files { for _, i := range files {
@ -113,20 +111,20 @@ func (e *ErrorPages) internalCompile(m map[int]func(rw http.ResponseWriter)) err
// if the extension is not 'html' then ignore the file // if the extension is not 'html' then ignore the file
if ext != ".html" { if ext != ".html" {
Logger.Warn("Ignoring non '.html' file in error pages directory", "name", name) log.Printf("[ErrorPages] WARNING: ignoring non '.html' file in error pages directory: '%s'\n", name)
continue continue
} }
// if the name can't be // if the name can't be
nameInt, err := strconv.Atoi(strings.TrimSuffix(name, ".html")) nameInt, err := strconv.Atoi(strings.TrimSuffix(name, ".html"))
if err != nil { if err != nil {
Logger.Warn("Ignoring invalid error page in error pages directory", "name", name) log.Printf("[ErrorPages] WARNING: ignoring invalid error page in error pages directory: '%s'\n", name)
continue continue
} }
// check if code is in range 100-599 // check if code is in range 100-599
if nameInt < 100 || nameInt >= 600 { if nameInt < 100 || nameInt >= 600 {
Logger.Warn("Ignoring invalid error page in error pages directory must be 100-599", "name", name) log.Printf("[ErrorPages] WARNING: ignoring invalid error page in error pages directory must be 100-599: '%s'\n", name)
continue continue
} }

View File

@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS favicons (
id INTEGER PRIMARY KEY AUTOINCREMENT,
host VARCHAR,
svg VARCHAR,
png VARCHAR,
ico VARCHAR
);

View File

@ -1,7 +1,5 @@
package favicons package favicons
import "database/sql"
// FaviconImage stores the url, hash and raw bytes of an image // FaviconImage stores the url, hash and raw bytes of an image
type FaviconImage struct { type FaviconImage struct {
Url string Url string
@ -11,9 +9,9 @@ type FaviconImage struct {
// CreateFaviconImage outputs a FaviconImage with the specified URL or nil if // CreateFaviconImage outputs a FaviconImage with the specified URL or nil if
// the URL is an empty string. // the URL is an empty string.
func CreateFaviconImage(url sql.NullString) *FaviconImage { func CreateFaviconImage(url string) *FaviconImage {
if !url.Valid { if url == "" {
return nil return nil
} }
return &FaviconImage{Url: url.String} return &FaviconImage{Url: url}
} }

View File

@ -6,7 +6,7 @@ import (
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt" "fmt"
"github.com/mrmelon54/png2ico" "github.com/MrMelon54/png2ico"
"image/png" "image/png"
"io" "io"
"net/http" "net/http"
@ -74,7 +74,7 @@ func (l *FaviconList) PreProcess(convert func(in []byte) ([]byte, error)) error
// download SVG // download SVG
l.Svg.Raw, err = getFaviconViaRequest(l.Svg.Url) l.Svg.Raw, err = getFaviconViaRequest(l.Svg.Url)
if err != nil { if err != nil {
return fmt.Errorf("favicons: failed to fetch SVG icon: %w", err) return fmt.Errorf("[Favicons] Failed to fetch SVG icon: %w", err)
} }
l.Svg.Hash = hex.EncodeToString(sha256.New().Sum(l.Svg.Raw)) l.Svg.Hash = hex.EncodeToString(sha256.New().Sum(l.Svg.Raw))
} }
@ -84,14 +84,14 @@ func (l *FaviconList) PreProcess(convert func(in []byte) ([]byte, error)) error
// download PNG // download PNG
l.Png.Raw, err = getFaviconViaRequest(l.Png.Url) l.Png.Raw, err = getFaviconViaRequest(l.Png.Url)
if err != nil { if err != nil {
return fmt.Errorf("favicons: failed to fetch PNG icon: %w", err) return fmt.Errorf("[Favicons] Failed to fetch PNG icon: %w", err)
} }
} else if l.Svg != nil { } else if l.Svg != nil {
// generate PNG from SVG // generate PNG from SVG
l.Png = &FaviconImage{} l.Png = &FaviconImage{}
l.Png.Raw, err = convert(l.Svg.Raw) l.Png.Raw, err = convert(l.Svg.Raw)
if err != nil { if err != nil {
return fmt.Errorf("favicons: failed to generate PNG icon: %w", err) return fmt.Errorf("[Favicons] Failed to generate PNG icon: %w", err)
} }
} }
@ -100,19 +100,19 @@ func (l *FaviconList) PreProcess(convert func(in []byte) ([]byte, error)) error
// download ICO // download ICO
l.Ico.Raw, err = getFaviconViaRequest(l.Ico.Url) l.Ico.Raw, err = getFaviconViaRequest(l.Ico.Url)
if err != nil { if err != nil {
return fmt.Errorf("favicons: failed to fetch ICO icon: %w", err) return fmt.Errorf("[Favicons] Failed to fetch ICO icon: %w", err)
} }
} else if l.Png != nil { } else if l.Png != nil {
// generate ICO from PNG // generate ICO from PNG
l.Ico = &FaviconImage{} l.Ico = &FaviconImage{}
decode, err := png.Decode(bytes.NewReader(l.Png.Raw)) decode, err := png.Decode(bytes.NewReader(l.Png.Raw))
if err != nil { if err != nil {
return fmt.Errorf("favicons: failed to decode PNG icon: %w", err) return fmt.Errorf("[Favicons] Failed to decode PNG icon: %w", err)
} }
b := decode.Bounds() b := decode.Bounds()
l.Ico.Raw, err = png2ico.ConvertPngToIco(l.Png.Raw, b.Dx(), b.Dy()) l.Ico.Raw, err = png2ico.ConvertPngToIco(l.Png.Raw, b.Dx(), b.Dy())
if err != nil { if err != nil {
return fmt.Errorf("favicons: failed to generate ICO icon: %w", err) return fmt.Errorf("[Favicons] Failed to generate ICO icon: %w", err)
} }
} }
@ -139,16 +139,16 @@ func (l *FaviconList) genSha256() {
var getFaviconViaRequest = func(url string) ([]byte, error) { var getFaviconViaRequest = func(url string) ([]byte, error) {
req, err := http.NewRequest(http.MethodGet, url, nil) req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil { if err != nil {
return nil, fmt.Errorf("favicons: Failed to send request '%s': %w", url, err) return nil, fmt.Errorf("[Favicons] Failed to send request '%s': %w", url, err)
} }
req.Header.Set("X-Violet-Raw-Favicon", "1") req.Header.Set("X-Violet-Raw-Favicon", "1")
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
return nil, fmt.Errorf("favicons: failed to do request '%s': %w", url, err) return nil, fmt.Errorf("[Favicons] Failed to do request '%s': %w", url, err)
} }
rawBody, err := io.ReadAll(resp.Body) rawBody, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return nil, fmt.Errorf("favicons: failed to read response '%s': %w", url, err) return nil, fmt.Errorf("[Favicons] Failed to read response '%s': %w", url, err)
} }
return rawBody, nil return rawBody, nil
} }

View File

@ -1,24 +1,24 @@
package favicons package favicons
import ( import (
"context" "database/sql"
_ "embed" _ "embed"
"errors" "errors"
"fmt" "fmt"
"github.com/1f349/violet/database" "github.com/MrMelon54/rescheduler"
"github.com/1f349/violet/logger"
"github.com/mrmelon54/rescheduler"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"log"
"sync" "sync"
) )
var Logger = logger.Logger.WithPrefix("Violet Favicons")
var ErrFaviconNotFound = errors.New("favicon not found") var ErrFaviconNotFound = errors.New("favicon not found")
//go:embed create-table-favicons.sql
var createTableFavicons string
// Favicons is a dynamic favicon generator which supports overwriting favicons // Favicons is a dynamic favicon generator which supports overwriting favicons
type Favicons struct { type Favicons struct {
db *database.Queries db *sql.DB
cmd string cmd string
cLock *sync.RWMutex cLock *sync.RWMutex
faviconMap map[string]*FaviconList faviconMap map[string]*FaviconList
@ -26,7 +26,7 @@ type Favicons struct {
} }
// New creates a new dynamic favicon generator // New creates a new dynamic favicon generator
func New(db *database.Queries, inkscapeCmd string) *Favicons { func New(db *sql.DB, inkscapeCmd string) *Favicons {
f := &Favicons{ f := &Favicons{
db: db, db: db,
cmd: inkscapeCmd, cmd: inkscapeCmd,
@ -35,6 +35,13 @@ func New(db *database.Queries, inkscapeCmd string) *Favicons {
} }
f.r = rescheduler.NewRescheduler(f.threadCompile) f.r = rescheduler.NewRescheduler(f.threadCompile)
// init favicons table
_, err := f.db.Exec(createTableFavicons)
if err != nil {
log.Printf("[WARN] Failed to generate 'favicons' table\n")
return nil
}
// run compile to get the initial data // run compile to get the initial data
f.Compile() f.Compile()
return f return f
@ -68,7 +75,7 @@ func (f *Favicons) threadCompile() {
err := f.internalCompile(favicons) err := f.internalCompile(favicons)
if err != nil { if err != nil {
// log compile errors // log compile errors
Logger.Info("Compile failed", "err", err) log.Printf("[Favicons] Compile failed: %s\n", err)
return return
} }
@ -82,23 +89,29 @@ func (f *Favicons) threadCompile() {
// favicons. // favicons.
func (f *Favicons) internalCompile(m map[string]*FaviconList) error { func (f *Favicons) internalCompile(m map[string]*FaviconList) error {
// query all rows in database // query all rows in database
rows, err := f.db.GetFavicons(context.Background()) query, err := f.db.Query(`select host, svg, png, ico from favicons`)
if err != nil { if err != nil {
return fmt.Errorf("failed to prepare rows: %w", err) return fmt.Errorf("failed to prepare query: %w", err)
} }
// loop over rows and scan in data using error group to catch errors // loop over rows and scan in data using error group to catch errors
var g errgroup.Group var g errgroup.Group
for _, row := range rows { for query.Next() {
var host, rawSvg, rawPng, rawIco string
err := query.Scan(&host, &rawSvg, &rawPng, &rawIco)
if err != nil {
return fmt.Errorf("failed to scan row: %w", err)
}
// create favicon list for this row // create favicon list for this row
l := &FaviconList{ l := &FaviconList{
Ico: CreateFaviconImage(row.Ico), Ico: CreateFaviconImage(rawIco),
Png: CreateFaviconImage(row.Png), Png: CreateFaviconImage(rawPng),
Svg: CreateFaviconImage(row.Svg), Svg: CreateFaviconImage(rawSvg),
} }
// save the favicon list to the map // save the favicon list to the map
m[row.Host] = l m[host] = l
// run the pre-process in a separate goroutine // run the pre-process in a separate goroutine
g.Go(func() error { g.Go(func() error {

View File

@ -2,11 +2,8 @@ package favicons
import ( import (
"bytes" "bytes"
"context"
"database/sql" "database/sql"
_ "embed" _ "embed"
"github.com/1f349/violet"
"github.com/1f349/violet/database"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"image/png" "image/png"
@ -25,17 +22,11 @@ var (
func TestFaviconsNew(t *testing.T) { func TestFaviconsNew(t *testing.T) {
getFaviconViaRequest = func(_ string) ([]byte, error) { return exampleSvg, nil } getFaviconViaRequest = func(_ string) ([]byte, error) { return exampleSvg, nil }
db, err := violet.InitDB("file:TestFaviconsNew?mode=memory&cache=shared") db, err := sql.Open("sqlite3", "file::memory:?cache=shared")
assert.NoError(t, err) assert.NoError(t, err)
favicons := New(db, "inkscape") favicons := New(db, "inkscape")
err = db.UpdateFaviconCache(context.Background(), database.UpdateFaviconCacheParams{ _, err = db.Exec("insert into favicons (host, svg, png, ico) values (?, ?, ?, ?)", "example.com", "https://example.com/assets/logo.svg", "", "")
Host: "example.com",
Svg: sql.NullString{
String: "https://example.com/assets/logo.svg",
Valid: true,
},
})
assert.NoError(t, err) assert.NoError(t, err)
favicons.cLock.Lock() favicons.cLock.Lock()
assert.NoError(t, favicons.internalCompile(favicons.faviconMap)) assert.NoError(t, favicons.internalCompile(favicons.faviconMap))

60
go.mod
View File

@ -1,62 +1,42 @@
module github.com/1f349/violet module github.com/1f349/violet
go 1.22 go 1.21.4
require ( require (
github.com/1f349/mjwt v0.2.5 github.com/1f349/mjwt v0.2.1
github.com/AlecAivazis/survey/v2 v2.3.7 github.com/AlecAivazis/survey/v2 v2.3.7
github.com/charmbracelet/log v0.4.0 github.com/MrMelon54/certgen v0.0.1
github.com/cloudflare/tableflip v1.2.3 github.com/MrMelon54/exit-reload v0.0.1
github.com/golang-migrate/migrate/v4 v4.17.1 github.com/MrMelon54/png2ico v1.0.1
github.com/MrMelon54/rescheduler v0.0.2
github.com/MrMelon54/trie v0.0.2
github.com/google/subcommands v1.2.0 github.com/google/subcommands v1.2.0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.4.0
github.com/gorilla/websocket v1.5.1 github.com/gorilla/websocket v1.5.1
github.com/julienschmidt/httprouter v1.3.0 github.com/julienschmidt/httprouter v1.3.0
github.com/mattn/go-sqlite3 v1.14.22 github.com/mattn/go-sqlite3 v1.14.18
github.com/mrmelon54/certgen v0.0.2 github.com/rs/cors v1.10.1
github.com/mrmelon54/png2ico v1.0.2 github.com/sethvargo/go-limiter v0.7.2
github.com/mrmelon54/rescheduler v0.0.3 github.com/stretchr/testify v1.8.4
github.com/mrmelon54/trie v0.0.3 golang.org/x/net v0.19.0
github.com/prometheus/client_golang v1.19.1 golang.org/x/sync v0.5.0
github.com/rs/cors v1.11.0
github.com/sethvargo/go-limiter v1.0.0
github.com/stretchr/testify v1.9.0
golang.org/x/net v0.25.0
golang.org/x/sync v0.7.0
) )
require ( require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/becheran/wildmatch-go v1.0.0 // indirect github.com/becheran/wildmatch-go v1.0.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/lipgloss v0.10.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kr/text v0.2.0 // indirect github.com/kr/pretty v0.3.1 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect
github.com/prometheus/common v0.53.0 // indirect golang.org/x/sys v0.15.0 // indirect
github.com/prometheus/procfs v0.14.0 // indirect golang.org/x/term v0.15.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect golang.org/x/text v0.14.0 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
go.uber.org/atomic v1.11.0 // indirect
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/term v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

124
go.sum
View File

@ -1,62 +1,48 @@
github.com/1f349/mjwt v0.2.5 h1:IxjLaali22ayTzZ628lH7j0JDdYJoj6+CJ/VktCqtXQ= github.com/1f349/mjwt v0.2.1 h1:REdiM/MaNjYQwHvI39LaMPhlvMg4Vy9SgomWMsKTNz8=
github.com/1f349/mjwt v0.2.5/go.mod h1:KEs6jd9JjWrQW+8feP2pGAU7pdA3aYTqjkT/YQr73PU= github.com/1f349/mjwt v0.2.1/go.mod h1:KEs6jd9JjWrQW+8feP2pGAU7pdA3aYTqjkT/YQr73PU=
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
github.com/MrMelon54/certgen v0.0.1 h1:ycWdZ2RlxQ5qSuejeBVv4aXjGo5hdqqL4j4EjrXnFMk=
github.com/MrMelon54/certgen v0.0.1/go.mod h1:GHflVlSbtFLJZLpN1oWyUvDBRrR8qCWiwZLXCCnS2Gc=
github.com/MrMelon54/exit-reload v0.0.1 h1:sxHa59tNEQMcikwuX2+93lw6Vi1+R7oCRF8a0C3alXc=
github.com/MrMelon54/exit-reload v0.0.1/go.mod h1:PLiSfmUzwdpTTQP3BBfUPhkqPwaIZjx0DuXBnM76Bug=
github.com/MrMelon54/png2ico v1.0.1 h1:zJoSSl4OkvSIMWGyGPvb8fWNa0KrUvMIjgNGLNLJhVQ=
github.com/MrMelon54/png2ico v1.0.1/go.mod h1:NOv3tO4497mInG+3tcFkIohmxCywUwMLU8WNxJZLVmU=
github.com/MrMelon54/rescheduler v0.0.2 h1:efrRwr0BYlkaXFucZDjQqRyIawZiMEAnzjea46Bs9Oc=
github.com/MrMelon54/rescheduler v0.0.2/go.mod h1:OQDFtZHdS4/qA/r7rtJUQA22/hbpnZ9MGQCXOPjhC6w=
github.com/MrMelon54/trie v0.0.2 h1:ZXWcX5ij62O9K4I/anuHmVg8L3tF0UGdlPceAASwKEY=
github.com/MrMelon54/trie v0.0.2/go.mod h1:sGCGOcqb+DxSxvHgSOpbpkmA7mFZR47YDExy9OCbVZI=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/becheran/wildmatch-go v1.0.0 h1:mE3dGGkTmpKtT4Z+88t8RStG40yN9T+kFEGj2PZFSzA= github.com/becheran/wildmatch-go v1.0.0 h1:mE3dGGkTmpKtT4Z+88t8RStG40yN9T+kFEGj2PZFSzA=
github.com/becheran/wildmatch-go v1.0.0/go.mod h1:gbMvj0NtVdJ15Mg/mH9uxk2R1QCistMyU7d9KFzroX4= github.com/becheran/wildmatch-go v1.0.0/go.mod h1:gbMvj0NtVdJ15Mg/mH9uxk2R1QCistMyU7d9KFzroX4=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s=
github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE=
github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=
github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM=
github.com/cloudflare/tableflip v1.2.3 h1:8I+B99QnnEWPHOY3fWipwVKxS70LGgUsslG7CSfmHMw=
github.com/cloudflare/tableflip v1.2.3/go.mod h1:P4gRehmV6Z2bY5ao5ml9Pd8u6kuEnlB37pUFMmv7j2E=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-migrate/migrate/v4 v4.17.1 h1:4zQ6iqL6t6AiItphxJctQb3cFqWiSpMnX7wLTPnnYO4=
github.com/golang-migrate/migrate/v4 v4.17.1/go.mod h1:m8hinFyWBn0SA4QKHuKh175Pm9wjmxj3S2Mia7dbXzM=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE= github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
@ -64,96 +50,64 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mrmelon54/certgen v0.0.2 h1:4CMDkA/gGZu+E4iikU+5qdOWK7qOQrk58KtUfnmyYmY= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/mrmelon54/certgen v0.0.2/go.mod h1:vwrWSXQmxZYqEyh+cf05IvDIFV2aYuxL4+O6ABIlN8M=
github.com/mrmelon54/png2ico v1.0.2 h1:KyJd3ATmDjxAJS28MTSf44GxzYnlZ+7KT8SXzGb3sN8=
github.com/mrmelon54/png2ico v1.0.2/go.mod h1:vp8Be9y5cz102ANon+BnsIzTUdet3VQRvOuWJTH9h0M=
github.com/mrmelon54/rescheduler v0.0.3 h1:TrkJL6S7PKvXuo1mvdgRgsILA/pk5L1lrXhV/q7IEzQ=
github.com/mrmelon54/rescheduler v0.0.3/go.mod h1:q415n6W1xcePPP5Rix6FOiADgcN66BYMyNOsFnNyoWQ=
github.com/mrmelon54/trie v0.0.3 h1:wZmws84FiGNBZJ00garLyQ2EQhtx0SipVoV7fK8+kZE=
github.com/mrmelon54/trie v0.0.3/go.mod h1:d3hl0YUBSWR3XN4S9BDLkGVzLT4VgwP2mZkBJM6uFpw=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo=
github.com/prometheus/common v0.53.0 h1:U2pL9w9nmJwJDa4qqLQ3ZaePJ6ZTwt7cMD3AG3+aLCE= github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/prometheus/common v0.53.0/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U= github.com/sethvargo/go-limiter v0.7.2 h1:FgC4N7RMpV5gMrUdda15FaFTkQ/L4fEqM7seXMs4oO8=
github.com/prometheus/procfs v0.14.0 h1:Lw4VdGGoKEZilJsayHf0B+9YgLGREba2C6xr+Fdfq6s= github.com/sethvargo/go-limiter v0.7.2/go.mod h1:C0kbSFbiriE5k2FFOe18M1YZbAR2Fiwf72uGu0CXCcU=
github.com/prometheus/procfs v0.14.0/go.mod h1:XL+Iwz8k8ZabyZfMFHPiilCniixqQarAy5Mu67pHlNQ=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po=
github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/sethvargo/go-limiter v1.0.0 h1:JqW13eWEMn0VFv86OKn8wiYJY/m250WoXdrjRV0kLe4=
github.com/sethvargo/go-limiter v1.0.0/go.mod h1:01b6tW25Ap+MeLYBuD4aHunMrJoNO5PVUFdS9rac3II=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

View File

@ -1,38 +0,0 @@
package violet
import (
"database/sql"
"embed"
"errors"
"github.com/1f349/violet/database"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/sqlite3"
"github.com/golang-migrate/migrate/v4/source/iofs"
)
//go:embed database/migrations/*.sql
var migrations embed.FS
func InitDB(p string) (*database.Queries, error) {
migDrv, err := iofs.New(migrations, "database/migrations")
if err != nil {
return nil, err
}
dbOpen, err := sql.Open("sqlite3", p)
if err != nil {
return nil, err
}
dbDrv, err := sqlite3.WithInstance(dbOpen, &sqlite3.Config{})
if err != nil {
return nil, err
}
mig, err := migrate.NewWithInstance("iofs", migDrv, "sqlite3", dbDrv)
if err != nil {
return nil, err
}
err = mig.Up()
if err != nil && !errors.Is(err, migrate.ErrNoChange) {
return nil, err
}
return database.New(dbOpen), nil
}

View File

@ -1,12 +0,0 @@
package logger
import (
"github.com/charmbracelet/log"
"os"
)
var Logger = log.NewWithOptions(os.Stderr, log.Options{
ReportCaller: true,
ReportTimestamp: true,
Prefix: "Violet",
})

View File

@ -2,18 +2,15 @@ package proxy
import ( import (
"crypto/tls" "crypto/tls"
"github.com/1f349/violet/logger"
"github.com/1f349/violet/proxy/websocket" "github.com/1f349/violet/proxy/websocket"
"github.com/google/uuid"
"log"
"net" "net"
"net/http" "net/http"
"sync" "sync"
"time" "time"
) )
var loggerSecure = logger.Logger.WithPrefix("Violet Secure Transport")
var loggerInsecure = logger.Logger.WithPrefix("Violet Insecure Transport")
var loggerWebsocket = logger.Logger.WithPrefix("Violet Websocket Transport")
type HybridTransport struct { type HybridTransport struct {
baseDialer *net.Dialer baseDialer *net.Dialer
normalTransport http.RoundTripper normalTransport http.RoundTripper
@ -74,15 +71,24 @@ func NewHybridTransportWithCalls(normal, insecure http.RoundTripper, ws *websock
// SecureRoundTrip calls the secure transport // SecureRoundTrip calls the secure transport
func (h *HybridTransport) SecureRoundTrip(req *http.Request) (*http.Response, error) { func (h *HybridTransport) SecureRoundTrip(req *http.Request) (*http.Response, error) {
u := uuid.New()
log.Println("[Transport] Start upgrade:", u)
defer log.Println("[Transport] Stop upgrade:", u)
return h.normalTransport.RoundTrip(req) return h.normalTransport.RoundTrip(req)
} }
// InsecureRoundTrip calls the insecure transport // InsecureRoundTrip calls the insecure transport
func (h *HybridTransport) InsecureRoundTrip(req *http.Request) (*http.Response, error) { func (h *HybridTransport) InsecureRoundTrip(req *http.Request) (*http.Response, error) {
u := uuid.New()
log.Println("[Transport insecure] Start upgrade:", u)
defer log.Println("[Transport insecure] Stop upgrade:", u)
return h.insecureTransport.RoundTrip(req) return h.insecureTransport.RoundTrip(req)
} }
// ConnectWebsocket calls the websocket upgrader and thus hijacks the connection // ConnectWebsocket calls the websocket upgrader and thus hijacks the connection
func (h *HybridTransport) ConnectWebsocket(rw http.ResponseWriter, req *http.Request) { func (h *HybridTransport) ConnectWebsocket(rw http.ResponseWriter, req *http.Request) {
u := uuid.New()
log.Println("[Websocket] Start upgrade:", u)
h.ws.Upgrade(rw, req) h.ws.Upgrade(rw, req)
log.Println("[Websocket] Stop upgrade:", u)
} }

View File

@ -1,16 +1,13 @@
package websocket package websocket
import ( import (
"github.com/1f349/violet/logger"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"log"
"net/http" "net/http"
"slices"
"sync" "sync"
"time" "time"
) )
var Logger = logger.Logger.WithPrefix("Violet Websocket")
var upgrader = websocket.Upgrader{ var upgrader = websocket.Upgrader{
HandshakeTimeout: time.Second * 5, HandshakeTimeout: time.Second * 5,
ReadBufferSize: 1024, ReadBufferSize: 1024,
@ -37,7 +34,7 @@ func NewServer() *Server {
func (s *Server) Upgrade(rw http.ResponseWriter, req *http.Request) { func (s *Server) Upgrade(rw http.ResponseWriter, req *http.Request) {
req.URL.Scheme = "ws" req.URL.Scheme = "ws"
Logger.Info("Upgrading request", "url", req.URL, "origin", req.Header.Get("Origin")) log.Printf("[Websocket] Upgrading request to '%s' from '%s'\n", req.URL.String(), req.Header.Get("Origin"))
c, err := upgrader.Upgrade(rw, req, nil) c, err := upgrader.Upgrade(rw, req, nil)
if err != nil { if err != nil {
@ -57,12 +54,12 @@ func (s *Server) Upgrade(rw http.ResponseWriter, req *http.Request) {
s.conns[c.RemoteAddr().String()] = c s.conns[c.RemoteAddr().String()] = c
s.connLock.Unlock() s.connLock.Unlock()
Logger.Info("Dialing", "url", req.URL) log.Printf("[Websocket] Dialing: '%s'\n", req.URL.String())
// dial for internal connection // dial for internal connection
ic, _, err := websocket.DefaultDialer.DialContext(req.Context(), req.URL.String(), filterWebsocketHeaders(req.Header)) ic, _, err := websocket.DefaultDialer.DialContext(req.Context(), req.URL.String(), nil)
if err != nil { if err != nil {
Logger.Info("Failed to dial", "url", req.URL, "err", err) log.Printf("[Websocket] Failed to dial '%s': %s\n", req.URL.String(), err)
s.Remove(c) s.Remove(c)
return return
} }
@ -76,7 +73,7 @@ func (s *Server) Upgrade(rw http.ResponseWriter, req *http.Request) {
go s.wsRelay(d2, ic, c) go s.wsRelay(d2, ic, c)
// wait for done signal and close both connections // wait for done signal and close both connections
Logger.Info("Completed websocket hijacking") log.Println("[Websocket] Completed websocket hijacking")
// waiting until d1 or d2 close then automatically defer close both connections // waiting until d1 or d2 close then automatically defer close both connections
select { select {
@ -85,17 +82,6 @@ func (s *Server) Upgrade(rw http.ResponseWriter, req *http.Request) {
} }
} }
// filterWebsocketHeaders allows specific headers to forward to the underlying websocket connection
func filterWebsocketHeaders(headers http.Header) (out http.Header) {
out = make(http.Header)
for k, v := range headers {
if k == "Origin" {
out[k] = slices.Clone(v)
}
}
return
}
func (s *Server) wsRelay(done chan struct{}, a, b *websocket.Conn) { func (s *Server) wsRelay(done chan struct{}, a, b *websocket.Conn) {
defer func() { defer func() {
close(done) close(done)
@ -103,7 +89,7 @@ func (s *Server) wsRelay(done chan struct{}, a, b *websocket.Conn) {
for { for {
mt, message, err := a.ReadMessage() mt, message, err := a.ReadMessage()
if err != nil { if err != nil {
Logger.Info("Read message", "err", err) log.Println("Websocket read message error: ", err)
return return
} }
if b.WriteMessage(mt, message) != nil { if b.WriteMessage(mt, message) != nil {

20
router/create-tables.sql Normal file
View File

@ -0,0 +1,20 @@
CREATE TABLE IF NOT EXISTS routes
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
source TEXT UNIQUE,
destination TEXT,
description TEXT,
flags INTEGER DEFAULT 0,
active INTEGER DEFAULT 1
);
CREATE TABLE IF NOT EXISTS redirects
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
source TEXT UNIQUE,
destination TEXT,
description TEXT,
flags INTEGER DEFAULT 0,
code INTEGER DEFAULT 0,
active INTEGER DEFAULT 1
);

View File

@ -1,33 +1,35 @@
package router package router
import ( import (
"context" "database/sql"
_ "embed" _ "embed"
"github.com/1f349/violet/database"
"github.com/1f349/violet/logger"
"github.com/1f349/violet/proxy" "github.com/1f349/violet/proxy"
"github.com/1f349/violet/target" "github.com/1f349/violet/target"
"github.com/mrmelon54/rescheduler" "github.com/MrMelon54/rescheduler"
"log"
"net/http" "net/http"
"strings" "strings"
"sync" "sync"
) )
var Logger = logger.Logger.WithPrefix("Violet Manager")
// Manager is a database and mutex wrap around router allowing it to be // Manager is a database and mutex wrap around router allowing it to be
// dynamically regenerated after updating the database of routes. // dynamically regenerated after updating the database of routes.
type Manager struct { type Manager struct {
db *database.Queries db *sql.DB
s *sync.RWMutex s *sync.RWMutex
r *Router r *Router
p *proxy.HybridTransport p *proxy.HybridTransport
z *rescheduler.Rescheduler z *rescheduler.Rescheduler
} }
var (
//go:embed create-tables.sql
createTables string
)
// NewManager create a new manager, initialises the routes and redirects tables // NewManager create a new manager, initialises the routes and redirects tables
// in the database and runs a first time compile. // in the database and runs a first time compile.
func NewManager(db *database.Queries, proxy *proxy.HybridTransport) *Manager { func NewManager(db *sql.DB, proxy *proxy.HybridTransport) *Manager {
m := &Manager{ m := &Manager{
db: db, db: db,
s: &sync.RWMutex{}, s: &sync.RWMutex{},
@ -35,6 +37,13 @@ func NewManager(db *database.Queries, proxy *proxy.HybridTransport) *Manager {
p: proxy, p: proxy,
} }
m.z = rescheduler.NewRescheduler(m.threadCompile) m.z = rescheduler.NewRescheduler(m.threadCompile)
// init routes table
_, err := m.db.Exec(createTables)
if err != nil {
log.Printf("[WARN] Failed to generate tables\n")
return nil
}
return m return m
} }
@ -56,7 +65,7 @@ func (m *Manager) threadCompile() {
// compile router and check errors // compile router and check errors
err := m.internalCompile(router) err := m.internalCompile(router)
if err != nil { if err != nil {
Logger.Info("Compile failed", "err", err) log.Printf("[Manager] Compile failed: %s\n", err)
return return
} }
@ -69,39 +78,67 @@ func (m *Manager) threadCompile() {
// internalCompile is a hidden internal method for querying the database during // internalCompile is a hidden internal method for querying the database during
// the Compile() method. // the Compile() method.
func (m *Manager) internalCompile(router *Router) error { func (m *Manager) internalCompile(router *Router) error {
Logger.Info("Updating routes from database") log.Println("[Manager] Updating routes from database")
// sql or something? // sql or something?
routeRows, err := m.db.GetActiveRoutes(context.Background()) rows, err := m.db.Query(`SELECT source, destination, flags FROM routes WHERE active = 1`)
if err != nil { if err != nil {
return err return err
} }
defer rows.Close()
// loop through rows and scan the options
for rows.Next() {
var (
src, dst string
flags target.Flags
)
err := rows.Scan(&src, &dst, &flags)
if err != nil {
return err
}
for _, row := range routeRows {
router.AddRoute(target.Route{ router.AddRoute(target.Route{
Src: row.Source, Src: src,
Dst: row.Destination, Dst: dst,
Flags: row.Flags.NormaliseRouteFlags(), Flags: flags.NormaliseRouteFlags(),
})
}
// sql or something?
redirectsRows, err := m.db.GetActiveRedirects(context.Background())
if err != nil {
return err
}
for _, row := range redirectsRows {
router.AddRedirect(target.Redirect{
Src: row.Source,
Dst: row.Destination,
Flags: row.Flags.NormaliseRedirectFlags(),
Code: row.Code,
}) })
} }
// check for errors // check for errors
return nil if err := rows.Err(); err != nil {
return err
}
// sql or something?
rows, err = m.db.Query(`SELECT source,destination,flags,code FROM redirects WHERE active = 1`)
if err != nil {
return err
}
defer rows.Close()
// loop through rows and scan the options
for rows.Next() {
var (
src, dst string
flags target.Flags
code int
)
err := rows.Scan(&src, &dst, &flags, &code)
if err != nil {
return err
}
router.AddRedirect(target.Redirect{
Src: src,
Dst: dst,
Flags: flags.NormaliseRedirectFlags(),
Code: code,
})
}
// check for errors
return rows.Err()
} }
func (m *Manager) GetAllRoutes(hosts []string) ([]target.RouteWithActive, error) { func (m *Manager) GetAllRoutes(hosts []string) ([]target.RouteWithActive, error) {
@ -111,20 +148,15 @@ func (m *Manager) GetAllRoutes(hosts []string) ([]target.RouteWithActive, error)
s := make([]target.RouteWithActive, 0) s := make([]target.RouteWithActive, 0)
rows, err := m.db.GetAllRoutes(context.Background()) query, err := m.db.Query(`SELECT source, destination, description, flags, active FROM routes`)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, row := range rows { for query.Next() {
a := target.RouteWithActive{ var a target.RouteWithActive
Route: target.Route{ if err := query.Scan(&a.Src, &a.Dst, &a.Desc, &a.Flags, &a.Active); err != nil {
Src: row.Source, return nil, err
Dst: row.Destination,
Desc: row.Description,
Flags: row.Flags,
},
Active: row.Active,
} }
for _, i := range hosts { for _, i := range hosts {
@ -140,17 +172,13 @@ func (m *Manager) GetAllRoutes(hosts []string) ([]target.RouteWithActive, error)
} }
func (m *Manager) InsertRoute(route target.RouteWithActive) error { func (m *Manager) InsertRoute(route target.RouteWithActive) error {
return m.db.AddRoute(context.Background(), database.AddRouteParams{ _, err := m.db.Exec(`INSERT INTO routes (source, destination, description, flags, active) VALUES (?, ?, ?, ?, ?) ON CONFLICT(source) DO UPDATE SET destination = excluded.destination, description = excluded.description, flags = excluded.flags, active = excluded.active`, route.Src, route.Dst, route.Desc, route.Flags, route.Active)
Source: route.Src, return err
Destination: route.Dst,
Description: route.Desc,
Flags: route.Flags,
Active: route.Active,
})
} }
func (m *Manager) DeleteRoute(source string) error { func (m *Manager) DeleteRoute(source string) error {
return m.db.RemoveRoute(context.Background(), source) _, err := m.db.Exec(`DELETE FROM routes WHERE source = ?`, source)
return err
} }
func (m *Manager) GetAllRedirects(hosts []string) ([]target.RedirectWithActive, error) { func (m *Manager) GetAllRedirects(hosts []string) ([]target.RedirectWithActive, error) {
@ -160,21 +188,15 @@ func (m *Manager) GetAllRedirects(hosts []string) ([]target.RedirectWithActive,
s := make([]target.RedirectWithActive, 0) s := make([]target.RedirectWithActive, 0)
rows, err := m.db.GetAllRedirects(context.Background()) query, err := m.db.Query(`SELECT source, destination, description, flags, code, active FROM redirects`)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, row := range rows { for query.Next() {
a := target.RedirectWithActive{ var a target.RedirectWithActive
Redirect: target.Redirect{ if err := query.Scan(&a.Src, &a.Dst, &a.Desc, &a.Flags, &a.Code, &a.Active); err != nil {
Src: row.Source, return nil, err
Dst: row.Destination,
Desc: row.Description,
Flags: row.Flags,
Code: row.Code,
},
Active: row.Active,
} }
for _, i := range hosts { for _, i := range hosts {
@ -190,18 +212,13 @@ func (m *Manager) GetAllRedirects(hosts []string) ([]target.RedirectWithActive,
} }
func (m *Manager) InsertRedirect(redirect target.RedirectWithActive) error { func (m *Manager) InsertRedirect(redirect target.RedirectWithActive) error {
return m.db.AddRedirect(context.Background(), database.AddRedirectParams{ _, err := m.db.Exec(`INSERT INTO redirects (source, destination, description, flags, code, active) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(source) DO UPDATE SET destination = excluded.destination, description = excluded.description, flags = excluded.flags, code = excluded.code, active = excluded.active`, redirect.Src, redirect.Dst, redirect.Desc, redirect.Flags, redirect.Code, redirect.Active)
Source: redirect.Src, return err
Destination: redirect.Dst,
Description: redirect.Desc,
Flags: redirect.Flags,
Code: redirect.Code,
Active: redirect.Active,
})
} }
func (m *Manager) DeleteRedirect(source string) error { func (m *Manager) DeleteRedirect(source string) error {
return m.db.RemoveRedirect(context.Background(), source) _, err := m.db.Exec(`DELETE FROM redirects WHERE source = ?`, source)
return err
} }
// GenerateHostSearch this should help improve performance // GenerateHostSearch this should help improve performance

View File

@ -1,9 +1,7 @@
package router package router
import ( import (
"context" "database/sql"
"github.com/1f349/violet"
"github.com/1f349/violet/database"
"github.com/1f349/violet/proxy" "github.com/1f349/violet/proxy"
"github.com/1f349/violet/proxy/websocket" "github.com/1f349/violet/proxy/websocket"
"github.com/1f349/violet/target" "github.com/1f349/violet/target"
@ -24,7 +22,7 @@ func (f *fakeTransport) RoundTrip(req *http.Request) (*http.Response, error) {
} }
func TestNewManager(t *testing.T) { func TestNewManager(t *testing.T) {
db, err := violet.InitDB("file:TestNewManager?mode=memory&cache=shared") db, err := sql.Open("sqlite3", "file::memory:?cache=shared")
assert.NoError(t, err) assert.NoError(t, err)
ft := &fakeTransport{} ft := &fakeTransport{}
@ -41,13 +39,7 @@ func TestNewManager(t *testing.T) {
assert.Equal(t, http.StatusTeapot, res.StatusCode) assert.Equal(t, http.StatusTeapot, res.StatusCode)
assert.Nil(t, ft.req) assert.Nil(t, ft.req)
err = db.AddRoute(context.Background(), database.AddRouteParams{ _, err = db.Exec(`INSERT INTO routes (source, destination, flags, active) VALUES (?,?,?,1)`, "*.example.com", "127.0.0.1:8080", target.FlagAbs|target.FlagForwardHost|target.FlagForwardAddr)
Source: "*.example.com",
Destination: "127.0.0.1:8080",
Description: "",
Flags: target.FlagAbs | target.FlagForwardHost | target.FlagForwardAddr,
Active: true,
})
assert.NoError(t, err) assert.NoError(t, err)
assert.NoError(t, m.internalCompile(m.r)) assert.NoError(t, m.internalCompile(m.r))
@ -60,8 +52,10 @@ func TestNewManager(t *testing.T) {
} }
func TestManager_GetAllRoutes(t *testing.T) { func TestManager_GetAllRoutes(t *testing.T) {
db, err := violet.InitDB("file:TestManager_GetAllRoutes?mode=memory&cache=shared") db, err := sql.Open("sqlite3", "file:GetAllRoutes?mode=memory&cache=shared")
assert.NoError(t, err) if err != nil {
t.Fatal(err)
}
m := NewManager(db, nil) m := NewManager(db, nil)
a := []error{ a := []error{
m.InsertRoute(target.RouteWithActive{Route: target.Route{Src: "example.com"}, Active: true}), m.InsertRoute(target.RouteWithActive{Route: target.Route{Src: "example.com"}, Active: true}),
@ -91,8 +85,10 @@ func TestManager_GetAllRoutes(t *testing.T) {
} }
func TestManager_GetAllRedirects(t *testing.T) { func TestManager_GetAllRedirects(t *testing.T) {
db, err := violet.InitDB("file:TestManager_GetAllRedirects?mode=memory&cache=shared") db, err := sql.Open("sqlite3", "file:GetAllRedirects?mode=memory&cache=shared")
assert.NoError(t, err) if err != nil {
t.Fatal(err)
}
m := NewManager(db, nil) m := NewManager(db, nil)
a := []error{ a := []error{
m.InsertRedirect(target.RedirectWithActive{Redirect: target.Redirect{Src: "example.com"}, Active: true}), m.InsertRedirect(target.RedirectWithActive{Redirect: target.Redirect{Src: "example.com"}, Active: true}),

View File

@ -5,7 +5,7 @@ import (
"github.com/1f349/violet/proxy" "github.com/1f349/violet/proxy"
"github.com/1f349/violet/target" "github.com/1f349/violet/target"
"github.com/1f349/violet/utils" "github.com/1f349/violet/utils"
"github.com/mrmelon54/trie" "github.com/MrMelon54/trie"
"net/http" "net/http"
"strings" "strings"
) )

View File

@ -5,7 +5,7 @@ import (
"github.com/1f349/violet/proxy" "github.com/1f349/violet/proxy"
"github.com/1f349/violet/proxy/websocket" "github.com/1f349/violet/proxy/websocket"
"github.com/1f349/violet/target" "github.com/1f349/violet/target"
"github.com/mrmelon54/trie" "github.com/MrMelon54/trie"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"

View File

@ -7,8 +7,6 @@ import (
"github.com/1f349/violet/servers/conf" "github.com/1f349/violet/servers/conf"
"github.com/1f349/violet/utils" "github.com/1f349/violet/utils"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"net/http" "net/http"
"time" "time"
) )
@ -17,15 +15,12 @@ import (
// endpoints for the software // endpoints for the software
// //
// `/compile` - reloads all domains, routes and redirects // `/compile` - reloads all domains, routes and redirects
func NewApiServer(conf *conf.Conf, compileTarget utils.MultiCompilable, registry *prometheus.Registry) *http.Server { func NewApiServer(conf *conf.Conf, compileTarget utils.MultiCompilable) *http.Server {
r := httprouter.New() r := httprouter.New()
r.GET("/", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params) { r.GET("/", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params) {
http.Error(rw, "Violet API Endpoint", http.StatusOK) http.Error(rw, "Violet API Endpoint", http.StatusOK)
}) })
r.GET("/metrics", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params) {
promhttp.HandlerFor(registry, promhttp.HandlerOpts{}).ServeHTTP(rw, req)
})
// Endpoint for compile action // Endpoint for compile action
r.POST("/compile", checkAuthWithPerm(conf.Signer, "violet:compile", func(rw http.ResponseWriter, req *http.Request, _ httprouter.Params, b AuthClaims) { r.POST("/compile", checkAuthWithPerm(conf.Signer, "violet:compile", func(rw http.ResponseWriter, req *http.Request, _ httprouter.Params, b AuthClaims) {
@ -48,6 +43,7 @@ func NewApiServer(conf *conf.Conf, compileTarget utils.MultiCompilable, registry
// Create and run http server // Create and run http server
return &http.Server{ return &http.Server{
Addr: conf.ApiListen,
Handler: r, Handler: r,
ReadTimeout: time.Minute, ReadTimeout: time.Minute,
ReadHeaderTimeout: time.Minute, ReadHeaderTimeout: time.Minute,

View File

@ -17,7 +17,7 @@ func TestNewApiServer_Compile(t *testing.T) {
Signer: fake.SnakeOilProv, Signer: fake.SnakeOilProv,
} }
f := &fake.Compilable{} f := &fake.Compilable{}
srv := NewApiServer(apiConf, utils.MultiCompilable{f}, nil) srv := NewApiServer(apiConf, utils.MultiCompilable{f})
req, err := http.NewRequest(http.MethodPost, "https://example.com/compile", nil) req, err := http.NewRequest(http.MethodPost, "https://example.com/compile", nil)
assert.NoError(t, err) assert.NoError(t, err)
@ -43,7 +43,7 @@ func TestNewApiServer_AcmeChallenge_Put(t *testing.T) {
Acme: utils.NewAcmeChallenge(), Acme: utils.NewAcmeChallenge(),
Signer: fake.SnakeOilProv, Signer: fake.SnakeOilProv,
} }
srv := NewApiServer(apiConf, utils.MultiCompilable{}, nil) srv := NewApiServer(apiConf, utils.MultiCompilable{})
acmeKey := fake.GenSnakeOilKey("violet:acme-challenge") acmeKey := fake.GenSnakeOilKey("violet:acme-challenge")
// Valid domain // Valid domain
@ -87,7 +87,7 @@ func TestNewApiServer_AcmeChallenge_Delete(t *testing.T) {
Acme: utils.NewAcmeChallenge(), Acme: utils.NewAcmeChallenge(),
Signer: fake.SnakeOilProv, Signer: fake.SnakeOilProv,
} }
srv := NewApiServer(apiConf, utils.MultiCompilable{}, nil) srv := NewApiServer(apiConf, utils.MultiCompilable{})
acmeKey := fake.GenSnakeOilKey("violet:acme-challenge") acmeKey := fake.GenSnakeOilKey("violet:acme-challenge")
// Valid domain // Valid domain

View File

@ -3,11 +3,11 @@ package api
import ( import (
"encoding/json" "encoding/json"
"github.com/1f349/mjwt" "github.com/1f349/mjwt"
"github.com/1f349/violet/logger"
"github.com/1f349/violet/router" "github.com/1f349/violet/router"
"github.com/1f349/violet/target" "github.com/1f349/violet/target"
"github.com/1f349/violet/utils" "github.com/1f349/violet/utils"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
"log"
"net/http" "net/http"
"strings" "strings"
) )
@ -19,7 +19,7 @@ func SetupTargetApis(r *httprouter.Router, verify mjwt.Verifier, manager *router
routes, err := manager.GetAllRoutes(domains) routes, err := manager.GetAllRoutes(domains)
if err != nil { if err != nil {
logger.Logger.Infof("Failed to get routes from database: %s\n", err) log.Printf("[Violet] Failed to get routes from database: %s\n", err)
apiError(rw, http.StatusInternalServerError, "Failed to get routes from database") apiError(rw, http.StatusInternalServerError, "Failed to get routes from database")
return return
} }
@ -29,7 +29,7 @@ func SetupTargetApis(r *httprouter.Router, verify mjwt.Verifier, manager *router
r.POST("/route", parseJsonAndCheckOwnership[routeSource](verify, "route", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims, t routeSource) { r.POST("/route", parseJsonAndCheckOwnership[routeSource](verify, "route", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims, t routeSource) {
err := manager.InsertRoute(target.RouteWithActive(t)) err := manager.InsertRoute(target.RouteWithActive(t))
if err != nil { if err != nil {
logger.Logger.Infof("Failed to insert route into database: %s\n", err) log.Printf("[Violet] Failed to insert route into database: %s\n", err)
apiError(rw, http.StatusInternalServerError, "Failed to insert route into database") apiError(rw, http.StatusInternalServerError, "Failed to insert route into database")
return return
} }
@ -38,7 +38,7 @@ func SetupTargetApis(r *httprouter.Router, verify mjwt.Verifier, manager *router
r.DELETE("/route", parseJsonAndCheckOwnership[sourceJson](verify, "route", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims, t sourceJson) { r.DELETE("/route", parseJsonAndCheckOwnership[sourceJson](verify, "route", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims, t sourceJson) {
err := manager.DeleteRoute(t.Src) err := manager.DeleteRoute(t.Src)
if err != nil { if err != nil {
logger.Logger.Infof("Failed to delete route from database: %s\n", err) log.Printf("[Violet] Failed to delete route from database: %s\n", err)
apiError(rw, http.StatusInternalServerError, "Failed to delete route from database") apiError(rw, http.StatusInternalServerError, "Failed to delete route from database")
return return
} }
@ -51,7 +51,7 @@ func SetupTargetApis(r *httprouter.Router, verify mjwt.Verifier, manager *router
redirects, err := manager.GetAllRedirects(domains) redirects, err := manager.GetAllRedirects(domains)
if err != nil { if err != nil {
logger.Logger.Infof("Failed to get redirects from database: %s\n", err) log.Printf("[Violet] Failed to get redirects from database: %s\n", err)
apiError(rw, http.StatusInternalServerError, "Failed to get redirects from database") apiError(rw, http.StatusInternalServerError, "Failed to get redirects from database")
return return
} }
@ -61,7 +61,7 @@ func SetupTargetApis(r *httprouter.Router, verify mjwt.Verifier, manager *router
r.POST("/redirect", parseJsonAndCheckOwnership[redirectSource](verify, "redirect", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims, t redirectSource) { r.POST("/redirect", parseJsonAndCheckOwnership[redirectSource](verify, "redirect", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims, t redirectSource) {
err := manager.InsertRedirect(target.RedirectWithActive(t)) err := manager.InsertRedirect(target.RedirectWithActive(t))
if err != nil { if err != nil {
logger.Logger.Infof("Failed to insert redirect into database: %s\n", err) log.Printf("[Violet] Failed to insert redirect into database: %s\n", err)
apiError(rw, http.StatusInternalServerError, "Failed to insert redirect into database") apiError(rw, http.StatusInternalServerError, "Failed to insert redirect into database")
return return
} }
@ -70,7 +70,7 @@ func SetupTargetApis(r *httprouter.Router, verify mjwt.Verifier, manager *router
r.DELETE("/redirect", parseJsonAndCheckOwnership[sourceJson](verify, "redirect", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims, t sourceJson) { r.DELETE("/redirect", parseJsonAndCheckOwnership[sourceJson](verify, "redirect", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims, t sourceJson) {
err := manager.DeleteRedirect(t.Src) err := manager.DeleteRedirect(t.Src)
if err != nil { if err != nil {
logger.Logger.Infof("Failed to delete redirect from database: %s\n", err) log.Printf("[Violet] Failed to delete redirect from database: %s\n", err)
apiError(rw, http.StatusInternalServerError, "Failed to delete redirect from database") apiError(rw, http.StatusInternalServerError, "Failed to delete redirect from database")
return return
} }

View File

@ -1,8 +1,8 @@
package conf package conf
import ( import (
"database/sql"
"github.com/1f349/mjwt" "github.com/1f349/mjwt"
"github.com/1f349/violet/database"
errorPages "github.com/1f349/violet/error-pages" errorPages "github.com/1f349/violet/error-pages"
"github.com/1f349/violet/favicons" "github.com/1f349/violet/favicons"
"github.com/1f349/violet/router" "github.com/1f349/violet/router"
@ -11,13 +11,16 @@ import (
// Conf stores the shared configuration for the API, HTTP and HTTPS servers. // Conf stores the shared configuration for the API, HTTP and HTTPS servers.
type Conf struct { type Conf struct {
RateLimit uint64 // rate limit per minute ApiListen string // api server listen address
DB *database.Queries HttpListen string // http server listen address
Domains utils.DomainProvider HttpsListen string // https server listen address
Acme utils.AcmeChallengeProvider RateLimit uint64 // rate limit per minute
Certs utils.CertProvider DB *sql.DB
Favicons *favicons.Favicons Domains utils.DomainProvider
Signer mjwt.Verifier Acme utils.AcmeChallengeProvider
ErrorPages *errorPages.ErrorPages Certs utils.CertProvider
Router *router.Manager Favicons *favicons.Favicons
Signer mjwt.Verifier
ErrorPages *errorPages.ErrorPages
Router *router.Manager
} }

View File

@ -3,10 +3,8 @@ package servers
import ( import (
"fmt" "fmt"
"github.com/1f349/violet/servers/conf" "github.com/1f349/violet/servers/conf"
"github.com/1f349/violet/servers/metrics"
"github.com/1f349/violet/utils" "github.com/1f349/violet/utils"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
"github.com/prometheus/client_golang/prometheus"
"net/http" "net/http"
"net/url" "net/url"
"time" "time"
@ -17,9 +15,13 @@ import (
// //
// `/.well-known/acme-challenge/{token}` is used for outputting answers for // `/.well-known/acme-challenge/{token}` is used for outputting answers for
// acme challenges, this is used for Let's Encrypt HTTP verification. // acme challenges, this is used for Let's Encrypt HTTP verification.
func NewHttpServer(httpsPort uint16, conf *conf.Conf, registry *prometheus.Registry) *http.Server { func NewHttpServer(conf *conf.Conf) *http.Server {
r := httprouter.New() r := httprouter.New()
var secureExtend string var secureExtend string
_, httpsPort, ok := utils.SplitDomainPort(conf.HttpsListen, 443)
if !ok {
httpsPort = 443
}
if httpsPort != 443 { if httpsPort != 443 {
secureExtend = fmt.Sprintf(":%d", httpsPort) secureExtend = fmt.Sprintf(":%d", httpsPort)
} }
@ -59,16 +61,10 @@ func NewHttpServer(httpsPort uint16, conf *conf.Conf, registry *prometheus.Regis
utils.FastRedirect(rw, req, u.String(), http.StatusPermanentRedirect) utils.FastRedirect(rw, req, u.String(), http.StatusPermanentRedirect)
}) })
metricsMiddleware := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
r.ServeHTTP(rw, req)
})
if registry != nil {
metricsMiddleware = metrics.New(registry, nil).WrapHandler("violet-http-insecure", r)
}
// Create and run http server // Create and run http server
return &http.Server{ return &http.Server{
Handler: metricsMiddleware, Addr: conf.HttpListen,
Handler: r,
ReadTimeout: time.Minute, ReadTimeout: time.Minute,
ReadHeaderTimeout: time.Minute, ReadHeaderTimeout: time.Minute,
WriteTimeout: time.Minute, WriteTimeout: time.Minute,

View File

@ -18,7 +18,7 @@ func TestNewHttpServer_AcmeChallenge(t *testing.T) {
Acme: utils.NewAcmeChallenge(), Acme: utils.NewAcmeChallenge(),
Signer: fake.SnakeOilProv, Signer: fake.SnakeOilProv,
} }
srv := NewHttpServer(443, httpConf, nil) srv := NewHttpServer(httpConf)
httpConf.Acme.Put("example.com", "456", "456def") httpConf.Acme.Put("example.com", "456", "456def")
req, err := http.NewRequest(http.MethodGet, "https://example.com/.well-known/acme-challenge/456", nil) req, err := http.NewRequest(http.MethodGet, "https://example.com/.well-known/acme-challenge/456", nil)

View File

@ -4,13 +4,11 @@ import (
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"github.com/1f349/violet/favicons" "github.com/1f349/violet/favicons"
"github.com/1f349/violet/logger"
"github.com/1f349/violet/servers/conf" "github.com/1f349/violet/servers/conf"
"github.com/1f349/violet/servers/metrics"
"github.com/1f349/violet/utils" "github.com/1f349/violet/utils"
"github.com/prometheus/client_golang/prometheus"
"github.com/sethvargo/go-limiter/httplimit" "github.com/sethvargo/go-limiter/httplimit"
"github.com/sethvargo/go-limiter/memorystore" "github.com/sethvargo/go-limiter/memorystore"
"log"
"net/http" "net/http"
"path" "path"
"runtime" "runtime"
@ -19,41 +17,31 @@ import (
// NewHttpsServer creates and runs a http server containing the public https // NewHttpsServer creates and runs a http server containing the public https
// endpoints for the reverse proxy. // endpoints for the reverse proxy.
func NewHttpsServer(conf *conf.Conf, registry *prometheus.Registry) *http.Server { func NewHttpsServer(conf *conf.Conf) *http.Server {
r := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { r := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
logger.Logger.Debug("Request", "method", req.Method, "url", req.URL, "remote", req.RemoteAddr, "host", req.Host, "length", req.ContentLength, "goroutine", runtime.NumGoroutine()) log.Printf("[Debug] Request: %s - '%s' - '%s' - '%s' - len: %d - thread: %d\n", req.Method, req.URL.String(), req.RemoteAddr, req.Host, req.ContentLength, runtime.NumGoroutine())
conf.Router.ServeHTTP(rw, req) conf.Router.ServeHTTP(rw, req)
}) })
favMiddleware := setupFaviconMiddleware(conf.Favicons, r) favMiddleware := setupFaviconMiddleware(conf.Favicons, r)
rateLimiter := setupRateLimiter(conf.RateLimit, favMiddleware)
metricsMeta := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
r.ServeHTTP(rw, req)
})
if registry != nil {
metricsMiddleware := metrics.New(registry, nil).WrapHandler("violet-https", favMiddleware)
metricsMeta = func(rw http.ResponseWriter, req *http.Request) {
metricsMiddleware.ServeHTTP(rw, metrics.AddHostCtx(req))
}
}
rateLimiter := setupRateLimiter(conf.RateLimit, metricsMeta)
hsts := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.Header().Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
rateLimiter.ServeHTTP(rw, req)
})
return &http.Server{ return &http.Server{
Handler: hsts, Addr: conf.HttpsListen,
Handler: http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.Header().Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
rateLimiter.ServeHTTP(rw, req)
}),
TLSConfig: &tls.Config{ TLSConfig: &tls.Config{
// Suggested by https://ssl-config.mozilla.org/#server=go&version=1.21.5&config=intermediate // Suggested by https://ssl-config.mozilla.org/#server=go&version=1.21.5&config=intermediate
MinVersion: tls.VersionTLS12, MinVersion: tls.VersionTLS12,
CipherSuites: []uint16{ CipherSuites: []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
}, },
GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) { GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
// error out on invalid domains // error out on invalid domains
if !conf.Domains.IsValid(info.ServerName) { if !conf.Domains.IsValid(info.ServerName) {
@ -87,13 +75,13 @@ func setupRateLimiter(rateLimit uint64, next http.Handler) http.Handler {
Interval: time.Minute, Interval: time.Minute,
}) })
if err != nil { if err != nil {
logger.Logger.Fatal("Failed to initialize memory store", "err", err) log.Fatalln(err)
} }
// create a middleware using ips as the key for rate limits // create a middleware using ips as the key for rate limits
middleware, err := httplimit.NewMiddleware(store, httplimit.IPKeyFunc()) middleware, err := httplimit.NewMiddleware(store, httplimit.IPKeyFunc())
if err != nil { if err != nil {
logger.Logger.Fatal("Failed to initialize httplimit middleware", "err", err) log.Fatalln(err)
} }
return middleware.Handle(next) return middleware.Handle(next)
} }

View File

@ -1,7 +1,7 @@
package servers package servers
import ( import (
"github.com/1f349/violet" "database/sql"
"github.com/1f349/violet/certs" "github.com/1f349/violet/certs"
"github.com/1f349/violet/proxy" "github.com/1f349/violet/proxy"
"github.com/1f349/violet/proxy/websocket" "github.com/1f349/violet/proxy/websocket"
@ -25,7 +25,7 @@ func (f *fakeTransport) RoundTrip(_ *http.Request) (*http.Response, error) {
} }
func TestNewHttpsServer_RateLimit(t *testing.T) { func TestNewHttpsServer_RateLimit(t *testing.T) {
db, err := violet.InitDB("file:TestNewHttpsServer_RateLimit?mode=memory&cache=shared") db, err := sql.Open("sqlite3", "file::memory:?cache=shared")
assert.NoError(t, err) assert.NoError(t, err)
ft := &fakeTransport{} ft := &fakeTransport{}
@ -36,7 +36,7 @@ func TestNewHttpsServer_RateLimit(t *testing.T) {
Signer: fake.SnakeOilProv, Signer: fake.SnakeOilProv,
Router: router.NewManager(db, proxy.NewHybridTransportWithCalls(ft, ft, &websocket.Server{})), Router: router.NewManager(db, proxy.NewHybridTransportWithCalls(ft, ft, &websocket.Server{})),
} }
srv := NewHttpsServer(httpsConf, nil) srv := NewHttpsServer(httpsConf)
req, err := http.NewRequest(http.MethodGet, "https://example.com", nil) req, err := http.NewRequest(http.MethodGet, "https://example.com", nil)
req.RemoteAddr = "127.0.0.1:1447" req.RemoteAddr = "127.0.0.1:1447"

View File

@ -1,118 +0,0 @@
package metrics
import (
"context"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/prometheus/client_golang/prometheus/promhttp"
"net/http"
)
// Copyright 2022 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package metrics is adapted from
// https://github.com/bwplotka/correlator/tree/main/examples/observability/ping/pkg/httpinstrumentation
// https://github.com/prometheus/client_golang/blob/main/examples/middleware/httpmiddleware/httpmiddleware.go
type Middleware interface {
// WrapHandler wraps the given HTTP handler for instrumentation.
WrapHandler(handlerName string, handler http.Handler) http.HandlerFunc
}
type middleware struct {
buckets []float64
registry prometheus.Registerer
}
// WrapHandler wraps the given HTTP handler for instrumentation:
// It registers four metric collectors (if not already done) and reports HTTP
// metrics to the (newly or already) registered collectors.
// Each has a constant label named "handler" with the provided handlerName as
// value.
func (m *middleware) WrapHandler(handlerName string, handler http.Handler) http.HandlerFunc {
reg := prometheus.WrapRegistererWith(prometheus.Labels{"handler": handlerName}, m.registry)
requestsTotal := promauto.With(reg).NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Tracks the number of HTTP requests.",
}, []string{"method", "code", "host"},
)
requestDuration := promauto.With(reg).NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "Tracks the latencies for HTTP requests.",
Buckets: m.buckets,
},
[]string{"method", "code", "host"},
)
requestSize := promauto.With(reg).NewSummaryVec(
prometheus.SummaryOpts{
Name: "http_request_size_bytes",
Help: "Tracks the size of HTTP requests.",
},
[]string{"method", "code", "host"},
)
responseSize := promauto.With(reg).NewSummaryVec(
prometheus.SummaryOpts{
Name: "http_response_size_bytes",
Help: "Tracks the size of HTTP responses.",
},
[]string{"method", "code", "host"},
)
hostCtxGetter := promhttp.WithLabelFromCtx("host", func(ctx context.Context) string {
s, _ := ctx.Value(hostCtxKey(0)).(string)
return s
})
// Wraps the provided http.Handler to observe the request result with the provided metrics.
base := promhttp.InstrumentHandlerCounter(
requestsTotal,
promhttp.InstrumentHandlerDuration(
requestDuration,
promhttp.InstrumentHandlerRequestSize(
requestSize,
promhttp.InstrumentHandlerResponseSize(
responseSize,
handler,
hostCtxGetter,
),
hostCtxGetter,
),
hostCtxGetter,
),
hostCtxGetter,
)
return base.ServeHTTP
}
// New returns a Middleware interface.
func New(registry prometheus.Registerer, buckets []float64) Middleware {
if buckets == nil {
buckets = prometheus.ExponentialBuckets(0.1, 1.5, 5)
}
return &middleware{
buckets: buckets,
registry: registry,
}
}
type hostCtxKey uint8
func AddHostCtx(req *http.Request) *http.Request {
return req.WithContext(context.WithValue(req.Context(), hostCtxKey(0), req.Host))
}

View File

@ -1,15 +0,0 @@
version: "2"
sql:
- engine: sqlite
queries: database/queries
schema: database/migrations
gen:
go:
package: "database"
out: "database"
emit_json_tags: true
overrides:
- column: "routes.flags"
go_type: "github.com/1f349/violet/target.Flags"
- column: "redirects.flags"
go_type: "github.com/1f349/violet/target.Flags"

View File

@ -16,7 +16,7 @@ type Redirect struct {
Dst string `json:"dst"` // redirect destination Dst string `json:"dst"` // redirect destination
Desc string `json:"desc"` // description for admin panel use Desc string `json:"desc"` // description for admin panel use
Flags Flags `json:"flags"` // extra flags Flags Flags `json:"flags"` // extra flags
Code int64 `json:"code"` // status code used to redirect Code int `json:"code"` // status code used to redirect
} }
type RedirectWithActive struct { type RedirectWithActive struct {
@ -78,7 +78,7 @@ func (r Redirect) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
} }
// use fast redirect for speed // use fast redirect for speed
utils.FastRedirect(rw, req, u.String(), int(code)) utils.FastRedirect(rw, req, u.String(), code)
} }
// String outputs a debug string for the redirect. // String outputs a debug string for the redirect.

View File

@ -35,7 +35,7 @@ func TestRedirect_ServeHTTP(t *testing.T) {
res := httptest.NewRecorder() res := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "https://www.example.com/hello/world", nil) req := httptest.NewRequest(http.MethodGet, "https://www.example.com/hello/world", nil)
i.ServeHTTP(res, req) i.ServeHTTP(res, req)
assert.Equal(t, i.Code, int64(res.Code)) assert.Equal(t, i.Code, res.Code)
assert.Equal(t, i.target, res.Header().Get("Location")) assert.Equal(t, i.target, res.Header().Get("Location"))
} }
} }

View File

@ -2,13 +2,14 @@ package target
import ( import (
"fmt" "fmt"
"github.com/1f349/violet/logger"
"github.com/1f349/violet/proxy" "github.com/1f349/violet/proxy"
"github.com/1f349/violet/utils" "github.com/1f349/violet/utils"
"github.com/google/uuid"
websocket2 "github.com/gorilla/websocket" websocket2 "github.com/gorilla/websocket"
"github.com/rs/cors" "github.com/rs/cors"
"golang.org/x/net/http/httpguts" "golang.org/x/net/http/httpguts"
"io" "io"
"log"
"net" "net"
"net/http" "net/http"
"net/textproto" "net/textproto"
@ -17,13 +18,10 @@ import (
"strings" "strings"
) )
var Logger = logger.Logger.WithPrefix("Violet Serve Route")
// serveApiCors outputs the cors headers to make APIs work. // serveApiCors outputs the cors headers to make APIs work.
var serveApiCors = cors.New(cors.Options{ var serveApiCors = cors.New(cors.Options{
// allow all origins for api requests AllowedOrigins: []string{"*"}, // allow all origins for api requests
AllowOriginFunc: func(origin string) bool { return true }, AllowedHeaders: []string{"Content-Type", "Authorization"},
AllowedHeaders: []string{"Content-Type", "Authorization"},
AllowedMethods: []string{ AllowedMethods: []string{
http.MethodGet, http.MethodGet,
http.MethodHead, http.MethodHead,
@ -129,7 +127,8 @@ func (r Route) internalServeHTTP(rw http.ResponseWriter, req *http.Request) {
// create the internal request // create the internal request
req2, err := http.NewRequest(req.Method, u.String(), req.Body) req2, err := http.NewRequest(req.Method, u.String(), req.Body)
if err != nil { if err != nil {
utils.RespondVioletError(rw, http.StatusBadGateway, "Invalid request for proxy") log.Printf("[ServeRoute::ServeHTTP()] Error generating new request: %s\n", err)
utils.RespondVioletError(rw, http.StatusBadGateway, "error generating new request")
return return
} }
@ -179,8 +178,8 @@ func (r Route) internalServeHTTP(rw http.ResponseWriter, req *http.Request) {
resp, err = r.Proxy.SecureRoundTrip(req2) resp, err = r.Proxy.SecureRoundTrip(req2)
} }
if err != nil { if err != nil {
Logger.Warn("Error receiving internal round trip response", "route src", r.Src, "url", req2.URL.String(), "err", err) log.Printf("[ServeRoute::ServeHTTP()] Error receiving internal round trip response: %s\n", err)
utils.RespondVioletError(rw, http.StatusBadGateway, "Error receiving internal round trip response") utils.RespondVioletError(rw, http.StatusBadGateway, "error receiving internal round trip response")
return return
} }
@ -190,8 +189,9 @@ func (r Route) internalServeHTTP(rw http.ResponseWriter, req *http.Request) {
} }
if resp.StatusCode == http.StatusLoopDetected { if resp.StatusCode == http.StatusLoopDetected {
Logger.Warn("Loop Detected", "method", req.Method, "url", req.URL, "url2", req2.URL.String()) u := uuid.New()
utils.RespondVioletError(rw, http.StatusLoopDetected, "Error loop detected") log.Printf("[ServeRoute::ServeHTTP()] Loop Detected: %s %s '%s' -> '%s'\n", u, req.Method, req.URL.String(), req2.URL.String())
utils.RespondVioletError(rw, http.StatusLoopDetected, "error loop detected: "+u.String())
return return
} }
@ -222,7 +222,7 @@ func (r Route) internalReverseProxyMeta(rw http.ResponseWriter, req, req2 *http.
reqUpType := upgradeType(req2.Header) reqUpType := upgradeType(req2.Header)
if !asciiIsPrint(reqUpType) { if !asciiIsPrint(reqUpType) {
utils.RespondVioletError(rw, http.StatusBadRequest, fmt.Sprintf("Invalid protocol %s", reqUpType)) utils.RespondVioletError(rw, http.StatusBadRequest, fmt.Sprintf("client tried to switch to invalid protocol %q", reqUpType))
return true return true
} }
removeHopByHopHeaders(req2.Header) removeHopByHopHeaders(req2.Header)

View File

@ -90,7 +90,7 @@ func TestRoute_ServeHTTP_Cors(t *testing.T) {
assert.Equal(t, http.MethodOptions, pt.req.Method) assert.Equal(t, http.MethodOptions, pt.req.Method)
assert.Equal(t, "http://1.1.1.1:8080/hello/test", pt.req.URL.String()) assert.Equal(t, "http://1.1.1.1:8080/hello/test", pt.req.URL.String())
assert.Equal(t, "Origin", res.Header().Get("Vary")) assert.Equal(t, "Origin", res.Header().Get("Vary"))
assert.Equal(t, "https://test.example.com", res.Header().Get("Access-Control-Allow-Origin")) assert.Equal(t, "*", res.Header().Get("Access-Control-Allow-Origin"))
assert.Equal(t, "true", res.Header().Get("Access-Control-Allow-Credentials")) assert.Equal(t, "true", res.Header().Get("Access-Control-Allow-Credentials"))
assert.Equal(t, "Origin", res.Header().Get("Vary")) assert.Equal(t, "Origin", res.Header().Get("Vary"))
} }

View File

@ -2,34 +2,33 @@ package utils
import ( import (
"errors" "errors"
"github.com/charmbracelet/log" "log"
"net"
"net/http" "net/http"
"strings" "strings"
) )
// logHttpServerError is the internal function powering the logging in // logHttpServerError is the internal function powering the logging in
// RunBackgroundHttp and RunBackgroundHttps. // RunBackgroundHttp and RunBackgroundHttps.
func logHttpServerError(logger *log.Logger, err error) { func logHttpServerError(prefix string, err error) {
if err != nil { if err != nil {
if errors.Is(err, http.ErrServerClosed) { if errors.Is(err, http.ErrServerClosed) {
logger.Info("The http server shutdown successfully") log.Printf("[%s] The http server shutdown successfully\n", prefix)
} else { } else {
logger.Info("Error trying to host the http server", "err", err.Error()) log.Printf("[%s] Error trying to host the http server: %s\n", prefix, err.Error())
} }
} }
} }
// RunBackgroundHttp runs a http server and logs when the server closes or // RunBackgroundHttp runs a http server and logs when the server closes or
// errors. // errors.
func RunBackgroundHttp(logger *log.Logger, s *http.Server, ln net.Listener) { func RunBackgroundHttp(prefix string, s *http.Server) {
logHttpServerError(logger, s.Serve(ln)) logHttpServerError(prefix, s.ListenAndServe())
} }
// RunBackgroundHttps runs a http server with TLS encryption and logs when the // RunBackgroundHttps runs a http server with TLS encryption and logs when the
// server closes or errors. // server closes or errors.
func RunBackgroundHttps(logger *log.Logger, s *http.Server, ln net.Listener) { func RunBackgroundHttps(prefix string, s *http.Server) {
logHttpServerError(logger, s.ServeTLS(ln, "", "")) logHttpServerError(prefix, s.ListenAndServeTLS("", ""))
} }
// GetBearer returns the bearer from the Authorization header or an empty string // GetBearer returns the bearer from the Authorization header or an empty string