/
main.go
175 lines (148 loc) · 5 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
package main
import (
"context"
"crypto/tls"
"errors"
"flag"
"fmt"
"log"
"os"
"os/signal"
"path/filepath"
"syscall"
"time"
"github.com/n0x1m/gmifs/fileserver"
"github.com/n0x1m/gmifs/gemini"
"github.com/n0x1m/gmifs/middleware"
)
const (
defaultAddress = ":1965"
defaultMaxConns = 128
defaultTimeout = 5
defaultCacheObjects = 0
defaultRootPath = "public"
defaultHost = "localhost"
defaultCertPath = ""
defaultKeyPath = ""
defaultLogsDir = ""
defaultDebugMode = false
defaultAutoIndex = false
defaultAutoCertValidity = 1
)
func main() {
var addr, root, crt, key, host, logs string
var maxconns, timeout, cache, autocertvalidity int
var debug, autoindex bool
flag.StringVar(&addr, "addr", defaultAddress, "address to listen on, e.g. 127.0.0.1:1965")
flag.IntVar(&maxconns, "max-conns", defaultMaxConns, "maximum number of concurrently open connections")
flag.IntVar(&timeout, "timeout", defaultTimeout, "connection timeout in seconds")
flag.IntVar(&cache, "cache", defaultCacheObjects, "simple fifo document cache for n items. Disabled when zero.")
flag.StringVar(&root, "root", defaultRootPath, "server root directory to serve from")
flag.StringVar(&host, "host", defaultHost, "hostname for sni and x509 CN when using temporary self-signed certs")
flag.StringVar(&crt, "cert", defaultCertPath, "TLS chain of one or more certificates")
flag.StringVar(&key, "key", defaultKeyPath, "TLS private key")
flag.IntVar(&autocertvalidity, "autocertvalidity", defaultAutoCertValidity, "valid days when using a gmifs provisioned certificate")
flag.StringVar(&logs, "logs", defaultLogsDir, "enables file based logging and specifies the directory")
flag.BoolVar(&debug, "debug", defaultDebugMode, "enable verbose logging of the gemini server")
flag.BoolVar(&autoindex, "autoindex", defaultAutoIndex, "enables auto indexing, directory listings")
flag.Parse()
var err error
var flogger, dlogger *log.Logger
flogger, err = setupLogger(logs, "access.log")
if err != nil {
log.Fatal(err)
}
if debug {
dlogger, err = setupLogger(logs, "debug.log")
if err != nil {
log.Fatal(err)
}
}
if host == "" {
fmt.Fprintf(os.Stderr, "a keypair with cert and key or at least a common name (hostname) is required for sni\n")
fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
flag.PrintDefaults()
os.Exit(1)
}
logprefix := host + " "
mux := gemini.NewMux()
mux.Use(middleware.Logger(flogger, logprefix))
mux.Use(middleware.Cache(cache))
mux.Handle(gemini.HandlerFunc(fileserver.Serve(root, autoindex)))
server := &gemini.Server{
Addr: addr,
Hostname: host,
TLSConfigLoader: setupCertificate(crt, key, host, autocertvalidity),
Handler: mux,
MaxOpenConns: maxconns,
ReadTimeout: time.Duration(timeout) * time.Second,
Logger: dlogger,
}
confirm := make(chan struct{}, 1)
go func() {
if err := server.ListenAndServe(); err != nil && !errors.Is(err, gemini.ErrServerClosed) {
log.Fatalf("ListenAndServe terminated unexpectedly: %v", err)
}
close(confirm)
}()
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt)
<-stop
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
if err := server.Shutdown(ctx); err != nil {
cancel()
log.Fatalf("ListenAndServe shutdown with error: %v", err)
}
<-confirm
cancel()
}
func setupCertificate(crt, key, host string, validdays int) func() (*tls.Config, error) {
return func() (*tls.Config, error) {
if crt != "" && key != "" {
cert, err := tls.LoadX509KeyPair(crt, key)
if err != nil {
return nil, fmt.Errorf("load x509 keypair: %w", err)
}
return gemini.TLSConfig(host, cert), nil
}
// only used for testing
log.Printf("generating a self-signed temporary certificate, valid for %d days\n", validdays)
cert, err := gemini.GenX509KeyPair(host, validdays)
if err != nil {
return nil, fmt.Errorf("generate x509 keypair: %w", err)
}
return gemini.TLSConfig(host, cert), nil
}
}
func setupLogger(dir, filename string) (*log.Logger, error) {
logger := log.New(os.Stdout, "", log.LUTC|log.Ldate|log.Ltime)
if dir != "" {
// non 12factor stuff
logpath := filepath.Join(dir, filename)
_, err := setupFileLogging(logger, logpath)
if err != nil {
return logger, fmt.Errorf("setup logger: %w", err)
}
go func(logger *log.Logger, logpath string) {
hup := make(chan os.Signal, 1)
signal.Notify(hup, syscall.SIGHUP)
for {
<-hup
// logger.Println("rotating log file after SIGHUP")
_, err := setupFileLogging(logger, logpath)
if err != nil {
log.Fatalf("failed to rotate log file: %v", err)
}
}
}(logger, logpath)
}
return logger, nil
}
func setupFileLogging(logger *log.Logger, logpath string) (*os.File, error) {
logfile, err := os.OpenFile(logpath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
return logfile, fmt.Errorf("log file open: %w", err)
}
logger.SetOutput(logfile)
return logfile, nil
}