diff --git a/garnet/go/src/pm/BUILD.gn b/garnet/go/src/pm/BUILD.gn index c8e5dbe3845fc24c58e2b38baa5034adef078933..4c2651e9811acde7ecc18cce9cb13229d36b4fa7 100644 --- a/garnet/go/src/pm/BUILD.gn +++ b/garnet/go/src/pm/BUILD.gn @@ -16,6 +16,7 @@ go_library("pm_lib") { "//garnet/go/src/merkle", "//garnet/go/src/sse", "//garnet/public/go/third_party:github.com/flynn/go-tuf", + "//garnet/public/go/third_party:github.com/fsnotify/fsnotify", "//garnet/public/go/third_party:golang.org/x/crypto", ] } diff --git a/garnet/go/src/pm/cmd/pm/serve/serve.go b/garnet/go/src/pm/cmd/pm/serve/serve.go index 283ba6a157b9c1879e4f33f8d4c5c3c795060533..c7706f2240dae1e42637474beac93669b481ad9e 100644 --- a/garnet/go/src/pm/cmd/pm/serve/serve.go +++ b/garnet/go/src/pm/cmd/pm/serve/serve.go @@ -6,148 +6,18 @@ package serve import ( "compress/gzip" - "encoding/json" "flag" "fmt" - "io" - "io/ioutil" - "log" "net/http" "os" "path/filepath" "strings" - "sync" "time" "fuchsia.googlesource.com/pm/build" + "fuchsia.googlesource.com/pm/fswatch" + "fuchsia.googlesource.com/pm/pmhttp" "fuchsia.googlesource.com/pm/repo" - "fuchsia.googlesource.com/sse" -) - -const js = ` -async function main() { - let $ = (s) => document.querySelector(s); - $("#icon").src = $('link[rel="icon"]').href; - - let res = await fetch("/targets.json"); - let manifest = await res.json(); - let targets = manifest.signed.targets; - - $("#version").innerText = manifest.signed.version; - $("#expires").innerText = manifest.signed.expires; - - let $c = (e) => document.createElement(e); - - let table = $("#package-table > tbody"); - for (let pkg in targets) { - let row = $c("tr"); - let pkgcol = $c("td"); - let merklecol = $c("td"); - merklecol.classList.add('merkle'); - row.appendChild(pkgcol); - row.appendChild(merklecol); - let a = $c("a"); - a.href = "fuchsia-pkg://" + window.location.host + pkg; - a.innerText = pkg.slice(1); - pkgcol.appendChild(a); - merklecol.innerText = targets[pkg].custom.merkle; - table.appendChild(row); - } - - $("#spinner").classList.remove("is-active"); -} -main(); -` - -const indexHTML = ` -<!doctype html> -<link rel="stylesheet" defer href="https://code.getmdl.io/1.3.0/material.indigo-pink.min.css"> -<link rel="icon" href="data:image/png;base64,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"> -<script defer src="https://code.getmdl.io/1.3.0/material.min.js"></script> -<title>Package Repository</title> -<style> -body { - margin: 10px; -} -#package-table .merkle { - font-family: monospace; -} -#icon { - height: 32px; - width: 32px; - margin: 12px; -} -h1 { - color: #666; -} -</style> -<header><h1><img id=icon></img>Package Repository</h1></header> -<div id=metadata> -<div>Version: <span id=version></span></div> -<div>Expires: <span id=expires></span></div> -</div> -<div id=spinner class="mdl-spinner mdl-js-spinner is-active"></div> -<table id=package-table class=mdl-data-table> -<thead> -<tr> -<th>Package</th> -<th>Merkle</th> -</tr> -</thead> -<tbody> -</tbody> -</table> -<script async src=js></script> -` - -type gzipWriter struct { - http.ResponseWriter - *gzip.Writer -} - -func (w *gzipWriter) Header() http.Header { - return w.ResponseWriter.Header() -} - -func (w *gzipWriter) Write(b []byte) (int, error) { - return w.Writer.Write(b) -} - -func (w *gzipWriter) Flush() { - if err := w.Writer.Flush(); err != nil { - panic(err) - } - if f, ok := w.ResponseWriter.(http.Flusher); ok { - f.Flush() - } else { - log.Fatal("server misconfigured, can not flush") - } -} - -type loggingWriter struct { - http.ResponseWriter - status int -} - -func (lw *loggingWriter) WriteHeader(status int) { - lw.status = status - lw.ResponseWriter.WriteHeader(status) -} - -func (lw *loggingWriter) Flush() { - if f, ok := lw.ResponseWriter.(http.Flusher); ok { - f.Flush() - } else { - log.Fatal("server misconfigured, can not flush") - } -} - -var _ http.Flusher = &loggingWriter{} -var _ http.Flusher = &gzipWriter{} - -var ( - mu sync.Mutex - autoClients = map[http.ResponseWriter]struct{}{} ) func Run(cfg *build.Config, args []string) error { @@ -159,7 +29,6 @@ func Run(cfg *build.Config, args []string) error { 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") encryptionKey := fs.String("e", "", "Path to a symmetric blob encryption key *UNSAFE*") @@ -188,156 +57,44 @@ func Run(cfg *build.Config, args []string) error { } if *auto { - // TODO(raggi): move to fsnotify + as := pmhttp.NewAutoServer() + + w, err := fswatch.NewWatcher() + if err != nil { + return fmt.Errorf("failed to initialize fsnotify: %s", err) + } + timestampPath := filepath.Join(*repoDir, "timestamp.json") + err = w.Add(timestampPath) + if err != nil { + return fmt.Errorf("failed to watch %s: %s", timestampPath, err) + } go func() { - timestampPath := filepath.Join(*repoDir, "timestamp.json") - lastUpdateTime := time.Now() - t := time.NewTicker(*autoRate) - for range t.C { + for range w.Events { 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() - } + as.Broadcast("timestamp.json", fi.ModTime().Format(http.TimeFormat)) } }() - 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("/auto", as) } dirServer := http.FileServer(http.Dir(*repoDir)) http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/": - w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.WriteHeader(200) - io.WriteString(w, indexHTML) + pmhttp.ServeIndex(w) case "/js": - w.Header().Set("Content-Type", "text/javascript; charset=utf-8") - w.WriteHeader(200) - io.WriteString(w, js) + pmhttp.ServeJS(w) default: dirServer.ServeHTTP(w, r) } })) - 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 - BlobKey *struct { - Data [32]uint8 - } - }{ - ID: repoUrl, - RepoURL: repoUrl, - BlobRepoURL: repoUrl + "/blobs", - RatePeriod: 60, - StatusConfig: struct { - Enabled bool - }{ - Enabled: true, - }, - Auto: true, - } - - if *encryptionKey != "" { - keyBytes, err := ioutil.ReadFile(*encryptionKey) - if err != nil { - log.Fatal(err) - } - if len(keyBytes) != 32 { - log.Fatalf("encryption key %s of improper size", *encryptionKey) - } - cfg.BlobKey = &struct{ Data [32]uint8 }{} - copy(cfg.BlobKey.Data[:], keyBytes) - } - - 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) - })) + cs := pmhttp.NewConfigServer(*repoDir, *encryptionKey) + http.Handle("/config.json", cs) if !*quiet { fmt.Printf("%s [pm serve] serving %s at http://%s\n", @@ -346,7 +103,7 @@ func Run(cfg *build.Config, args []string) error { return http.ListenAndServe(*listen, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !strings.HasPrefix(r.RequestURI, "/blobs") && strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { - gw := &gzipWriter{ + gw := &pmhttp.GZIPWriter{ w, gzip.NewWriter(w), } @@ -354,11 +111,11 @@ func Run(cfg *build.Config, args []string) error { gw.Header().Set("Content-Encoding", "gzip") w = gw } - lw := &loggingWriter{w, 0} + lw := &pmhttp.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) + time.Now().Format("2006-01-02 15:04:05"), lw.Status, r.RequestURI) } })) } diff --git a/garnet/go/src/pm/fswatch/fswatch.go b/garnet/go/src/pm/fswatch/fswatch.go new file mode 100644 index 0000000000000000000000000000000000000000..f8e4dc2a534f20d0e27ba0aca444f163e0f54077 --- /dev/null +++ b/garnet/go/src/pm/fswatch/fswatch.go @@ -0,0 +1,15 @@ +// Copyright 2019 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. + +//+build !fuchsia + +package fswatch + +import ( + "github.com/fsnotify/fsnotify" +) + +type Watcher fsnotify.Watcher + +var NewWatcher = fsnotify.NewWatcher diff --git a/garnet/go/src/pm/fswatch/fswatch_fuchsia.go b/garnet/go/src/pm/fswatch/fswatch_fuchsia.go new file mode 100644 index 0000000000000000000000000000000000000000..6cf9c8fcc98cb3fa158aebbd5745bfcfc9cb6ac9 --- /dev/null +++ b/garnet/go/src/pm/fswatch/fswatch_fuchsia.go @@ -0,0 +1,91 @@ +// Copyright 2019 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. + +//+build fuchsia + +package fswatch + +import ( + "os" + "sync" + "time" + + "github.com/fsnotify/fsnotify" +) + +// TODO(raggi): implement this using the real filesystem watcher API +type Watcher struct { + Events chan fsnotify.Event + Errors chan error + + mu sync.Mutex + times map[string]time.Time +} + +func NewWatcher() (*Watcher, error) { + w := &Watcher{ + Events: make(chan fsnotify.Event), + Errors: make(chan error), + times: map[string]time.Time{}, + } + go func() { + t := time.NewTicker(time.Second) + for range t.C { + w.mu.Lock() + paths := make([]string, 0, len(w.times)) + for path := range w.times { + paths = append(paths, path) + } + w.mu.Unlock() + + for _, path := range paths { + if fi, err := os.Stat(path); err == nil { + t := fi.ModTime() + sendEvent := false + w.mu.Lock() + if t != w.times[path] { + sendEvent = true + w.times[path] = t + } + w.mu.Unlock() + if sendEvent { + w.Events <- fsnotify.Event{ + Name: path, + Op: fsnotify.Write, + } + } + } + } + } + }() + + return w, nil +} + +func (w *Watcher) Add(path string) error { + w.mu.Lock() + defer w.mu.Unlock() + fi, err := os.Stat(path) + if err != nil { + return err + } + w.times[path] = fi.ModTime() + return nil +} + +func (w *Watcher) Close() error { + w.mu.Lock() + defer w.mu.Unlock() + + w.times = map[string]time.Time{} + close(w.Events) + return nil +} + +func (w *Watcher) Remove(path string) error { + w.mu.Lock() + defer w.mu.Unlock() + delete(w.times, path) + return nil +} diff --git a/garnet/go/src/pm/pmhttp/auto.go b/garnet/go/src/pm/pmhttp/auto.go new file mode 100644 index 0000000000000000000000000000000000000000..fa087680cfd6a2c0b00568ba4f6e7bdfb8ca27ed --- /dev/null +++ b/garnet/go/src/pm/pmhttp/auto.go @@ -0,0 +1,52 @@ +// Copyright 2019 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 pmhttp + +import ( + "log" + "net/http" + "sync" + + "fuchsia.googlesource.com/sse" +) + +type AutoServer struct { + mu sync.Mutex + + clients map[http.ResponseWriter]struct{} +} + +func NewAutoServer() *AutoServer { + return &AutoServer{clients: map[http.ResponseWriter]struct{}{}} +} + +func (a *AutoServer) ServeHTTP(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 + } + a.mu.Lock() + a.clients[w] = struct{}{} + defer func() { + a.mu.Lock() + delete(a.clients, w) + a.mu.Unlock() + }() + a.mu.Unlock() + <-r.Context().Done() +} + +func (a *AutoServer) Broadcast(name, data string) { + a.mu.Lock() + defer a.mu.Unlock() + for w := range a.clients { + sse.Write(w, &sse.Event{ + Event: name, + Data: []byte(data), + }) + } +} diff --git a/garnet/go/src/pm/pmhttp/config.go b/garnet/go/src/pm/pmhttp/config.go new file mode 100644 index 0000000000000000000000000000000000000000..43511e64d54c721b949b4278c2433b1955d15ef5 --- /dev/null +++ b/garnet/go/src/pm/pmhttp/config.go @@ -0,0 +1,112 @@ +// Copyright 2019 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 pmhttp + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "path/filepath" +) + +type ConfigServer struct { + repoDir string + encryptionKey string +} + +func NewConfigServer(repoDir, encryptionKey string) *ConfigServer { + return &ConfigServer{repoDir: repoDir, encryptionKey: encryptionKey} +} + +func (c *ConfigServer) ServeHTTP(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(c.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 + BlobKey *struct { + Data [32]uint8 + } + }{ + ID: repoUrl, + RepoURL: repoUrl, + BlobRepoURL: repoUrl + "/blobs", + RatePeriod: 60, + StatusConfig: struct { + Enabled bool + }{ + Enabled: true, + }, + Auto: true, + } + + if c.encryptionKey != "" { + keyBytes, err := ioutil.ReadFile(c.encryptionKey) + if err != nil { + log.Fatal(err) + } + if len(keyBytes) != 32 { + log.Fatalf("encryption key %s of improper size", c.encryptionKey) + } + cfg.BlobKey = &struct{ Data [32]uint8 }{} + copy(cfg.BlobKey.Data[:], keyBytes) + } + + 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) +} diff --git a/garnet/go/src/pm/pmhttp/index.go b/garnet/go/src/pm/pmhttp/index.go new file mode 100644 index 0000000000000000000000000000000000000000..e59b2bb13a19b8557ef823dea406da11241d7776 --- /dev/null +++ b/garnet/go/src/pm/pmhttp/index.go @@ -0,0 +1,98 @@ +// Copyright 2019 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 pmhttp + +import ( + "io" + "net/http" +) + +const js = ` +async function main() { + let $ = (s) => document.querySelector(s); + $("#icon").src = $('link[rel="icon"]').href; + + let res = await fetch("/targets.json"); + let manifest = await res.json(); + let targets = manifest.signed.targets; + + $("#version").innerText = manifest.signed.version; + $("#expires").innerText = manifest.signed.expires; + + let $c = (e) => document.createElement(e); + + let table = $("#package-table > tbody"); + for (let pkg in targets) { + let row = $c("tr"); + let pkgcol = $c("td"); + let merklecol = $c("td"); + merklecol.classList.add('merkle'); + row.appendChild(pkgcol); + row.appendChild(merklecol); + let a = $c("a"); + a.href = "fuchsia-pkg://" + window.location.host + pkg; + a.innerText = pkg.slice(1); + pkgcol.appendChild(a); + merklecol.innerText = targets[pkg].custom.merkle; + table.appendChild(row); + } + + $("#spinner").classList.remove("is-active"); +} +main(); +` + +const indexHTML = ` +<!doctype html> +<link rel="stylesheet" defer href="https://code.getmdl.io/1.3.0/material.indigo-pink.min.css"> +<link rel="icon" href="data:image/png;base64,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"> +<script defer src="https://code.getmdl.io/1.3.0/material.min.js"></script> +<title>Package Repository</title> +<style> +body { + margin: 10px; +} +#package-table .merkle { + font-family: monospace; +} +#icon { + height: 32px; + width: 32px; + margin: 12px; +} +h1 { + color: #666; +} +</style> +<header><h1><img id=icon></img>Package Repository</h1></header> +<div id=metadata> +<div>Version: <span id=version></span></div> +<div>Expires: <span id=expires></span></div> +</div> +<div id=spinner class="mdl-spinner mdl-js-spinner is-active"></div> +<table id=package-table class=mdl-data-table> +<thead> +<tr> +<th>Package</th> +<th>Merkle</th> +</tr> +</thead> +<tbody> +</tbody> +</table> +<script async src=js></script> +` + +func ServeIndex(w http.ResponseWriter) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(200) + io.WriteString(w, indexHTML) +} + +func ServeJS(w http.ResponseWriter) { + w.Header().Set("Content-Type", "text/javascript; charset=utf-8") + w.WriteHeader(200) + io.WriteString(w, js) +} diff --git a/garnet/go/src/pm/pmhttp/pmhttp.go b/garnet/go/src/pm/pmhttp/pmhttp.go new file mode 100644 index 0000000000000000000000000000000000000000..f38eef3cdb25121419f65946e12d07c1c10aad6e --- /dev/null +++ b/garnet/go/src/pm/pmhttp/pmhttp.go @@ -0,0 +1,56 @@ +// Copyright 2019 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 pmhttp + +import ( + "compress/gzip" + "log" + "net/http" +) + +type GZIPWriter struct { + http.ResponseWriter + *gzip.Writer +} + +func (w *GZIPWriter) Header() http.Header { + return w.ResponseWriter.Header() +} + +func (w *GZIPWriter) Write(b []byte) (int, error) { + return w.Writer.Write(b) +} + +func (w *GZIPWriter) Flush() { + if err := w.Writer.Flush(); err != nil { + panic(err) + } + if f, ok := w.ResponseWriter.(http.Flusher); ok { + f.Flush() + } else { + log.Fatal("server misconfigured, can not flush") + } +} + +type LoggingWriter struct { + http.ResponseWriter + Status int +} + +func (lw *LoggingWriter) WriteHeader(status int) { + lw.Status = status + lw.ResponseWriter.WriteHeader(status) +} + +func (lw *LoggingWriter) Flush() { + if f, ok := lw.ResponseWriter.(http.Flusher); ok { + f.Flush() + } else { + log.Fatal("server misconfigured, can not flush") + } +} + +var _ http.Flusher = &LoggingWriter{} +var _ http.Flusher = &GZIPWriter{}