blob: ff1780a821c9a52158aa043ecd5809930a11d0a0 [file] [log] [blame]
// Copyright 2018 The Fuchsia 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 serve
import (
"encoding/json"
"flag"
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"sync"
"time"
"fuchsia.googlesource.com/pm/build"
"fuchsia.googlesource.com/pm/repo"
"fuchsia.googlesource.com/sse"
)
type loggingWriter struct {
http.ResponseWriter
status int
}
func (lw *loggingWriter) WriteHeader(status int) {
lw.status = status
lw.ResponseWriter.WriteHeader(status)
}
func (lw *loggingWriter) Flush() {
lw.ResponseWriter.(http.Flusher).Flush()
}
var (
mu sync.Mutex
autoClients = map[http.ResponseWriter]struct{}{}
)
func Run(cfg *build.Config, args []string) error {
fs := flag.NewFlagSet("serve", flag.ExitOnError)
repoDir := fs.String("d", "", "(deprecated, use -repo) path to the repository")
config := &repo.Config{}
config.Vars(fs)
listen := fs.String("l", ":8083", "HTTP listen address")
auto := fs.Bool("a", true, "Host auto endpoint for realtime client updates")
autoRate := fs.Duration("auto-rate", time.Second, "rate at which to poll filesystem if realtime watch is not available")
quiet := fs.Bool("q", false, "Don't print out information about requests")
fs.Usage = func() {
fmt.Fprintf(os.Stderr, "usage: %s serve", filepath.Base(os.Args[0]))
fmt.Fprintln(os.Stderr)
fs.PrintDefaults()
}
if err := fs.Parse(args); err != nil {
return err
}
config.ApplyDefaults()
if *repoDir == "" {
*repoDir = filepath.Join(config.RepoDir, "repository")
}
fi, err := os.Stat(*repoDir)
if err != nil {
return fmt.Errorf("repository path %q is not valid: %s", *repoDir, err)
}
if !fi.IsDir() {
return fmt.Errorf("repository path %q is not a directory", *repoDir)
}
if *auto {
// TODO(raggi): move to fsnotify
go func() {
timestampPath := filepath.Join(*repoDir, "timestamp.json")
lastUpdateTime := time.Now()
t := time.NewTicker(*autoRate)
for range t.C {
fi, err := os.Stat(timestampPath)
if err != nil {
continue
}
if fi.ModTime().After(lastUpdateTime) {
lastUpdateTime = fi.ModTime()
mu.Lock()
for w := range autoClients {
// errors are ignored, as close notifier in the handler
// ultimately handles cleanup
sse.Write(w, &sse.Event{
Event: "timestamp.json",
Data: []byte(lastUpdateTime.Format(http.TimeFormat)),
})
}
mu.Unlock()
}
}
}()
http.HandleFunc("/auto", func(w http.ResponseWriter, r *http.Request) {
err := sse.Start(w, r)
if err != nil {
log.Printf("SSE request failure: %s", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
mu.Lock()
autoClients[w] = struct{}{}
defer func() {
mu.Lock()
delete(autoClients, w)
mu.Unlock()
}()
mu.Unlock()
<-r.Context().Done()
})
}
http.Handle("/", http.FileServer(http.Dir(*repoDir)))
http.Handle("/config.json", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var scheme = "http://"
if r.TLS != nil {
scheme = "https://"
}
repoUrl := fmt.Sprintf("%s%s", scheme, r.Host)
var signedKeys struct {
Signed struct {
Keys map[string]struct {
Keytype string
Keyval struct {
Public string
}
}
Roles struct {
Root struct {
Keyids []string
}
Threshold int
}
}
}
f, err := os.Open(filepath.Join(*repoDir, "root.json"))
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
log.Printf("root.json missing or unreadable: %s", err)
return
}
defer f.Close()
if err := json.NewDecoder(f).Decode(&signedKeys); err != nil {
w.WriteHeader(http.StatusInternalServerError)
log.Printf("root.json parsing error: %s", err)
return
}
cfg := struct {
ID string
RepoURL string
BlobRepoURL string
RatePeriod int
RootKeys []struct {
Type string
Value string
}
StatusConfig struct {
Enabled bool
}
Auto bool
}{
ID: repoUrl,
RepoURL: repoUrl,
BlobRepoURL: repoUrl + "/blobs",
RatePeriod: 60,
StatusConfig: struct {
Enabled bool
}{
Enabled: true,
},
Auto: true,
}
for _, id := range signedKeys.Signed.Roles.Root.Keyids {
k := signedKeys.Signed.Keys[id]
cfg.RootKeys = append(cfg.RootKeys, struct{ Type, Value string }{
Type: k.Keytype,
Value: k.Keyval.Public,
})
}
json.NewEncoder(w).Encode(cfg)
}))
if !*quiet {
fmt.Printf("%s [pm serve] serving %s at http://%s\n",
time.Now().Format("2006-01-02 15:04:05"), *repoDir, *listen)
}
return http.ListenAndServe(*listen, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
lw := &loggingWriter{w, 0}
http.DefaultServeMux.ServeHTTP(lw, r)
if !*quiet {
fmt.Printf("%s [pm serve] %d %s\n",
time.Now().Format("2006-01-02 15:04:05"), lw.status, r.RequestURI)
}
}))
}