blob: fc1b42051079cf7a7990d7c22c10d4709397e82e [file] [log] [blame]
// Copyright 2017 The Upspin Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"context"
"crypto/sha256"
"fmt"
"io/ioutil"
"net/http"
"os"
"golang.org/x/oauth2/google"
dns "google.golang.org/api/dns/v1"
"google.golang.org/api/googleapi"
"upspin.io/errors"
"upspin.io/upspin"
)
// TODO(adg): make this configurable?
const (
dnsProject = "upspin-prod"
dnsZone = "upspin-services"
dnsDomain = "upspin.services"
)
// userToHost converts an Upspin user name to a fully-qualified domain name
// under the upspin.services domain. The host portion of the name is the
// hex-encoded first 16 bytes of the SHA256 checksum of the user name.
// The security of this service relies on there not being collisions in this
// space, which should be astronomically unlikely.
func userToHost(name upspin.UserName) string {
hash := sha256.New()
hash.Write([]byte(name))
return fmt.Sprintf("%x."+dnsDomain, hash.Sum(nil)[:16])
}
// setupDNSService loads the credentials for accessing the Cloud DNS service
// and sets the server's dnsSvc with a ready-to-use dns.Service.
func (s *server) setupDNSService() error {
ctx := context.Background()
var client *http.Client
// First try to read the serviceaccount.json in the Docker image.
b, err := ioutil.ReadFile("/upspin/serviceaccount.json")
if err == nil {
cfg, err := google.JWTConfigFromJSON(b, dns.CloudPlatformScope)
if err != nil {
return err
}
client = cfg.Client(ctx)
} else if os.IsNotExist(err) {
// Otherwise use the default application credentials,
// which should work when testing locally.
client, err = google.DefaultClient(ctx, dns.CloudPlatformScope)
if err != nil {
return err
}
} else {
return err
}
s.dnsSvc, err = dns.New(client)
return err
}
// listRecordSets returns the list of record sets for a given host name.
func (s *server) listRecordSets(host string) ([]*dns.ResourceRecordSet, error) {
resp, err := s.dnsSvc.ResourceRecordSets.List(dnsProject, dnsZone).Name(host + ".").Do()
if err != nil {
return nil, err
}
return resp.Rrsets, nil
}
// lookupName returns the IP address and host name for a given user, or a
// NotExist error if there is no host name for that user.
func (s *server) lookupName(name upspin.UserName) (ip, host string, err error) {
host = userToHost(name)
rrsets, err := s.listRecordSets(host)
if err != nil {
return "", "", err
}
for _, rrs := range rrsets {
for _, rrd := range rrs.Rrdatas {
return rrd, host, nil
}
}
return "", "", errors.E(errors.NotExist)
}
// updateName creates (or replaces) an A record for the given user's host name
// that points to the given IP address, and returns the user's host name.
func (s *server) updateName(name upspin.UserName, ip string) (host string, err error) {
host = userToHost(name)
rrsets, err := s.listRecordSets(host)
if err != nil {
return "", err
}
// Check whether the appropriate A record already exists,
// and do nothing if so.
if len(rrsets) == 1 && rrsets[0].Type == "A" {
if ds := rrsets[0].Rrdatas; len(ds) == 1 && ds[0] == ip {
return host, nil
}
}
// No appropriate A record exists; replace the existing
// records for this host with a new one.
change := &dns.Change{
Additions: []*dns.ResourceRecordSet{{
Name: host + ".",
Rrdatas: []string{ip},
Ttl: 3600, // 1 hour
Type: "A",
}},
Deletions: rrsets,
}
change, err = s.dnsSvc.Changes.Create(dnsProject, dnsZone, change).Do()
if err != nil && !googleapi.IsNotModified(err) {
return "", err
}
return host, nil
}