mirror of
https://github.com/1f349/violet.git
synced 2025-04-14 06:45:59 +01:00
Compare commits
21 Commits
Author | SHA1 | Date | |
---|---|---|---|
d3d6782b22 | |||
0f095056d4 | |||
aa77dccaaf | |||
ecee594219 | |||
d7b7721378 | |||
3e86b91ec3 | |||
f442409ebf | |||
8aa82303ce | |||
1f4f4414d5 | |||
a8db73d957 | |||
1181fde508 | |||
900203b560 | |||
69bce2d12d | |||
a13db89c44 | |||
e901a73129 | |||
333394cf89 | |||
f8dde8eebe | |||
822c7b570a | |||
bc6e98db8c | |||
2cce26429b | |||
5643f05aa0 |
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@ -4,7 +4,7 @@ jobs:
|
||||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.20.x]
|
||||
go-version: [1.22.x]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/setup-go@v3
|
||||
|
@ -4,11 +4,11 @@ import (
|
||||
"crypto/tls"
|
||||
"crypto/x509/pkix"
|
||||
"fmt"
|
||||
"github.com/1f349/violet/logger"
|
||||
"github.com/1f349/violet/utils"
|
||||
"github.com/MrMelon54/certgen"
|
||||
"github.com/MrMelon54/rescheduler"
|
||||
"github.com/mrmelon54/certgen"
|
||||
"github.com/mrmelon54/rescheduler"
|
||||
"io/fs"
|
||||
"log"
|
||||
"math/big"
|
||||
"os"
|
||||
"strings"
|
||||
@ -17,6 +17,8 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
var Logger = logger.Logger.WithPrefix("Violet Certs")
|
||||
|
||||
// Certs is the certificate loader and management system.
|
||||
type Certs struct {
|
||||
cDir fs.FS
|
||||
@ -27,6 +29,8 @@ type Certs struct {
|
||||
ca *certgen.CertGen
|
||||
sn atomic.Int64
|
||||
r *rescheduler.Rescheduler
|
||||
t *time.Ticker
|
||||
ts chan struct{}
|
||||
}
|
||||
|
||||
// New creates a new cert list
|
||||
@ -37,15 +41,26 @@ func New(certDir fs.FS, keyDir fs.FS, selfCert bool) *Certs {
|
||||
ss: selfCert,
|
||||
s: &sync.RWMutex{},
|
||||
m: make(map[string]*tls.Certificate),
|
||||
ts: make(chan struct{}, 1),
|
||||
}
|
||||
|
||||
// the rescheduler isn't even used in self cert mode so why initialise it
|
||||
if !selfCert {
|
||||
// the rescheduler isn't even used in self cert mode so why initialise it
|
||||
c.r = rescheduler.NewRescheduler(c.threadCompile)
|
||||
}
|
||||
|
||||
// in self-signed mode generate a CA certificate to sign other certificates
|
||||
if c.ss {
|
||||
c.t = time.NewTicker(2 * time.Hour)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-c.t.C:
|
||||
c.Compile()
|
||||
case <-c.ts:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
// in self-signed mode generate a CA certificate to sign other certificates
|
||||
ca, err := certgen.MakeCaTls(4096, pkix.Name{
|
||||
Country: []string{"GB"},
|
||||
Organization: []string{"Violet"},
|
||||
@ -56,7 +71,7 @@ func New(certDir fs.FS, keyDir fs.FS, selfCert bool) *Certs {
|
||||
return now.AddDate(10, 0, 0)
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to generate CA cert for self-signed mode:", err)
|
||||
logger.Logger.Fatal("Failed to generate CA cert for self-signed mode", "err", err)
|
||||
}
|
||||
c.ca = ca
|
||||
}
|
||||
@ -118,6 +133,13 @@ func (c *Certs) Compile() {
|
||||
c.r.Run()
|
||||
}
|
||||
|
||||
func (c *Certs) Stop() {
|
||||
if c.t != nil {
|
||||
c.t.Stop()
|
||||
}
|
||||
close(c.ts)
|
||||
}
|
||||
|
||||
func (c *Certs) threadCompile() {
|
||||
// new map
|
||||
certMap := make(map[string]*tls.Certificate)
|
||||
@ -125,7 +147,7 @@ func (c *Certs) threadCompile() {
|
||||
// compile map and check errors
|
||||
err := c.internalCompile(certMap)
|
||||
if err != nil {
|
||||
log.Printf("[Certs] Compile failed: %s\n", err)
|
||||
Logger.Infof("Compile failed: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -148,7 +170,7 @@ func (c *Certs) internalCompile(m map[string]*tls.Certificate) error {
|
||||
return fmt.Errorf("failed to read cert dir: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("[Certs] Compiling lookup table for %d certificates\n", len(files))
|
||||
Logger.Infof("Compiling lookup table for %d certificates\n", len(files))
|
||||
|
||||
// find and parse certs
|
||||
for _, i := range files {
|
||||
|
@ -3,7 +3,7 @@ package certs
|
||||
import (
|
||||
"crypto/x509/pkix"
|
||||
"fmt"
|
||||
"github.com/MrMelon54/certgen"
|
||||
"github.com/mrmelon54/certgen"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"math/big"
|
||||
"testing"
|
||||
|
@ -2,14 +2,15 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"github.com/1f349/mjwt"
|
||||
"github.com/1f349/violet"
|
||||
"github.com/1f349/violet/certs"
|
||||
"github.com/1f349/violet/domains"
|
||||
errorPages "github.com/1f349/violet/error-pages"
|
||||
"github.com/1f349/violet/favicons"
|
||||
"github.com/1f349/violet/logger"
|
||||
"github.com/1f349/violet/proxy"
|
||||
"github.com/1f349/violet/proxy/websocket"
|
||||
"github.com/1f349/violet/router"
|
||||
@ -17,59 +18,64 @@ import (
|
||||
"github.com/1f349/violet/servers/api"
|
||||
"github.com/1f349/violet/servers/conf"
|
||||
"github.com/1f349/violet/utils"
|
||||
"github.com/MrMelon54/exit-reload"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/cloudflare/tableflip"
|
||||
"github.com/google/subcommands"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/collectors"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime/pprof"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
type serveCmd struct {
|
||||
configPath string
|
||||
cpuprofile string
|
||||
debugLog bool
|
||||
pidFile string
|
||||
}
|
||||
|
||||
func (s *serveCmd) Name() string { return "serve" }
|
||||
func (s *serveCmd) Synopsis() string { return "Serve reverse proxy server" }
|
||||
func (s *serveCmd) SetFlags(f *flag.FlagSet) {
|
||||
f.StringVar(&s.configPath, "conf", "", "/path/to/config.json : path to the config file")
|
||||
f.StringVar(&s.cpuprofile, "cpuprofile", "", "write cpu profile to file")
|
||||
f.BoolVar(&s.debugLog, "debug", false, "enable debug logging")
|
||||
f.StringVar(&s.pidFile, "pid-file", "", "path to pid file")
|
||||
}
|
||||
func (s *serveCmd) Usage() string {
|
||||
return `serve [-conf <config file>] [-cpuprofile <profile file>]
|
||||
return `serve [-conf <config file>] [-debug] [-pid-file <pid file>]
|
||||
Serve reverse proxy server using information from config file
|
||||
`
|
||||
}
|
||||
|
||||
func (s *serveCmd) Execute(_ context.Context, _ *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
|
||||
log.Println("[Violet] Starting...")
|
||||
|
||||
// Enable cpu profiling
|
||||
if s.cpuprofile != "" {
|
||||
f, err := os.Create(s.cpuprofile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Printf("[Violet] CPU profiling enabled, writing to '%s'\n", s.cpuprofile)
|
||||
_ = pprof.StartCPUProfile(f)
|
||||
defer pprof.StopCPUProfile()
|
||||
if s.debugLog {
|
||||
logger.Logger.SetLevel(log.DebugLevel)
|
||||
}
|
||||
logger.Logger.Info("Starting...")
|
||||
|
||||
upg, err := tableflip.New(tableflip.Options{
|
||||
PIDFile: s.pidFile,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer upg.Stop()
|
||||
|
||||
if s.configPath == "" {
|
||||
log.Println("[Violet] Error: config flag is missing")
|
||||
logger.Logger.Info("Error: config flag is missing")
|
||||
return subcommands.ExitUsageError
|
||||
}
|
||||
|
||||
openConf, err := os.Open(s.configPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
log.Println("[Violet] Error: missing config file")
|
||||
logger.Logger.Info("Error: missing config file")
|
||||
} else {
|
||||
log.Println("[Violet] Error: open config file: ", err)
|
||||
logger.Logger.Info("Error: open config file: ", err)
|
||||
}
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
@ -77,124 +83,167 @@ func (s *serveCmd) Execute(_ context.Context, _ *flag.FlagSet, _ ...interface{})
|
||||
var config startUpConfig
|
||||
err = json.NewDecoder(openConf).Decode(&config)
|
||||
if err != nil {
|
||||
log.Println("[Violet] Error: invalid config file: ", err)
|
||||
logger.Logger.Info("Error: invalid config file: ", err)
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
|
||||
// working directory is the parent of the config file
|
||||
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
|
||||
if !startUp.SelfSigned {
|
||||
if !config.SelfSigned {
|
||||
// create path to cert dir
|
||||
err := os.MkdirAll(filepath.Join(wd, "certs"), os.ModePerm)
|
||||
if err != nil {
|
||||
log.Fatal("[Violet] Failed to create certificate path")
|
||||
logger.Logger.Fatal("Failed to create certificate path")
|
||||
}
|
||||
// create path to key dir
|
||||
err = os.MkdirAll(filepath.Join(wd, "keys"), os.ModePerm)
|
||||
if err != nil {
|
||||
log.Fatal("[Violet] Failed to create certificate key path")
|
||||
logger.Logger.Fatal("Failed to create certificate key path")
|
||||
}
|
||||
}
|
||||
|
||||
// errorPageDir stores an FS interface for accessing the error page directory
|
||||
var errorPageDir fs.FS
|
||||
if startUp.ErrorPagePath != "" {
|
||||
errorPageDir = os.DirFS(startUp.ErrorPagePath)
|
||||
err := os.MkdirAll(startUp.ErrorPagePath, os.ModePerm)
|
||||
if config.ErrorPagePath != "" {
|
||||
errorPageDir = os.DirFS(config.ErrorPagePath)
|
||||
err := os.MkdirAll(config.ErrorPagePath, os.ModePerm)
|
||||
if err != nil {
|
||||
log.Fatalf("[Violet] Failed to create error page path '%s'", startUp.ErrorPagePath)
|
||||
logger.Logger.Fatal("Failed to create error page", "path", config.ErrorPagePath)
|
||||
}
|
||||
}
|
||||
|
||||
// load the MJWT RSA public key from a pem encoded file
|
||||
mJwtVerify, err := mjwt.NewMJwtVerifierFromFile(filepath.Join(wd, "signer.public.pem"))
|
||||
if err != nil {
|
||||
log.Fatalf("[Violet] Failed to load MJWT verifier public key from file '%s': %s", filepath.Join(wd, "signer.public.pem"), err)
|
||||
logger.Logger.Fatal("Failed to load MJWT verifier public key", "file", filepath.Join(wd, "signer.public.pem"), "err", err)
|
||||
}
|
||||
|
||||
// open sqlite database
|
||||
db, err := sql.Open("sqlite3", filepath.Join(wd, "violet.db.sqlite"))
|
||||
db, err := violet.InitDB(filepath.Join(wd, "violet.db.sqlite"))
|
||||
if err != nil {
|
||||
log.Fatal("[Violet] Failed to open database")
|
||||
logger.Logger.Fatal("Failed to open database", "err", err)
|
||||
}
|
||||
|
||||
certDir := os.DirFS(filepath.Join(wd, "certs"))
|
||||
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()
|
||||
allowedDomains := domains.New(db) // load allowed domains
|
||||
acmeChallenges := utils.NewAcmeChallenge() // load acme challenge store
|
||||
allowedCerts := certs.New(certDir, keyDir, startUp.SelfSigned) // load certificate manager
|
||||
hybridTransport := proxy.NewHybridTransport(ws) // load reverse proxy
|
||||
dynamicFavicons := favicons.New(db, startUp.InkscapeCmd) // load dynamic favicon provider
|
||||
dynamicErrorPages := errorPages.New(errorPageDir) // load dynamic error page provider
|
||||
dynamicRouter := router.NewManager(db, hybridTransport) // load dynamic router manager
|
||||
allowedDomains := domains.New(db) // load allowed domains
|
||||
acmeChallenges := utils.NewAcmeChallenge() // load acme challenge store
|
||||
allowedCerts := certs.New(certDir, keyDir, config.SelfSigned) // load certificate manager
|
||||
hybridTransport := proxy.NewHybridTransport(ws) // load reverse proxy
|
||||
dynamicFavicons := favicons.New(db, config.InkscapeCmd) // load dynamic favicon provider
|
||||
dynamicErrorPages := errorPages.New(errorPageDir) // load dynamic error page provider
|
||||
dynamicRouter := router.NewManager(db, hybridTransport) // load dynamic router manager
|
||||
|
||||
// struct containing config for the http servers
|
||||
srvConf := &conf.Conf{
|
||||
ApiListen: startUp.Listen.Api,
|
||||
HttpListen: startUp.Listen.Http,
|
||||
HttpsListen: startUp.Listen.Https,
|
||||
RateLimit: startUp.RateLimit,
|
||||
DB: db,
|
||||
Domains: allowedDomains,
|
||||
Acme: acmeChallenges,
|
||||
Certs: allowedCerts,
|
||||
Favicons: dynamicFavicons,
|
||||
Signer: mJwtVerify,
|
||||
ErrorPages: dynamicErrorPages,
|
||||
Router: dynamicRouter,
|
||||
RateLimit: config.RateLimit,
|
||||
DB: db,
|
||||
Domains: allowedDomains,
|
||||
Acme: acmeChallenges,
|
||||
Certs: allowedCerts,
|
||||
Favicons: dynamicFavicons,
|
||||
Signer: mJwtVerify,
|
||||
ErrorPages: dynamicErrorPages,
|
||||
Router: dynamicRouter,
|
||||
}
|
||||
|
||||
// create the compilable list and run a first time compile
|
||||
allCompilables := utils.MultiCompilable{allowedDomains, allowedCerts, dynamicFavicons, dynamicErrorPages, dynamicRouter}
|
||||
allCompilables.Compile()
|
||||
|
||||
var srvApi, srvHttp, srvHttps *http.Server
|
||||
if srvConf.ApiListen != "" {
|
||||
srvApi = api.NewApiServer(srvConf, allCompilables)
|
||||
srvApi.SetKeepAlivesEnabled(false)
|
||||
log.Printf("[API] Starting API server on: '%s'\n", srvApi.Addr)
|
||||
go utils.RunBackgroundHttp("API", srvApi)
|
||||
}
|
||||
if srvConf.HttpListen != "" {
|
||||
srvHttp = servers.NewHttpServer(srvConf)
|
||||
srvHttp.SetKeepAlivesEnabled(false)
|
||||
log.Printf("[HTTP] Starting HTTP server on: '%s'\n", srvHttp.Addr)
|
||||
go utils.RunBackgroundHttp("HTTP", srvHttp)
|
||||
}
|
||||
if srvConf.HttpsListen != "" {
|
||||
srvHttps = servers.NewHttpsServer(srvConf)
|
||||
srvHttps.SetKeepAlivesEnabled(false)
|
||||
log.Printf("[HTTPS] Starting HTTPS server on: '%s'\n", srvHttps.Addr)
|
||||
go utils.RunBackgroundHttps("HTTPS", srvHttps)
|
||||
_, httpsPort, ok := utils.SplitDomainPort(config.Listen.Https, 443)
|
||||
if !ok {
|
||||
httpsPort = 443
|
||||
}
|
||||
|
||||
var srvApi, srvHttp, srvHttps *http.Server
|
||||
if config.Listen.Api != "" {
|
||||
// Listen must be called before Ready
|
||||
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)
|
||||
l := logger.Logger.With("server", "API")
|
||||
l.Info("Starting server", "addr", config.Listen.Api)
|
||||
go utils.RunBackgroundHttp(l, srvApi, lnApi)
|
||||
}
|
||||
if config.Listen.Http != "" {
|
||||
// Listen must be called before Ready
|
||||
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)
|
||||
l := logger.Logger.With("server", "HTTP")
|
||||
l.Info("Starting server", "addr", config.Listen.Http)
|
||||
go utils.RunBackgroundHttp(l, srvHttp, lnHttp)
|
||||
}
|
||||
if config.Listen.Https != "" {
|
||||
// Listen must be called before Ready
|
||||
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)
|
||||
l := logger.Logger.With("server", "HTTPS")
|
||||
l.Info("Starting server", "addr", config.Listen.Https)
|
||||
go utils.RunBackgroundHttps(l, srvHttps, lnHttps)
|
||||
}
|
||||
|
||||
// Do an upgrade on SIGHUP
|
||||
go func() {
|
||||
log.Println(http.ListenAndServe("localhost:6600", nil))
|
||||
sig := make(chan os.Signal, 1)
|
||||
signal.Notify(sig, syscall.SIGHUP)
|
||||
for range sig {
|
||||
err := upg.Upgrade()
|
||||
if err != nil {
|
||||
logger.Logger.Error("Failed upgrade", "err", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
exit_reload.ExitReload("Violet", func() {
|
||||
allCompilables.Compile()
|
||||
}, func() {
|
||||
// close websockets first
|
||||
ws.Shutdown()
|
||||
logger.Logger.Info("Ready")
|
||||
if err := upg.Ready(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
<-upg.Exit()
|
||||
|
||||
// close http servers
|
||||
if srvApi != nil {
|
||||
_ = srvApi.Close()
|
||||
}
|
||||
if srvHttp != nil {
|
||||
_ = srvHttp.Close()
|
||||
}
|
||||
if srvHttps != nil {
|
||||
_ = srvHttps.Close()
|
||||
}
|
||||
time.AfterFunc(30*time.Second, func() {
|
||||
logger.Logger.Warn("Graceful shutdown timed out")
|
||||
os.Exit(1)
|
||||
})
|
||||
|
||||
// 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
|
||||
}
|
||||
|
@ -2,18 +2,18 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/1f349/violet"
|
||||
"github.com/1f349/violet/domains"
|
||||
"github.com/1f349/violet/logger"
|
||||
"github.com/1f349/violet/proxy"
|
||||
"github.com/1f349/violet/proxy/websocket"
|
||||
"github.com/1f349/violet/router"
|
||||
"github.com/1f349/violet/target"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/google/subcommands"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@ -42,7 +42,7 @@ func (s *setupCmd) Execute(_ context.Context, _ *flag.FlagSet, _ ...interface{})
|
||||
// get absolute path to specify files
|
||||
wdAbs, err := filepath.Abs(s.wdPath)
|
||||
if err != nil {
|
||||
fmt.Println("[Violet] Failed to get full directory path: ", err)
|
||||
fmt.Println("Failed to get full directory path: ", err)
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
|
||||
@ -50,11 +50,11 @@ func (s *setupCmd) Execute(_ context.Context, _ *flag.FlagSet, _ ...interface{})
|
||||
createFile := false
|
||||
err = survey.AskOne(&survey.Confirm{Message: fmt.Sprintf("Create Violet config files in this directory: '%s'?", wdAbs)}, &createFile)
|
||||
if err != nil {
|
||||
fmt.Println("[Violet] Error: ", err)
|
||||
fmt.Println("Error: ", err)
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
if !createFile {
|
||||
fmt.Println("[Violet] Goodbye")
|
||||
fmt.Println("Goodbye")
|
||||
return subcommands.ExitSuccess
|
||||
}
|
||||
|
||||
@ -111,7 +111,7 @@ func (s *setupCmd) Execute(_ context.Context, _ *flag.FlagSet, _ ...interface{})
|
||||
},
|
||||
}, &answers)
|
||||
if err != nil {
|
||||
fmt.Println("[Violet] Error: ", err)
|
||||
fmt.Println("Error: ", err)
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
|
||||
@ -142,14 +142,14 @@ func (s *setupCmd) Execute(_ context.Context, _ *flag.FlagSet, _ ...interface{})
|
||||
RateLimit: answers.RateLimit,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Println("[Violet] Failed to write config file: ", err)
|
||||
fmt.Println("Failed to write config file: ", err)
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
|
||||
// open sqlite database
|
||||
db, err := sql.Open("sqlite3", databaseFile)
|
||||
db, err := violet.InitDB(databaseFile)
|
||||
if err != nil {
|
||||
log.Fatalf("[Violet] Failed to open database '%s'...", databaseFile)
|
||||
logger.Logger.Fatal("Failed to open database", "err", err)
|
||||
}
|
||||
|
||||
// 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
|
||||
}))
|
||||
if err != nil {
|
||||
fmt.Println("[Violet] Error: ", err)
|
||||
fmt.Println("Error: ", err)
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
|
||||
// parse the api url
|
||||
apiUrl, err := url.Parse(answers.ApiUrl)
|
||||
if err != nil {
|
||||
fmt.Println("[Violet] Failed to parse API URL: ", err)
|
||||
fmt.Println("Failed to parse API URL: ", err)
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
|
||||
@ -191,13 +191,13 @@ func (s *setupCmd) Execute(_ context.Context, _ *flag.FlagSet, _ ...interface{})
|
||||
Active: true,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Println("[Violet] Failed to insert api route into database: ", err)
|
||||
fmt.Println("Failed to insert api route into database: ", err)
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("[Violet] Setup complete")
|
||||
fmt.Printf("[Violet] Run the reverse proxy with `violet serve -conf %s`\n", confFile)
|
||||
fmt.Println("Setup complete")
|
||||
fmt.Printf("Run the reverse proxy with `violet serve -conf %s`\n", confFile)
|
||||
|
||||
return subcommands.ExitSuccess
|
||||
}
|
||||
|
31
database/db.go
Normal file
31
database/db.go
Normal file
@ -0,0 +1,31 @@
|
||||
// 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,
|
||||
}
|
||||
}
|
68
database/domain.sql.go
Normal file
68
database/domain.sql.go
Normal file
@ -0,0 +1,68 @@
|
||||
// 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
|
||||
}
|
74
database/favicon.sql.go
Normal file
74
database/favicon.sql.go
Normal file
@ -0,0 +1,74 @@
|
||||
// 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
|
||||
}
|
4
database/migrations/20240308125121_init.down.sql
Normal file
4
database/migrations/20240308125121_init.down.sql
Normal file
@ -0,0 +1,4 @@
|
||||
DROP TABLE domains;
|
||||
DROP TABLE favicons;
|
||||
DROP TABLE routes;
|
||||
DROP TABLE redirects;
|
36
database/migrations/20240308125121_init.up.sql
Normal file
36
database/migrations/20240308125121_init.up.sql
Normal file
@ -0,0 +1,36 @@
|
||||
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
|
||||
);
|
44
database/models.go
Normal file
44
database/models.go
Normal file
@ -0,0 +1,44 @@
|
||||
// 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"`
|
||||
}
|
16
database/queries/domain.sql
Normal file
16
database/queries/domain.sql
Normal file
@ -0,0 +1,16 @@
|
||||
-- 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);
|
8
database/queries/favicon.sql
Normal file
8
database/queries/favicon.sql
Normal file
@ -0,0 +1,8 @@
|
||||
-- name: GetFavicons :many
|
||||
SELECT host, svg, png, ico
|
||||
FROM favicons;
|
||||
|
||||
-- name: UpdateFaviconCache :exec
|
||||
INSERT OR
|
||||
REPLACE INTO favicons (host, svg, png, ico)
|
||||
VALUES (?, ?, ?, ?);
|
39
database/queries/routing.sql
Normal file
39
database/queries/routing.sql
Normal file
@ -0,0 +1,39 @@
|
||||
-- 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 = ?;
|
250
database/routing.sql.go
Normal file
250
database/routing.sql.go
Normal file
@ -0,0 +1,250 @@
|
||||
// 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
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS domains
|
||||
(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
domain TEXT UNIQUE,
|
||||
active INTEGER DEFAULT 1
|
||||
);
|
@ -1,41 +1,34 @@
|
||||
package domains
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"context"
|
||||
_ "embed"
|
||||
"github.com/1f349/violet/database"
|
||||
"github.com/1f349/violet/logger"
|
||||
"github.com/1f349/violet/utils"
|
||||
"github.com/MrMelon54/rescheduler"
|
||||
"log"
|
||||
"github.com/mrmelon54/rescheduler"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
//go:embed create-table-domains.sql
|
||||
var createTableDomains string
|
||||
var Logger = logger.Logger.WithPrefix("Violet Domains")
|
||||
|
||||
// Domains is the domain list and management system.
|
||||
type Domains struct {
|
||||
db *sql.DB
|
||||
db *database.Queries
|
||||
s *sync.RWMutex
|
||||
m map[string]struct{}
|
||||
r *rescheduler.Rescheduler
|
||||
}
|
||||
|
||||
// New creates a new domain list
|
||||
func New(db *sql.DB) *Domains {
|
||||
func New(db *database.Queries) *Domains {
|
||||
a := &Domains{
|
||||
db: db,
|
||||
s: &sync.RWMutex{},
|
||||
m: make(map[string]struct{}),
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@ -77,7 +70,7 @@ func (d *Domains) threadCompile() {
|
||||
// compile map and check errors
|
||||
err := d.internalCompile(domainMap)
|
||||
if err != nil {
|
||||
log.Printf("[Domains] Compile failed: %s\n", err)
|
||||
Logger.Info("Compile faile", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -90,43 +83,39 @@ func (d *Domains) threadCompile() {
|
||||
// internalCompile is a hidden internal method for querying the database during
|
||||
// the Compile() method.
|
||||
func (d *Domains) internalCompile(m map[string]struct{}) error {
|
||||
log.Println("[Domains] Updating domains from database")
|
||||
Logger.Info("Updating domains from database")
|
||||
|
||||
// sql or something?
|
||||
rows, err := d.db.Query(`select domain from domains where active = 1`)
|
||||
rows, err := d.db.GetActiveDomains(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
// loop through rows and scan the allowed domain names
|
||||
for rows.Next() {
|
||||
var name string
|
||||
err = rows.Scan(&name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m[name] = struct{}{}
|
||||
for _, i := range rows {
|
||||
m[i] = struct{}{}
|
||||
}
|
||||
|
||||
// check for errors
|
||||
return rows.Err()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Domains) Put(domain string, active bool) {
|
||||
d.s.Lock()
|
||||
defer d.s.Unlock()
|
||||
_, err := d.db.Exec("INSERT OR REPLACE INTO domains (domain, active) VALUES (?, ?)", domain, active)
|
||||
err := d.db.AddDomain(context.Background(), database.AddDomainParams{
|
||||
Domain: domain,
|
||||
Active: active,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("[Violet] Database error: %s\n", err)
|
||||
logger.Logger.Infof("Database error: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Domains) Delete(domain string) {
|
||||
d.s.Lock()
|
||||
defer d.s.Unlock()
|
||||
_, err := d.db.Exec("INSERT OR REPLACE INTO domains (domain, active) VALUES (?, ?)", domain, false)
|
||||
err := d.db.DeleteDomain(context.Background(), domain)
|
||||
if err != nil {
|
||||
log.Printf("[Violet] Database error: %s\n", err)
|
||||
logger.Logger.Infof("Database error: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +1,20 @@
|
||||
package domains
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"context"
|
||||
"github.com/1f349/violet"
|
||||
"github.com/1f349/violet/database"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDomainsNew(t *testing.T) {
|
||||
db, err := sql.Open("sqlite3", "file::memory:?cache=shared")
|
||||
db, err := violet.InitDB("file:TestDomainsNew?mode=memory&cache=shared")
|
||||
assert.NoError(t, err)
|
||||
|
||||
domains := New(db)
|
||||
_, err = db.Exec("INSERT OR IGNORE INTO domains (domain, active) VALUES (?, ?)", "example.com", 1)
|
||||
err = db.AddDomain(context.Background(), database.AddDomainParams{Domain: "example.com", Active: true})
|
||||
assert.NoError(t, err)
|
||||
domains.Compile()
|
||||
|
||||
@ -27,11 +29,11 @@ func TestDomainsNew(t *testing.T) {
|
||||
|
||||
func TestDomains_IsValid(t *testing.T) {
|
||||
// open sqlite database
|
||||
db, err := sql.Open("sqlite3", "file::memory:?cache=shared")
|
||||
db, err := violet.InitDB("file:TestDomains_IsValid?mode=memory&cache=shared")
|
||||
assert.NoError(t, err)
|
||||
|
||||
domains := New(db)
|
||||
_, err = domains.db.Exec("INSERT OR IGNORE INTO domains (domain, active) VALUES (?, ?)", "example.com", 1)
|
||||
err = db.AddDomain(context.Background(), database.AddDomainParams{Domain: "example.com", Active: true})
|
||||
assert.NoError(t, err)
|
||||
|
||||
domains.s.Lock()
|
||||
|
@ -2,9 +2,9 @@ package error_pages
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/MrMelon54/rescheduler"
|
||||
"github.com/1f349/violet/logger"
|
||||
"github.com/mrmelon54/rescheduler"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
@ -12,6 +12,8 @@ import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
var Logger = logger.Logger.WithPrefix("Violet Error Pages")
|
||||
|
||||
// ErrorPages stores the custom error pages and is called by the servers to
|
||||
// output meaningful pages for HTTP error codes
|
||||
type ErrorPages struct {
|
||||
@ -78,7 +80,7 @@ func (e *ErrorPages) threadCompile() {
|
||||
if e.dir != nil {
|
||||
err := e.internalCompile(errorPageMap)
|
||||
if err != nil {
|
||||
log.Printf("[ErrorPages] Compile failed: %s\n", err)
|
||||
Logger.Info("Compile failed", "err", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -96,7 +98,7 @@ func (e *ErrorPages) internalCompile(m map[int]func(rw http.ResponseWriter)) err
|
||||
return fmt.Errorf("failed to read error pages dir: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("[ErrorPages] Compiling lookup table for %d error pages\n", len(files))
|
||||
Logger.Info("Compiling lookup table", "page count", len(files))
|
||||
|
||||
// find and load error pages
|
||||
for _, i := range files {
|
||||
@ -111,20 +113,20 @@ func (e *ErrorPages) internalCompile(m map[int]func(rw http.ResponseWriter)) err
|
||||
|
||||
// if the extension is not 'html' then ignore the file
|
||||
if ext != ".html" {
|
||||
log.Printf("[ErrorPages] WARNING: ignoring non '.html' file in error pages directory: '%s'\n", name)
|
||||
Logger.Warn("Ignoring non '.html' file in error pages directory", "name", name)
|
||||
continue
|
||||
}
|
||||
|
||||
// if the name can't be
|
||||
nameInt, err := strconv.Atoi(strings.TrimSuffix(name, ".html"))
|
||||
if err != nil {
|
||||
log.Printf("[ErrorPages] WARNING: ignoring invalid error page in error pages directory: '%s'\n", name)
|
||||
Logger.Warn("Ignoring invalid error page in error pages directory", "name", name)
|
||||
continue
|
||||
}
|
||||
|
||||
// check if code is in range 100-599
|
||||
if nameInt < 100 || nameInt >= 600 {
|
||||
log.Printf("[ErrorPages] WARNING: ignoring invalid error page in error pages directory must be 100-599: '%s'\n", name)
|
||||
Logger.Warn("Ignoring invalid error page in error pages directory must be 100-599", "name", name)
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -1,7 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS favicons (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
host VARCHAR,
|
||||
svg VARCHAR,
|
||||
png VARCHAR,
|
||||
ico VARCHAR
|
||||
);
|
@ -1,5 +1,7 @@
|
||||
package favicons
|
||||
|
||||
import "database/sql"
|
||||
|
||||
// FaviconImage stores the url, hash and raw bytes of an image
|
||||
type FaviconImage struct {
|
||||
Url string
|
||||
@ -9,9 +11,9 @@ type FaviconImage struct {
|
||||
|
||||
// CreateFaviconImage outputs a FaviconImage with the specified URL or nil if
|
||||
// the URL is an empty string.
|
||||
func CreateFaviconImage(url string) *FaviconImage {
|
||||
if url == "" {
|
||||
func CreateFaviconImage(url sql.NullString) *FaviconImage {
|
||||
if !url.Valid {
|
||||
return nil
|
||||
}
|
||||
return &FaviconImage{Url: url}
|
||||
return &FaviconImage{Url: url.String}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/MrMelon54/png2ico"
|
||||
"github.com/mrmelon54/png2ico"
|
||||
"image/png"
|
||||
"io"
|
||||
"net/http"
|
||||
@ -74,7 +74,7 @@ func (l *FaviconList) PreProcess(convert func(in []byte) ([]byte, error)) error
|
||||
// download SVG
|
||||
l.Svg.Raw, err = getFaviconViaRequest(l.Svg.Url)
|
||||
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))
|
||||
}
|
||||
@ -84,14 +84,14 @@ func (l *FaviconList) PreProcess(convert func(in []byte) ([]byte, error)) error
|
||||
// download PNG
|
||||
l.Png.Raw, err = getFaviconViaRequest(l.Png.Url)
|
||||
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 {
|
||||
// generate PNG from SVG
|
||||
l.Png = &FaviconImage{}
|
||||
l.Png.Raw, err = convert(l.Svg.Raw)
|
||||
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
|
||||
l.Ico.Raw, err = getFaviconViaRequest(l.Ico.Url)
|
||||
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 {
|
||||
// generate ICO from PNG
|
||||
l.Ico = &FaviconImage{}
|
||||
decode, err := png.Decode(bytes.NewReader(l.Png.Raw))
|
||||
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()
|
||||
l.Ico.Raw, err = png2ico.ConvertPngToIco(l.Png.Raw, b.Dx(), b.Dy())
|
||||
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) {
|
||||
req, err := http.NewRequest(http.MethodGet, url, 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")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
@ -1,24 +1,24 @@
|
||||
package favicons
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"context"
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/MrMelon54/rescheduler"
|
||||
"github.com/1f349/violet/database"
|
||||
"github.com/1f349/violet/logger"
|
||||
"github.com/mrmelon54/rescheduler"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"log"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var ErrFaviconNotFound = errors.New("favicon not found")
|
||||
var Logger = logger.Logger.WithPrefix("Violet Favicons")
|
||||
|
||||
//go:embed create-table-favicons.sql
|
||||
var createTableFavicons string
|
||||
var ErrFaviconNotFound = errors.New("favicon not found")
|
||||
|
||||
// Favicons is a dynamic favicon generator which supports overwriting favicons
|
||||
type Favicons struct {
|
||||
db *sql.DB
|
||||
db *database.Queries
|
||||
cmd string
|
||||
cLock *sync.RWMutex
|
||||
faviconMap map[string]*FaviconList
|
||||
@ -26,7 +26,7 @@ type Favicons struct {
|
||||
}
|
||||
|
||||
// New creates a new dynamic favicon generator
|
||||
func New(db *sql.DB, inkscapeCmd string) *Favicons {
|
||||
func New(db *database.Queries, inkscapeCmd string) *Favicons {
|
||||
f := &Favicons{
|
||||
db: db,
|
||||
cmd: inkscapeCmd,
|
||||
@ -35,13 +35,6 @@ func New(db *sql.DB, inkscapeCmd string) *Favicons {
|
||||
}
|
||||
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
|
||||
f.Compile()
|
||||
return f
|
||||
@ -75,7 +68,7 @@ func (f *Favicons) threadCompile() {
|
||||
err := f.internalCompile(favicons)
|
||||
if err != nil {
|
||||
// log compile errors
|
||||
log.Printf("[Favicons] Compile failed: %s\n", err)
|
||||
Logger.Info("Compile failed", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -89,29 +82,23 @@ func (f *Favicons) threadCompile() {
|
||||
// favicons.
|
||||
func (f *Favicons) internalCompile(m map[string]*FaviconList) error {
|
||||
// query all rows in database
|
||||
query, err := f.db.Query(`select host, svg, png, ico from favicons`)
|
||||
rows, err := f.db.GetFavicons(context.Background())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to prepare query: %w", err)
|
||||
return fmt.Errorf("failed to prepare rows: %w", err)
|
||||
}
|
||||
|
||||
// loop over rows and scan in data using error group to catch errors
|
||||
var g errgroup.Group
|
||||
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)
|
||||
}
|
||||
|
||||
for _, row := range rows {
|
||||
// create favicon list for this row
|
||||
l := &FaviconList{
|
||||
Ico: CreateFaviconImage(rawIco),
|
||||
Png: CreateFaviconImage(rawPng),
|
||||
Svg: CreateFaviconImage(rawSvg),
|
||||
Ico: CreateFaviconImage(row.Ico),
|
||||
Png: CreateFaviconImage(row.Png),
|
||||
Svg: CreateFaviconImage(row.Svg),
|
||||
}
|
||||
|
||||
// save the favicon list to the map
|
||||
m[host] = l
|
||||
m[row.Host] = l
|
||||
|
||||
// run the pre-process in a separate goroutine
|
||||
g.Go(func() error {
|
||||
|
@ -2,8 +2,11 @@ package favicons
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"database/sql"
|
||||
_ "embed"
|
||||
"github.com/1f349/violet"
|
||||
"github.com/1f349/violet/database"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"image/png"
|
||||
@ -22,11 +25,17 @@ var (
|
||||
func TestFaviconsNew(t *testing.T) {
|
||||
getFaviconViaRequest = func(_ string) ([]byte, error) { return exampleSvg, nil }
|
||||
|
||||
db, err := sql.Open("sqlite3", "file::memory:?cache=shared")
|
||||
db, err := violet.InitDB("file:TestFaviconsNew?mode=memory&cache=shared")
|
||||
assert.NoError(t, err)
|
||||
|
||||
favicons := New(db, "inkscape")
|
||||
_, err = db.Exec("insert into favicons (host, svg, png, ico) values (?, ?, ?, ?)", "example.com", "https://example.com/assets/logo.svg", "", "")
|
||||
err = db.UpdateFaviconCache(context.Background(), database.UpdateFaviconCacheParams{
|
||||
Host: "example.com",
|
||||
Svg: sql.NullString{
|
||||
String: "https://example.com/assets/logo.svg",
|
||||
Valid: true,
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
favicons.cLock.Lock()
|
||||
assert.NoError(t, favicons.internalCompile(favicons.faviconMap))
|
||||
|
61
go.mod
61
go.mod
@ -1,43 +1,62 @@
|
||||
module github.com/1f349/violet
|
||||
|
||||
go 1.20
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/1f349/mjwt v0.2.0
|
||||
github.com/1f349/mjwt v0.2.5
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7
|
||||
github.com/MrMelon54/certgen v0.0.1
|
||||
github.com/MrMelon54/exit-reload v0.0.1
|
||||
github.com/MrMelon54/png2ico v1.0.1
|
||||
github.com/MrMelon54/rescheduler v0.0.2
|
||||
github.com/MrMelon54/trie v0.0.2
|
||||
github.com/charmbracelet/log v0.4.0
|
||||
github.com/cloudflare/tableflip v1.2.3
|
||||
github.com/golang-migrate/migrate/v4 v4.17.1
|
||||
github.com/google/subcommands v1.2.0
|
||||
github.com/google/uuid v1.4.0
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/julienschmidt/httprouter v1.3.0
|
||||
github.com/mattn/go-sqlite3 v1.14.17
|
||||
github.com/rs/cors v1.10.1
|
||||
github.com/sethvargo/go-limiter v0.7.2
|
||||
github.com/stretchr/testify v1.8.4
|
||||
golang.org/x/net v0.17.0
|
||||
golang.org/x/sync v0.4.0
|
||||
github.com/mattn/go-sqlite3 v1.14.22
|
||||
github.com/mrmelon54/certgen v0.0.2
|
||||
github.com/mrmelon54/png2ico v1.0.2
|
||||
github.com/mrmelon54/rescheduler v0.0.3
|
||||
github.com/mrmelon54/trie v0.0.3
|
||||
github.com/prometheus/client_golang v1.19.1
|
||||
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 (
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // 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/go-logfmt/logfmt v0.6.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/kr/pretty v0.3.1 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // 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/muesli/reflow v0.3.0 // indirect
|
||||
github.com/muesli/termenv v0.15.2 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
||||
golang.org/x/sys v0.13.0 // indirect
|
||||
golang.org/x/term v0.13.0 // indirect
|
||||
golang.org/x/text v0.13.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.53.0 // indirect
|
||||
github.com/prometheus/procfs v0.14.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rogpeppe/go-internal v1.12.0 // 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
|
||||
)
|
||||
|
131
go.sum
131
go.sum
@ -1,48 +1,62 @@
|
||||
github.com/1f349/mjwt v0.2.0 h1:1c3+J05RRBsClGxA91SzT3I2DkwasGA4OgLcIeXWmq4=
|
||||
github.com/1f349/mjwt v0.2.0/go.mod h1:KEs6jd9JjWrQW+8feP2pGAU7pdA3aYTqjkT/YQr73PU=
|
||||
github.com/1f349/mjwt v0.2.5 h1:IxjLaali22ayTzZ628lH7j0JDdYJoj6+CJ/VktCqtXQ=
|
||||
github.com/1f349/mjwt v0.2.5/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/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/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/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.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
|
||||
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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
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/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/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
||||
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
|
||||
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.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/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/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
|
||||
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
|
||||
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/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/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/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.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
@ -50,68 +64,97 @@ 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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
||||
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
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-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/mrmelon54/certgen v0.0.2 h1:4CMDkA/gGZu+E4iikU+5qdOWK7qOQrk58KtUfnmyYmY=
|
||||
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/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
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/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo=
|
||||
github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||
github.com/sethvargo/go-limiter v0.7.2 h1:FgC4N7RMpV5gMrUdda15FaFTkQ/L4fEqM7seXMs4oO8=
|
||||
github.com/sethvargo/go-limiter v0.7.2/go.mod h1:C0kbSFbiriE5k2FFOe18M1YZbAR2Fiwf72uGu0CXCcU=
|
||||
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
|
||||
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.53.0 h1:U2pL9w9nmJwJDa4qqLQ3ZaePJ6ZTwt7cMD3AG3+aLCE=
|
||||
github.com/prometheus/common v0.53.0/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U=
|
||||
github.com/prometheus/procfs v0.14.0 h1:Lw4VdGGoKEZilJsayHf0B+9YgLGREba2C6xr+Fdfq6s=
|
||||
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/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
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-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/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-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
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.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
|
||||
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.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-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-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-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-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
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.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.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.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-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/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 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
38
initdb.go
Normal file
38
initdb.go
Normal file
@ -0,0 +1,38 @@
|
||||
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
|
||||
}
|
12
logger/logger.go
Normal file
12
logger/logger.go
Normal file
@ -0,0 +1,12 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/log"
|
||||
"os"
|
||||
)
|
||||
|
||||
var Logger = log.NewWithOptions(os.Stderr, log.Options{
|
||||
ReportCaller: true,
|
||||
ReportTimestamp: true,
|
||||
Prefix: "Violet",
|
||||
})
|
@ -2,15 +2,18 @@ package proxy
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"github.com/1f349/violet/logger"
|
||||
"github.com/1f349/violet/proxy/websocket"
|
||||
"github.com/google/uuid"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"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 {
|
||||
baseDialer *net.Dialer
|
||||
normalTransport http.RoundTripper
|
||||
@ -71,24 +74,15 @@ func NewHybridTransportWithCalls(normal, insecure http.RoundTripper, ws *websock
|
||||
|
||||
// SecureRoundTrip calls the secure transport
|
||||
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)
|
||||
}
|
||||
|
||||
// InsecureRoundTrip calls the insecure transport
|
||||
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)
|
||||
}
|
||||
|
||||
// ConnectWebsocket calls the websocket upgrader and thus hijacks the connection
|
||||
func (h *HybridTransport) ConnectWebsocket(rw http.ResponseWriter, req *http.Request) {
|
||||
u := uuid.New()
|
||||
log.Println("[Websocket] Start upgrade:", u)
|
||||
h.ws.Upgrade(rw, req)
|
||||
log.Println("[Websocket] Stop upgrade:", u)
|
||||
}
|
||||
|
@ -1,13 +1,16 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"github.com/1f349/violet/logger"
|
||||
"github.com/gorilla/websocket"
|
||||
"log"
|
||||
"net/http"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var Logger = logger.Logger.WithPrefix("Violet Websocket")
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
HandshakeTimeout: time.Second * 5,
|
||||
ReadBufferSize: 1024,
|
||||
@ -34,7 +37,7 @@ func NewServer() *Server {
|
||||
|
||||
func (s *Server) Upgrade(rw http.ResponseWriter, req *http.Request) {
|
||||
req.URL.Scheme = "ws"
|
||||
log.Printf("[Websocket] Upgrading request to '%s' from '%s'\n", req.URL.String(), req.Header.Get("Origin"))
|
||||
Logger.Info("Upgrading request", "url", req.URL, "origin", req.Header.Get("Origin"))
|
||||
|
||||
c, err := upgrader.Upgrade(rw, req, nil)
|
||||
if err != nil {
|
||||
@ -54,12 +57,12 @@ func (s *Server) Upgrade(rw http.ResponseWriter, req *http.Request) {
|
||||
s.conns[c.RemoteAddr().String()] = c
|
||||
s.connLock.Unlock()
|
||||
|
||||
log.Printf("[Websocket] Dialing: '%s'\n", req.URL.String())
|
||||
Logger.Info("Dialing", "url", req.URL)
|
||||
|
||||
// dial for internal connection
|
||||
ic, _, err := websocket.DefaultDialer.DialContext(req.Context(), req.URL.String(), nil)
|
||||
ic, _, err := websocket.DefaultDialer.DialContext(req.Context(), req.URL.String(), filterWebsocketHeaders(req.Header))
|
||||
if err != nil {
|
||||
log.Printf("[Websocket] Failed to dial '%s': %s\n", req.URL.String(), err)
|
||||
Logger.Info("Failed to dial", "url", req.URL, "err", err)
|
||||
s.Remove(c)
|
||||
return
|
||||
}
|
||||
@ -73,7 +76,7 @@ func (s *Server) Upgrade(rw http.ResponseWriter, req *http.Request) {
|
||||
go s.wsRelay(d2, ic, c)
|
||||
|
||||
// wait for done signal and close both connections
|
||||
log.Println("[Websocket] Completed websocket hijacking")
|
||||
Logger.Info("Completed websocket hijacking")
|
||||
|
||||
// waiting until d1 or d2 close then automatically defer close both connections
|
||||
select {
|
||||
@ -82,6 +85,17 @@ 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) {
|
||||
defer func() {
|
||||
close(done)
|
||||
@ -89,7 +103,7 @@ func (s *Server) wsRelay(done chan struct{}, a, b *websocket.Conn) {
|
||||
for {
|
||||
mt, message, err := a.ReadMessage()
|
||||
if err != nil {
|
||||
log.Println("Websocket read message error: ", err)
|
||||
Logger.Info("Read message", "err", err)
|
||||
return
|
||||
}
|
||||
if b.WriteMessage(mt, message) != nil {
|
||||
|
@ -1,20 +0,0 @@
|
||||
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
|
||||
);
|
@ -1,35 +1,33 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"context"
|
||||
_ "embed"
|
||||
"github.com/1f349/violet/database"
|
||||
"github.com/1f349/violet/logger"
|
||||
"github.com/1f349/violet/proxy"
|
||||
"github.com/1f349/violet/target"
|
||||
"github.com/MrMelon54/rescheduler"
|
||||
"log"
|
||||
"github.com/mrmelon54/rescheduler"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var Logger = logger.Logger.WithPrefix("Violet Manager")
|
||||
|
||||
// Manager is a database and mutex wrap around router allowing it to be
|
||||
// dynamically regenerated after updating the database of routes.
|
||||
type Manager struct {
|
||||
db *sql.DB
|
||||
db *database.Queries
|
||||
s *sync.RWMutex
|
||||
r *Router
|
||||
p *proxy.HybridTransport
|
||||
z *rescheduler.Rescheduler
|
||||
}
|
||||
|
||||
var (
|
||||
//go:embed create-tables.sql
|
||||
createTables string
|
||||
)
|
||||
|
||||
// NewManager create a new manager, initialises the routes and redirects tables
|
||||
// in the database and runs a first time compile.
|
||||
func NewManager(db *sql.DB, proxy *proxy.HybridTransport) *Manager {
|
||||
func NewManager(db *database.Queries, proxy *proxy.HybridTransport) *Manager {
|
||||
m := &Manager{
|
||||
db: db,
|
||||
s: &sync.RWMutex{},
|
||||
@ -37,13 +35,6 @@ func NewManager(db *sql.DB, proxy *proxy.HybridTransport) *Manager {
|
||||
p: proxy,
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@ -65,7 +56,7 @@ func (m *Manager) threadCompile() {
|
||||
// compile router and check errors
|
||||
err := m.internalCompile(router)
|
||||
if err != nil {
|
||||
log.Printf("[Manager] Compile failed: %s\n", err)
|
||||
Logger.Info("Compile failed", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -78,67 +69,39 @@ func (m *Manager) threadCompile() {
|
||||
// internalCompile is a hidden internal method for querying the database during
|
||||
// the Compile() method.
|
||||
func (m *Manager) internalCompile(router *Router) error {
|
||||
log.Println("[Manager] Updating routes from database")
|
||||
Logger.Info("Updating routes from database")
|
||||
|
||||
// sql or something?
|
||||
rows, err := m.db.Query(`SELECT source, destination, flags FROM routes WHERE active = 1`)
|
||||
routeRows, err := m.db.GetActiveRoutes(context.Background())
|
||||
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
|
||||
)
|
||||
err := rows.Scan(&src, &dst, &flags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, row := range routeRows {
|
||||
router.AddRoute(target.Route{
|
||||
Src: src,
|
||||
Dst: dst,
|
||||
Flags: flags.NormaliseRouteFlags(),
|
||||
Src: row.Source,
|
||||
Dst: row.Destination,
|
||||
Flags: row.Flags.NormaliseRouteFlags(),
|
||||
})
|
||||
}
|
||||
|
||||
// check for errors
|
||||
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`)
|
||||
redirectsRows, err := m.db.GetActiveRedirects(context.Background())
|
||||
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
|
||||
}
|
||||
|
||||
for _, row := range redirectsRows {
|
||||
router.AddRedirect(target.Redirect{
|
||||
Src: src,
|
||||
Dst: dst,
|
||||
Flags: flags.NormaliseRedirectFlags(),
|
||||
Code: code,
|
||||
Src: row.Source,
|
||||
Dst: row.Destination,
|
||||
Flags: row.Flags.NormaliseRedirectFlags(),
|
||||
Code: row.Code,
|
||||
})
|
||||
}
|
||||
|
||||
// check for errors
|
||||
return rows.Err()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) GetAllRoutes(hosts []string) ([]target.RouteWithActive, error) {
|
||||
@ -148,15 +111,20 @@ func (m *Manager) GetAllRoutes(hosts []string) ([]target.RouteWithActive, error)
|
||||
|
||||
s := make([]target.RouteWithActive, 0)
|
||||
|
||||
query, err := m.db.Query(`SELECT source, destination, description, flags, active FROM routes`)
|
||||
rows, err := m.db.GetAllRoutes(context.Background())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for query.Next() {
|
||||
var a target.RouteWithActive
|
||||
if err := query.Scan(&a.Src, &a.Dst, &a.Desc, &a.Flags, &a.Active); err != nil {
|
||||
return nil, err
|
||||
for _, row := range rows {
|
||||
a := target.RouteWithActive{
|
||||
Route: target.Route{
|
||||
Src: row.Source,
|
||||
Dst: row.Destination,
|
||||
Desc: row.Description,
|
||||
Flags: row.Flags,
|
||||
},
|
||||
Active: row.Active,
|
||||
}
|
||||
|
||||
for _, i := range hosts {
|
||||
@ -172,13 +140,17 @@ func (m *Manager) GetAllRoutes(hosts []string) ([]target.RouteWithActive, error)
|
||||
}
|
||||
|
||||
func (m *Manager) InsertRoute(route target.RouteWithActive) error {
|
||||
_, 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)
|
||||
return err
|
||||
return m.db.AddRoute(context.Background(), database.AddRouteParams{
|
||||
Source: route.Src,
|
||||
Destination: route.Dst,
|
||||
Description: route.Desc,
|
||||
Flags: route.Flags,
|
||||
Active: route.Active,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Manager) DeleteRoute(source string) error {
|
||||
_, err := m.db.Exec(`DELETE FROM routes WHERE source = ?`, source)
|
||||
return err
|
||||
return m.db.RemoveRoute(context.Background(), source)
|
||||
}
|
||||
|
||||
func (m *Manager) GetAllRedirects(hosts []string) ([]target.RedirectWithActive, error) {
|
||||
@ -188,15 +160,21 @@ func (m *Manager) GetAllRedirects(hosts []string) ([]target.RedirectWithActive,
|
||||
|
||||
s := make([]target.RedirectWithActive, 0)
|
||||
|
||||
query, err := m.db.Query(`SELECT source, destination, description, flags, code, active FROM redirects`)
|
||||
rows, err := m.db.GetAllRedirects(context.Background())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for query.Next() {
|
||||
var a target.RedirectWithActive
|
||||
if err := query.Scan(&a.Src, &a.Dst, &a.Desc, &a.Flags, &a.Code, &a.Active); err != nil {
|
||||
return nil, err
|
||||
for _, row := range rows {
|
||||
a := target.RedirectWithActive{
|
||||
Redirect: target.Redirect{
|
||||
Src: row.Source,
|
||||
Dst: row.Destination,
|
||||
Desc: row.Description,
|
||||
Flags: row.Flags,
|
||||
Code: row.Code,
|
||||
},
|
||||
Active: row.Active,
|
||||
}
|
||||
|
||||
for _, i := range hosts {
|
||||
@ -212,13 +190,18 @@ func (m *Manager) GetAllRedirects(hosts []string) ([]target.RedirectWithActive,
|
||||
}
|
||||
|
||||
func (m *Manager) InsertRedirect(redirect target.RedirectWithActive) error {
|
||||
_, 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)
|
||||
return err
|
||||
return m.db.AddRedirect(context.Background(), database.AddRedirectParams{
|
||||
Source: redirect.Src,
|
||||
Destination: redirect.Dst,
|
||||
Description: redirect.Desc,
|
||||
Flags: redirect.Flags,
|
||||
Code: redirect.Code,
|
||||
Active: redirect.Active,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Manager) DeleteRedirect(source string) error {
|
||||
_, err := m.db.Exec(`DELETE FROM redirects WHERE source = ?`, source)
|
||||
return err
|
||||
return m.db.RemoveRedirect(context.Background(), source)
|
||||
}
|
||||
|
||||
// GenerateHostSearch this should help improve performance
|
||||
|
@ -1,7 +1,9 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"context"
|
||||
"github.com/1f349/violet"
|
||||
"github.com/1f349/violet/database"
|
||||
"github.com/1f349/violet/proxy"
|
||||
"github.com/1f349/violet/proxy/websocket"
|
||||
"github.com/1f349/violet/target"
|
||||
@ -22,7 +24,7 @@ func (f *fakeTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
}
|
||||
|
||||
func TestNewManager(t *testing.T) {
|
||||
db, err := sql.Open("sqlite3", "file::memory:?cache=shared")
|
||||
db, err := violet.InitDB("file:TestNewManager?mode=memory&cache=shared")
|
||||
assert.NoError(t, err)
|
||||
|
||||
ft := &fakeTransport{}
|
||||
@ -39,7 +41,13 @@ func TestNewManager(t *testing.T) {
|
||||
assert.Equal(t, http.StatusTeapot, res.StatusCode)
|
||||
assert.Nil(t, ft.req)
|
||||
|
||||
_, 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)
|
||||
err = db.AddRoute(context.Background(), database.AddRouteParams{
|
||||
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, m.internalCompile(m.r))
|
||||
@ -52,10 +60,8 @@ func TestNewManager(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestManager_GetAllRoutes(t *testing.T) {
|
||||
db, err := sql.Open("sqlite3", "file:GetAllRoutes?mode=memory&cache=shared")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
db, err := violet.InitDB("file:TestManager_GetAllRoutes?mode=memory&cache=shared")
|
||||
assert.NoError(t, err)
|
||||
m := NewManager(db, nil)
|
||||
a := []error{
|
||||
m.InsertRoute(target.RouteWithActive{Route: target.Route{Src: "example.com"}, Active: true}),
|
||||
@ -85,10 +91,8 @@ func TestManager_GetAllRoutes(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestManager_GetAllRedirects(t *testing.T) {
|
||||
db, err := sql.Open("sqlite3", "file:GetAllRedirects?mode=memory&cache=shared")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
db, err := violet.InitDB("file:TestManager_GetAllRedirects?mode=memory&cache=shared")
|
||||
assert.NoError(t, err)
|
||||
m := NewManager(db, nil)
|
||||
a := []error{
|
||||
m.InsertRedirect(target.RedirectWithActive{Redirect: target.Redirect{Src: "example.com"}, Active: true}),
|
||||
|
@ -5,7 +5,7 @@ import (
|
||||
"github.com/1f349/violet/proxy"
|
||||
"github.com/1f349/violet/target"
|
||||
"github.com/1f349/violet/utils"
|
||||
"github.com/MrMelon54/trie"
|
||||
"github.com/mrmelon54/trie"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
@ -5,7 +5,7 @@ import (
|
||||
"github.com/1f349/violet/proxy"
|
||||
"github.com/1f349/violet/proxy/websocket"
|
||||
"github.com/1f349/violet/target"
|
||||
"github.com/MrMelon54/trie"
|
||||
"github.com/mrmelon54/trie"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
@ -7,6 +7,8 @@ import (
|
||||
"github.com/1f349/violet/servers/conf"
|
||||
"github.com/1f349/violet/utils"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
@ -15,12 +17,15 @@ import (
|
||||
// endpoints for the software
|
||||
//
|
||||
// `/compile` - reloads all domains, routes and redirects
|
||||
func NewApiServer(conf *conf.Conf, compileTarget utils.MultiCompilable) *http.Server {
|
||||
func NewApiServer(conf *conf.Conf, compileTarget utils.MultiCompilable, registry *prometheus.Registry) *http.Server {
|
||||
r := httprouter.New()
|
||||
|
||||
r.GET("/", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params) {
|
||||
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
|
||||
r.POST("/compile", checkAuthWithPerm(conf.Signer, "violet:compile", func(rw http.ResponseWriter, req *http.Request, _ httprouter.Params, b AuthClaims) {
|
||||
@ -43,7 +48,6 @@ func NewApiServer(conf *conf.Conf, compileTarget utils.MultiCompilable) *http.Se
|
||||
|
||||
// Create and run http server
|
||||
return &http.Server{
|
||||
Addr: conf.ApiListen,
|
||||
Handler: r,
|
||||
ReadTimeout: time.Minute,
|
||||
ReadHeaderTimeout: time.Minute,
|
||||
|
@ -17,7 +17,7 @@ func TestNewApiServer_Compile(t *testing.T) {
|
||||
Signer: fake.SnakeOilProv,
|
||||
}
|
||||
f := &fake.Compilable{}
|
||||
srv := NewApiServer(apiConf, utils.MultiCompilable{f})
|
||||
srv := NewApiServer(apiConf, utils.MultiCompilable{f}, nil)
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, "https://example.com/compile", nil)
|
||||
assert.NoError(t, err)
|
||||
@ -43,7 +43,7 @@ func TestNewApiServer_AcmeChallenge_Put(t *testing.T) {
|
||||
Acme: utils.NewAcmeChallenge(),
|
||||
Signer: fake.SnakeOilProv,
|
||||
}
|
||||
srv := NewApiServer(apiConf, utils.MultiCompilable{})
|
||||
srv := NewApiServer(apiConf, utils.MultiCompilable{}, nil)
|
||||
acmeKey := fake.GenSnakeOilKey("violet:acme-challenge")
|
||||
|
||||
// Valid domain
|
||||
@ -87,7 +87,7 @@ func TestNewApiServer_AcmeChallenge_Delete(t *testing.T) {
|
||||
Acme: utils.NewAcmeChallenge(),
|
||||
Signer: fake.SnakeOilProv,
|
||||
}
|
||||
srv := NewApiServer(apiConf, utils.MultiCompilable{})
|
||||
srv := NewApiServer(apiConf, utils.MultiCompilable{}, nil)
|
||||
acmeKey := fake.GenSnakeOilKey("violet:acme-challenge")
|
||||
|
||||
// Valid domain
|
||||
|
@ -3,11 +3,11 @@ package api
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/1f349/mjwt"
|
||||
"github.com/1f349/violet/logger"
|
||||
"github.com/1f349/violet/router"
|
||||
"github.com/1f349/violet/target"
|
||||
"github.com/1f349/violet/utils"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
@ -19,7 +19,7 @@ func SetupTargetApis(r *httprouter.Router, verify mjwt.Verifier, manager *router
|
||||
|
||||
routes, err := manager.GetAllRoutes(domains)
|
||||
if err != nil {
|
||||
log.Printf("[Violet] Failed to get routes from database: %s\n", err)
|
||||
logger.Logger.Infof("Failed to get routes from database: %s\n", err)
|
||||
apiError(rw, http.StatusInternalServerError, "Failed to get routes from database")
|
||||
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) {
|
||||
err := manager.InsertRoute(target.RouteWithActive(t))
|
||||
if err != nil {
|
||||
log.Printf("[Violet] Failed to insert route into database: %s\n", err)
|
||||
logger.Logger.Infof("Failed to insert route into database: %s\n", err)
|
||||
apiError(rw, http.StatusInternalServerError, "Failed to insert route into database")
|
||||
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) {
|
||||
err := manager.DeleteRoute(t.Src)
|
||||
if err != nil {
|
||||
log.Printf("[Violet] Failed to delete route from database: %s\n", err)
|
||||
logger.Logger.Infof("Failed to delete route from database: %s\n", err)
|
||||
apiError(rw, http.StatusInternalServerError, "Failed to delete route from database")
|
||||
return
|
||||
}
|
||||
@ -51,7 +51,7 @@ func SetupTargetApis(r *httprouter.Router, verify mjwt.Verifier, manager *router
|
||||
|
||||
redirects, err := manager.GetAllRedirects(domains)
|
||||
if err != nil {
|
||||
log.Printf("[Violet] Failed to get redirects from database: %s\n", err)
|
||||
logger.Logger.Infof("Failed to get redirects from database: %s\n", err)
|
||||
apiError(rw, http.StatusInternalServerError, "Failed to get redirects from database")
|
||||
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) {
|
||||
err := manager.InsertRedirect(target.RedirectWithActive(t))
|
||||
if err != nil {
|
||||
log.Printf("[Violet] Failed to insert redirect into database: %s\n", err)
|
||||
logger.Logger.Infof("Failed to insert redirect into database: %s\n", err)
|
||||
apiError(rw, http.StatusInternalServerError, "Failed to insert redirect into database")
|
||||
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) {
|
||||
err := manager.DeleteRedirect(t.Src)
|
||||
if err != nil {
|
||||
log.Printf("[Violet] Failed to delete redirect from database: %s\n", err)
|
||||
logger.Logger.Infof("Failed to delete redirect from database: %s\n", err)
|
||||
apiError(rw, http.StatusInternalServerError, "Failed to delete redirect from database")
|
||||
return
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"github.com/1f349/mjwt"
|
||||
"github.com/1f349/violet/database"
|
||||
errorPages "github.com/1f349/violet/error-pages"
|
||||
"github.com/1f349/violet/favicons"
|
||||
"github.com/1f349/violet/router"
|
||||
@ -11,16 +11,13 @@ import (
|
||||
|
||||
// Conf stores the shared configuration for the API, HTTP and HTTPS servers.
|
||||
type Conf struct {
|
||||
ApiListen string // api server listen address
|
||||
HttpListen string // http server listen address
|
||||
HttpsListen string // https server listen address
|
||||
RateLimit uint64 // rate limit per minute
|
||||
DB *sql.DB
|
||||
Domains utils.DomainProvider
|
||||
Acme utils.AcmeChallengeProvider
|
||||
Certs utils.CertProvider
|
||||
Favicons *favicons.Favicons
|
||||
Signer mjwt.Verifier
|
||||
ErrorPages *errorPages.ErrorPages
|
||||
Router *router.Manager
|
||||
RateLimit uint64 // rate limit per minute
|
||||
DB *database.Queries
|
||||
Domains utils.DomainProvider
|
||||
Acme utils.AcmeChallengeProvider
|
||||
Certs utils.CertProvider
|
||||
Favicons *favicons.Favicons
|
||||
Signer mjwt.Verifier
|
||||
ErrorPages *errorPages.ErrorPages
|
||||
Router *router.Manager
|
||||
}
|
||||
|
@ -3,8 +3,10 @@ package servers
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/1f349/violet/servers/conf"
|
||||
"github.com/1f349/violet/servers/metrics"
|
||||
"github.com/1f349/violet/utils"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
@ -15,13 +17,9 @@ import (
|
||||
//
|
||||
// `/.well-known/acme-challenge/{token}` is used for outputting answers for
|
||||
// acme challenges, this is used for Let's Encrypt HTTP verification.
|
||||
func NewHttpServer(conf *conf.Conf) *http.Server {
|
||||
func NewHttpServer(httpsPort uint16, conf *conf.Conf, registry *prometheus.Registry) *http.Server {
|
||||
r := httprouter.New()
|
||||
var secureExtend string
|
||||
_, httpsPort, ok := utils.SplitDomainPort(conf.HttpsListen, 443)
|
||||
if !ok {
|
||||
httpsPort = 443
|
||||
}
|
||||
if httpsPort != 443 {
|
||||
secureExtend = fmt.Sprintf(":%d", httpsPort)
|
||||
}
|
||||
@ -61,10 +59,16 @@ func NewHttpServer(conf *conf.Conf) *http.Server {
|
||||
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
|
||||
return &http.Server{
|
||||
Addr: conf.HttpListen,
|
||||
Handler: r,
|
||||
Handler: metricsMiddleware,
|
||||
ReadTimeout: time.Minute,
|
||||
ReadHeaderTimeout: time.Minute,
|
||||
WriteTimeout: time.Minute,
|
||||
|
@ -18,7 +18,7 @@ func TestNewHttpServer_AcmeChallenge(t *testing.T) {
|
||||
Acme: utils.NewAcmeChallenge(),
|
||||
Signer: fake.SnakeOilProv,
|
||||
}
|
||||
srv := NewHttpServer(httpConf)
|
||||
srv := NewHttpServer(443, httpConf, nil)
|
||||
httpConf.Acme.Put("example.com", "456", "456def")
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "https://example.com/.well-known/acme-challenge/456", nil)
|
||||
|
@ -4,11 +4,13 @@ import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"github.com/1f349/violet/favicons"
|
||||
"github.com/1f349/violet/logger"
|
||||
"github.com/1f349/violet/servers/conf"
|
||||
"github.com/1f349/violet/servers/metrics"
|
||||
"github.com/1f349/violet/utils"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/sethvargo/go-limiter/httplimit"
|
||||
"github.com/sethvargo/go-limiter/memorystore"
|
||||
"log"
|
||||
"net/http"
|
||||
"path"
|
||||
"runtime"
|
||||
@ -17,35 +19,57 @@ import (
|
||||
|
||||
// NewHttpsServer creates and runs a http server containing the public https
|
||||
// endpoints for the reverse proxy.
|
||||
func NewHttpsServer(conf *conf.Conf) *http.Server {
|
||||
func NewHttpsServer(conf *conf.Conf, registry *prometheus.Registry) *http.Server {
|
||||
r := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
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())
|
||||
logger.Logger.Debug("Request", "method", req.Method, "url", req.URL, "remote", req.RemoteAddr, "host", req.Host, "length", req.ContentLength, "goroutine", runtime.NumGoroutine())
|
||||
conf.Router.ServeHTTP(rw, req)
|
||||
})
|
||||
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{
|
||||
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{GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
// error out on invalid domains
|
||||
if !conf.Domains.IsValid(info.ServerName) {
|
||||
return nil, fmt.Errorf("invalid hostname used: '%s'", info.ServerName)
|
||||
}
|
||||
Handler: hsts,
|
||||
TLSConfig: &tls.Config{
|
||||
// Suggested by https://ssl-config.mozilla.org/#server=go&version=1.21.5&config=intermediate
|
||||
MinVersion: tls.VersionTLS12,
|
||||
CipherSuites: []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_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_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
||||
},
|
||||
GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
// error out on invalid domains
|
||||
if !conf.Domains.IsValid(info.ServerName) {
|
||||
return nil, fmt.Errorf("invalid hostname used: '%s'", info.ServerName)
|
||||
}
|
||||
|
||||
// find a certificate
|
||||
cert := conf.Certs.GetCertForDomain(info.ServerName)
|
||||
if cert == nil {
|
||||
return nil, fmt.Errorf("failed to find certificate for: '%s'", info.ServerName)
|
||||
}
|
||||
// find a certificate
|
||||
cert := conf.Certs.GetCertForDomain(info.ServerName)
|
||||
if cert == nil {
|
||||
return nil, fmt.Errorf("failed to find certificate for: '%s'", info.ServerName)
|
||||
}
|
||||
|
||||
// time to return
|
||||
return cert, nil
|
||||
}},
|
||||
// time to return
|
||||
return cert, nil
|
||||
},
|
||||
},
|
||||
ReadTimeout: 150 * time.Second,
|
||||
ReadHeaderTimeout: 150 * time.Second,
|
||||
WriteTimeout: 150 * time.Second,
|
||||
@ -63,13 +87,13 @@ func setupRateLimiter(rateLimit uint64, next http.Handler) http.Handler {
|
||||
Interval: time.Minute,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
logger.Logger.Fatal("Failed to initialize memory store", "err", err)
|
||||
}
|
||||
|
||||
// create a middleware using ips as the key for rate limits
|
||||
middleware, err := httplimit.NewMiddleware(store, httplimit.IPKeyFunc())
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
logger.Logger.Fatal("Failed to initialize httplimit middleware", "err", err)
|
||||
}
|
||||
return middleware.Handle(next)
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
package servers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"github.com/1f349/violet"
|
||||
"github.com/1f349/violet/certs"
|
||||
"github.com/1f349/violet/proxy"
|
||||
"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) {
|
||||
db, err := sql.Open("sqlite3", "file::memory:?cache=shared")
|
||||
db, err := violet.InitDB("file:TestNewHttpsServer_RateLimit?mode=memory&cache=shared")
|
||||
assert.NoError(t, err)
|
||||
|
||||
ft := &fakeTransport{}
|
||||
@ -36,7 +36,7 @@ func TestNewHttpsServer_RateLimit(t *testing.T) {
|
||||
Signer: fake.SnakeOilProv,
|
||||
Router: router.NewManager(db, proxy.NewHybridTransportWithCalls(ft, ft, &websocket.Server{})),
|
||||
}
|
||||
srv := NewHttpsServer(httpsConf)
|
||||
srv := NewHttpsServer(httpsConf, nil)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "https://example.com", nil)
|
||||
req.RemoteAddr = "127.0.0.1:1447"
|
||||
|
118
servers/metrics/httpmiddleware.go
Normal file
118
servers/metrics/httpmiddleware.go
Normal file
@ -0,0 +1,118 @@
|
||||
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))
|
||||
}
|
15
sqlc.yaml
Normal file
15
sqlc.yaml
Normal file
@ -0,0 +1,15 @@
|
||||
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"
|
@ -16,7 +16,7 @@ type Redirect struct {
|
||||
Dst string `json:"dst"` // redirect destination
|
||||
Desc string `json:"desc"` // description for admin panel use
|
||||
Flags Flags `json:"flags"` // extra flags
|
||||
Code int `json:"code"` // status code used to redirect
|
||||
Code int64 `json:"code"` // status code used to redirect
|
||||
}
|
||||
|
||||
type RedirectWithActive struct {
|
||||
@ -78,7 +78,7 @@ func (r Redirect) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
// use fast redirect for speed
|
||||
utils.FastRedirect(rw, req, u.String(), code)
|
||||
utils.FastRedirect(rw, req, u.String(), int(code))
|
||||
}
|
||||
|
||||
// String outputs a debug string for the redirect.
|
||||
|
@ -35,7 +35,7 @@ func TestRedirect_ServeHTTP(t *testing.T) {
|
||||
res := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "https://www.example.com/hello/world", nil)
|
||||
i.ServeHTTP(res, req)
|
||||
assert.Equal(t, i.Code, res.Code)
|
||||
assert.Equal(t, i.Code, int64(res.Code))
|
||||
assert.Equal(t, i.target, res.Header().Get("Location"))
|
||||
}
|
||||
}
|
||||
|
@ -2,14 +2,13 @@ package target
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/1f349/violet/logger"
|
||||
"github.com/1f349/violet/proxy"
|
||||
"github.com/1f349/violet/utils"
|
||||
"github.com/google/uuid"
|
||||
websocket2 "github.com/gorilla/websocket"
|
||||
"github.com/rs/cors"
|
||||
"golang.org/x/net/http/httpguts"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
@ -18,10 +17,13 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
var Logger = logger.Logger.WithPrefix("Violet Serve Route")
|
||||
|
||||
// serveApiCors outputs the cors headers to make APIs work.
|
||||
var serveApiCors = cors.New(cors.Options{
|
||||
AllowedOrigins: []string{"*"}, // allow all origins for api requests
|
||||
AllowedHeaders: []string{"Content-Type", "Authorization"},
|
||||
// allow all origins for api requests
|
||||
AllowOriginFunc: func(origin string) bool { return true },
|
||||
AllowedHeaders: []string{"Content-Type", "Authorization"},
|
||||
AllowedMethods: []string{
|
||||
http.MethodGet,
|
||||
http.MethodHead,
|
||||
@ -127,8 +129,7 @@ func (r Route) internalServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
// create the internal request
|
||||
req2, err := http.NewRequest(req.Method, u.String(), req.Body)
|
||||
if err != nil {
|
||||
log.Printf("[ServeRoute::ServeHTTP()] Error generating new request: %s\n", err)
|
||||
utils.RespondVioletError(rw, http.StatusBadGateway, "error generating new request")
|
||||
utils.RespondVioletError(rw, http.StatusBadGateway, "Invalid request for proxy")
|
||||
return
|
||||
}
|
||||
|
||||
@ -178,8 +179,8 @@ func (r Route) internalServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
resp, err = r.Proxy.SecureRoundTrip(req2)
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("[ServeRoute::ServeHTTP()] Error receiving internal round trip response: %s\n", err)
|
||||
utils.RespondVioletError(rw, http.StatusBadGateway, "error receiving internal round trip response")
|
||||
Logger.Warn("Error receiving internal round trip response", "route src", r.Src, "url", req2.URL.String(), "err", err)
|
||||
utils.RespondVioletError(rw, http.StatusBadGateway, "Error receiving internal round trip response")
|
||||
return
|
||||
}
|
||||
|
||||
@ -189,9 +190,8 @@ func (r Route) internalServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusLoopDetected {
|
||||
u := uuid.New()
|
||||
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())
|
||||
Logger.Warn("Loop Detected", "method", req.Method, "url", req.URL, "url2", req2.URL.String())
|
||||
utils.RespondVioletError(rw, http.StatusLoopDetected, "Error loop detected")
|
||||
return
|
||||
}
|
||||
|
||||
@ -222,7 +222,7 @@ func (r Route) internalReverseProxyMeta(rw http.ResponseWriter, req, req2 *http.
|
||||
|
||||
reqUpType := upgradeType(req2.Header)
|
||||
if !asciiIsPrint(reqUpType) {
|
||||
utils.RespondVioletError(rw, http.StatusBadRequest, fmt.Sprintf("client tried to switch to invalid protocol %q", reqUpType))
|
||||
utils.RespondVioletError(rw, http.StatusBadRequest, fmt.Sprintf("Invalid protocol %s", reqUpType))
|
||||
return true
|
||||
}
|
||||
removeHopByHopHeaders(req2.Header)
|
||||
|
@ -90,7 +90,7 @@ func TestRoute_ServeHTTP_Cors(t *testing.T) {
|
||||
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, "Origin", res.Header().Get("Vary"))
|
||||
assert.Equal(t, "*", res.Header().Get("Access-Control-Allow-Origin"))
|
||||
assert.Equal(t, "https://test.example.com", res.Header().Get("Access-Control-Allow-Origin"))
|
||||
assert.Equal(t, "true", res.Header().Get("Access-Control-Allow-Credentials"))
|
||||
assert.Equal(t, "Origin", res.Header().Get("Vary"))
|
||||
}
|
||||
|
@ -2,33 +2,34 @@ package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"github.com/charmbracelet/log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// logHttpServerError is the internal function powering the logging in
|
||||
// RunBackgroundHttp and RunBackgroundHttps.
|
||||
func logHttpServerError(prefix string, err error) {
|
||||
func logHttpServerError(logger *log.Logger, err error) {
|
||||
if err != nil {
|
||||
if errors.Is(err, http.ErrServerClosed) {
|
||||
log.Printf("[%s] The http server shutdown successfully\n", prefix)
|
||||
logger.Info("The http server shutdown successfully")
|
||||
} else {
|
||||
log.Printf("[%s] Error trying to host the http server: %s\n", prefix, err.Error())
|
||||
logger.Info("Error trying to host the http server", "err", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RunBackgroundHttp runs a http server and logs when the server closes or
|
||||
// errors.
|
||||
func RunBackgroundHttp(prefix string, s *http.Server) {
|
||||
logHttpServerError(prefix, s.ListenAndServe())
|
||||
func RunBackgroundHttp(logger *log.Logger, s *http.Server, ln net.Listener) {
|
||||
logHttpServerError(logger, s.Serve(ln))
|
||||
}
|
||||
|
||||
// RunBackgroundHttps runs a http server with TLS encryption and logs when the
|
||||
// server closes or errors.
|
||||
func RunBackgroundHttps(prefix string, s *http.Server) {
|
||||
logHttpServerError(prefix, s.ListenAndServeTLS("", ""))
|
||||
func RunBackgroundHttps(logger *log.Logger, s *http.Server, ln net.Listener) {
|
||||
logHttpServerError(logger, s.ServeTLS(ln, "", ""))
|
||||
}
|
||||
|
||||
// GetBearer returns the bearer from the Authorization header or an empty string
|
||||
|
Loading…
x
Reference in New Issue
Block a user