From d888962082c6e4c74eb13ddedf6a2fbb05a392d5 Mon Sep 17 00:00:00 2001 From: Harvey Kandola Date: Fri, 21 Jul 2017 18:14:19 +0100 Subject: [PATCH] refactored routing/web serving logic --- core/api/endpoint/authentication_endpoint.go | 2 +- core/api/endpoint/router.go | 252 ------------------- core/api/mail/mailer.go | 2 +- core/api/request/organization.go | 2 +- core/database/check.go | 27 +- core/database/create.go | 5 +- core/database/migrate.go | 66 ++--- core/env/logger.go | 6 +- edition/boot/runtime.go | 4 +- edition/community.go | 6 +- embed/embed.go | 3 +- server/routing/entries.go | 148 +++++++++++ server/routing/table.go | 133 ++++++++++ {core/api/endpoint => server}/server.go | 50 ++-- server/web/embed.go | 48 ++++ core/web/web.go => server/web/serve.go | 38 +-- 16 files changed, 410 insertions(+), 382 deletions(-) delete mode 100644 core/api/endpoint/router.go create mode 100644 server/routing/entries.go create mode 100644 server/routing/table.go rename {core/api/endpoint => server}/server.go (76%) create mode 100644 server/web/embed.go rename core/web/web.go => server/web/serve.go (57%) diff --git a/core/api/endpoint/authentication_endpoint.go b/core/api/endpoint/authentication_endpoint.go index 2943bb75..800023ad 100644 --- a/core/api/endpoint/authentication_endpoint.go +++ b/core/api/endpoint/authentication_endpoint.go @@ -26,8 +26,8 @@ import ( "github.com/documize/community/core/api/util" "github.com/documize/community/core/log" "github.com/documize/community/core/secrets" - "github.com/documize/community/core/web" "github.com/documize/community/domain/section/provider" + "github.com/documize/community/server/web" ) // Authenticate user based up HTTP Authorization header. diff --git a/core/api/endpoint/router.go b/core/api/endpoint/router.go deleted file mode 100644 index 21bf75dd..00000000 --- a/core/api/endpoint/router.go +++ /dev/null @@ -1,252 +0,0 @@ -// Copyright 2016 Documize Inc. . All rights reserved. -// -// This software (Documize Community Edition) is licensed under -// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html -// -// You can operate outside the AGPL restrictions by purchasing -// Documize Enterprise Edition and obtaining a commercial license -// by contacting . -// -// https://documize.com - -package endpoint - -import ( - "encoding/json" - "net/http" - "sort" - "strings" - - "github.com/documize/community/core/log" - "github.com/documize/community/core/web" - "github.com/gorilla/mux" -) - -const ( - // RoutePrefixPublic used for the unsecured api - RoutePrefixPublic = "/api/public/" - // RoutePrefixPrivate used for secured api (requiring api) - RoutePrefixPrivate = "/api/" - // RoutePrefixRoot used for unsecured endpoints at root (e.g. robots.txt) - RoutePrefixRoot = "/" -) - -type routeDef struct { - Prefix string - Path string - Methods []string - Queries []string -} - -// RouteFunc describes end-point functions -type RouteFunc func(http.ResponseWriter, *http.Request) - -type routeMap map[string]RouteFunc - -var routes = make(routeMap) - -func routesKey(prefix, path string, methods, queries []string) (string, error) { - rd := routeDef{ - Prefix: prefix, - Path: path, - Methods: methods, - Queries: queries, - } - b, e := json.Marshal(rd) - return string(b), e -} - -// Add an endpoint to those that will be processed when Serve() is called. -func Add(prefix, path string, methods, queries []string, endPtFn RouteFunc) error { - k, e := routesKey(prefix, path, methods, queries) - if e != nil { - return e - } - routes[k] = endPtFn - return nil -} - -// Remove an endpoint. -func Remove(prefix, path string, methods, queries []string) error { - k, e := routesKey(prefix, path, methods, queries) - if e != nil { - return e - } - delete(routes, k) - return nil -} - -type routeSortItem struct { - def routeDef - fun RouteFunc - ord int -} - -type routeSorter []routeSortItem - -func (s routeSorter) Len() int { return len(s) } -func (s routeSorter) Swap(i, j int) { s[i], s[j] = s[j], s[i] } -func (s routeSorter) Less(i, j int) bool { - if s[i].def.Prefix == s[j].def.Prefix && s[i].def.Path == s[j].def.Path { - return len(s[i].def.Queries) > len(s[j].def.Queries) - } - return s[i].ord < s[j].ord -} - -func buildRoutes(prefix string) *mux.Router { - var rs routeSorter - for k, v := range routes { - var rd routeDef - if err := json.Unmarshal([]byte(k), &rd); err != nil { - log.Error("buildRoutes json.Unmarshal", err) - } else { - if rd.Prefix == prefix { - order := strings.Index(rd.Path, "{") - if order == -1 { - order = len(rd.Path) - } - order = -order - rs = append(rs, routeSortItem{def: rd, fun: v, ord: order}) - } - } - } - sort.Sort(rs) - router := mux.NewRouter() - for _, it := range rs { - //fmt.Printf("DEBUG buildRoutes: %d %#v\n", it.ord, it.def) - - x := router.HandleFunc(it.def.Prefix+it.def.Path, it.fun) - if len(it.def.Methods) > 0 { - y := x.Methods(it.def.Methods...) - if len(it.def.Queries) > 0 { - y.Queries(it.def.Queries...) - } - } - } - return router -} - -func init() { - //************************************************** - // Non-secure routes - //************************************************** - - log.IfErr(Add(RoutePrefixPublic, "meta", []string{"GET", "OPTIONS"}, nil, GetMeta)) - log.IfErr(Add(RoutePrefixPublic, "authenticate/keycloak", []string{"POST", "OPTIONS"}, nil, AuthenticateKeycloak)) - log.IfErr(Add(RoutePrefixPublic, "authenticate", []string{"POST", "OPTIONS"}, nil, Authenticate)) - log.IfErr(Add(RoutePrefixPublic, "validate", []string{"GET", "OPTIONS"}, nil, ValidateAuthToken)) - log.IfErr(Add(RoutePrefixPublic, "forgot", []string{"POST", "OPTIONS"}, nil, ForgotUserPassword)) - log.IfErr(Add(RoutePrefixPublic, "reset/{token}", []string{"POST", "OPTIONS"}, nil, ResetUserPassword)) - log.IfErr(Add(RoutePrefixPublic, "share/{folderID}", []string{"POST", "OPTIONS"}, nil, AcceptSharedFolder)) - log.IfErr(Add(RoutePrefixPublic, "attachments/{orgID}/{attachmentID}", []string{"GET", "OPTIONS"}, nil, AttachmentDownload)) - log.IfErr(Add(RoutePrefixPublic, "version", []string{"GET", "OPTIONS"}, nil, version)) - - //************************************************** - // Secure routes - //************************************************** - - // Import & Convert Document - log.IfErr(Add(RoutePrefixPrivate, "import/folder/{folderID}", []string{"POST", "OPTIONS"}, nil, UploadConvertDocument)) - - // Document - log.IfErr(Add(RoutePrefixPrivate, "documents/{documentID}/export", []string{"GET", "OPTIONS"}, nil, GetDocumentAsDocx)) - log.IfErr(Add(RoutePrefixPrivate, "documents", []string{"GET", "OPTIONS"}, []string{"filter", "tag"}, GetDocumentsByTag)) - log.IfErr(Add(RoutePrefixPrivate, "documents", []string{"GET", "OPTIONS"}, nil, GetDocumentsByFolder)) - log.IfErr(Add(RoutePrefixPrivate, "documents/{documentID}", []string{"GET", "OPTIONS"}, nil, GetDocument)) - log.IfErr(Add(RoutePrefixPrivate, "documents/{documentID}", []string{"PUT", "OPTIONS"}, nil, UpdateDocument)) - log.IfErr(Add(RoutePrefixPrivate, "documents/{documentID}", []string{"DELETE", "OPTIONS"}, nil, DeleteDocument)) - log.IfErr(Add(RoutePrefixPrivate, "documents/{documentID}/activity", []string{"GET", "OPTIONS"}, nil, GetDocumentActivity)) - - // Document Page - log.IfErr(Add(RoutePrefixPrivate, "documents/{documentID}/pages/level", []string{"POST", "OPTIONS"}, nil, ChangeDocumentPageLevel)) - log.IfErr(Add(RoutePrefixPrivate, "documents/{documentID}/pages/sequence", []string{"POST", "OPTIONS"}, nil, ChangeDocumentPageSequence)) - log.IfErr(Add(RoutePrefixPrivate, "documents/{documentID}/pages/batch", []string{"POST", "OPTIONS"}, nil, GetDocumentPagesBatch)) - log.IfErr(Add(RoutePrefixPrivate, "documents/{documentID}/pages/{pageID}/revisions", []string{"GET", "OPTIONS"}, nil, GetDocumentPageRevisions)) - log.IfErr(Add(RoutePrefixPrivate, "documents/{documentID}/pages/{pageID}/revisions/{revisionID}", []string{"GET", "OPTIONS"}, nil, GetDocumentPageDiff)) - log.IfErr(Add(RoutePrefixPrivate, "documents/{documentID}/pages/{pageID}/revisions/{revisionID}", []string{"POST", "OPTIONS"}, nil, RollbackDocumentPage)) - log.IfErr(Add(RoutePrefixPrivate, "documents/{documentID}/revisions", []string{"GET", "OPTIONS"}, nil, GetDocumentRevisions)) - - log.IfErr(Add(RoutePrefixPrivate, "documents/{documentID}/pages", []string{"GET", "OPTIONS"}, nil, GetDocumentPages)) - log.IfErr(Add(RoutePrefixPrivate, "documents/{documentID}/pages/{pageID}", []string{"PUT", "OPTIONS"}, nil, UpdateDocumentPage)) - log.IfErr(Add(RoutePrefixPrivate, "documents/{documentID}/pages/{pageID}", []string{"DELETE", "OPTIONS"}, nil, DeleteDocumentPage)) - log.IfErr(Add(RoutePrefixPrivate, "documents/{documentID}/pages", []string{"DELETE", "OPTIONS"}, nil, DeleteDocumentPages)) - log.IfErr(Add(RoutePrefixPrivate, "documents/{documentID}/pages/{pageID}", []string{"GET", "OPTIONS"}, nil, GetDocumentPage)) - log.IfErr(Add(RoutePrefixPrivate, "documents/{documentID}/pages", []string{"POST", "OPTIONS"}, nil, AddDocumentPage)) - log.IfErr(Add(RoutePrefixPrivate, "documents/{documentID}/attachments", []string{"GET", "OPTIONS"}, nil, GetAttachments)) - log.IfErr(Add(RoutePrefixPrivate, "documents/{documentID}/attachments/{attachmentID}", []string{"DELETE", "OPTIONS"}, nil, DeleteAttachment)) - log.IfErr(Add(RoutePrefixPrivate, "documents/{documentID}/attachments", []string{"POST", "OPTIONS"}, nil, AddAttachments)) - log.IfErr(Add(RoutePrefixPrivate, "documents/{documentID}/pages/{pageID}/meta", []string{"GET", "OPTIONS"}, nil, GetDocumentPageMeta)) - log.IfErr(Add(RoutePrefixPrivate, "documents/{documentID}/pages/{pageID}/copy/{targetID}", []string{"POST", "OPTIONS"}, nil, CopyPage)) - - // Organization - log.IfErr(Add(RoutePrefixPrivate, "organizations/{orgID}", []string{"GET", "OPTIONS"}, nil, GetOrganization)) - log.IfErr(Add(RoutePrefixPrivate, "organizations/{orgID}", []string{"PUT", "OPTIONS"}, nil, UpdateOrganization)) - - // Folder - log.IfErr(Add(RoutePrefixPrivate, "folders/{folderID}", []string{"DELETE", "OPTIONS"}, nil, DeleteFolder)) - log.IfErr(Add(RoutePrefixPrivate, "folders/{folderID}/move/{moveToId}", []string{"DELETE", "OPTIONS"}, nil, RemoveFolder)) - log.IfErr(Add(RoutePrefixPrivate, "folders/{folderID}/permissions", []string{"PUT", "OPTIONS"}, nil, SetFolderPermissions)) - log.IfErr(Add(RoutePrefixPrivate, "folders/{folderID}/permissions", []string{"GET", "OPTIONS"}, nil, GetFolderPermissions)) - log.IfErr(Add(RoutePrefixPrivate, "folders/{folderID}/invitation", []string{"POST", "OPTIONS"}, nil, InviteToFolder)) - log.IfErr(Add(RoutePrefixPrivate, "folders", []string{"GET", "OPTIONS"}, []string{"filter", "viewers"}, GetFolderVisibility)) - log.IfErr(Add(RoutePrefixPrivate, "folders", []string{"POST", "OPTIONS"}, nil, AddFolder)) - log.IfErr(Add(RoutePrefixPrivate, "folders", []string{"GET", "OPTIONS"}, nil, GetFolders)) - log.IfErr(Add(RoutePrefixPrivate, "folders/{folderID}", []string{"GET", "OPTIONS"}, nil, GetFolder)) - log.IfErr(Add(RoutePrefixPrivate, "folders/{folderID}", []string{"PUT", "OPTIONS"}, nil, UpdateFolder)) - - // Users - log.IfErr(Add(RoutePrefixPrivate, "users/{userID}/password", []string{"POST", "OPTIONS"}, nil, ChangeUserPassword)) - log.IfErr(Add(RoutePrefixPrivate, "users/{userID}/permissions", []string{"GET", "OPTIONS"}, nil, GetUserFolderPermissions)) - log.IfErr(Add(RoutePrefixPrivate, "users", []string{"POST", "OPTIONS"}, nil, AddUser)) - log.IfErr(Add(RoutePrefixPrivate, "users/folder/{folderID}", []string{"GET", "OPTIONS"}, nil, GetFolderUsers)) - log.IfErr(Add(RoutePrefixPrivate, "users", []string{"GET", "OPTIONS"}, nil, GetOrganizationUsers)) - log.IfErr(Add(RoutePrefixPrivate, "users/{userID}", []string{"GET", "OPTIONS"}, nil, GetUser)) - log.IfErr(Add(RoutePrefixPrivate, "users/{userID}", []string{"PUT", "OPTIONS"}, nil, UpdateUser)) - log.IfErr(Add(RoutePrefixPrivate, "users/{userID}", []string{"DELETE", "OPTIONS"}, nil, DeleteUser)) - log.IfErr(Add(RoutePrefixPrivate, "users/sync", []string{"GET", "OPTIONS"}, nil, SyncKeycloak)) - - // Search - log.IfErr(Add(RoutePrefixPrivate, "search", []string{"GET", "OPTIONS"}, nil, SearchDocuments)) - - // Templates - log.IfErr(Add(RoutePrefixPrivate, "templates", []string{"POST", "OPTIONS"}, nil, SaveAsTemplate)) - log.IfErr(Add(RoutePrefixPrivate, "templates", []string{"GET", "OPTIONS"}, nil, GetSavedTemplates)) - log.IfErr(Add(RoutePrefixPrivate, "templates/stock", []string{"GET", "OPTIONS"}, nil, GetStockTemplates)) - log.IfErr(Add(RoutePrefixPrivate, "templates/{templateID}/folder/{folderID}", []string{"POST", "OPTIONS"}, []string{"type", "stock"}, StartDocumentFromStockTemplate)) - log.IfErr(Add(RoutePrefixPrivate, "templates/{templateID}/folder/{folderID}", []string{"POST", "OPTIONS"}, []string{"type", "saved"}, StartDocumentFromSavedTemplate)) - - // Sections - log.IfErr(Add(RoutePrefixPrivate, "sections", []string{"GET", "OPTIONS"}, nil, GetSections)) - log.IfErr(Add(RoutePrefixPrivate, "sections", []string{"POST", "OPTIONS"}, nil, RunSectionCommand)) - log.IfErr(Add(RoutePrefixPrivate, "sections/refresh", []string{"GET", "OPTIONS"}, nil, RefreshSections)) - log.IfErr(Add(RoutePrefixPrivate, "sections/blocks/space/{folderID}", []string{"GET", "OPTIONS"}, nil, GetBlocksForSpace)) - log.IfErr(Add(RoutePrefixPrivate, "sections/blocks/{blockID}", []string{"GET", "OPTIONS"}, nil, GetBlock)) - log.IfErr(Add(RoutePrefixPrivate, "sections/blocks/{blockID}", []string{"PUT", "OPTIONS"}, nil, UpdateBlock)) - log.IfErr(Add(RoutePrefixPrivate, "sections/blocks/{blockID}", []string{"DELETE", "OPTIONS"}, nil, DeleteBlock)) - log.IfErr(Add(RoutePrefixPrivate, "sections/blocks", []string{"POST", "OPTIONS"}, nil, AddBlock)) - log.IfErr(Add(RoutePrefixPrivate, "sections/targets", []string{"GET", "OPTIONS"}, nil, GetPageMoveCopyTargets)) - - // Links - log.IfErr(Add(RoutePrefixPrivate, "links/{folderID}/{documentID}/{pageID}", []string{"GET", "OPTIONS"}, nil, GetLinkCandidates)) - log.IfErr(Add(RoutePrefixPrivate, "links", []string{"GET", "OPTIONS"}, nil, SearchLinkCandidates)) - log.IfErr(Add(RoutePrefixPrivate, "documents/{documentID}/links", []string{"GET", "OPTIONS"}, nil, GetDocumentLinks)) - - // Global installation-wide config - log.IfErr(Add(RoutePrefixPrivate, "global/smtp", []string{"GET", "OPTIONS"}, nil, GetSMTPConfig)) - log.IfErr(Add(RoutePrefixPrivate, "global/smtp", []string{"PUT", "OPTIONS"}, nil, SaveSMTPConfig)) - log.IfErr(Add(RoutePrefixPrivate, "global/license", []string{"GET", "OPTIONS"}, nil, GetLicense)) - log.IfErr(Add(RoutePrefixPrivate, "global/license", []string{"PUT", "OPTIONS"}, nil, SaveLicense)) - log.IfErr(Add(RoutePrefixPrivate, "global/auth", []string{"GET", "OPTIONS"}, nil, GetAuthConfig)) - log.IfErr(Add(RoutePrefixPrivate, "global/auth", []string{"PUT", "OPTIONS"}, nil, SaveAuthConfig)) - - // Pinned items - log.IfErr(Add(RoutePrefixPrivate, "pin/{userID}", []string{"POST", "OPTIONS"}, nil, AddPin)) - log.IfErr(Add(RoutePrefixPrivate, "pin/{userID}", []string{"GET", "OPTIONS"}, nil, GetUserPins)) - log.IfErr(Add(RoutePrefixPrivate, "pin/{userID}/sequence", []string{"POST", "OPTIONS"}, nil, UpdatePinSequence)) - log.IfErr(Add(RoutePrefixPrivate, "pin/{userID}/{pinID}", []string{"DELETE", "OPTIONS"}, nil, DeleteUserPin)) - - // Single page app handler - log.IfErr(Add(RoutePrefixRoot, "robots.txt", []string{"GET", "OPTIONS"}, nil, GetRobots)) - log.IfErr(Add(RoutePrefixRoot, "sitemap.xml", []string{"GET", "OPTIONS"}, nil, GetSitemap)) - log.IfErr(Add(RoutePrefixRoot, "{rest:.*}", nil, nil, web.EmberHandler)) -} diff --git a/core/api/mail/mailer.go b/core/api/mail/mailer.go index 93e622bb..fd1c6c7b 100644 --- a/core/api/mail/mailer.go +++ b/core/api/mail/mailer.go @@ -21,7 +21,7 @@ import ( "github.com/documize/community/core/api/request" "github.com/documize/community/core/log" - "github.com/documize/community/core/web" + "github.com/documize/community/server/web" ) // InviteNewUser invites someone new providing credentials, explaining the product and stating who is inviting them. diff --git a/core/api/request/organization.go b/core/api/request/organization.go index 3034f65b..4aa76c6d 100644 --- a/core/api/request/organization.go +++ b/core/api/request/organization.go @@ -21,7 +21,7 @@ import ( "github.com/documize/community/core/api/entity" "github.com/documize/community/core/log" "github.com/documize/community/core/streamutil" - "github.com/documize/community/core/web" + "github.com/documize/community/server/web" "github.com/jmoiron/sqlx" ) diff --git a/core/database/check.go b/core/database/check.go index f671366d..61c00229 100644 --- a/core/database/check.go +++ b/core/database/check.go @@ -18,9 +18,8 @@ import ( "strings" "github.com/documize/community/core/env" - "github.com/documize/community/core/log" "github.com/documize/community/core/streamutil" - "github.com/documize/community/core/web" + "github.com/documize/community/server/web" "github.com/jmoiron/sqlx" ) @@ -39,7 +38,7 @@ var dbPtr *sqlx.DB func Check(runtime *env.Runtime) bool { dbPtr = runtime.Db - log.Info("Database checks: started") + runtime.Log.Info("Database checks: started") csBits := strings.Split(runtime.Flags.DBConn, "/") if len(csBits) > 1 { @@ -48,7 +47,7 @@ func Check(runtime *env.Runtime) bool { rows, err := runtime.Db.Query("SELECT VERSION() AS version, @@version_comment as comment, @@character_set_database AS charset, @@collation_database AS collation;") if err != nil { - log.Error("Can't get MySQL configuration", err) + runtime.Log.Error("Can't get MySQL configuration", err) web.SiteInfo.Issue = "Can't get MySQL configuration: " + err.Error() runtime.Flags.SiteMode = web.SiteModeBadDB return false @@ -64,7 +63,7 @@ func Check(runtime *env.Runtime) bool { } if err != nil { - log.Error("no MySQL configuration returned", err) + runtime.Log.Error("no MySQL configuration returned", err) web.SiteInfo.Issue = "no MySQL configuration return issue: " + err.Error() runtime.Flags.SiteMode = web.SiteModeBadDB return false @@ -74,12 +73,12 @@ func Check(runtime *env.Runtime) bool { // MySQL and Percona share same version scheme (e..g 5.7.10). // MariaDB starts at 10.2.x sqlVariant := GetSQLVariant(dbComment) - log.Info("Database checks: SQL variant " + sqlVariant) - log.Info("Database checks: SQL version " + version) + runtime.Log.Info("Database checks: SQL variant " + sqlVariant) + runtime.Log.Info("Database checks: SQL version " + version) verNums, err := GetSQLVersion(version) if err != nil { - log.Error("Database version check failed", err) + runtime.Log.Error("Database version check failed", err) } // Check minimum MySQL version as we need JSON column type. @@ -91,7 +90,7 @@ func Check(runtime *env.Runtime) bool { for k, v := range verInts { if verNums[k] < v { want := fmt.Sprintf("%d.%d.%d", verInts[0], verInts[1], verInts[2]) - log.Error("MySQL version element "+strconv.Itoa(k+1)+" of '"+version+"' not high enough, need at least version "+want, errors.New("bad MySQL version")) + runtime.Log.Error("MySQL version element "+strconv.Itoa(k+1)+" of '"+version+"' not high enough, need at least version "+want, errors.New("bad MySQL version")) web.SiteInfo.Issue = "MySQL version element " + strconv.Itoa(k+1) + " of '" + version + "' not high enough, need at least version " + want runtime.Flags.SiteMode = web.SiteModeBadDB return false @@ -100,13 +99,13 @@ func Check(runtime *env.Runtime) bool { { // check the MySQL character set and collation if charset != "utf8" { - log.Error("MySQL character set not utf8:", errors.New(charset)) + runtime.Log.Error("MySQL character set not utf8:", errors.New(charset)) web.SiteInfo.Issue = "MySQL character set not utf8: " + charset runtime.Flags.SiteMode = web.SiteModeBadDB return false } if !strings.HasPrefix(collation, "utf8") { - log.Error("MySQL collation sequence not utf8...:", errors.New(collation)) + runtime.Log.Error("MySQL collation sequence not utf8...:", errors.New(collation)) web.SiteInfo.Issue = "MySQL collation sequence not utf8...: " + collation runtime.Flags.SiteMode = web.SiteModeBadDB return false @@ -118,13 +117,13 @@ func Check(runtime *env.Runtime) bool { if err := runtime.Db.Select(&flds, `SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = '`+web.SiteInfo.DBname+ `' and TABLE_TYPE='BASE TABLE'`); err != nil { - log.Error("Can't get MySQL number of tables", err) + runtime.Log.Error("Can't get MySQL number of tables", err) web.SiteInfo.Issue = "Can't get MySQL number of tables: " + err.Error() runtime.Flags.SiteMode = web.SiteModeBadDB return false } if strings.TrimSpace(flds[0]) == "0" { - log.Info("Entering database set-up mode because the database is empty.....") + runtime.Log.Info("Entering database set-up mode because the database is empty.....") runtime.Flags.SiteMode = web.SiteModeSetup return false } @@ -139,7 +138,7 @@ func Check(runtime *env.Runtime) bool { for _, table := range tables { var dummy []string if err := runtime.Db.Select(&dummy, "SELECT 1 FROM "+table+" LIMIT 1;"); err != nil { - log.Error("Entering bad database mode because: SELECT 1 FROM "+table+" LIMIT 1;", err) + runtime.Log.Error("Entering bad database mode because: SELECT 1 FROM "+table+" LIMIT 1;", err) web.SiteInfo.Issue = "MySQL database is not empty, but does not contain table: " + table runtime.Flags.SiteMode = web.SiteModeBadDB return false diff --git a/core/database/create.go b/core/database/create.go index 2badc942..5b05e90b 100644 --- a/core/database/create.go +++ b/core/database/create.go @@ -23,7 +23,7 @@ import ( "github.com/documize/community/core/secrets" "github.com/documize/community/core/stringutil" "github.com/documize/community/core/uniqueid" - "github.com/documize/community/core/web" + "github.com/documize/community/server/web" ) // runSQL creates a transaction per call @@ -61,7 +61,6 @@ func runSQL(sql string) (id uint64, err error) { // Create the tables in a blank database func Create(w http.ResponseWriter, r *http.Request) { - defer func() { target := "/setup" status := http.StatusBadRequest @@ -123,7 +122,7 @@ func Create(w http.ResponseWriter, r *http.Request) { return } - if err = Migrate(false /* no tables exist yet */); err != nil { + if err = Migrate(api.Runtime, false /* no tables exist yet */); err != nil { log.Error("database.Create()", err) return } diff --git a/core/database/migrate.go b/core/database/migrate.go index a09f5a85..17abfc93 100644 --- a/core/database/migrate.go +++ b/core/database/migrate.go @@ -22,9 +22,9 @@ import ( "strings" "time" - "github.com/documize/community/core/log" + "github.com/documize/community/core/env" "github.com/documize/community/core/streamutil" - "github.com/documize/community/core/web" + "github.com/documize/community/server/web" "github.com/jmoiron/sqlx" ) @@ -69,9 +69,9 @@ func migrations(lastMigration string) (migrationsT, error) { } // migrate the database as required, by applying the migrations. -func (m migrationsT) migrate(tx *sqlx.Tx) error { +func (m migrationsT) migrate(runtime env.Runtime, tx *sqlx.Tx) error { for _, v := range m { - log.Info("Processing migration file: " + v) + runtime.Log.Info("Processing migration file: " + v) buf, err := web.Asset(migrationsDir + "/" + v) if err != nil { @@ -96,7 +96,7 @@ func (m migrationsT) migrate(tx *sqlx.Tx) error { return nil } -func lockDB() (bool, error) { +func lockDB(runtime env.Runtime) (bool, error) { b := make([]byte, 2) _, err := rand.Read(b) if err != nil { @@ -117,8 +117,10 @@ func lockDB() (bool, error) { defer func() { _, err = tx.Exec("UNLOCK TABLES;") - log.IfErr(err) - log.IfErr(tx.Commit()) + if err != nil { + runtime.Log.Error("unable to unlock tables", err) + } + tx.Commit() }() _, err = tx.Exec("INSERT INTO `config` (`key`,`config`) " + @@ -126,13 +128,13 @@ func lockDB() (bool, error) { if err != nil { // good error would be "Error 1062: Duplicate entry 'DBLOCK' for key 'idx_config_area'" if strings.HasPrefix(err.Error(), "Error 1062:") { - log.Info("Database locked by annother Documize instance") + runtime.Log.Info("Database locked by annother Documize instance") return false, nil } return false, err } - log.Info("Database locked by this Documize instance") + runtime.Log.Info("Database locked by this Documize instance") return true, err // success! } @@ -148,18 +150,18 @@ func unlockDB() error { return tx.Commit() } -func migrateEnd(tx *sqlx.Tx, err error, amLeader bool) error { +func migrateEnd(runtime env.Runtime, tx *sqlx.Tx, err error, amLeader bool) error { if amLeader { - defer func() { log.IfErr(unlockDB()) }() + defer func() { unlockDB() }() if tx != nil { if err == nil { - log.IfErr(tx.Commit()) - log.Info("Database checks: completed") + tx.Commit() + runtime.Log.Info("Database checks: completed") return nil } - log.IfErr(tx.Rollback()) + tx.Rollback() } - log.Error("Database checks: failed: ", err) + runtime.Log.Error("Database checks: failed: ", err) return err } return nil // not the leader, so ignore errors @@ -186,21 +188,22 @@ func getLastMigration(tx *sqlx.Tx) (lastMigration string, err error) { } // Migrate the database as required, consolidated action. -func Migrate(ConfigTableExists bool) error { - +func Migrate(runtime env.Runtime, ConfigTableExists bool) error { amLeader := false if ConfigTableExists { var err error - amLeader, err = lockDB() - log.IfErr(err) + amLeader, err = lockDB(runtime) + if err != nil { + runtime.Log.Error("unable to lock DB", err) + } } else { amLeader = true // what else can you do? } tx, err := (*dbPtr).Beginx() if err != nil { - return migrateEnd(tx, err, amLeader) + return migrateEnd(runtime, tx, err, amLeader) } lastMigration := "" @@ -208,53 +211,50 @@ func Migrate(ConfigTableExists bool) error { if ConfigTableExists { lastMigration, err = getLastMigration(tx) if err != nil { - return migrateEnd(tx, err, amLeader) + return migrateEnd(runtime, tx, err, amLeader) } - log.Info("Database checks: last applied " + lastMigration) + runtime.Log.Info("Database checks: last applied " + lastMigration) } mig, err := migrations(lastMigration) if err != nil { - return migrateEnd(tx, err, amLeader) + return migrateEnd(runtime, tx, err, amLeader) } if len(mig) == 0 { - log.Info("Database checks: no updates required") - return migrateEnd(tx, nil, amLeader) // no migrations to perform + runtime.Log.Info("Database checks: no updates required") + return migrateEnd(runtime, tx, nil, amLeader) // no migrations to perform } if amLeader { - log.Info("Database checks: will execute the following update files: " + strings.Join([]string(mig), ", ")) - return migrateEnd(tx, mig.migrate(tx), amLeader) + runtime.Log.Info("Database checks: will execute the following update files: " + strings.Join([]string(mig), ", ")) + return migrateEnd(runtime, tx, mig.migrate(runtime, tx), amLeader) } // a follower instance targetMigration := string(mig[len(mig)-1]) for targetMigration != lastMigration { time.Sleep(time.Second) - log.Info("Waiting for database migration completion") + runtime.Log.Info("Waiting for database migration completion") tx.Rollback() // ignore error tx, err := (*dbPtr).Beginx() // need this in order to see the changed situation since last tx if err != nil { - return migrateEnd(tx, err, amLeader) + return migrateEnd(runtime, tx, err, amLeader) } lastMigration, _ = getLastMigration(tx) } - return migrateEnd(tx, nil, amLeader) + return migrateEnd(runtime, tx, nil, amLeader) } func processSQLfile(tx *sqlx.Tx, buf []byte) error { - stmts := getStatements(buf) for _, stmt := range stmts { - _, err := tx.Exec(stmt) if err != nil { return err } - } return nil diff --git a/core/env/logger.go b/core/env/logger.go index 85b419a5..12f15b8d 100644 --- a/core/env/logger.go +++ b/core/env/logger.go @@ -12,13 +12,9 @@ // Package env provides runtime, server level setup and configuration package env -import ( - "github.com/jmoiron/sqlx" -) - // Logger provides the interface for Documize compatible loggers. type Logger interface { Info(message string) Error(message string, err error) - SetDB(l Logger, db *sqlx.DB) Logger + // SetDB(l Logger, db *sqlx.DB) Logger } diff --git a/edition/boot/runtime.go b/edition/boot/runtime.go index 561094c9..21db8cd7 100644 --- a/edition/boot/runtime.go +++ b/edition/boot/runtime.go @@ -19,7 +19,7 @@ import ( "github.com/documize/community/core/database" "github.com/documize/community/core/env" "github.com/documize/community/core/secrets" - "github.com/documize/community/core/web" + "github.com/documize/community/server/web" "github.com/jmoiron/sqlx" ) @@ -68,7 +68,7 @@ func InitRuntime(r *env.Runtime) bool { // go into setup mode if required if r.Flags.SiteMode != web.SiteModeOffline { if database.Check(r) { - if err := database.Migrate(true /* the config table exists */); err != nil { + if err := database.Migrate(*r, true /* the config table exists */); err != nil { r.Log.Error("unable to run database migration", err) return false } diff --git a/edition/community.go b/edition/community.go index 85b28e3b..d954050b 100644 --- a/edition/community.go +++ b/edition/community.go @@ -16,14 +16,14 @@ import ( "fmt" "github.com/documize/community/core/api" - "github.com/documize/community/core/api/endpoint" "github.com/documize/community/core/api/request" "github.com/documize/community/core/env" "github.com/documize/community/domain/section" "github.com/documize/community/edition/boot" "github.com/documize/community/edition/logging" _ "github.com/documize/community/embed" // the compressed front-end code and static data - _ "github.com/go-sql-driver/mysql" // the mysql driver is required behind the scenes + "github.com/documize/community/server" + _ "github.com/go-sql-driver/mysql" // the mysql driver is required behind the scenes ) var rt env.Runtime @@ -65,5 +65,5 @@ func main() { section.Register(rt) ready := make(chan struct{}, 1) // channel signals router ready - endpoint.Serve(rt, ready) + server.Start(rt, ready) } diff --git a/embed/embed.go b/embed/embed.go index 4d441b5c..9d6fd668 100644 --- a/embed/embed.go +++ b/embed/embed.go @@ -17,8 +17,7 @@ package embed import ( "net/http" - "github.com/documize/community/core/web" - + "github.com/documize/community/server/web" assetfs "github.com/elazarl/go-bindata-assetfs" ) diff --git a/server/routing/entries.go b/server/routing/entries.go new file mode 100644 index 00000000..0aab63c0 --- /dev/null +++ b/server/routing/entries.go @@ -0,0 +1,148 @@ +// Copyright 2016 Documize Inc. . All rights reserved. +// +// This software (Documize Community Edition) is licensed under +// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html +// +// You can operate outside the AGPL restrictions by purchasing +// Documize Enterprise Edition and obtaining a commercial license +// by contacting . +// +// https://documize.com + +package routing + +import ( + "net/http" + + "github.com/documize/community/core/api" + "github.com/documize/community/core/api/endpoint" + "github.com/documize/community/core/env" + "github.com/documize/community/server/web" +) + +// RegisterEndpoints register routes for serving API endpoints +func RegisterEndpoints(rt env.Runtime) { + //************************************************** + // Non-secure routes + //************************************************** + Add(rt, RoutePrefixPublic, "meta", []string{"GET", "OPTIONS"}, nil, endpoint.GetMeta) + Add(rt, RoutePrefixPublic, "authenticate/keycloak", []string{"POST", "OPTIONS"}, nil, endpoint.AuthenticateKeycloak) + Add(rt, RoutePrefixPublic, "authenticate", []string{"POST", "OPTIONS"}, nil, endpoint.Authenticate) + Add(rt, RoutePrefixPublic, "validate", []string{"GET", "OPTIONS"}, nil, endpoint.ValidateAuthToken) + Add(rt, RoutePrefixPublic, "forgot", []string{"POST", "OPTIONS"}, nil, endpoint.ForgotUserPassword) + Add(rt, RoutePrefixPublic, "reset/{token}", []string{"POST", "OPTIONS"}, nil, endpoint.ResetUserPassword) + Add(rt, RoutePrefixPublic, "share/{folderID}", []string{"POST", "OPTIONS"}, nil, endpoint.AcceptSharedFolder) + Add(rt, RoutePrefixPublic, "attachments/{orgID}/{attachmentID}", []string{"GET", "OPTIONS"}, nil, endpoint.AttachmentDownload) + Add(rt, RoutePrefixPublic, "version", []string{"GET", "OPTIONS"}, nil, func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(api.Runtime.Product.Version)) + }) + + //************************************************** + // Secure routes + //************************************************** + + // Import & Convert Document + Add(rt, RoutePrefixPrivate, "import/folder/{folderID}", []string{"POST", "OPTIONS"}, nil, endpoint.UploadConvertDocument) + + // Document + Add(rt, RoutePrefixPrivate, "documents/{documentID}/export", []string{"GET", "OPTIONS"}, nil, endpoint.GetDocumentAsDocx) + Add(rt, RoutePrefixPrivate, "documents", []string{"GET", "OPTIONS"}, []string{"filter", "tag"}, endpoint.GetDocumentsByTag) + Add(rt, RoutePrefixPrivate, "documents", []string{"GET", "OPTIONS"}, nil, endpoint.GetDocumentsByFolder) + Add(rt, RoutePrefixPrivate, "documents/{documentID}", []string{"GET", "OPTIONS"}, nil, endpoint.GetDocument) + Add(rt, RoutePrefixPrivate, "documents/{documentID}", []string{"PUT", "OPTIONS"}, nil, endpoint.UpdateDocument) + Add(rt, RoutePrefixPrivate, "documents/{documentID}", []string{"DELETE", "OPTIONS"}, nil, endpoint.DeleteDocument) + Add(rt, RoutePrefixPrivate, "documents/{documentID}/activity", []string{"GET", "OPTIONS"}, nil, endpoint.GetDocumentActivity) + + // Document Page + Add(rt, RoutePrefixPrivate, "documents/{documentID}/pages/level", []string{"POST", "OPTIONS"}, nil, endpoint.ChangeDocumentPageLevel) + Add(rt, RoutePrefixPrivate, "documents/{documentID}/pages/sequence", []string{"POST", "OPTIONS"}, nil, endpoint.ChangeDocumentPageSequence) + Add(rt, RoutePrefixPrivate, "documents/{documentID}/pages/batch", []string{"POST", "OPTIONS"}, nil, endpoint.GetDocumentPagesBatch) + Add(rt, RoutePrefixPrivate, "documents/{documentID}/pages/{pageID}/revisions", []string{"GET", "OPTIONS"}, nil, endpoint.GetDocumentPageRevisions) + Add(rt, RoutePrefixPrivate, "documents/{documentID}/pages/{pageID}/revisions/{revisionID}", []string{"GET", "OPTIONS"}, nil, endpoint.GetDocumentPageDiff) + Add(rt, RoutePrefixPrivate, "documents/{documentID}/pages/{pageID}/revisions/{revisionID}", []string{"POST", "OPTIONS"}, nil, endpoint.RollbackDocumentPage) + Add(rt, RoutePrefixPrivate, "documents/{documentID}/revisions", []string{"GET", "OPTIONS"}, nil, endpoint.GetDocumentRevisions) + + Add(rt, RoutePrefixPrivate, "documents/{documentID}/pages", []string{"GET", "OPTIONS"}, nil, endpoint.GetDocumentPages) + Add(rt, RoutePrefixPrivate, "documents/{documentID}/pages/{pageID}", []string{"PUT", "OPTIONS"}, nil, endpoint.UpdateDocumentPage) + Add(rt, RoutePrefixPrivate, "documents/{documentID}/pages/{pageID}", []string{"DELETE", "OPTIONS"}, nil, endpoint.DeleteDocumentPage) + Add(rt, RoutePrefixPrivate, "documents/{documentID}/pages", []string{"DELETE", "OPTIONS"}, nil, endpoint.DeleteDocumentPages) + Add(rt, RoutePrefixPrivate, "documents/{documentID}/pages/{pageID}", []string{"GET", "OPTIONS"}, nil, endpoint.GetDocumentPage) + Add(rt, RoutePrefixPrivate, "documents/{documentID}/pages", []string{"POST", "OPTIONS"}, nil, endpoint.AddDocumentPage) + Add(rt, RoutePrefixPrivate, "documents/{documentID}/attachments", []string{"GET", "OPTIONS"}, nil, endpoint.GetAttachments) + Add(rt, RoutePrefixPrivate, "documents/{documentID}/attachments/{attachmentID}", []string{"DELETE", "OPTIONS"}, nil, endpoint.DeleteAttachment) + Add(rt, RoutePrefixPrivate, "documents/{documentID}/attachments", []string{"POST", "OPTIONS"}, nil, endpoint.AddAttachments) + Add(rt, RoutePrefixPrivate, "documents/{documentID}/pages/{pageID}/meta", []string{"GET", "OPTIONS"}, nil, endpoint.GetDocumentPageMeta) + Add(rt, RoutePrefixPrivate, "documents/{documentID}/pages/{pageID}/copy/{targetID}", []string{"POST", "OPTIONS"}, nil, endpoint.CopyPage) + + // Organization + Add(rt, RoutePrefixPrivate, "organizations/{orgID}", []string{"GET", "OPTIONS"}, nil, endpoint.GetOrganization) + Add(rt, RoutePrefixPrivate, "organizations/{orgID}", []string{"PUT", "OPTIONS"}, nil, endpoint.UpdateOrganization) + + // Folder + Add(rt, RoutePrefixPrivate, "folders/{folderID}", []string{"DELETE", "OPTIONS"}, nil, endpoint.DeleteFolder) + Add(rt, RoutePrefixPrivate, "folders/{folderID}/move/{moveToId}", []string{"DELETE", "OPTIONS"}, nil, endpoint.RemoveFolder) + Add(rt, RoutePrefixPrivate, "folders/{folderID}/permissions", []string{"PUT", "OPTIONS"}, nil, endpoint.SetFolderPermissions) + Add(rt, RoutePrefixPrivate, "folders/{folderID}/permissions", []string{"GET", "OPTIONS"}, nil, endpoint.GetFolderPermissions) + Add(rt, RoutePrefixPrivate, "folders/{folderID}/invitation", []string{"POST", "OPTIONS"}, nil, endpoint.InviteToFolder) + Add(rt, RoutePrefixPrivate, "folders", []string{"GET", "OPTIONS"}, []string{"filter", "viewers"}, endpoint.GetFolderVisibility) + Add(rt, RoutePrefixPrivate, "folders", []string{"POST", "OPTIONS"}, nil, endpoint.AddFolder) + Add(rt, RoutePrefixPrivate, "folders", []string{"GET", "OPTIONS"}, nil, endpoint.GetFolders) + Add(rt, RoutePrefixPrivate, "folders/{folderID}", []string{"GET", "OPTIONS"}, nil, endpoint.GetFolder) + Add(rt, RoutePrefixPrivate, "folders/{folderID}", []string{"PUT", "OPTIONS"}, nil, endpoint.UpdateFolder) + + // Users + Add(rt, RoutePrefixPrivate, "users/{userID}/password", []string{"POST", "OPTIONS"}, nil, endpoint.ChangeUserPassword) + Add(rt, RoutePrefixPrivate, "users/{userID}/permissions", []string{"GET", "OPTIONS"}, nil, endpoint.GetUserFolderPermissions) + Add(rt, RoutePrefixPrivate, "users", []string{"POST", "OPTIONS"}, nil, endpoint.AddUser) + Add(rt, RoutePrefixPrivate, "users/folder/{folderID}", []string{"GET", "OPTIONS"}, nil, endpoint.GetFolderUsers) + Add(rt, RoutePrefixPrivate, "users", []string{"GET", "OPTIONS"}, nil, endpoint.GetOrganizationUsers) + Add(rt, RoutePrefixPrivate, "users/{userID}", []string{"GET", "OPTIONS"}, nil, endpoint.GetUser) + Add(rt, RoutePrefixPrivate, "users/{userID}", []string{"PUT", "OPTIONS"}, nil, endpoint.UpdateUser) + Add(rt, RoutePrefixPrivate, "users/{userID}", []string{"DELETE", "OPTIONS"}, nil, endpoint.DeleteUser) + Add(rt, RoutePrefixPrivate, "users/sync", []string{"GET", "OPTIONS"}, nil, endpoint.SyncKeycloak) + + // Search + Add(rt, RoutePrefixPrivate, "search", []string{"GET", "OPTIONS"}, nil, endpoint.SearchDocuments) + + // Templates + Add(rt, RoutePrefixPrivate, "templates", []string{"POST", "OPTIONS"}, nil, endpoint.SaveAsTemplate) + Add(rt, RoutePrefixPrivate, "templates", []string{"GET", "OPTIONS"}, nil, endpoint.GetSavedTemplates) + Add(rt, RoutePrefixPrivate, "templates/stock", []string{"GET", "OPTIONS"}, nil, endpoint.GetStockTemplates) + Add(rt, RoutePrefixPrivate, "templates/{templateID}/folder/{folderID}", []string{"POST", "OPTIONS"}, []string{"type", "stock"}, endpoint.StartDocumentFromStockTemplate) + Add(rt, RoutePrefixPrivate, "templates/{templateID}/folder/{folderID}", []string{"POST", "OPTIONS"}, []string{"type", "saved"}, endpoint.StartDocumentFromSavedTemplate) + + // Sections + Add(rt, RoutePrefixPrivate, "sections", []string{"GET", "OPTIONS"}, nil, endpoint.GetSections) + Add(rt, RoutePrefixPrivate, "sections", []string{"POST", "OPTIONS"}, nil, endpoint.RunSectionCommand) + Add(rt, RoutePrefixPrivate, "sections/refresh", []string{"GET", "OPTIONS"}, nil, endpoint.RefreshSections) + Add(rt, RoutePrefixPrivate, "sections/blocks/space/{folderID}", []string{"GET", "OPTIONS"}, nil, endpoint.GetBlocksForSpace) + Add(rt, RoutePrefixPrivate, "sections/blocks/{blockID}", []string{"GET", "OPTIONS"}, nil, endpoint.GetBlock) + Add(rt, RoutePrefixPrivate, "sections/blocks/{blockID}", []string{"PUT", "OPTIONS"}, nil, endpoint.UpdateBlock) + Add(rt, RoutePrefixPrivate, "sections/blocks/{blockID}", []string{"DELETE", "OPTIONS"}, nil, endpoint.DeleteBlock) + Add(rt, RoutePrefixPrivate, "sections/blocks", []string{"POST", "OPTIONS"}, nil, endpoint.AddBlock) + Add(rt, RoutePrefixPrivate, "sections/targets", []string{"GET", "OPTIONS"}, nil, endpoint.GetPageMoveCopyTargets) + + // Links + Add(rt, RoutePrefixPrivate, "links/{folderID}/{documentID}/{pageID}", []string{"GET", "OPTIONS"}, nil, endpoint.GetLinkCandidates) + Add(rt, RoutePrefixPrivate, "links", []string{"GET", "OPTIONS"}, nil, endpoint.SearchLinkCandidates) + Add(rt, RoutePrefixPrivate, "documents/{documentID}/links", []string{"GET", "OPTIONS"}, nil, endpoint.GetDocumentLinks) + + // Global installation-wide config + Add(rt, RoutePrefixPrivate, "global/smtp", []string{"GET", "OPTIONS"}, nil, endpoint.GetSMTPConfig) + Add(rt, RoutePrefixPrivate, "global/smtp", []string{"PUT", "OPTIONS"}, nil, endpoint.SaveSMTPConfig) + Add(rt, RoutePrefixPrivate, "global/license", []string{"GET", "OPTIONS"}, nil, endpoint.GetLicense) + Add(rt, RoutePrefixPrivate, "global/license", []string{"PUT", "OPTIONS"}, nil, endpoint.SaveLicense) + Add(rt, RoutePrefixPrivate, "global/auth", []string{"GET", "OPTIONS"}, nil, endpoint.GetAuthConfig) + Add(rt, RoutePrefixPrivate, "global/auth", []string{"PUT", "OPTIONS"}, nil, endpoint.SaveAuthConfig) + + // Pinned items + Add(rt, RoutePrefixPrivate, "pin/{userID}", []string{"POST", "OPTIONS"}, nil, endpoint.AddPin) + Add(rt, RoutePrefixPrivate, "pin/{userID}", []string{"GET", "OPTIONS"}, nil, endpoint.GetUserPins) + Add(rt, RoutePrefixPrivate, "pin/{userID}/sequence", []string{"POST", "OPTIONS"}, nil, endpoint.UpdatePinSequence) + Add(rt, RoutePrefixPrivate, "pin/{userID}/{pinID}", []string{"DELETE", "OPTIONS"}, nil, endpoint.DeleteUserPin) + + // Single page app handler + Add(rt, RoutePrefixRoot, "robots.txt", []string{"GET", "OPTIONS"}, nil, endpoint.GetRobots) + Add(rt, RoutePrefixRoot, "sitemap.xml", []string{"GET", "OPTIONS"}, nil, endpoint.GetSitemap) + Add(rt, RoutePrefixRoot, "{rest:.*}", nil, nil, web.EmberHandler) +} diff --git a/server/routing/table.go b/server/routing/table.go new file mode 100644 index 00000000..0327f68e --- /dev/null +++ b/server/routing/table.go @@ -0,0 +1,133 @@ +// Copyright 2016 Documize Inc. . All rights reserved. +// +// This software (Documize Community Edition) is licensed under +// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html +// +// You can operate outside the AGPL restrictions by purchasing +// Documize Enterprise Edition and obtaining a commercial license +// by contacting . +// +// https://documize.com + +package routing + +import ( + "encoding/json" + "net/http" + "sort" + "strings" + + "github.com/documize/community/core/env" + "github.com/gorilla/mux" +) + +const ( + // RoutePrefixPublic used for the unsecured api + RoutePrefixPublic = "/api/public/" + // RoutePrefixPrivate used for secured api (requiring api) + RoutePrefixPrivate = "/api/" + // RoutePrefixRoot used for unsecured endpoints at root (e.g. robots.txt) + RoutePrefixRoot = "/" +) + +type routeDef struct { + Prefix string + Path string + Methods []string + Queries []string +} + +// RouteFunc describes end-point functions +type RouteFunc func(http.ResponseWriter, *http.Request) +type routeMap map[string]RouteFunc + +var routes = make(routeMap) + +func routesKey(rt env.Runtime, prefix, path string, methods, queries []string) (string, error) { + rd := routeDef{ + Prefix: prefix, + Path: path, + Methods: methods, + Queries: queries, + } + b, e := json.Marshal(rd) + + if e != nil { + rt.Log.Error("routesKey failed for "+path, e) + } + + return string(b), e +} + +// Add an endpoint to those that will be processed when Serve() is called. +func Add(rt env.Runtime, prefix, path string, methods, queries []string, endPtFn RouteFunc) error { + k, e := routesKey(rt, prefix, path, methods, queries) + if e != nil { + return e + } + routes[k] = endPtFn + return nil +} + +// Remove an endpoint. +func Remove(rt env.Runtime, prefix, path string, methods, queries []string) error { + k, e := routesKey(rt, prefix, path, methods, queries) + if e != nil { + return e + } + delete(routes, k) + return nil +} + +type routeSortItem struct { + def routeDef + fun RouteFunc + ord int +} + +type routeSorter []routeSortItem + +func (s routeSorter) Len() int { return len(s) } +func (s routeSorter) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s routeSorter) Less(i, j int) bool { + if s[i].def.Prefix == s[j].def.Prefix && s[i].def.Path == s[j].def.Path { + return len(s[i].def.Queries) > len(s[j].def.Queries) + } + return s[i].ord < s[j].ord +} + +// BuildRoutes returns all matching routes for specified scope. +func BuildRoutes(rt env.Runtime, prefix string) *mux.Router { + var rs routeSorter + for k, v := range routes { + var rd routeDef + if err := json.Unmarshal([]byte(k), &rd); err != nil { + rt.Log.Error("buildRoutes json.Unmarshal", err) + } else { + if rd.Prefix == prefix { + order := strings.Index(rd.Path, "{") + if order == -1 { + order = len(rd.Path) + } + order = -order + rs = append(rs, routeSortItem{def: rd, fun: v, ord: order}) + } + } + } + sort.Sort(rs) + router := mux.NewRouter() + + for _, it := range rs { + //fmt.Printf("DEBUG buildRoutes: %d %#v\n", it.ord, it.def) + + x := router.HandleFunc(it.def.Prefix+it.def.Path, it.fun) + if len(it.def.Methods) > 0 { + y := x.Methods(it.def.Methods...) + if len(it.def.Queries) > 0 { + y.Queries(it.def.Queries...) + } + } + } + + return router +} diff --git a/core/api/endpoint/server.go b/server/server.go similarity index 76% rename from core/api/endpoint/server.go rename to server/server.go index 8ef3cfac..de7e691c 100644 --- a/core/api/endpoint/server.go +++ b/server/server.go @@ -9,7 +9,7 @@ // // https://documize.com -package endpoint +package server import ( "fmt" @@ -18,57 +18,60 @@ import ( "strings" "github.com/codegangsta/negroni" - "github.com/documize/api/wordsmith/log" "github.com/documize/community/core/api" + "github.com/documize/community/core/api/endpoint" "github.com/documize/community/core/api/plugins" "github.com/documize/community/core/database" "github.com/documize/community/core/env" - "github.com/documize/community/core/web" + "github.com/documize/community/server/routing" + "github.com/documize/community/server/web" "github.com/gorilla/mux" ) var testHost string // used during automated testing -// Serve the Documize endpoint. -func Serve(rt env.Runtime, ready chan struct{}) { +// Start router to handle all HTTP traffic. +func Start(rt env.Runtime, ready chan struct{}) { + routing.RegisterEndpoints(rt) + err := plugins.LibSetup() if err != nil { rt.Log.Error("Terminating before running - invalid plugin.json", err) os.Exit(1) } - log.Info(fmt.Sprintf("Starting %s version %s", api.Runtime.Product.Title, api.Runtime.Product.Version)) + rt.Log.Info(fmt.Sprintf("Starting %s version %s", api.Runtime.Product.Title, api.Runtime.Product.Version)) switch api.Runtime.Flags.SiteMode { case web.SiteModeOffline: - rt.Log.Info("Serving OFFLINE web app") + rt.Log.Info("Serving OFFLINE web server") case web.SiteModeSetup: - Add(RoutePrefixPrivate, "setup", []string{"POST", "OPTIONS"}, nil, database.Create) - rt.Log.Info("Serving SETUP web app") + routing.Add(rt, routing.RoutePrefixPrivate, "setup", []string{"POST", "OPTIONS"}, nil, database.Create) + rt.Log.Info("Serving SETUP web server") case web.SiteModeBadDB: - rt.Log.Info("Serving BAD DATABASE web app") + rt.Log.Info("Serving BAD DATABASE web server") default: - rt.Log.Info("Starting web app") + rt.Log.Info("Starting web server") } router := mux.NewRouter() // "/api/public/..." - router.PathPrefix(RoutePrefixPublic).Handler(negroni.New( + router.PathPrefix(routing.RoutePrefixPublic).Handler(negroni.New( negroni.HandlerFunc(cors), - negroni.Wrap(buildRoutes(RoutePrefixPublic)), + negroni.Wrap(routing.BuildRoutes(rt, routing.RoutePrefixPublic)), )) // "/api/..." - router.PathPrefix(RoutePrefixPrivate).Handler(negroni.New( - negroni.HandlerFunc(Authorize), - negroni.Wrap(buildRoutes(RoutePrefixPrivate)), + router.PathPrefix(routing.RoutePrefixPrivate).Handler(negroni.New( + negroni.HandlerFunc(endpoint.Authorize), + negroni.Wrap(routing.BuildRoutes(rt, routing.RoutePrefixPrivate)), )) // "/..." - router.PathPrefix(RoutePrefixRoot).Handler(negroni.New( + router.PathPrefix(routing.RoutePrefixRoot).Handler(negroni.New( negroni.HandlerFunc(cors), - negroni.Wrap(buildRoutes(RoutePrefixRoot)), + negroni.Wrap(routing.BuildRoutes(rt, routing.RoutePrefixRoot)), )) n := negroni.New() @@ -76,7 +79,6 @@ func Serve(rt env.Runtime, ready chan struct{}) { n.Use(negroni.HandlerFunc(cors)) n.Use(negroni.HandlerFunc(metrics)) n.UseHandler(router) - ready <- struct{}{} if !api.Runtime.Flags.SSLEnabled() { rt.Log.Info("Starting non-SSL server on " + api.Runtime.Flags.HTTPPort) @@ -132,17 +134,9 @@ func cors(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { func metrics(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { w.Header().Add("X-Documize-Version", api.Runtime.Product.Version) w.Header().Add("Cache-Control", "no-cache") + // Prevent page from being displayed in an iframe w.Header().Add("X-Frame-Options", "DENY") - // Force SSL delivery - // if certFile != "" && keyFile != "" { - // w.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains") - // } - next(w, r) } - -func version(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(api.Runtime.Product.Version)) -} diff --git a/server/web/embed.go b/server/web/embed.go new file mode 100644 index 00000000..1d855605 --- /dev/null +++ b/server/web/embed.go @@ -0,0 +1,48 @@ +// Copyright 2016 Documize Inc. . All rights reserved. +// +// This software (Documize Community Edition) is licensed under +// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html +// +// You can operate outside the AGPL restrictions by purchasing +// Documize Enterprise Edition and obtaining a commercial license +// by contacting . +// +// https://documize.com + +// Package web contains the Documize static web data. +package web + +import ( + "net/http" +) + +// EmbedHandler is defined in each embed directory +type EmbedHandler interface { + Asset(string) ([]byte, error) + AssetDir(string) ([]string, error) + StaticAssetsFileSystem() http.FileSystem +} + +// Embed allows access to the embedded data +var Embed EmbedHandler + +// StaticAssetsFileSystem data encoded in the go:generate above. +func StaticAssetsFileSystem() http.FileSystem { + return Embed.StaticAssetsFileSystem() + //return &assetfs.AssetFS{Asset: Asset, AssetDir: AssetDir, AssetInfo: AssetInfo, Prefix: "bindata/public"} +} + +// ReadFile is intended to substitute for ioutil.ReadFile(). +func ReadFile(filename string) ([]byte, error) { + return Embed.Asset("bindata/" + filename) +} + +// Asset fetch. +func Asset(location string) ([]byte, error) { + return Embed.Asset(location) +} + +// AssetDir returns web app "assets" folder. +func AssetDir(dir string) ([]string, error) { + return Embed.AssetDir(dir) +} diff --git a/core/web/web.go b/server/web/serve.go similarity index 57% rename from core/web/web.go rename to server/web/serve.go index 692c00c5..c72ae4c3 100644 --- a/core/web/web.go +++ b/server/web/serve.go @@ -20,9 +20,6 @@ import ( "github.com/documize/community/core/secrets" ) -// SiteMode defines that the web server should show the system to be in a particular state. -// var SiteMode string - const ( // SiteModeNormal serves app SiteModeNormal = "" @@ -40,21 +37,10 @@ var SiteInfo struct { } func init() { - // env.GetString(&SiteMode, "offline", false, "set to '1' for OFFLINE mode", nil) // no sense overriding this setting from the DB SiteInfo.DBhash = secrets.GenerateRandomPassword() // do this only once } -// EmbedHandler is defined in each embed directory -type EmbedHandler interface { - Asset(string) ([]byte, error) - AssetDir(string) ([]string, error) - StaticAssetsFileSystem() http.FileSystem -} - -// Embed allows access to the embedded data -var Embed EmbedHandler - -// EmberHandler provides the webserver for pages developed using the Ember programming environment. +// EmberHandler serves HTML web pages func EmberHandler(w http.ResponseWriter, r *http.Request) { filename := "index.html" switch api.Runtime.Flags.SiteMode { @@ -70,7 +56,6 @@ func EmberHandler(w http.ResponseWriter, r *http.Request) { data, err := Embed.Asset("bindata/" + filename) if err != nil { - // Asset was not found. http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -81,24 +66,3 @@ func EmberHandler(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusInternalServerError) } } - -// StaticAssetsFileSystem data encoded in the go:generate above. -func StaticAssetsFileSystem() http.FileSystem { - return Embed.StaticAssetsFileSystem() - //return &assetfs.AssetFS{Asset: Asset, AssetDir: AssetDir, AssetInfo: AssetInfo, Prefix: "bindata/public"} -} - -// ReadFile is intended to substitute for ioutil.ReadFile(). -func ReadFile(filename string) ([]byte, error) { - return Embed.Asset("bindata/" + filename) -} - -// Asset fetch. -func Asset(location string) ([]byte, error) { - return Embed.Asset(location) -} - -// AssetDir returns web app "assets" folder. -func AssetDir(dir string) ([]string, error) { - return Embed.AssetDir(dir) -}