Home Download Docs Code Community
     1	/*
     2	Copyright 2011 The Perkeep Authors
     3	
     4	Licensed under the Apache License, Version 2.0 (the "License");
     5	you may not use this file except in compliance with the License.
     6	You may obtain a copy of the License at
     7	
     8	     http://www.apache.org/licenses/LICENSE-2.0
     9	
    10	Unless required by applicable law or agreed to in writing, software
    11	distributed under the License is distributed on an "AS IS" BASIS,
    12	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13	See the License for the specific language governing permissions and
    14	limitations under the License.
    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	// RootHandler handles serving the about/splash page.
    43	type 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	
    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				// 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	
   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	}
Website layout inspired by memcached.
Content by the authors.