Skip to content

Commit d3d6782

Browse files
committed
Merge branch 'tableflip'
2 parents 0f09505 + d7b7721 commit d3d6782

File tree

9 files changed

+130
-86
lines changed

9 files changed

+130
-86
lines changed

cmd/violet/serve.go

+110-58
Original file line numberDiff line numberDiff line change
@@ -18,35 +18,53 @@ import (
1818
"github.com/1f349/violet/servers/api"
1919
"github.com/1f349/violet/servers/conf"
2020
"github.com/1f349/violet/utils"
21+
"github.com/charmbracelet/log"
22+
"github.com/cloudflare/tableflip"
2123
"github.com/google/subcommands"
22-
"github.com/mrmelon54/exit-reload"
2324
"github.com/prometheus/client_golang/prometheus"
2425
"github.com/prometheus/client_golang/prometheus/collectors"
2526
"io/fs"
2627
"net/http"
2728
"os"
29+
"os/signal"
2830
"path/filepath"
31+
"syscall"
32+
"time"
2933
)
3034

3135
type serveCmd struct {
3236
configPath string
33-
cpuprofile string
37+
debugLog bool
38+
pidFile string
3439
}
3540

3641
func (s *serveCmd) Name() string { return "serve" }
3742
func (s *serveCmd) Synopsis() string { return "Serve reverse proxy server" }
3843
func (s *serveCmd) SetFlags(f *flag.FlagSet) {
3944
f.StringVar(&s.configPath, "conf", "", "/path/to/config.json : path to the config file")
45+
f.BoolVar(&s.debugLog, "debug", false, "enable debug logging")
46+
f.StringVar(&s.pidFile, "pid-file", "", "path to pid file")
4047
}
4148
func (s *serveCmd) Usage() string {
42-
return `serve [-conf <config file>]
49+
return `serve [-conf <config file>] [-debug] [-pid-file <pid file>]
4350
Serve reverse proxy server using information from config file
4451
`
4552
}
4653

4754
func (s *serveCmd) Execute(_ context.Context, _ *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
55+
if s.debugLog {
56+
logger.Logger.SetLevel(log.DebugLevel)
57+
}
4858
logger.Logger.Info("Starting...")
4959

60+
upg, err := tableflip.New(tableflip.Options{
61+
PIDFile: s.pidFile,
62+
})
63+
if err != nil {
64+
panic(err)
65+
}
66+
defer upg.Stop()
67+
5068
if s.configPath == "" {
5169
logger.Logger.Info("Error: config flag is missing")
5270
return subcommands.ExitUsageError
@@ -71,13 +89,9 @@ func (s *serveCmd) Execute(_ context.Context, _ *flag.FlagSet, _ ...interface{})
7189

7290
// working directory is the parent of the config file
7391
wd := filepath.Dir(s.configPath)
74-
normalLoad(config, wd)
75-
return subcommands.ExitSuccess
76-
}
7792

78-
func normalLoad(startUp startUpConfig, wd string) {
7993
// the cert and key paths are useless in self-signed mode
80-
if !startUp.SelfSigned {
94+
if !config.SelfSigned {
8195
// create path to cert dir
8296
err := os.MkdirAll(filepath.Join(wd, "certs"), os.ModePerm)
8397
if err != nil {
@@ -92,11 +106,11 @@ func normalLoad(startUp startUpConfig, wd string) {
92106

93107
// errorPageDir stores an FS interface for accessing the error page directory
94108
var errorPageDir fs.FS
95-
if startUp.ErrorPagePath != "" {
96-
errorPageDir = os.DirFS(startUp.ErrorPagePath)
97-
err := os.MkdirAll(startUp.ErrorPagePath, os.ModePerm)
109+
if config.ErrorPagePath != "" {
110+
errorPageDir = os.DirFS(config.ErrorPagePath)
111+
err := os.MkdirAll(config.ErrorPagePath, os.ModePerm)
98112
if err != nil {
99-
logger.Logger.Fatal("Failed to create error page", "path", startUp.ErrorPagePath)
113+
logger.Logger.Fatal("Failed to create error page", "path", config.ErrorPagePath)
100114
}
101115
}
102116

@@ -123,75 +137,113 @@ func normalLoad(startUp startUpConfig, wd string) {
123137
)
124138

125139
ws := websocket.NewServer()
126-
allowedDomains := domains.New(db) // load allowed domains
127-
acmeChallenges := utils.NewAcmeChallenge() // load acme challenge store
128-
allowedCerts := certs.New(certDir, keyDir, startUp.SelfSigned) // load certificate manager
129-
hybridTransport := proxy.NewHybridTransport(ws) // load reverse proxy
130-
dynamicFavicons := favicons.New(db, startUp.InkscapeCmd) // load dynamic favicon provider
131-
dynamicErrorPages := errorPages.New(errorPageDir) // load dynamic error page provider
132-
dynamicRouter := router.NewManager(db, hybridTransport) // load dynamic router manager
140+
allowedDomains := domains.New(db) // load allowed domains
141+
acmeChallenges := utils.NewAcmeChallenge() // load acme challenge store
142+
allowedCerts := certs.New(certDir, keyDir, config.SelfSigned) // load certificate manager
143+
hybridTransport := proxy.NewHybridTransport(ws) // load reverse proxy
144+
dynamicFavicons := favicons.New(db, config.InkscapeCmd) // load dynamic favicon provider
145+
dynamicErrorPages := errorPages.New(errorPageDir) // load dynamic error page provider
146+
dynamicRouter := router.NewManager(db, hybridTransport) // load dynamic router manager
133147

134148
// struct containing config for the http servers
135149
srvConf := &conf.Conf{
136-
ApiListen: startUp.Listen.Api,
137-
HttpListen: startUp.Listen.Http,
138-
HttpsListen: startUp.Listen.Https,
139-
RateLimit: startUp.RateLimit,
140-
DB: db,
141-
Domains: allowedDomains,
142-
Acme: acmeChallenges,
143-
Certs: allowedCerts,
144-
Favicons: dynamicFavicons,
145-
Signer: mJwtVerify,
146-
ErrorPages: dynamicErrorPages,
147-
Router: dynamicRouter,
150+
RateLimit: config.RateLimit,
151+
DB: db,
152+
Domains: allowedDomains,
153+
Acme: acmeChallenges,
154+
Certs: allowedCerts,
155+
Favicons: dynamicFavicons,
156+
Signer: mJwtVerify,
157+
ErrorPages: dynamicErrorPages,
158+
Router: dynamicRouter,
148159
}
149160

150161
// create the compilable list and run a first time compile
151162
allCompilables := utils.MultiCompilable{allowedDomains, allowedCerts, dynamicFavicons, dynamicErrorPages, dynamicRouter}
152163
allCompilables.Compile()
153164

165+
_, httpsPort, ok := utils.SplitDomainPort(config.Listen.Https, 443)
166+
if !ok {
167+
httpsPort = 443
168+
}
169+
154170
var srvApi, srvHttp, srvHttps *http.Server
155-
if srvConf.ApiListen != "" {
171+
if config.Listen.Api != "" {
172+
// Listen must be called before Ready
173+
lnApi, err := upg.Listen("tcp", config.Listen.Api)
174+
if err != nil {
175+
logger.Logger.Fatal("Listen failed", "err", err)
176+
}
156177
srvApi = api.NewApiServer(srvConf, allCompilables, promRegistry)
157178
srvApi.SetKeepAlivesEnabled(false)
158179
l := logger.Logger.With("server", "API")
159-
l.Info("Starting server", "addr", srvApi.Addr)
160-
go utils.RunBackgroundHttp(l, srvApi)
180+
l.Info("Starting server", "addr", config.Listen.Api)
181+
go utils.RunBackgroundHttp(l, srvApi, lnApi)
161182
}
162-
if srvConf.HttpListen != "" {
163-
srvHttp = servers.NewHttpServer(srvConf, promRegistry)
183+
if config.Listen.Http != "" {
184+
// Listen must be called before Ready
185+
lnHttp, err := upg.Listen("tcp", config.Listen.Http)
186+
if err != nil {
187+
logger.Logger.Fatal("Listen failed", "err", err)
188+
}
189+
srvHttp = servers.NewHttpServer(uint16(httpsPort), srvConf, promRegistry)
164190
srvHttp.SetKeepAlivesEnabled(false)
165191
l := logger.Logger.With("server", "HTTP")
166-
l.Info("Starting server", "addr", srvHttp.Addr)
167-
go utils.RunBackgroundHttp(l, srvHttp)
192+
l.Info("Starting server", "addr", config.Listen.Http)
193+
go utils.RunBackgroundHttp(l, srvHttp, lnHttp)
168194
}
169-
if srvConf.HttpsListen != "" {
195+
if config.Listen.Https != "" {
196+
// Listen must be called before Ready
197+
lnHttps, err := upg.Listen("tcp", config.Listen.Https)
198+
if err != nil {
199+
logger.Logger.Fatal("Listen failed", "err", err)
200+
}
170201
srvHttps = servers.NewHttpsServer(srvConf, promRegistry)
171202
srvHttps.SetKeepAlivesEnabled(false)
172203
l := logger.Logger.With("server", "HTTPS")
173-
l.Info("Starting server", "addr", srvHttps.Addr)
174-
go utils.RunBackgroundHttps(l, srvHttps)
204+
l.Info("Starting server", "addr", config.Listen.Https)
205+
go utils.RunBackgroundHttps(l, srvHttps, lnHttps)
175206
}
176207

177-
exit_reload.ExitReload("Violet", func() {
178-
allCompilables.Compile()
179-
}, func() {
180-
// stop updating certificates
181-
allowedCerts.Stop()
208+
// Do an upgrade on SIGHUP
209+
go func() {
210+
sig := make(chan os.Signal, 1)
211+
signal.Notify(sig, syscall.SIGHUP)
212+
for range sig {
213+
err := upg.Upgrade()
214+
if err != nil {
215+
logger.Logger.Error("Failed upgrade", "err", err)
216+
}
217+
}
218+
}()
182219

183-
// close websockets first
184-
ws.Shutdown()
220+
logger.Logger.Info("Ready")
221+
if err := upg.Ready(); err != nil {
222+
panic(err)
223+
}
224+
<-upg.Exit()
185225

186-
// close http servers
187-
if srvApi != nil {
188-
_ = srvApi.Close()
189-
}
190-
if srvHttp != nil {
191-
_ = srvHttp.Close()
192-
}
193-
if srvHttps != nil {
194-
_ = srvHttps.Close()
195-
}
226+
time.AfterFunc(30*time.Second, func() {
227+
logger.Logger.Warn("Graceful shutdown timed out")
228+
os.Exit(1)
196229
})
230+
231+
// stop updating certificates
232+
allowedCerts.Stop()
233+
234+
// close websockets first
235+
ws.Shutdown()
236+
237+
// close http servers
238+
if srvApi != nil {
239+
_ = srvApi.Close()
240+
}
241+
if srvHttp != nil {
242+
_ = srvHttp.Close()
243+
}
244+
if srvHttps != nil {
245+
_ = srvHttps.Close()
246+
}
247+
248+
return subcommands.ExitSuccess
197249
}

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ require (
66
github.com/1f349/mjwt v0.2.5
77
github.com/AlecAivazis/survey/v2 v2.3.7
88
github.com/charmbracelet/log v0.4.0
9+
github.com/cloudflare/tableflip v1.2.3
910
github.com/golang-migrate/migrate/v4 v4.17.1
1011
github.com/google/subcommands v1.2.0
1112
github.com/google/uuid v1.6.0
1213
github.com/gorilla/websocket v1.5.1
1314
github.com/julienschmidt/httprouter v1.3.0
1415
github.com/mattn/go-sqlite3 v1.14.22
1516
github.com/mrmelon54/certgen v0.0.2
16-
github.com/mrmelon54/exit-reload v0.0.2
1717
github.com/mrmelon54/png2ico v1.0.2
1818
github.com/mrmelon54/rescheduler v0.0.3
1919
github.com/mrmelon54/trie v0.0.3

go.sum

+3-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMt
1616
github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE=
1717
github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=
1818
github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM=
19+
github.com/cloudflare/tableflip v1.2.3 h1:8I+B99QnnEWPHOY3fWipwVKxS70LGgUsslG7CSfmHMw=
20+
github.com/cloudflare/tableflip v1.2.3/go.mod h1:P4gRehmV6Z2bY5ao5ml9Pd8u6kuEnlB37pUFMmv7j2E=
1921
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
2022
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
2123
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
@@ -72,8 +74,6 @@ github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQ
7274
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
7375
github.com/mrmelon54/certgen v0.0.2 h1:4CMDkA/gGZu+E4iikU+5qdOWK7qOQrk58KtUfnmyYmY=
7476
github.com/mrmelon54/certgen v0.0.2/go.mod h1:vwrWSXQmxZYqEyh+cf05IvDIFV2aYuxL4+O6ABIlN8M=
75-
github.com/mrmelon54/exit-reload v0.0.2 h1:vqgfrMD/bF21HkDsWgg5+NLjFDrD3KGVEN/iTrMn9Ms=
76-
github.com/mrmelon54/exit-reload v0.0.2/go.mod h1:aE3NhsqGMLUqmv6cJZRouC/8gXkZTvVSabRGOpI+Vjc=
7777
github.com/mrmelon54/png2ico v1.0.2 h1:KyJd3ATmDjxAJS28MTSf44GxzYnlZ+7KT8SXzGb3sN8=
7878
github.com/mrmelon54/png2ico v1.0.2/go.mod h1:vp8Be9y5cz102ANon+BnsIzTUdet3VQRvOuWJTH9h0M=
7979
github.com/mrmelon54/rescheduler v0.0.3 h1:TrkJL6S7PKvXuo1mvdgRgsILA/pk5L1lrXhV/q7IEzQ=
@@ -130,6 +130,7 @@ golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
130130
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
131131
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
132132
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
133+
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
133134
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
134135
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
135136
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

servers/api/api.go

-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ func NewApiServer(conf *conf.Conf, compileTarget utils.MultiCompilable, registry
4848

4949
// Create and run http server
5050
return &http.Server{
51-
Addr: conf.ApiListen,
5251
Handler: r,
5352
ReadTimeout: time.Minute,
5453
ReadHeaderTimeout: time.Minute,

servers/conf/conf.go

+9-12
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,13 @@ import (
1111

1212
// Conf stores the shared configuration for the API, HTTP and HTTPS servers.
1313
type Conf struct {
14-
ApiListen string // api server listen address
15-
HttpListen string // http server listen address
16-
HttpsListen string // https server listen address
17-
RateLimit uint64 // rate limit per minute
18-
DB *database.Queries
19-
Domains utils.DomainProvider
20-
Acme utils.AcmeChallengeProvider
21-
Certs utils.CertProvider
22-
Favicons *favicons.Favicons
23-
Signer mjwt.Verifier
24-
ErrorPages *errorPages.ErrorPages
25-
Router *router.Manager
14+
RateLimit uint64 // rate limit per minute
15+
DB *database.Queries
16+
Domains utils.DomainProvider
17+
Acme utils.AcmeChallengeProvider
18+
Certs utils.CertProvider
19+
Favicons *favicons.Favicons
20+
Signer mjwt.Verifier
21+
ErrorPages *errorPages.ErrorPages
22+
Router *router.Manager
2623
}

servers/http.go

+1-6
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,9 @@ import (
1717
//
1818
// `/.well-known/acme-challenge/{token}` is used for outputting answers for
1919
// acme challenges, this is used for Let's Encrypt HTTP verification.
20-
func NewHttpServer(conf *conf.Conf, registry *prometheus.Registry) *http.Server {
20+
func NewHttpServer(httpsPort uint16, conf *conf.Conf, registry *prometheus.Registry) *http.Server {
2121
r := httprouter.New()
2222
var secureExtend string
23-
_, httpsPort, ok := utils.SplitDomainPort(conf.HttpsListen, 443)
24-
if !ok {
25-
httpsPort = 443
26-
}
2723
if httpsPort != 443 {
2824
secureExtend = fmt.Sprintf(":%d", httpsPort)
2925
}
@@ -72,7 +68,6 @@ func NewHttpServer(conf *conf.Conf, registry *prometheus.Registry) *http.Server
7268

7369
// Create and run http server
7470
return &http.Server{
75-
Addr: conf.HttpListen,
7671
Handler: metricsMiddleware,
7772
ReadTimeout: time.Minute,
7873
ReadHeaderTimeout: time.Minute,

servers/http_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ func TestNewHttpServer_AcmeChallenge(t *testing.T) {
1818
Acme: utils.NewAcmeChallenge(),
1919
Signer: fake.SnakeOilProv,
2020
}
21-
srv := NewHttpServer(httpConf, nil)
21+
srv := NewHttpServer(443, httpConf, nil)
2222
httpConf.Acme.Put("example.com", "456", "456def")
2323

2424
req, err := http.NewRequest(http.MethodGet, "https://example.com/.well-known/acme-challenge/456", nil)

servers/https.go

-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ func NewHttpsServer(conf *conf.Conf, registry *prometheus.Registry) *http.Server
4242
})
4343

4444
return &http.Server{
45-
Addr: conf.HttpsListen,
4645
Handler: hsts,
4746
TLSConfig: &tls.Config{
4847
// Suggested by https://ssl-config.mozilla.org/#server=go&version=1.21.5&config=intermediate

utils/server-utils.go

+5-4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package utils
33
import (
44
"errors"
55
"github.com/charmbracelet/log"
6+
"net"
67
"net/http"
78
"strings"
89
)
@@ -21,14 +22,14 @@ func logHttpServerError(logger *log.Logger, err error) {
2122

2223
// RunBackgroundHttp runs a http server and logs when the server closes or
2324
// errors.
24-
func RunBackgroundHttp(logger *log.Logger, s *http.Server) {
25-
logHttpServerError(logger, s.ListenAndServe())
25+
func RunBackgroundHttp(logger *log.Logger, s *http.Server, ln net.Listener) {
26+
logHttpServerError(logger, s.Serve(ln))
2627
}
2728

2829
// RunBackgroundHttps runs a http server with TLS encryption and logs when the
2930
// server closes or errors.
30-
func RunBackgroundHttps(logger *log.Logger, s *http.Server) {
31-
logHttpServerError(logger, s.ListenAndServeTLS("", ""))
31+
func RunBackgroundHttps(logger *log.Logger, s *http.Server, ln net.Listener) {
32+
logHttpServerError(logger, s.ServeTLS(ln, "", ""))
3233
}
3334

3435
// GetBearer returns the bearer from the Authorization header or an empty string

0 commit comments

Comments
 (0)