1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
16
17
18 package main
19
20 import (
21 "context"
22 "flag"
23 "fmt"
24 "io"
25 "log"
26 "net"
27 "net/http"
28 "os"
29 "os/signal"
30 "path/filepath"
31 "runtime"
32 "strconv"
33 "strings"
34 "syscall"
35 "time"
36
37 "perkeep.org/internal/geocode"
38 "perkeep.org/internal/httputil"
39 "perkeep.org/internal/netutil"
40 "perkeep.org/internal/osutil"
41 "perkeep.org/internal/osutil/gce"
42 "perkeep.org/pkg/buildinfo"
43 "perkeep.org/pkg/env"
44 "perkeep.org/pkg/serverinit"
45 "perkeep.org/pkg/webserver"
46
47
48
49
50
51 _ "perkeep.org/pkg/blobserver/azure"
52 "perkeep.org/pkg/blobserver/blobpacked"
53 _ "perkeep.org/pkg/blobserver/cond"
54 _ "perkeep.org/pkg/blobserver/diskpacked"
55 _ "perkeep.org/pkg/blobserver/encrypt"
56 _ "perkeep.org/pkg/blobserver/google/cloudstorage"
57 _ "perkeep.org/pkg/blobserver/google/drive"
58 _ "perkeep.org/pkg/blobserver/localdisk"
59 _ "perkeep.org/pkg/blobserver/mongo"
60 _ "perkeep.org/pkg/blobserver/overlay"
61 _ "perkeep.org/pkg/blobserver/proxycache"
62 _ "perkeep.org/pkg/blobserver/remote"
63 _ "perkeep.org/pkg/blobserver/replica"
64 _ "perkeep.org/pkg/blobserver/s3"
65 _ "perkeep.org/pkg/blobserver/shard"
66 _ "perkeep.org/pkg/blobserver/union"
67
68
69
70 _ "perkeep.org/pkg/sorted/kvfile"
71 _ "perkeep.org/pkg/sorted/leveldb"
72 _ "perkeep.org/pkg/sorted/mongo"
73 _ "perkeep.org/pkg/sorted/mysql"
74 _ "perkeep.org/pkg/sorted/postgres"
75
76
77 _ "perkeep.org/pkg/search"
78 _ "perkeep.org/pkg/server"
79
80
81 _ "perkeep.org/pkg/importer/allimporters"
82
83
84 _ "perkeep.org/pkg/camlegal"
85
86 "go4.org/legal"
87 "go4.org/wkfs"
88
89 "golang.org/x/crypto/acme/autocert"
90 )
91
92 var (
93 flagVersion = flag.Bool("version", false, "show version")
94 flagHelp = flag.Bool("help", false, "show usage")
95 flagLegal = flag.Bool("legal", false, "show licenses")
96 flagConfigFile = flag.String("configfile", "",
97 "Config file to use, relative to the Perkeep configuration directory root. "+
98 "If blank, the default is used or auto-generated. "+
99 "If it starts with 'http:' or 'https:', it is fetched from the network.")
100 flagListen = flag.String("listen", "", "host:port to listen on, or :0 to auto-select. If blank, the value in the config will be used instead.")
101 flagOpenBrowser = flag.Bool("openbrowser", true, "Launches the UI on startup")
102 flagReindex = flag.Bool("reindex", false, "Reindex all blobs on startup")
103 flagRecovery = flag.Int("recovery", 0, "Recovery mode: it corresponds for now to the recovery modes of the blobpacked package. Which means: 0 does nothing, 1 rebuilds the blobpacked index without erasing it, and 2 wipes the blobpacked index before rebuilding it.")
104 flagSyslog = flag.Bool("syslog", false, "Log everything only to syslog. It is an error to use this flag on windows.")
105 flagKeepGoing = flag.Bool("keep-going", false, "Continue after reindex or blobpacked recovery errors")
106 flagPollParent bool
107 )
108
109 func init() {
110 if debug, _ := strconv.ParseBool(os.Getenv("CAMLI_MORE_FLAGS")); debug {
111 flag.BoolVar(&flagPollParent, "pollparent", false, "Perkeepd regularly polls its parent process to detect if it has been orphaned, and terminates in that case. Mainly useful for tests.")
112 }
113 }
114
115 func exitf(pattern string, args ...interface{}) {
116 if !strings.HasSuffix(pattern, "\n") {
117 pattern = pattern + "\n"
118 }
119 fmt.Fprintf(os.Stderr, pattern, args...)
120 os.Exit(1)
121 }
122
123 func slurpURL(urls string, limit int64) ([]byte, error) {
124 res, err := http.Get(urls)
125 if err != nil {
126 return nil, err
127 }
128 defer res.Body.Close()
129 return io.ReadAll(io.LimitReader(res.Body, limit))
130 }
131
132
133
134
135
136
137
138
139 func loadConfig(arg string) (*serverinit.Config, error) {
140 if strings.HasPrefix(arg, "http://") || strings.HasPrefix(arg, "https://") {
141 contents, err := slurpURL(arg, 256<<10)
142 if err != nil {
143 return nil, err
144 }
145 return serverinit.Load(contents)
146 }
147 var absPath string
148 switch {
149 case arg == "":
150 absPath = osutil.UserServerConfigPath()
151 _, err := wkfs.Stat(absPath)
152 if err == nil {
153 break
154 }
155 if !os.IsNotExist(err) {
156 return nil, err
157 }
158 conf, err := serverinit.DefaultEnvConfig()
159 if err != nil || conf != nil {
160 return conf, err
161 }
162 configDir, err := osutil.PerkeepConfigDir()
163 if err != nil {
164 return nil, err
165 }
166 if err := wkfs.MkdirAll(configDir, 0700); err != nil {
167 return nil, err
168 }
169 log.Printf("Generating template config file %s", absPath)
170 if err := serverinit.WriteDefaultConfigFile(absPath); err != nil {
171 return nil, err
172 }
173 case filepath.IsAbs(arg):
174 absPath = arg
175 default:
176 configDir, err := osutil.PerkeepConfigDir()
177 if err != nil {
178 return nil, err
179 }
180 absPath = filepath.Join(configDir, arg)
181 }
182 return serverinit.LoadFile(absPath)
183 }
184
185
186
187
188
189 func setupTLS(ws *webserver.Server, config *serverinit.Config, hostname string) {
190 cert, key := config.HTTPSCert(), config.HTTPSKey()
191 if !config.HTTPS() {
192 return
193 }
194 if (cert != "") != (key != "") {
195 exitf("httpsCert and httpsKey must both be either present or absent")
196 }
197 if config.IsTailscaleListener() {
198 if cert != "" {
199 exitf("httpsCert and httpsKey must be empty when using the tailscale listener")
200 }
201
202
203
204 ws.SetTLS(webserver.TLSSetup{})
205 return
206 }
207
208 defCert := osutil.DefaultTLSCert()
209 defKey := osutil.DefaultTLSKey()
210 const hint = "You must add this certificate's fingerprint to your client's trusted certs list to use it. Like so:\n\"trustedCerts\": [\"%s\"],"
211 if cert == defCert && key == defKey {
212 _, err1 := wkfs.Stat(cert)
213 _, err2 := wkfs.Stat(key)
214 if err1 != nil || err2 != nil {
215 if os.IsNotExist(err1) || os.IsNotExist(err2) {
216 sig, err := httputil.GenSelfTLSFiles(hostname, defCert, defKey)
217 if err != nil {
218 exitf("Could not generate self-signed TLS cert: %q", err)
219 }
220 log.Printf(hint, sig)
221 } else {
222 exitf("Could not stat cert or key: %q, %q", err1, err2)
223 }
224 }
225 }
226 if cert == "" && key == "" {
227
228 if netutil.IsFQDN(hostname) {
229 m := autocert.Manager{
230 Prompt: autocert.AcceptTOS,
231 HostPolicy: autocert.HostWhitelist(hostname),
232 Cache: autocert.DirCache(osutil.DefaultLetsEncryptCache()),
233 }
234 ws.SetTLS(webserver.TLSSetup{
235 CertManager: m.GetCertificate,
236 })
237 log.Printf("Using Let's Encrypt tls-alpn-01 for %v", hostname)
238 return
239 }
240
241 sig, err := httputil.GenSelfTLSFiles(hostname, defCert, defKey)
242 if err != nil {
243 exitf("Could not generate self signed creds: %q", err)
244 }
245 log.Printf(hint, sig)
246 cert = defCert
247 key = defKey
248 }
249 data, err := wkfs.ReadFile(cert)
250 if err != nil {
251 exitf("Failed to read pem certificate: %s", err)
252 }
253 sig, err := httputil.CertFingerprint(data)
254 if err != nil {
255 exitf("certificate error: %v", err)
256 }
257 log.Printf("TLS enabled, with SHA-256 certificate fingerprint: %v", sig)
258 ws.SetTLS(webserver.TLSSetup{
259 CertFile: cert,
260 KeyFile: key,
261 })
262 }
263
264 func handleSignals(shutdownc <-chan io.Closer) {
265 c := make(chan os.Signal, 1)
266 signal.Notify(c, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
267 for {
268 sig := <-c
269 sysSig, ok := sig.(syscall.Signal)
270 if !ok {
271 log.Fatal("Not a unix signal")
272 }
273 switch sysSig {
274 case syscall.SIGHUP:
275 log.Printf(`Got "%v" signal: restarting camli`, sig)
276 err := osutil.RestartProcess()
277 if err != nil {
278 log.Fatal("Failed to restart: " + err.Error())
279 }
280 case syscall.SIGINT, syscall.SIGTERM:
281 log.Printf(`Got "%v" signal: shutting down`, sig)
282 donec := make(chan bool)
283 go func() {
284 cl := <-shutdownc
285 if err := cl.Close(); err != nil {
286 exitf("Error shutting down: %v", err)
287 }
288 donec <- true
289 }()
290 select {
291 case <-donec:
292 log.Printf("Shut down.")
293 os.Exit(0)
294 case <-time.After(2 * time.Second):
295 exitf("Timeout shutting down. Exiting uncleanly.")
296 }
297 default:
298 log.Fatal("Received another signal, should not happen.")
299 }
300 }
301 }
302
303
304
305 func listen(ws *webserver.Server, config *serverinit.Config) (baseURL string, err error) {
306 baseURL = config.BaseURL()
307
308
309 listen := *flagListen
310 if listen == "" {
311 listen = config.ListenAddr()
312 }
313 if listen == "" {
314 exitf("\"listen\" needs to be specified either in the config or on the command line")
315 }
316
317 if config.IsTailscaleListener() {
318 setupTLS(ws, config, "unused-hostname")
319 } else {
320 hostname, err := certHostname(listen, baseURL)
321 if err != nil {
322 return "", fmt.Errorf("Bad baseURL or listen address: %w", err)
323 }
324 setupTLS(ws, config, hostname)
325 }
326
327 err = ws.Listen(listen)
328 if err != nil {
329 return "", fmt.Errorf("Listen: %w", err)
330 }
331 if baseURL == "" {
332 baseURL = ws.ListenURL()
333 }
334 return baseURL, nil
335 }
336
337
338
339 func certHostname(listen, baseURL string) (string, error) {
340 hostPort, err := netutil.HostPort(baseURL)
341 if err != nil {
342 hostPort = listen
343 }
344 hostname, _, err := net.SplitHostPort(hostPort)
345 if err != nil {
346 return "", fmt.Errorf("failed to find hostname for cert from address %q (baseURL %q): %w", hostPort, baseURL, err)
347 }
348 return hostname, nil
349 }
350
351 func setBlobpackedRecovery() {
352 if *flagRecovery == 0 && env.OnGCE() {
353 *flagRecovery = gce.BlobpackedRecoveryValue()
354 }
355 if blobpacked.RecoveryMode(*flagRecovery) > blobpacked.NoRecovery {
356 blobpacked.SetRecovery(blobpacked.RecoveryMode(*flagRecovery))
357 }
358 }
359
360
361
362 func checkGeoKey() error {
363 if _, err := geocode.GetAPIKey(); err == nil {
364 return nil
365 }
366 keyPath, err := geocode.GetAPIKeyPath()
367 if err != nil {
368 return fmt.Errorf("error getting Geocoding API key path: %w", err)
369 }
370 if env.OnGCE() {
371 keyPath = strings.TrimPrefix(keyPath, "/gcs/")
372 return fmt.Errorf("using OpenStreetMap for location related requests. To use the Google Geocoding API, create a key (see https://developers.google.com/maps/documentation/geocoding/get-api-key ) and save it in your VM's configuration bucket as: %v", keyPath)
373 }
374 return fmt.Errorf("using OpenStreetMap for location related requests. To use the Google Geocoding API, create a key (see https://developers.google.com/maps/documentation/geocoding/get-api-key ) and save it in Perkeep's configuration directory as: %v", keyPath)
375 }
376
377 func main() {
378 flag.Parse()
379
380 if *flagVersion {
381 fmt.Fprintf(os.Stderr, "perkeepd version: %s\nGo version: %s (%s/%s)\n",
382 buildinfo.Summary(), runtime.Version(), runtime.GOOS, runtime.GOARCH)
383 return
384 }
385 if *flagHelp {
386 flag.Usage()
387 os.Exit(0)
388 }
389 if *flagLegal {
390 for _, l := range legal.Licenses() {
391 fmt.Fprintln(os.Stderr, l)
392 }
393 return
394 }
395 setBlobpackedRecovery()
396
397
398
399
400
401
402
403 httputil.InstallCerts()
404
405 logCloser := setupLogging()
406 defer func() {
407 if err := logCloser.Close(); err != nil {
408 log.SetOutput(os.Stderr)
409 log.Printf("Error closing logger: %v", err)
410 }
411 }()
412
413 log.Printf("Starting perkeepd version %s; Go %s (%s/%s)", buildinfo.Summary(), runtime.Version(),
414 runtime.GOOS, runtime.GOARCH)
415
416 shutdownc := make(chan io.Closer, 1)
417 go handleSignals(shutdownc)
418
419 config, err := loadConfig(*flagConfigFile)
420 if err != nil {
421 exitf("Error loading config file: %v", err)
422 }
423
424 ws := webserver.New()
425 baseURL, err := listen(ws, config)
426 if err != nil {
427 exitf("Error starting webserver: %v", err)
428 }
429
430 config.SetReindex(*flagReindex)
431 config.SetKeepGoing(*flagKeepGoing)
432
433
434 shutdownCloser, err := config.InstallHandlers(ws, baseURL)
435 if err != nil {
436 exitf("Error parsing config: %v", err)
437 }
438 shutdownc <- shutdownCloser
439
440 go ws.Serve()
441
442 if env.OnGCE() {
443 gce.FixUserDataForPerkeepRename()
444 }
445
446 if err := checkGeoKey(); err != nil {
447 log.Printf("perkeepd: %v", err)
448 }
449
450 urlToOpen := baseURL + config.UIPath()
451
452 if *flagOpenBrowser {
453 go osutil.OpenURL(urlToOpen)
454 }
455
456 if flagPollParent {
457 osutil.DieOnParentDeath()
458 }
459
460 ctx := context.Background()
461 if err := config.UploadPublicKey(ctx); err != nil {
462 exitf("Error uploading public key on startup: %v", err)
463 }
464
465 if err := config.StartApps(); err != nil {
466 exitf("StartApps: %v", err)
467 }
468
469 for appName, appURL := range config.AppURL() {
470 addr, err := netutil.HostPort(appURL)
471 if err != nil {
472 log.Printf("Could not get app %v address: %v", appName, err)
473 continue
474 }
475 if err := netutil.AwaitReachable(addr, 5*time.Second); err != nil {
476 log.Printf("Could not reach app %v: %v", appName, err)
477 }
478 }
479 log.Printf("server: available at %s", urlToOpen)
480
481 select {}
482 }