perkeep/server/perkeepd/autodns.go

160 lines
5.2 KiB
Go

/*
Copyright 2018 The Perkeep 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.
*/
// Code related to obtaining camlistore.net DNS subdomains.
package main
import (
"crypto/tls"
"errors"
"fmt"
"log"
"net"
"net/http"
"strings"
"time"
"golang.org/x/crypto/acme/autocert"
"perkeep.org/internal/httputil"
"perkeep.org/internal/osutil"
"perkeep.org/internal/osutil/gce"
"perkeep.org/pkg/env"
"perkeep.org/pkg/gpgchallenge"
"perkeep.org/pkg/serverinit"
"perkeep.org/pkg/webserver"
)
// For getting a name in camlistore.net
const (
camliNetDNS = serverinit.CamliNetDNS
camliNetDomain = serverinit.CamliNetDomain
)
var camliNetHostName string // <keyId>.camlistore.net
// listenForCamliNet prepares the TLS listener for both the GPG challenge, and
// for Let's Encrypt. It then starts listening and returns the baseURL derived from
// the hostname we should obtain from the GPG challenge.
func listenForCamliNet(ws *webserver.Server, config *serverinit.Config) (baseURL string, err error) {
camliNetIP := config.CamliNetIP()
if camliNetIP == "" {
return "", errors.New("no camliNetIP")
}
if ip := net.ParseIP(camliNetIP); ip == nil {
return "", fmt.Errorf("camliNetIP value %q is not a valid IP address", camliNetIP)
} else if ip.To4() == nil {
// TODO: support IPv6 when GCE supports IPv6: https://code.google.com/p/google-compute-engine/issues/detail?id=8
return "", errors.New("CamliNetIP should be an IPv4, as IPv6 is not yet supported on GCE")
}
challengeHostname := camliNetIP + gpgchallenge.SNISuffix
selfCert, selfKey, err := httputil.GenSelfTLS(challengeHostname)
if err != nil {
return "", fmt.Errorf("could not generate self-signed certificate: %v", err)
}
gpgchallengeCert, err := tls.X509KeyPair(selfCert, selfKey)
if err != nil {
return "", fmt.Errorf("could not load TLS certificate: %v", err)
}
_, keyId, err := config.KeyRingAndId()
if err != nil {
return "", fmt.Errorf("could not get keyId for camliNet hostname: %v", err)
}
// catch future length changes
if len(keyId) != 16 {
panic("length of GPG keyId is not 16 anymore")
}
shortKeyId := keyId[8:]
camliNetHostName = strings.ToLower(shortKeyId + "." + camliNetDomain)
m := autocert.Manager{
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist(camliNetHostName),
Cache: autocert.DirCache(osutil.DefaultLetsEncryptCache()),
}
go func() {
err := http.ListenAndServe(":http", m.HTTPHandler(nil))
log.Fatalf("Could not start server for http-01 challenge: %v", err)
}()
getCertificate := func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
if hello.ServerName == challengeHostname {
return &gpgchallengeCert, nil
}
return m.GetCertificate(hello)
}
log.Printf("TLS enabled, with Let's Encrypt for %v", camliNetHostName)
ws.SetTLS(webserver.TLSSetup{CertManager: getCertificate})
err = ws.Listen(fmt.Sprintf(":%d", gpgchallenge.ClientChallengedPort))
if err != nil {
return "", fmt.Errorf("Listen: %v", err)
}
return fmt.Sprintf("https://%s", camliNetHostName), nil
}
// registerDNSChallengeHandler initializes and returns the
// gpgchallenge Client if camliNetIP is configured and if so,
// registers its handler with Perkeep's muxer.
//
// If camlistore.net support isn't enabled, it returns (nil, nil).
func registerDNSChallengeHandler(ws *webserver.Server, config *serverinit.Config) (*gpgchallenge.Client, error) {
camliNetIP := config.CamliNetIP()
if camliNetIP == "" {
return nil, nil
}
if ip := net.ParseIP(camliNetIP); ip == nil {
return nil, fmt.Errorf("camliNetIP value %q is not a valid IP address", camliNetIP)
}
keyRing, keyId, err := config.KeyRingAndId()
if err != nil {
return nil, err
}
cl, err := gpgchallenge.NewClient(keyRing, keyId, camliNetIP)
if err != nil {
return nil, fmt.Errorf("could not init gpgchallenge client: %v", err)
}
ws.Handle(cl.Handler())
return cl, nil
}
// requestHostName performs the GPG challenge to register/obtain a name in the
// camlistore.net domain. The acquired name should be "<gpgKeyId>.camlistore.net",
// where <gpgKeyId> is the short form (8 trailing chars) of Perkeep's keyId.
// It also starts a goroutine that will rerun the challenge every hour, to keep
// the camlistore.net DNS server up to date.
func requestHostName(cl *gpgchallenge.Client) error {
if err := cl.Challenge(camliNetDNS); err != nil {
return err
}
if env.OnGCE() {
if err := gce.SetInstanceHostname(camliNetHostName); err != nil {
return fmt.Errorf("error setting instance camlistore-hostname: %v", err)
}
}
var repeatChallengeFn func()
repeatChallengeFn = func() {
if err := cl.Challenge(camliNetDNS); err != nil {
log.Printf("error with hourly DNS challenge: %v", err)
}
time.AfterFunc(time.Hour, repeatChallengeFn)
}
time.AfterFunc(time.Hour, repeatChallengeFn)
return nil
}