1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
16
17 package server
18
19 import (
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
43 type RootHandler struct {
44
45
46 Stealth bool
47
48 OwnerName string
49 Username string
50
51
52
53 BlobRoot string
54 SearchRoot string
55 helpRoot string
56 importerRoot string
57 statusRoot string
58 Prefix string
59 shareRoot string
60
61
62
63 JSONSignRoot string
64
65 Storage blobserver.Storage
66
67 searchInitOnce sync.Once
68 searchInit func()
69 searchHandler *search.Handler
70 hasLegacySHA1 bool
71
72 ui *UIHandler
73 sigh *signhandler.Handler
74 sync []*SyncHandler
75 }
76
77 func (rh *RootHandler) SearchHandler() (h *search.Handler, ok bool) {
78 rh.searchInitOnce.Do(rh.searchInit)
79 return rh.searchHandler, rh.searchHandler != nil
80 }
81
82 func init() {
83 blobserver.RegisterHandlerConstructor("root", newRootFromConfig)
84 }
85
86 func 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
152
153
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
166 func (rh *RootHandler) registerUIHandler(h *UIHandler) {
167 rh.ui = h
168 }
169
170 func (rh *RootHandler) registerSyncHandler(h *SyncHandler) {
171 rh.sync = append(rh.sync, h)
172 sort.Sort(byFromTo(rh.sync))
173 }
174
175 func (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
226 type byFromTo []*SyncHandler
227
228 func (b byFromTo) Len() int { return len(b) }
229 func (b byFromTo) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
230 func (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
237 func (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
279 func 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 }