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