1/* 2Copyright 2011 The Perkeep Authors 3 4Licensed under the Apache License, Version 2.0 (the "License"); 5you may not use this file except in compliance with the License. 6You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10Unless required by applicable law or agreed to in writing, software 11distributed under the License is distributed on an "AS IS" BASIS, 12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13See the License for the specific language governing permissions and 14limitations under the License. 15*/ 16 17package server 18 19import ( 20 "encoding/json" 21 "fmt" 22 "html" 23 "log" 24 "net/http" 25 "sort" 26 "sync" 27 28 "perkeep.org/internal/httputil" 29 "perkeep.org/internal/images" 30 "perkeep.org/internal/osutil" 31 "perkeep.org/pkg/auth" 32 "perkeep.org/pkg/blobserver" 33 "perkeep.org/pkg/buildinfo" 34 "perkeep.org/pkg/jsonsign/signhandler" 35 "perkeep.org/pkg/search" 36 "perkeep.org/pkg/types/camtypes" 37 38 "go4.org/jsonconfig" 39 "go4.org/types" 40) 41 42// RootHandler handles serving the about/splash page. 43type RootHandler struct { 44 // Stealth determines whether we hide from non-authenticated 45 // clients. 46 Stealth bool 47 48 OwnerName string // for display purposes only. 49 Username string // default user for mobile setup. 50 51 // URL prefixes (path or full URL) to the primary blob and 52 // search root. 53 BlobRoot string 54 SearchRoot string 55 helpRoot string 56 importerRoot string 57 statusRoot string 58 Prefix string // root handler's prefix 59 shareRoot string // share handler's prefix, if any. 60 61 // JSONSignRoot is the optional path or full URL to the JSON 62 // Signing helper. 63 JSONSignRoot string 64 65 Storage blobserver.Storage // of BlobRoot, or nil 66 67 searchInitOnce sync.Once // runs searchInit, which populates searchHandler 68 searchInit func() 69 searchHandler *search.Handler // of SearchRoot, or nil 70 hasLegacySHA1 bool // whether the index has SHA1 blobs. requires searchHandler. 71 72 ui *UIHandler // or nil, if none configured 73 sigh *signhandler.Handler // or nil, if none configured 74 sync []*SyncHandler // list of configured sync handlers, for discovery. 75} 76 77func (rh *RootHandler) SearchHandler() (h *search.Handler, ok bool) { 78 rh.searchInitOnce.Do(rh.searchInit) 79 return rh.searchHandler, rh.searchHandler != nil 80} 81 82func init() { 83 blobserver.RegisterHandlerConstructor("root", newRootFromConfig) 84} 85 86func newRootFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (h http.Handler, err error) { 87 checkType := func(key string, htype string) { 88 v := conf.OptionalString(key, "") 89 if v == "" { 90 return 91 } 92 ct := ld.GetHandlerType(v) 93 if ct == "" { 94 err = fmt.Errorf("root handler's %q references non-existent %q", key, v) 95 } else if ct != htype { 96 err = fmt.Errorf("root handler's %q references %q of type %q; expected type %q", key, v, ct, htype) 97 } 98 } 99 checkType("searchRoot", "search") 100 checkType("jsonSignRoot", "jsonsign") 101 if err != nil { 102 return 103 } 104 username, _ := getUserName() 105 root := &RootHandler{ 106 BlobRoot: conf.OptionalString("blobRoot", ""), 107 SearchRoot: conf.OptionalString("searchRoot", ""), 108 JSONSignRoot: conf.OptionalString("jsonSignRoot", ""), 109 OwnerName: conf.OptionalString("ownerName", username), 110 Username: osutil.Username(), 111 Prefix: ld.MyPrefix(), 112 } 113 root.Stealth = conf.OptionalBool("stealth", false) 114 root.statusRoot = conf.OptionalString("statusRoot", "") 115 root.helpRoot = conf.OptionalString("helpRoot", "") 116 root.shareRoot = conf.OptionalString("shareRoot", "") 117 if err = conf.Validate(); err != nil { 118 return 119 } 120 121 if root.BlobRoot != "" { 122 bs, err := ld.GetStorage(root.BlobRoot) 123 if err != nil { 124 return nil, fmt.Errorf("Root handler's blobRoot of %q error: %v", root.BlobRoot, err) 125 } 126 root.Storage = bs 127 } 128 129 if root.JSONSignRoot != "" { 130 h, _ := ld.GetHandler(root.JSONSignRoot) 131 if sigh, ok := h.(*signhandler.Handler); ok { 132 root.sigh = sigh 133 } 134 } 135 136 root.searchInit = func() {} 137 if root.SearchRoot != "" { 138 prefix := root.SearchRoot 139 if t := ld.GetHandlerType(prefix); t != "search" { 140 if t == "" { 141 return nil, fmt.Errorf("root handler's searchRoot of %q is invalid and doesn't refer to a declared handler", prefix) 142 } 143 return nil, fmt.Errorf("root handler's searchRoot of %q is of type %q, not %q", prefix, t, "search") 144 } 145 root.searchInit = func() { 146 h, err := ld.GetHandler(prefix) 147 if err != nil { 148 log.Fatalf("Error fetching SearchRoot at %q: %v", prefix, err) 149 } 150 root.searchHandler = h.(*search.Handler) 151 // the result from root.searchHandler.HasLegacySHA1() is determined on index 152 // startup, and never changes during the server's lifetime, so we might as well 153 // cache it here too. 154 root.hasLegacySHA1 = root.searchHandler.HasLegacySHA1() 155 root.searchInit = nil 156 } 157 } 158 159 if pfx, _, _ := ld.FindHandlerByType("importer"); err == nil { 160 root.importerRoot = pfx 161 } 162 163 return root, nil 164} 165 166func (rh *RootHandler) registerUIHandler(h *UIHandler) { 167 rh.ui = h 168} 169 170func (rh *RootHandler) registerSyncHandler(h *SyncHandler) { 171 rh.sync = append(rh.sync, h) 172 sort.Sort(byFromTo(rh.sync)) 173} 174 175func (rh *RootHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 176 if wantsDiscovery(r) { 177 if auth.Allowed(r, auth.OpDiscovery) { 178 rh.serveDiscovery(w, r) 179 return 180 } 181 if !rh.Stealth { 182 auth.SendUnauthorized(w, r) 183 } 184 return 185 } 186 187 if rh.Stealth { 188 return 189 } 190 if r.RequestURI == "/" && rh.ui != nil { 191 http.Redirect(w, r, "/ui/", http.StatusMovedPermanently) 192 return 193 } 194 switch r.URL.Path { 195 case "/favicon.ico": 196 ServeStaticFile(w, r, Files, "favicon.ico") 197 return 198 case "/mobile-setup": 199 http.Redirect(w, r, "/ui/mobile.html", http.StatusFound) 200 return 201 case "/": 202 break 203 default: 204 http.NotFound(w, r) 205 return 206 } 207 208 f := func(p string, a ...interface{}) { 209 fmt.Fprintf(w, p, a...) 210 } 211 f("<html><body><p>This is perkeepd (%s), a "+ 212 "<a href='http://perkeep.org'>Perkeep</a> server.</p>", 213 html.EscapeString(buildinfo.Summary())) 214 if rh.ui != nil { 215 f("<p>To manage your content, access the <a href='%s'>%s</a>.</p>", rh.ui.prefix, rh.ui.prefix) 216 } 217 if rh.statusRoot != "" { 218 f("<p>To view status, see <a href='%s'>%s</a>.</p>", rh.statusRoot, rh.statusRoot) 219 } 220 if rh.helpRoot != "" { 221 f("<p>To view more information on accessing the server, see <a href='%s'>%s</a>.</p>", rh.helpRoot, rh.helpRoot) 222 } 223 fmt.Fprintf(w, "</body></html>") 224} 225 226type byFromTo []*SyncHandler 227 228func (b byFromTo) Len() int { return len(b) } 229func (b byFromTo) Swap(i, j int) { b[i], b[j] = b[j], b[i] } 230func (b byFromTo) Less(i, j int) bool { 231 if b[i].fromName < b[j].fromName { 232 return true 233 } 234 return b[i].fromName == b[j].fromName && b[i].toName < b[j].toName 235} 236 237func (rh *RootHandler) serveDiscovery(rw http.ResponseWriter, req *http.Request) { 238 d := &camtypes.Discovery{ 239 BlobRoot: rh.BlobRoot, 240 JSONSignRoot: rh.JSONSignRoot, 241 HelpRoot: rh.helpRoot, 242 ImporterRoot: rh.importerRoot, 243 SearchRoot: rh.SearchRoot, 244 ShareRoot: rh.shareRoot, 245 StatusRoot: rh.statusRoot, 246 OwnerName: rh.OwnerName, 247 UserName: rh.Username, 248 AuthToken: auth.DiscoveryToken(), 249 ThumbVersion: images.ThumbnailVersion(), 250 } 251 if gener, ok := rh.Storage.(blobserver.Generationer); ok { 252 initTime, gen, err := gener.StorageGeneration() 253 if err != nil { 254 d.StorageGenerationError = err.Error() 255 } else { 256 d.StorageInitTime = types.Time3339(initTime) 257 d.StorageGeneration = gen 258 } 259 } else { 260 log.Printf("Storage type %T is not a blobserver.Generationer; not sending storageGeneration", rh.Storage) 261 } 262 if rh.ui != nil { 263 d.UIDiscovery = rh.ui.discovery() 264 } 265 if rh.sigh != nil { 266 d.Signing = rh.sigh.Discovery(rh.JSONSignRoot) 267 } 268 if len(rh.sync) > 0 { 269 syncHandlers := make([]camtypes.SyncHandlerDiscovery, 0, len(rh.sync)) 270 for _, sh := range rh.sync { 271 syncHandlers = append(syncHandlers, sh.discovery()) 272 } 273 d.SyncHandlers = syncHandlers 274 } 275 d.HasLegacySHA1Index = rh.hasLegacySHA1 276 discoveryHelper(rw, req, d) 277} 278 279func discoveryHelper(rw http.ResponseWriter, req *http.Request, dr *camtypes.Discovery) { 280 rw.Header().Set("Content-Type", "text/javascript") 281 if cb := req.FormValue("cb"); identOrDotPattern.MatchString(cb) { 282 fmt.Fprintf(rw, "%s(", cb) 283 defer rw.Write([]byte(");\n")) 284 } else if v := req.FormValue("var"); identOrDotPattern.MatchString(v) { 285 fmt.Fprintf(rw, "%s = ", v) 286 defer rw.Write([]byte(";\n")) 287 } 288 bytes, err := json.MarshalIndent(dr, "", " ") 289 if err != nil { 290 httputil.ServeJSONError(rw, httputil.ServerError("encoding discovery information: "+err.Error())) 291 return 292 } 293 rw.Write(bytes) 294} 295