blob: 8120adb2ccdfceb7fdfd3b927922020236f2fe63 [file] [log] [blame]
// Copyright 2016 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 upspinserver is a combined DirServer and StoreServer for use on
// stand-alone machines. It provides only the production implementations of the
// dir and store servers (dir/server and store/server).
// It provides the common cod used by all upspinserver commands.
package upspinserver // import "upspin.io/serverutil/upspinserver"
import (
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"time"
"upspin.io/client"
"upspin.io/config"
dirServer "upspin.io/dir/server"
"upspin.io/errors"
"upspin.io/factotum"
"upspin.io/flags"
"upspin.io/log"
"upspin.io/rpc/dirserver"
"upspin.io/rpc/storeserver"
"upspin.io/serverutil/perm"
"upspin.io/serverutil/web"
storeServer "upspin.io/store/server"
"upspin.io/subcmd"
"upspin.io/upspin"
"upspin.io/version"
// Packers.
_ "upspin.io/pack/ee"
_ "upspin.io/pack/eeintegrity"
_ "upspin.io/pack/plain"
// Required transports.
_ "upspin.io/transports"
)
var (
cfgPath = flag.String("serverconfig", defaultCfgPath(), "server configuration `directory`")
enableWeb = flag.Bool("web", false, "enable Upspin web interface")
readyCh = make(chan struct{})
)
func defaultCfgPath() string {
home, err := config.Homedir()
if err != nil {
home = "/"
}
return filepath.Join(home, "upspin", "server")
}
func Main() (ready chan struct{}) {
flags.Parse(flags.Server)
if git := version.GitSHA; git != "" {
log.Info.Printf("upspinserver built on %s at commit %s",
version.BuildTime.In(time.UTC).Format(time.Stamp+" UTC"),
git)
}
_, cfg, perm, err := initServer(startup)
if err == noConfig {
log.Info.Print("Configuration file not found. Running in setup mode.")
http.Handle("/", &setupHandler{})
} else if err != nil {
log.Fatal(err)
} else if *enableWeb {
http.Handle("/", web.New(cfg, perm))
}
return readyCh
}
var noConfig = errors.Str("no configuration")
type initMode int
const (
startup initMode = iota
setupServer
)
func initServer(mode initMode) (*subcmd.ServerConfig, upspin.Config, *perm.Perm, error) {
serverConfig, err := readServerConfig()
if os.IsNotExist(err) {
return nil, nil, nil, noConfig
} else if err != nil {
return nil, nil, nil, err
}
cfg := config.New()
cfg = config.SetUserName(cfg, serverConfig.User)
f, err := factotum.NewFromDir(*cfgPath)
if err != nil {
return nil, nil, nil, err
}
cfg = config.SetFactotum(cfg, f)
ep := upspin.Endpoint{
Transport: upspin.Remote,
NetAddr: serverConfig.Addr,
}
cfg = config.SetDirEndpoint(cfg, ep)
cfg = config.SetStoreEndpoint(cfg, ep)
storeCfg := config.SetPacking(cfg, upspin.EEIntegrityPack)
dirCfg := config.SetPacking(cfg, upspin.EEPack)
// Set up StoreServer.
var storeServerConfig []string
switch {
case len(serverConfig.StoreConfig) > 0:
// Use the provided configuration, if available.
storeServerConfig = serverConfig.StoreConfig
default:
// No bucket configured, use simple on-disk store.
storagePath := filepath.Join(*cfgPath, "storage")
storeServerConfig = []string{"backend=Disk", "basePath=" + storagePath}
}
store, err := storeServer.New(storeServerConfig...)
if err != nil {
return nil, nil, nil, err
}
// Set up DirServer.
logDir := filepath.Join(*cfgPath, "dirserver-logs")
if err := os.MkdirAll(logDir, 0700); err != nil {
return nil, nil, nil, err
}
dirServerConfig := append([]string{"logDir=" + logDir}, storeServerConfig...)
dir, err := dirServer.New(dirCfg, dirServerConfig...)
if err != nil {
return nil, nil, nil, err
}
// Wrap store and dir with permission checking.
perm := perm.NewWithDir(dirCfg, readyCh, serverConfig.User, dir)
store = perm.WrapStore(store)
dir = perm.WrapDir(dir)
// Set up RPC server.
httpStore := storeserver.New(storeCfg, store, serverConfig.Addr)
httpDir := dirserver.New(dirCfg, dir, serverConfig.Addr)
http.Handle("/api/Store/", httpStore)
http.Handle("/api/Dir/", httpDir)
// Set public-facing network address (used by Let's Encrypt).
flags.NetAddr = string(serverConfig.Addr)
log.Println("Store and Directory servers initialized.")
log.Printf("Store server configuration: %s", fmtStoreConfig(storeServerConfig))
if mode == setupServer {
// Create Writers file if this was triggered by 'upspin setupserver'.
go func() {
if err := setupWriters(storeCfg); err != nil {
log.Printf("Error creating Writers file: %v", err)
}
}()
}
return serverConfig, cfg, perm, nil
}
// fmtStoreConfig formats a ServerConfig.StoreConfig value as a string,
// ommitting any privateKeyData fields as they include sensitive information.
func fmtStoreConfig(cfg []string) string {
var out []string
for _, s := range cfg {
if !strings.HasPrefix(s, "privateKeyData=") {
out = append(out, s)
}
}
return strings.Join(out, " ")
}
type setupHandler struct {
mu sync.Mutex
done bool
web http.Handler
}
func (h *setupHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.mu.Lock()
if h.done {
web := h.web
h.mu.Unlock()
if web != nil {
web.ServeHTTP(w, r)
} else {
http.NotFound(w, r)
}
return
}
defer h.mu.Unlock()
switch r.URL.Path {
case "/":
fmt.Fprint(w, "Unconfigured Upspin Server")
return
default:
http.NotFound(w, r)
return
case "/setupserver":
if r.Method != "POST" {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
// The rest of this function is the setupserver handler.
}
files := map[string][]byte{}
if err := json.NewDecoder(r.Body).Decode(&files); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := os.MkdirAll(*cfgPath, 0700); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
for _, name := range subcmd.SetupServerFiles {
body, ok := files[name]
if !ok {
http.Error(w, fmt.Sprintf("missing config file %q", name), http.StatusBadRequest)
return
}
err := ioutil.WriteFile(filepath.Join(*cfgPath, name), body, 0600)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
os.RemoveAll(*cfgPath)
return
}
}
_, cfg, perm, err := initServer(setupServer)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprintf(w, "OK")
h.done = true
if *enableWeb {
h.web = web.New(cfg, perm)
}
}
func setupWriters(cfg upspin.Config) error {
writers, err := ioutil.ReadFile(filepath.Join(*cfgPath, "Writers"))
if err != nil {
return err
}
c := client.New(cfg)
dir := upspin.PathName(cfg.UserName())
if err := existsOK(c.MakeDirectory(dir)); err != nil {
return err
}
dir += "/Group"
if err := existsOK(c.MakeDirectory(dir)); err != nil {
return err
}
file := dir + "/Writers"
_, err = c.Put(file, writers)
return err
}
// existsOK returns err if it is not an errors.Exist error,
// in which case it returns nil.
func existsOK(_ *upspin.DirEntry, err error) error {
if errors.Is(errors.Exist, err) {
return nil
}
return err
}
func readServerConfig() (*subcmd.ServerConfig, error) {
cfgFile := filepath.Join(*cfgPath, subcmd.ServerConfigFile)
b, err := ioutil.ReadFile(cfgFile)
if err != nil {
// We can't return the usual errors.E because the caller wants
// to match on the raw error. But give the admin a clue.
log.Printf("unable to read configuration: %s", err)
return nil, err
}
cfg := &subcmd.ServerConfig{}
if err := json.Unmarshal(b, cfg); err != nil {
return nil, err
}
return cfg, nil
}