From 88bf3c3d3a2e03856cdaf6f16c812cbf0560b9ad Mon Sep 17 00:00:00 2001 From: bouncepaw Date: Wed, 11 Nov 2020 22:31:12 +0500 Subject: [PATCH 01/16] Edit README.md --- README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 21b4503..29f25a9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,15 @@ -# šŸ„ MycorrhizaWiki 0.10 +# šŸ„ MycorrhizaWiki 0.11 A wiki engine. +Features planned for this release: +* [ ] Authorization + * [ ] User groups: `anon`, `editor`, `trusted`, `moderator`, `admin` +* [ ] Mycomarkup improvements + * [ ] Strike-through syntax + * [ ] Fix empty line codeblock bug #26 + * [ ] `img{}` improvements + * [ ] ... + ## Building ```sh git clone --recurse-submodules https://github.com/bouncepaw/mycorrhiza @@ -45,5 +54,4 @@ Help is always needed. We have a [tg chat](https://t.me/mycorrhizadev) where som ## Future plans * Tagging system -* Authorization * Better history viewing From 8bc3fb982108acf9df57a59fa2b8b88fdcdb66e6 Mon Sep 17 00:00:00 2001 From: bouncepaw Date: Wed, 11 Nov 2020 22:42:31 +0500 Subject: [PATCH 02/16] Strike-through tag --- markup/lexer_test.go | 2 +- markup/paragraph.go | 8 +++++++- markup/paragraph_test.go | 1 + 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/markup/lexer_test.go b/markup/lexer_test.go index 3298a36..b19bec2 100644 --- a/markup/lexer_test.go +++ b/markup/lexer_test.go @@ -41,7 +41,7 @@ func TestLex(t *testing.T) { `}, {6, "

text

"}, {7, "

more text

"}, - {8, `

some link

`}, + {8, `

some link

`}, {9, ``}, diff --git a/markup/paragraph.go b/markup/paragraph.go index 1eb1f9b..8f90085 100644 --- a/markup/paragraph.go +++ b/markup/paragraph.go @@ -17,6 +17,7 @@ const ( spanSuper spanSub spanMark + spanStrike spanLink ) @@ -78,7 +79,7 @@ func getTextNode(input *bytes.Buffer) string { escaping = false } else if b == '\\' { escaping = true - } else if strings.IndexByte("/*`^,![", b) >= 0 { + } else if strings.IndexByte("/*`^,![~", b) >= 0 { input.UnreadByte() break } else { @@ -127,6 +128,9 @@ func ParagraphToHtml(hyphaName, input string) string { case startsWith("!!"): ret.WriteString(tagFromState(spanMark, tagState, "mark", "!!")) p.Next(2) + case startsWith("~~"): + ret.WriteString(tagFromState(spanMark, tagState, "s", "~~")) + p.Next(2) case startsWith("[["): ret.WriteString(getLinkNode(p, hyphaName)) default: @@ -149,6 +153,8 @@ func ParagraphToHtml(hyphaName, input string) string { ret.WriteString(tagFromState(spanSub, tagState, "sub", ",,")) case spanMark: ret.WriteString(tagFromState(spanMark, tagState, "mark", "!!")) + case spanStrike: + ret.WriteString(tagFromState(spanMark, tagState, "s", "~~")) case spanLink: ret.WriteString(tagFromState(spanLink, tagState, "a", "[[")) } diff --git a/markup/paragraph_test.go b/markup/paragraph_test.go index 6fd1d91..71bd5fc 100644 --- a/markup/paragraph_test.go +++ b/markup/paragraph_test.go @@ -32,6 +32,7 @@ func TestParagraphToHtml(t *testing.T) { {"Embedded //italic//", "Embedded italic"}, {"double //italian// //text//", "double italian text"}, {"it has `mono`", "it has mono"}, + {"it has ~~strike~~", "it has strike"}, {"this is a left **bold", "this is a left bold"}, {"this line has a ,comma, two of them", "this line has a ,comma, two of them"}, {"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."}, From c111e95468743c7032243b1c7e23297ec8ffd21f Mon Sep 17 00:00:00 2001 From: bouncepaw Date: Thu, 12 Nov 2020 23:21:49 +0500 Subject: [PATCH 03/16] Support formatting in headings --- markup/lexer.go | 21 +++++++++------------ metarrhiza | 2 +- mime_test.go | 19 ------------------- 3 files changed, 10 insertions(+), 32 deletions(-) delete mode 100644 mime_test.go diff --git a/markup/lexer.go b/markup/lexer.go index cad18d7..cfa6f96 100644 --- a/markup/lexer.go +++ b/markup/lexer.go @@ -59,6 +59,9 @@ func geminiLineToAST(line string, state *GemLexerState, ast *[]Line) { startsWith := func(token string) bool { return strings.HasPrefix(line, token) } + addHeading := func(i int) { + addLine(fmt.Sprintf("%s", i, state.id, ParagraphToHtml(state.name, line[i+1:]), i)) + } // Beware! Usage of goto. Some may say it is considered evil but in this case it helped to make a better-structured code. switch state.where { @@ -142,23 +145,17 @@ normalState: goto numberState case startsWith("###### "): - addLine(fmt.Sprintf( - "
%s
", state.id, line[7:])) + addHeading(6) case startsWith("##### "): - addLine(fmt.Sprintf( - "
%s
", state.id, line[6:])) + addHeading(5) case startsWith("#### "): - addLine(fmt.Sprintf( - "

%s

", state.id, line[5:])) + addHeading(4) case startsWith("### "): - addLine(fmt.Sprintf( - "

%s

", state.id, line[4:])) + addHeading(3) case startsWith("## "): - addLine(fmt.Sprintf( - "

%s

", state.id, line[3:])) + addHeading(2) case startsWith("# "): - addLine(fmt.Sprintf( - "

%s

", state.id, line[2:])) + addHeading(1) case startsWith(">"): addLine(fmt.Sprintf( diff --git a/metarrhiza b/metarrhiza index d78a907..8d595c9 160000 --- a/metarrhiza +++ b/metarrhiza @@ -1 +1 @@ -Subproject commit d78a90718ae9c36f7a114901ddad84b0e23221b3 +Subproject commit 8d595c930664f271e58d3cfb3fa12c6feabfec3c diff --git a/mime_test.go b/mime_test.go deleted file mode 100644 index 05b2d08..0000000 --- a/mime_test.go +++ /dev/null @@ -1,19 +0,0 @@ -package main - -import ( - "testing" -) - -func TestMimeData(t *testing.T) { - check := func(ext string, expectedIsText bool, expectedMimeId int) { - isText, mimeId := mimeData(ext) - if isText != expectedIsText || mimeId != expectedMimeId { - t.Error(ext, isText, mimeId) - } - } - check(".txt", true, int(TextPlain)) - check(".gmi", true, int(TextGemini)) - check(".bin", false, int(BinaryOctet)) - check(".jpg", false, int(BinaryJpeg)) - check(".bin", false, int(BinaryOctet)) -} From c83ea6f356d78a17353a2737f64b3c2fa4ecc878 Mon Sep 17 00:00:00 2001 From: bouncepaw Date: Fri, 13 Nov 2020 23:45:42 +0500 Subject: [PATCH 04/16] Start implementing fixed authorization system --- README.md | 9 +++- flag.go | 17 ++++++++ mycocredentials.json | 24 +++++++++++ user/user.go | 99 ++++++++++++++++++++++++++++++++++++++++++++ util/util.go | 11 +++-- 5 files changed, 155 insertions(+), 5 deletions(-) create mode 100644 mycocredentials.json create mode 100644 user/user.go diff --git a/README.md b/README.md index 29f25a9..125672e 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,8 @@ Features planned for this release: * [ ] Authorization * [ ] User groups: `anon`, `editor`, `trusted`, `moderator`, `admin` * [ ] Mycomarkup improvements - * [ ] Strike-through syntax + * [x] Strike-through syntax + * [x] Formatting in headings * [ ] Fix empty line codeblock bug #26 * [ ] `img{}` improvements * [ ] ... @@ -25,12 +26,18 @@ make mycorrhiza [OPTIONS...] WIKI_PATH Options: + -auth-method string + What auth method to use. Variants: "none", "fixed" (default "none") + -fixed-credentials-path string + Used when -auth-method=fixed. Path to file with user credentials. (default "mycocredentials.json") -home string The home page (default "home") -port string Port to serve the wiki at (default "1737") -title string How to call your wiki in the navititle (default "šŸ„") + -user-tree string + Hypha which is a superhypha of all user pages (default "u") ``` ## Features diff --git a/flag.go b/flag.go index d5d6df4..8a9857a 100644 --- a/flag.go +++ b/flag.go @@ -5,6 +5,7 @@ import ( "log" "path/filepath" + "github.com/bouncepaw/mycorrhiza/user" "github.com/bouncepaw/mycorrhiza/util" ) @@ -12,6 +13,9 @@ func init() { flag.StringVar(&util.ServerPort, "port", "1737", "Port to serve the wiki at") flag.StringVar(&util.HomePage, "home", "home", "The home page") flag.StringVar(&util.SiteTitle, "title", "šŸ„", "How to call your wiki in the navititle") + flag.StringVar(&util.UserTree, "user-tree", "u", "Hypha which is a superhypha of all user pages") + flag.StringVar(&util.AuthMethod, "auth-method", "none", "What auth method to use. Variants: \"none\", \"fixed\"") + flag.StringVar(&util.FixedCredentialsPath, "fixed-credentials-path", "mycocredentials.json", "Used when -auth-method=fixed. Path to file with user credentials.") } // Do the things related to cli args and die maybe @@ -33,4 +37,17 @@ func parseCliArgs() { if !isCanonicalName(util.HomePage) { log.Fatal("Error: you must use a proper name for the homepage") } + + if !isCanonicalName(util.UserTree) { + log.Fatal("Error: you must use a proper name for user tree") + } + + switch util.AuthMethod { + case "none": + case "fixed": + user.AuthUsed = true + user.PopulateFixedUserStorage() + default: + log.Fatal("Error: unknown auth method:", util.AuthMethod) + } } diff --git a/mycocredentials.json b/mycocredentials.json new file mode 100644 index 0000000..f2ad452 --- /dev/null +++ b/mycocredentials.json @@ -0,0 +1,24 @@ +[ + { + "name": "admin", + "password": "mycorrhiza", + "group": "admin" + }, + { + "name": "weird_fish", + "password": "DeepestOcean", + "group": "moderator" + }, + { + "name": "king_of_limbs", + "password": "ambush", + "group": "trusted" + }, + { + "name": "paranoid_android", + "password": "ok computer", + "group": "editor" + } +] + + diff --git a/user/user.go b/user/user.go new file mode 100644 index 0000000..10fb2cc --- /dev/null +++ b/user/user.go @@ -0,0 +1,99 @@ +package user + +import ( + "encoding/json" + "io/ioutil" + "log" + + "github.com/bouncepaw/mycorrhiza/util" +) + +type FixedUserStorage struct { + Users []*User +} + +var UserStorage = FixedUserStorage{} + +func PopulateFixedUserStorage() { + contents, err := ioutil.ReadFile(util.FixedCredentialsPath) + if err != nil { + log.Fatal(err) + } + err = json.Unmarshal(contents, &UserStorage.Users) + if err != nil { + log.Fatal(err) + } + for _, user := range UserStorage.Users { + user.Group = groupFromString(user.GroupString) + } + log.Println("Found", len(UserStorage.Users), "fixed users") +} + +// AuthUsed shows if a method of authentication is used. You should set it by yourself. +var AuthUsed bool + +// User is a user. +type User struct { + // Name is a username. It must follow hypha naming rules. + Name string `json:"name"` + // Group the user is part of. + Group UserGroup `json:"-"` + GroupString string `json:"group"` + Password string `json:"password"` +} + +func groupFromString(s string) UserGroup { + switch s { + case "admin": + return UserAdmin + case "moderator": + return UserModerator + case "trusted": + return UserTrusted + case "editor": + return UserEditor + default: + log.Fatal("Unknown user group", s) + return UserAnon + } +} + +// UserGroup represents a group that a user is part of. +type UserGroup int + +const ( + // UserAnon is the default user group which all unauthorized visitors have. + UserAnon UserGroup = iota + // UserEditor is a user who can edit and upload stuff. + UserEditor + // UserTrusted is a trusted editor who can also rename stuff. + UserTrusted + // UserModerator is a moderator who can also delete stuff. + UserModerator + // UserAdmin can do everything. + UserAdmin +) + +var minimalRights = map[string]UserGroup{ + "edit": UserEditor, + "upload-binary": UserEditor, + "upload-text": UserEditor, + "rename-ask": UserTrusted, + "rename-confirm": UserTrusted, + "delete-ask": UserModerator, + "delete-confirm": UserModerator, + "reindex": UserAdmin, +} + +func (ug UserGroup) CanAccessRoute(route string) bool { + if !AuthUsed { + return true + } + if minimalRight, ok := minimalRights[route]; ok { + if ug >= minimalRight { + return true + } + return false + } + return true +} diff --git a/util/util.go b/util/util.go index 8458d4f..3da4031 100644 --- a/util/util.go +++ b/util/util.go @@ -6,10 +6,13 @@ import ( ) var ( - ServerPort string - HomePage string - SiteTitle string - WikiDir string + ServerPort string + HomePage string + SiteTitle string + WikiDir string + UserTree string + AuthMethod string + FixedCredentialsPath string ) // ShorterPath is used by handlerList to display shorter path to the files. It simply strips WikiDir. From a0d1099b753213acbd24464219aa9998268a3a09 Mon Sep 17 00:00:00 2001 From: bouncepaw Date: Sat, 14 Nov 2020 15:39:18 +0500 Subject: [PATCH 05/16] Implement login form --- Makefile | 3 + README.md | 4 +- main.go | 28 +++++++ templates/login.qtpl | 45 ++++++++++++ templates/login.qtpl.go | 159 ++++++++++++++++++++++++++++++++++++++++ user/user.go | 69 ++++++++++++++++- util/util.go | 10 +++ 7 files changed, 314 insertions(+), 4 deletions(-) create mode 100644 templates/login.qtpl create mode 100644 templates/login.qtpl.go diff --git a/Makefile b/Makefile index dd9d604..5f2d62e 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,9 @@ run: build ./mycorrhiza metarrhiza +run_with_fixed_auth: build + ./mycorrhiza -auth-method fixed metarrhiza + build: go generate go build . diff --git a/README.md b/README.md index 125672e..14235e4 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,9 @@ A wiki engine. Features planned for this release: * [ ] Authorization - * [ ] User groups: `anon`, `editor`, `trusted`, `moderator`, `admin` + * [x] User groups: `anon`, `editor`, `trusted`, `moderator`, `admin` + * [ ] Login page + * [ ] Rights * [ ] Mycomarkup improvements * [x] Strike-through syntax * [x] Formatting in headings diff --git a/main.go b/main.go index 0f63208..9004c6e 100644 --- a/main.go +++ b/main.go @@ -15,6 +15,7 @@ import ( "github.com/bouncepaw/mycorrhiza/history" "github.com/bouncepaw/mycorrhiza/templates" + "github.com/bouncepaw/mycorrhiza/user" "github.com/bouncepaw/mycorrhiza/util" ) @@ -109,6 +110,31 @@ func handlerStyle(w http.ResponseWriter, rq *http.Request) { } } +func handlerLoginData(w http.ResponseWriter, rq *http.Request) { + log.Println(rq.URL) + var ( + username = CanonicalName(rq.PostFormValue("username")) + password = rq.PostFormValue("password") + err = user.LoginDataHTTP(w, rq, username, password) + ) + if err != "" { + w.Write([]byte(base(err, templates.LoginErrorHTML(err)))) + } else { + http.Redirect(w, rq, "/", http.StatusSeeOther) + } +} + +func handlerLogin(w http.ResponseWriter, rq *http.Request) { + log.Println(rq.URL) + w.Header().Set("Content-Type", "text/html;charset=utf-8") + if user.AuthUsed { + w.WriteHeader(http.StatusOK) + } else { + w.WriteHeader(http.StatusForbidden) + } + w.Write([]byte(base("Login", templates.LoginHTML()))) +} + func main() { log.Println("Running MycorrhizaWiki β") parseCliArgs() @@ -133,6 +159,8 @@ func main() { http.ServeFile(w, rq, WikiDir+"/static/favicon.ico") }) http.HandleFunc("/static/common.css", handlerStyle) + http.HandleFunc("/login", handlerLogin) + http.HandleFunc("/login-data", handlerLoginData) http.HandleFunc("/", func(w http.ResponseWriter, rq *http.Request) { http.Redirect(w, rq, "/page/"+util.HomePage, http.StatusSeeOther) }) diff --git a/templates/login.qtpl b/templates/login.qtpl new file mode 100644 index 0000000..01baf88 --- /dev/null +++ b/templates/login.qtpl @@ -0,0 +1,45 @@ +{% import "github.com/bouncepaw/mycorrhiza/user" %} + +{% func LoginHTML() %} +
+
+ {% if user.AuthUsed %} +

Login

+
+

Use the data you were given by the administrator.

+
+ Username + +
+
+ Password + +
+

By submitting this form you give this wiki a permission to store cookies in your browser. It lets the engine associate your edits with you.

+ + Cancel +
+ {% else %} +

Administrator of this wiki have not configured any authorization method. You can make edits anonymously.

+

← Go home

+ {% endif %} +
+
+{% endfunc %} + +{% func LoginErrorHTML(err string) %} +
+
+ {% switch err %} + {% case "unknown username" %} +

Unknown username.

+ {% case "wrong password" %} +

Wrong password.

+ {% default %} +

{%s err %}

+ {% endswitch %} +

← Try again

+
+
+{% endfunc %} + diff --git a/templates/login.qtpl.go b/templates/login.qtpl.go new file mode 100644 index 0000000..fa23246 --- /dev/null +++ b/templates/login.qtpl.go @@ -0,0 +1,159 @@ +// Code generated by qtc from "login.qtpl". DO NOT EDIT. +// See https://github.com/valyala/quicktemplate for details. + +//line templates/login.qtpl:1 +package templates + +//line templates/login.qtpl:1 +import "github.com/bouncepaw/mycorrhiza/user" + +//line templates/login.qtpl:3 +import ( + qtio422016 "io" + + qt422016 "github.com/valyala/quicktemplate" +) + +//line templates/login.qtpl:3 +var ( + _ = qtio422016.Copy + _ = qt422016.AcquireByteBuffer +) + +//line templates/login.qtpl:3 +func StreamLoginHTML(qw422016 *qt422016.Writer) { +//line templates/login.qtpl:3 + qw422016.N().S(` +
+
+ `) +//line templates/login.qtpl:6 + if user.AuthUsed { +//line templates/login.qtpl:6 + qw422016.N().S(` +

Login

+
+

Use the data you were given by the administrator.

+
+ Username + +
+
+ Password + +
+

By submitting this form you give this wiki a permission to store cookies in your browser. It lets the engine associate your edits with you.

+ + Cancel +
+ `) +//line templates/login.qtpl:22 + } else { +//line templates/login.qtpl:22 + qw422016.N().S(` +

Administrator of this wiki have not configured any authorization method. You can make edits anonymously.

+

← Go home

+ `) +//line templates/login.qtpl:25 + } +//line templates/login.qtpl:25 + qw422016.N().S(` +
+
+`) +//line templates/login.qtpl:28 +} + +//line templates/login.qtpl:28 +func WriteLoginHTML(qq422016 qtio422016.Writer) { +//line templates/login.qtpl:28 + qw422016 := qt422016.AcquireWriter(qq422016) +//line templates/login.qtpl:28 + StreamLoginHTML(qw422016) +//line templates/login.qtpl:28 + qt422016.ReleaseWriter(qw422016) +//line templates/login.qtpl:28 +} + +//line templates/login.qtpl:28 +func LoginHTML() string { +//line templates/login.qtpl:28 + qb422016 := qt422016.AcquireByteBuffer() +//line templates/login.qtpl:28 + WriteLoginHTML(qb422016) +//line templates/login.qtpl:28 + qs422016 := string(qb422016.B) +//line templates/login.qtpl:28 + qt422016.ReleaseByteBuffer(qb422016) +//line templates/login.qtpl:28 + return qs422016 +//line templates/login.qtpl:28 +} + +//line templates/login.qtpl:30 +func StreamLoginErrorHTML(qw422016 *qt422016.Writer, err string) { +//line templates/login.qtpl:30 + qw422016.N().S(` +
+
+ `) +//line templates/login.qtpl:33 + switch err { +//line templates/login.qtpl:34 + case "unknown username": +//line templates/login.qtpl:34 + qw422016.N().S(` +

Unknown username.

+ `) +//line templates/login.qtpl:36 + case "wrong password": +//line templates/login.qtpl:36 + qw422016.N().S(` +

Wrong password.

+ `) +//line templates/login.qtpl:38 + default: +//line templates/login.qtpl:38 + qw422016.N().S(` +

`) +//line templates/login.qtpl:39 + qw422016.E().S(err) +//line templates/login.qtpl:39 + qw422016.N().S(`

+ `) +//line templates/login.qtpl:40 + } +//line templates/login.qtpl:40 + qw422016.N().S(` +

← Try again

+
+
+`) +//line templates/login.qtpl:44 +} + +//line templates/login.qtpl:44 +func WriteLoginErrorHTML(qq422016 qtio422016.Writer, err string) { +//line templates/login.qtpl:44 + qw422016 := qt422016.AcquireWriter(qq422016) +//line templates/login.qtpl:44 + StreamLoginErrorHTML(qw422016, err) +//line templates/login.qtpl:44 + qt422016.ReleaseWriter(qw422016) +//line templates/login.qtpl:44 +} + +//line templates/login.qtpl:44 +func LoginErrorHTML(err string) string { +//line templates/login.qtpl:44 + qb422016 := qt422016.AcquireByteBuffer() +//line templates/login.qtpl:44 + WriteLoginErrorHTML(qb422016, err) +//line templates/login.qtpl:44 + qs422016 := string(qb422016.B) +//line templates/login.qtpl:44 + qt422016.ReleaseByteBuffer(qb422016) +//line templates/login.qtpl:44 + return qs422016 +//line templates/login.qtpl:44 +} diff --git a/user/user.go b/user/user.go index 10fb2cc..cde4eca 100644 --- a/user/user.go +++ b/user/user.go @@ -4,15 +4,68 @@ import ( "encoding/json" "io/ioutil" "log" + "net/http" + "time" "github.com/bouncepaw/mycorrhiza/util" ) -type FixedUserStorage struct { - Users []*User +func LoginDataHTTP(w http.ResponseWriter, rq *http.Request, username, password string) string { + w.Header().Set("Content-Type", "text/html;charset=utf-8") + if !HasUsername(username) { + w.WriteHeader(http.StatusBadRequest) + log.Println("Unknown username", username, "was entered") + return "unknown username" + } + if !CredentialsOK(username, password) { + w.WriteHeader(http.StatusBadRequest) + log.Println("A wrong password was entered for username", username) + return "wrong password" + } + token, err := AddSession(username) + if err != nil { + log.Println(err) + w.WriteHeader(http.StatusBadRequest) + return err.Error() + } + http.SetCookie(w, cookie("token", token, time.Now().Add(14*24*time.Hour))) + return "" } -var UserStorage = FixedUserStorage{} +// AddSession saves a session for `username` and returns a token to use. +func AddSession(username string) (string, error) { + token, err := util.RandomString(16) + if err == nil { + UserStorage.Tokens[token] = username + log.Println("New token for", username, "is", token) + } + return token, err +} + +func HasUsername(username string) bool { + for _, user := range UserStorage.Users { + if user.Name == username { + return true + } + } + return false +} + +func CredentialsOK(username, password string) bool { + for _, user := range UserStorage.Users { + if user.Name == username && user.Password == password { + return true + } + } + return false +} + +type FixedUserStorage struct { + Users []*User + Tokens map[string]string +} + +var UserStorage = FixedUserStorage{Tokens: make(map[string]string)} func PopulateFixedUserStorage() { contents, err := ioutil.ReadFile(util.FixedCredentialsPath) @@ -97,3 +150,13 @@ func (ug UserGroup) CanAccessRoute(route string) bool { } return true } + +// A handy cookie constructor +func cookie(name_suffix, val string, t time.Time) *http.Cookie { + return &http.Cookie{ + Name: "mycorrhiza_" + name_suffix, + Value: val, + Expires: t, + Path: "/", + } +} diff --git a/util/util.go b/util/util.go index 3da4031..745bf96 100644 --- a/util/util.go +++ b/util/util.go @@ -1,6 +1,8 @@ package util import ( + "crypto/rand" + "encoding/hex" "net/http" "strings" ) @@ -44,3 +46,11 @@ func FindSubhyphae(hyphaName string, hyphaIterator func(func(string))) []string }) return subhyphae } + +func RandomString(n int) (string, error) { + bytes := make([]byte, n) + if _, err := rand.Read(bytes); err != nil { + return "", err + } + return hex.EncodeToString(bytes), nil +} From f4ba0f5498f26797b83fa6aeeba19a0edebe299f Mon Sep 17 00:00:00 2001 From: bouncepaw Date: Sat, 14 Nov 2020 18:03:06 +0500 Subject: [PATCH 06/16] Implement logging out --- http_auth.go | 62 ++++++++ main.go | 29 +--- templates/{login.qtpl => auth.qtpl} | 15 ++ templates/auth.qtpl.go | 218 ++++++++++++++++++++++++++++ templates/login.qtpl.go | 159 -------------------- user/user.go | 37 ++++- 6 files changed, 330 insertions(+), 190 deletions(-) create mode 100644 http_auth.go rename templates/{login.qtpl => auth.qtpl} (78%) create mode 100644 templates/auth.qtpl.go delete mode 100644 templates/login.qtpl.go diff --git a/http_auth.go b/http_auth.go new file mode 100644 index 0000000..2e1039b --- /dev/null +++ b/http_auth.go @@ -0,0 +1,62 @@ +package main + +import ( + "log" + "net/http" + + "github.com/bouncepaw/mycorrhiza/templates" + "github.com/bouncepaw/mycorrhiza/user" +) + +func init() { + http.HandleFunc("/login", handlerLogin) + http.HandleFunc("/login-data", handlerLoginData) + http.HandleFunc("/logout", handlerLogout) + http.HandleFunc("/logout-confirm", handlerLogoutConfirm) +} + +func handlerLogout(w http.ResponseWriter, rq *http.Request) { + var ( + u = user.FromRequest(rq) + can = u != nil + ) + w.Header().Set("Content-Type", "text/html;charset=utf-8") + if can { + log.Println("User", u.Name, "tries to log out") + w.WriteHeader(http.StatusOK) + } else { + log.Println("Unknown user tries to log out") + w.WriteHeader(http.StatusForbidden) + } + w.Write([]byte(base("Logout?", templates.LogoutHTML(can)))) +} + +func handlerLogoutConfirm(w http.ResponseWriter, rq *http.Request) { + user.LogoutFromRequest(w, rq) + http.Redirect(w, rq, "/", http.StatusSeeOther) +} + +func handlerLoginData(w http.ResponseWriter, rq *http.Request) { + log.Println(rq.URL) + var ( + username = CanonicalName(rq.PostFormValue("username")) + password = rq.PostFormValue("password") + err = user.LoginDataHTTP(w, rq, username, password) + ) + if err != "" { + w.Write([]byte(base(err, templates.LoginErrorHTML(err)))) + } else { + http.Redirect(w, rq, "/", http.StatusSeeOther) + } +} + +func handlerLogin(w http.ResponseWriter, rq *http.Request) { + log.Println(rq.URL) + w.Header().Set("Content-Type", "text/html;charset=utf-8") + if user.AuthUsed { + w.WriteHeader(http.StatusOK) + } else { + w.WriteHeader(http.StatusForbidden) + } + w.Write([]byte(base("Login", templates.LoginHTML()))) +} diff --git a/main.go b/main.go index 9004c6e..4002cf1 100644 --- a/main.go +++ b/main.go @@ -15,7 +15,6 @@ import ( "github.com/bouncepaw/mycorrhiza/history" "github.com/bouncepaw/mycorrhiza/templates" - "github.com/bouncepaw/mycorrhiza/user" "github.com/bouncepaw/mycorrhiza/util" ) @@ -110,31 +109,6 @@ func handlerStyle(w http.ResponseWriter, rq *http.Request) { } } -func handlerLoginData(w http.ResponseWriter, rq *http.Request) { - log.Println(rq.URL) - var ( - username = CanonicalName(rq.PostFormValue("username")) - password = rq.PostFormValue("password") - err = user.LoginDataHTTP(w, rq, username, password) - ) - if err != "" { - w.Write([]byte(base(err, templates.LoginErrorHTML(err)))) - } else { - http.Redirect(w, rq, "/", http.StatusSeeOther) - } -} - -func handlerLogin(w http.ResponseWriter, rq *http.Request) { - log.Println(rq.URL) - w.Header().Set("Content-Type", "text/html;charset=utf-8") - if user.AuthUsed { - w.WriteHeader(http.StatusOK) - } else { - w.WriteHeader(http.StatusForbidden) - } - w.Write([]byte(base("Login", templates.LoginHTML()))) -} - func main() { log.Println("Running MycorrhizaWiki β") parseCliArgs() @@ -151,6 +125,7 @@ func main() { http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(WikiDir+"/static")))) // See http_readers.go for /page/, /text/, /binary/, /history/. // See http_mutators.go for /upload-binary/, /upload-text/, /edit/, /delete-ask/, /delete-confirm/, /rename-ask/, /rename-confirm/. + // See http_auth.go for /login, /login-data, /logout, /logout-confirm http.HandleFunc("/list", handlerList) http.HandleFunc("/reindex", handlerReindex) http.HandleFunc("/random", handlerRandom) @@ -159,8 +134,6 @@ func main() { http.ServeFile(w, rq, WikiDir+"/static/favicon.ico") }) http.HandleFunc("/static/common.css", handlerStyle) - http.HandleFunc("/login", handlerLogin) - http.HandleFunc("/login-data", handlerLoginData) http.HandleFunc("/", func(w http.ResponseWriter, rq *http.Request) { http.Redirect(w, rq, "/page/"+util.HomePage, http.StatusSeeOther) }) diff --git a/templates/login.qtpl b/templates/auth.qtpl similarity index 78% rename from templates/login.qtpl rename to templates/auth.qtpl index 01baf88..bbb7d25 100644 --- a/templates/login.qtpl +++ b/templates/auth.qtpl @@ -43,3 +43,18 @@ {% endfunc %} +{% func LogoutHTML(can bool) %} +
+
+ {% if can %} +

Log out?

+

Confirm

+

Cancel

+ {% else %} +

You cannot log out because you are not logged in.

+

Login

+

← Home

+ {% endif %} +
+
+{% endfunc %} diff --git a/templates/auth.qtpl.go b/templates/auth.qtpl.go new file mode 100644 index 0000000..27d13fe --- /dev/null +++ b/templates/auth.qtpl.go @@ -0,0 +1,218 @@ +// Code generated by qtc from "auth.qtpl". DO NOT EDIT. +// See https://github.com/valyala/quicktemplate for details. + +//line templates/auth.qtpl:1 +package templates + +//line templates/auth.qtpl:1 +import "github.com/bouncepaw/mycorrhiza/user" + +//line templates/auth.qtpl:3 +import ( + qtio422016 "io" + + qt422016 "github.com/valyala/quicktemplate" +) + +//line templates/auth.qtpl:3 +var ( + _ = qtio422016.Copy + _ = qt422016.AcquireByteBuffer +) + +//line templates/auth.qtpl:3 +func StreamLoginHTML(qw422016 *qt422016.Writer) { +//line templates/auth.qtpl:3 + qw422016.N().S(` +
+
+ `) +//line templates/auth.qtpl:6 + if user.AuthUsed { +//line templates/auth.qtpl:6 + qw422016.N().S(` +

Login

+
+

Use the data you were given by the administrator.

+
+ Username + +
+
+ Password + +
+

By submitting this form you give this wiki a permission to store cookies in your browser. It lets the engine associate your edits with you.

+ + Cancel +
+ `) +//line templates/auth.qtpl:22 + } else { +//line templates/auth.qtpl:22 + qw422016.N().S(` +

Administrator of this wiki have not configured any authorization method. You can make edits anonymously.

+

← Go home

+ `) +//line templates/auth.qtpl:25 + } +//line templates/auth.qtpl:25 + qw422016.N().S(` +
+
+`) +//line templates/auth.qtpl:28 +} + +//line templates/auth.qtpl:28 +func WriteLoginHTML(qq422016 qtio422016.Writer) { +//line templates/auth.qtpl:28 + qw422016 := qt422016.AcquireWriter(qq422016) +//line templates/auth.qtpl:28 + StreamLoginHTML(qw422016) +//line templates/auth.qtpl:28 + qt422016.ReleaseWriter(qw422016) +//line templates/auth.qtpl:28 +} + +//line templates/auth.qtpl:28 +func LoginHTML() string { +//line templates/auth.qtpl:28 + qb422016 := qt422016.AcquireByteBuffer() +//line templates/auth.qtpl:28 + WriteLoginHTML(qb422016) +//line templates/auth.qtpl:28 + qs422016 := string(qb422016.B) +//line templates/auth.qtpl:28 + qt422016.ReleaseByteBuffer(qb422016) +//line templates/auth.qtpl:28 + return qs422016 +//line templates/auth.qtpl:28 +} + +//line templates/auth.qtpl:30 +func StreamLoginErrorHTML(qw422016 *qt422016.Writer, err string) { +//line templates/auth.qtpl:30 + qw422016.N().S(` +
+
+ `) +//line templates/auth.qtpl:33 + switch err { +//line templates/auth.qtpl:34 + case "unknown username": +//line templates/auth.qtpl:34 + qw422016.N().S(` +

Unknown username.

+ `) +//line templates/auth.qtpl:36 + case "wrong password": +//line templates/auth.qtpl:36 + qw422016.N().S(` +

Wrong password.

+ `) +//line templates/auth.qtpl:38 + default: +//line templates/auth.qtpl:38 + qw422016.N().S(` +

`) +//line templates/auth.qtpl:39 + qw422016.E().S(err) +//line templates/auth.qtpl:39 + qw422016.N().S(`

+ `) +//line templates/auth.qtpl:40 + } +//line templates/auth.qtpl:40 + qw422016.N().S(` +

← Try again

+
+
+`) +//line templates/auth.qtpl:44 +} + +//line templates/auth.qtpl:44 +func WriteLoginErrorHTML(qq422016 qtio422016.Writer, err string) { +//line templates/auth.qtpl:44 + qw422016 := qt422016.AcquireWriter(qq422016) +//line templates/auth.qtpl:44 + StreamLoginErrorHTML(qw422016, err) +//line templates/auth.qtpl:44 + qt422016.ReleaseWriter(qw422016) +//line templates/auth.qtpl:44 +} + +//line templates/auth.qtpl:44 +func LoginErrorHTML(err string) string { +//line templates/auth.qtpl:44 + qb422016 := qt422016.AcquireByteBuffer() +//line templates/auth.qtpl:44 + WriteLoginErrorHTML(qb422016, err) +//line templates/auth.qtpl:44 + qs422016 := string(qb422016.B) +//line templates/auth.qtpl:44 + qt422016.ReleaseByteBuffer(qb422016) +//line templates/auth.qtpl:44 + return qs422016 +//line templates/auth.qtpl:44 +} + +//line templates/auth.qtpl:46 +func StreamLogoutHTML(qw422016 *qt422016.Writer, can bool) { +//line templates/auth.qtpl:46 + qw422016.N().S(` +
+
+ `) +//line templates/auth.qtpl:49 + if can { +//line templates/auth.qtpl:49 + qw422016.N().S(` +

Log out?

+

Confirm

+

Cancel

+ `) +//line templates/auth.qtpl:53 + } else { +//line templates/auth.qtpl:53 + qw422016.N().S(` +

You cannot log out because you are not logged in.

+

Login

+

← Home

+ `) +//line templates/auth.qtpl:57 + } +//line templates/auth.qtpl:57 + qw422016.N().S(` +
+
+`) +//line templates/auth.qtpl:60 +} + +//line templates/auth.qtpl:60 +func WriteLogoutHTML(qq422016 qtio422016.Writer, can bool) { +//line templates/auth.qtpl:60 + qw422016 := qt422016.AcquireWriter(qq422016) +//line templates/auth.qtpl:60 + StreamLogoutHTML(qw422016, can) +//line templates/auth.qtpl:60 + qt422016.ReleaseWriter(qw422016) +//line templates/auth.qtpl:60 +} + +//line templates/auth.qtpl:60 +func LogoutHTML(can bool) string { +//line templates/auth.qtpl:60 + qb422016 := qt422016.AcquireByteBuffer() +//line templates/auth.qtpl:60 + WriteLogoutHTML(qb422016, can) +//line templates/auth.qtpl:60 + qs422016 := string(qb422016.B) +//line templates/auth.qtpl:60 + qt422016.ReleaseByteBuffer(qb422016) +//line templates/auth.qtpl:60 + return qs422016 +//line templates/auth.qtpl:60 +} diff --git a/templates/login.qtpl.go b/templates/login.qtpl.go deleted file mode 100644 index fa23246..0000000 --- a/templates/login.qtpl.go +++ /dev/null @@ -1,159 +0,0 @@ -// Code generated by qtc from "login.qtpl". DO NOT EDIT. -// See https://github.com/valyala/quicktemplate for details. - -//line templates/login.qtpl:1 -package templates - -//line templates/login.qtpl:1 -import "github.com/bouncepaw/mycorrhiza/user" - -//line templates/login.qtpl:3 -import ( - qtio422016 "io" - - qt422016 "github.com/valyala/quicktemplate" -) - -//line templates/login.qtpl:3 -var ( - _ = qtio422016.Copy - _ = qt422016.AcquireByteBuffer -) - -//line templates/login.qtpl:3 -func StreamLoginHTML(qw422016 *qt422016.Writer) { -//line templates/login.qtpl:3 - qw422016.N().S(` -
-
- `) -//line templates/login.qtpl:6 - if user.AuthUsed { -//line templates/login.qtpl:6 - qw422016.N().S(` -

Login

-
-

Use the data you were given by the administrator.

-
- Username - -
-
- Password - -
-

By submitting this form you give this wiki a permission to store cookies in your browser. It lets the engine associate your edits with you.

- - Cancel -
- `) -//line templates/login.qtpl:22 - } else { -//line templates/login.qtpl:22 - qw422016.N().S(` -

Administrator of this wiki have not configured any authorization method. You can make edits anonymously.

-

← Go home

- `) -//line templates/login.qtpl:25 - } -//line templates/login.qtpl:25 - qw422016.N().S(` -
-
-`) -//line templates/login.qtpl:28 -} - -//line templates/login.qtpl:28 -func WriteLoginHTML(qq422016 qtio422016.Writer) { -//line templates/login.qtpl:28 - qw422016 := qt422016.AcquireWriter(qq422016) -//line templates/login.qtpl:28 - StreamLoginHTML(qw422016) -//line templates/login.qtpl:28 - qt422016.ReleaseWriter(qw422016) -//line templates/login.qtpl:28 -} - -//line templates/login.qtpl:28 -func LoginHTML() string { -//line templates/login.qtpl:28 - qb422016 := qt422016.AcquireByteBuffer() -//line templates/login.qtpl:28 - WriteLoginHTML(qb422016) -//line templates/login.qtpl:28 - qs422016 := string(qb422016.B) -//line templates/login.qtpl:28 - qt422016.ReleaseByteBuffer(qb422016) -//line templates/login.qtpl:28 - return qs422016 -//line templates/login.qtpl:28 -} - -//line templates/login.qtpl:30 -func StreamLoginErrorHTML(qw422016 *qt422016.Writer, err string) { -//line templates/login.qtpl:30 - qw422016.N().S(` -
-
- `) -//line templates/login.qtpl:33 - switch err { -//line templates/login.qtpl:34 - case "unknown username": -//line templates/login.qtpl:34 - qw422016.N().S(` -

Unknown username.

- `) -//line templates/login.qtpl:36 - case "wrong password": -//line templates/login.qtpl:36 - qw422016.N().S(` -

Wrong password.

- `) -//line templates/login.qtpl:38 - default: -//line templates/login.qtpl:38 - qw422016.N().S(` -

`) -//line templates/login.qtpl:39 - qw422016.E().S(err) -//line templates/login.qtpl:39 - qw422016.N().S(`

- `) -//line templates/login.qtpl:40 - } -//line templates/login.qtpl:40 - qw422016.N().S(` -

← Try again

-
-
-`) -//line templates/login.qtpl:44 -} - -//line templates/login.qtpl:44 -func WriteLoginErrorHTML(qq422016 qtio422016.Writer, err string) { -//line templates/login.qtpl:44 - qw422016 := qt422016.AcquireWriter(qq422016) -//line templates/login.qtpl:44 - StreamLoginErrorHTML(qw422016, err) -//line templates/login.qtpl:44 - qt422016.ReleaseWriter(qw422016) -//line templates/login.qtpl:44 -} - -//line templates/login.qtpl:44 -func LoginErrorHTML(err string) string { -//line templates/login.qtpl:44 - qb422016 := qt422016.AcquireByteBuffer() -//line templates/login.qtpl:44 - WriteLoginErrorHTML(qb422016, err) -//line templates/login.qtpl:44 - qs422016 := string(qb422016.B) -//line templates/login.qtpl:44 - qt422016.ReleaseByteBuffer(qb422016) -//line templates/login.qtpl:44 - return qs422016 -//line templates/login.qtpl:44 -} diff --git a/user/user.go b/user/user.go index cde4eca..b69268d 100644 --- a/user/user.go +++ b/user/user.go @@ -10,6 +10,29 @@ import ( "github.com/bouncepaw/mycorrhiza/util" ) +func LogoutFromRequest(w http.ResponseWriter, rq *http.Request) { + cookieFromUser, err := rq.Cookie("mycorrhiza_token") + if err == nil { + http.SetCookie(w, cookie("token", "", time.Unix(0, 0))) + terminateSession(cookieFromUser.Value) + } +} + +func (us *FixedUserStorage) userByToken(token string) *User { + if user, ok := us.Tokens[token]; ok { + return user + } + return nil +} + +func FromRequest(rq *http.Request) *User { + cookie, err := rq.Cookie("mycorrhiza_token") + if err != nil { + return nil + } + return UserStorage.userByToken(cookie.Value) +} + func LoginDataHTTP(w http.ResponseWriter, rq *http.Request, username, password string) string { w.Header().Set("Content-Type", "text/html;charset=utf-8") if !HasUsername(username) { @@ -36,12 +59,20 @@ func LoginDataHTTP(w http.ResponseWriter, rq *http.Request, username, password s func AddSession(username string) (string, error) { token, err := util.RandomString(16) if err == nil { - UserStorage.Tokens[token] = username + for _, user := range UserStorage.Users { + if user.Name == username { + UserStorage.Tokens[token] = user + } + } log.Println("New token for", username, "is", token) } return token, err } +func terminateSession(token string) { + delete(UserStorage.Tokens, token) +} + func HasUsername(username string) bool { for _, user := range UserStorage.Users { if user.Name == username { @@ -62,10 +93,10 @@ func CredentialsOK(username, password string) bool { type FixedUserStorage struct { Users []*User - Tokens map[string]string + Tokens map[string]*User } -var UserStorage = FixedUserStorage{Tokens: make(map[string]string)} +var UserStorage = FixedUserStorage{Tokens: make(map[string]*User)} func PopulateFixedUserStorage() { contents, err := ioutil.ReadFile(util.FixedCredentialsPath) From 4686b792263baa638fcea001828896ce73f5ddb4 Mon Sep 17 00:00:00 2001 From: bouncepaw Date: Sat, 14 Nov 2020 19:46:04 +0500 Subject: [PATCH 07/16] Save active sessions between launches --- go.mod | 5 ++- go.sum | 8 +++++ user/fs.go | 69 ++++++++++++++++++++++++++++++++++++++++++ user/group.go | 61 +++++++++++++++++++++++++++++++++++++ user/user.go | 84 +++++++-------------------------------------------- 5 files changed, 153 insertions(+), 74 deletions(-) create mode 100644 user/fs.go create mode 100644 user/group.go diff --git a/go.mod b/go.mod index e2ffb7c..995478b 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module github.com/bouncepaw/mycorrhiza go 1.14 -require github.com/valyala/quicktemplate v1.6.3 +require ( + github.com/adrg/xdg v0.2.2 + github.com/valyala/quicktemplate v1.6.3 +) diff --git a/go.sum b/go.sum index a9a43a1..dbbc51b 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,12 @@ +github.com/adrg/xdg v0.2.2 h1:A7ZHKRz5KGOLJX/bg7IPzStryhvCzAE1wX+KWawPiAo= +github.com/adrg/xdg v0.2.2/go.mod h1:7I2hH/IT30IsupOpKZ5ue7/qNi3CoKzD6tL3HwpaRMQ= github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.0/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.16.0/go.mod h1:YOKImeEosDdBPnxc0gy7INqi3m1zK6A+xl6TwOBhHCA= @@ -13,3 +19,5 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/user/fs.go b/user/fs.go new file mode 100644 index 0000000..f47c423 --- /dev/null +++ b/user/fs.go @@ -0,0 +1,69 @@ +package user + +import ( + "encoding/json" + "io/ioutil" + "log" + "os" + + "github.com/adrg/xdg" + "github.com/bouncepaw/mycorrhiza/util" +) + +func PopulateFixedUserStorage() { + contents, err := ioutil.ReadFile(util.FixedCredentialsPath) + if err != nil { + log.Fatal(err) + } + err = json.Unmarshal(contents, &UserStorage.Users) + if err != nil { + log.Fatal(err) + } + for _, user := range UserStorage.Users { + user.Group = groupFromString(user.GroupString) + } + log.Println("Found", len(UserStorage.Users), "fixed users") + + contents, err = ioutil.ReadFile(tokenStoragePath()) + if os.IsNotExist(err) { + return + } + if err != nil { + log.Fatal(err) + } + var tmp map[string]string + err = json.Unmarshal(contents, &tmp) + if err != nil { + log.Fatal(err) + } + for token, username := range tmp { + user := UserStorage.userByName(username) + UserStorage.Tokens[token] = user + } + log.Println("Found", len(tmp), "active sessions") +} + +func dumpTokens() { + tmp := make(map[string]string) + for token, user := range UserStorage.Tokens { + tmp[token] = user.Name + } + blob, err := json.Marshal(tmp) + if err != nil { + log.Println(err) + } else { + ioutil.WriteFile(tokenStoragePath(), blob, 0644) + } +} + +// Return path to tokens.json. +func tokenStoragePath() string { + dir, err := xdg.DataFile("mycorrhiza/tokens.json") + if err != nil { + // Yes, it is unix-only, but function above rarely fails, so this block is probably never reached. + path := "/home/" + os.Getenv("HOME") + "/.local/share/mycorrhiza/tokens.json" + os.MkdirAll(path, 0777) + return path + } + return dir +} diff --git a/user/group.go b/user/group.go new file mode 100644 index 0000000..8ff7f1f --- /dev/null +++ b/user/group.go @@ -0,0 +1,61 @@ +package user + +import ( + "log" +) + +func groupFromString(s string) UserGroup { + switch s { + case "admin": + return UserAdmin + case "moderator": + return UserModerator + case "trusted": + return UserTrusted + case "editor": + return UserEditor + default: + log.Fatal("Unknown user group", s) + return UserAnon + } +} + +// UserGroup represents a group that a user is part of. +type UserGroup int + +const ( + // UserAnon is the default user group which all unauthorized visitors have. + UserAnon UserGroup = iota + // UserEditor is a user who can edit and upload stuff. + UserEditor + // UserTrusted is a trusted editor who can also rename stuff. + UserTrusted + // UserModerator is a moderator who can also delete stuff. + UserModerator + // UserAdmin can do everything. + UserAdmin +) + +var minimalRights = map[string]UserGroup{ + "edit": UserEditor, + "upload-binary": UserEditor, + "upload-text": UserEditor, + "rename-ask": UserTrusted, + "rename-confirm": UserTrusted, + "delete-ask": UserModerator, + "delete-confirm": UserModerator, + "reindex": UserAdmin, +} + +func (ug UserGroup) CanAccessRoute(route string) bool { + if !AuthUsed { + return true + } + if minimalRight, ok := minimalRights[route]; ok { + if ug >= minimalRight { + return true + } + return false + } + return true +} diff --git a/user/user.go b/user/user.go index b69268d..ed7ec42 100644 --- a/user/user.go +++ b/user/user.go @@ -1,8 +1,6 @@ package user import ( - "encoding/json" - "io/ioutil" "log" "net/http" "time" @@ -25,6 +23,15 @@ func (us *FixedUserStorage) userByToken(token string) *User { return nil } +func (us *FixedUserStorage) userByName(username string) *User { + for _, user := range us.Users { + if user.Name == username { + return user + } + } + return nil +} + func FromRequest(rq *http.Request) *User { cookie, err := rq.Cookie("mycorrhiza_token") if err != nil { @@ -62,6 +69,7 @@ func AddSession(username string) (string, error) { for _, user := range UserStorage.Users { if user.Name == username { UserStorage.Tokens[token] = user + go dumpTokens() } } log.Println("New token for", username, "is", token) @@ -71,6 +79,7 @@ func AddSession(username string) (string, error) { func terminateSession(token string) { delete(UserStorage.Tokens, token) + go dumpTokens() } func HasUsername(username string) bool { @@ -98,21 +107,6 @@ type FixedUserStorage struct { var UserStorage = FixedUserStorage{Tokens: make(map[string]*User)} -func PopulateFixedUserStorage() { - contents, err := ioutil.ReadFile(util.FixedCredentialsPath) - if err != nil { - log.Fatal(err) - } - err = json.Unmarshal(contents, &UserStorage.Users) - if err != nil { - log.Fatal(err) - } - for _, user := range UserStorage.Users { - user.Group = groupFromString(user.GroupString) - } - log.Println("Found", len(UserStorage.Users), "fixed users") -} - // AuthUsed shows if a method of authentication is used. You should set it by yourself. var AuthUsed bool @@ -126,62 +120,6 @@ type User struct { Password string `json:"password"` } -func groupFromString(s string) UserGroup { - switch s { - case "admin": - return UserAdmin - case "moderator": - return UserModerator - case "trusted": - return UserTrusted - case "editor": - return UserEditor - default: - log.Fatal("Unknown user group", s) - return UserAnon - } -} - -// UserGroup represents a group that a user is part of. -type UserGroup int - -const ( - // UserAnon is the default user group which all unauthorized visitors have. - UserAnon UserGroup = iota - // UserEditor is a user who can edit and upload stuff. - UserEditor - // UserTrusted is a trusted editor who can also rename stuff. - UserTrusted - // UserModerator is a moderator who can also delete stuff. - UserModerator - // UserAdmin can do everything. - UserAdmin -) - -var minimalRights = map[string]UserGroup{ - "edit": UserEditor, - "upload-binary": UserEditor, - "upload-text": UserEditor, - "rename-ask": UserTrusted, - "rename-confirm": UserTrusted, - "delete-ask": UserModerator, - "delete-confirm": UserModerator, - "reindex": UserAdmin, -} - -func (ug UserGroup) CanAccessRoute(route string) bool { - if !AuthUsed { - return true - } - if minimalRight, ok := minimalRights[route]; ok { - if ug >= minimalRight { - return true - } - return false - } - return true -} - // A handy cookie constructor func cookie(name_suffix, val string, t time.Time) *http.Cookie { return &http.Cookie{ From cfdc7b82aef29d13e642c50a73eb8788e4ed6822 Mon Sep 17 00:00:00 2001 From: bouncepaw Date: Sun, 15 Nov 2020 17:58:13 +0500 Subject: [PATCH 08/16] Do not let mutate pages without rights for doing so --- http_mutators.go | 36 ++++++++++++++++++++++++++++++++++++ main.go | 6 ++++++ user/group.go | 9 +++++++++ 3 files changed, 51 insertions(+) diff --git a/http_mutators.go b/http_mutators.go index beb43e6..8deaaac 100644 --- a/http_mutators.go +++ b/http_mutators.go @@ -6,6 +6,7 @@ import ( "net/http" "github.com/bouncepaw/mycorrhiza/templates" + "github.com/bouncepaw/mycorrhiza/user" "github.com/bouncepaw/mycorrhiza/util" ) @@ -25,6 +26,11 @@ func handlerRenameAsk(w http.ResponseWriter, rq *http.Request) { hyphaName = HyphaNameFromRq(rq, "rename-ask") _, isOld = HyphaStorage[hyphaName] ) + if ok := user.CanProceed(rq, "rename-confirm"); !ok { + HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be a trusted editor to rename pages.") + log.Println("Rejected", rq.URL) + return + } util.HTTP200Page(w, base("Rename "+hyphaName+"?", templates.RenameAskHTML(hyphaName, isOld))) } @@ -37,6 +43,11 @@ func handlerRenameConfirm(w http.ResponseWriter, rq *http.Request) { _, newNameIsUsed = HyphaStorage[newName] recursive bool ) + if ok := user.CanProceed(rq, "rename-confirm"); !ok { + HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be a trusted editor to rename pages.") + log.Println("Rejected", rq.URL) + return + } if rq.PostFormValue("recursive") == "true" { recursive = true } @@ -71,6 +82,11 @@ func handlerDeleteAsk(w http.ResponseWriter, rq *http.Request) { hyphaName = HyphaNameFromRq(rq, "delete-ask") _, isOld = HyphaStorage[hyphaName] ) + if ok := user.CanProceed(rq, "delete-ask"); !ok { + HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be a moderator to delete pages.") + log.Println("Rejected", rq.URL) + return + } util.HTTP200Page(w, base("Delete "+hyphaName+"?", templates.DeleteAskHTML(hyphaName, isOld))) } @@ -81,6 +97,11 @@ func handlerDeleteConfirm(w http.ResponseWriter, rq *http.Request) { hyphaName = HyphaNameFromRq(rq, "delete-confirm") hyphaData, isOld = HyphaStorage[hyphaName] ) + if ok := user.CanProceed(rq, "delete-confirm"); !ok { + HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be a moderator to delete pages.") + log.Println("Rejected", rq.URL) + return + } if isOld { // If deleted successfully if hop := hyphaData.DeleteHypha(hyphaName); len(hop.Errs) == 0 { @@ -108,6 +129,11 @@ func handlerEdit(w http.ResponseWriter, rq *http.Request) { textAreaFill string err error ) + if ok := user.CanProceed(rq, "edit"); !ok { + HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be an editor to edit pages.") + log.Println("Rejected", rq.URL) + return + } if isOld { textAreaFill, err = FetchTextPart(hyphaData) if err != nil { @@ -129,6 +155,11 @@ func handlerUploadText(w http.ResponseWriter, rq *http.Request) { hyphaData, isOld = HyphaStorage[hyphaName] textData = rq.PostFormValue("text") ) + if ok := user.CanProceed(rq, "upload-text"); !ok { + HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be an editor to edit pages.") + log.Println("Rejected", rq.URL) + return + } if !isOld { hyphaData = &HyphaData{} } @@ -147,6 +178,11 @@ func handlerUploadText(w http.ResponseWriter, rq *http.Request) { func handlerUploadBinary(w http.ResponseWriter, rq *http.Request) { log.Println(rq.URL) hyphaName := HyphaNameFromRq(rq, "upload-binary") + if ok := user.CanProceed(rq, "upload-binary"); !ok { + HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be an editor to upload attachments.") + log.Println("Rejected", rq.URL) + return + } rq.ParseMultipartForm(10 << 20) file, handler, err := rq.FormFile("binary") diff --git a/main.go b/main.go index 4002cf1..aa29c7f 100644 --- a/main.go +++ b/main.go @@ -15,6 +15,7 @@ import ( "github.com/bouncepaw/mycorrhiza/history" "github.com/bouncepaw/mycorrhiza/templates" + "github.com/bouncepaw/mycorrhiza/user" "github.com/bouncepaw/mycorrhiza/util" ) @@ -63,6 +64,11 @@ var base = templates.BaseHTML // Reindex all hyphae by checking the wiki storage directory anew. func handlerReindex(w http.ResponseWriter, rq *http.Request) { log.Println(rq.URL) + if ok := user.CanProceed(rq, "reindex"); !ok { + HttpErr(w, http.StatusForbidden, util.HomePage, "Not enough rights", "You must be an admin to reindex hyphae.") + log.Println("Rejected", rq.URL) + return + } HyphaStorage = make(map[string]*HyphaData) log.Println("Wiki storage directory is", WikiDir) log.Println("Start indexing hyphae...") diff --git a/user/group.go b/user/group.go index 8ff7f1f..d06fb29 100644 --- a/user/group.go +++ b/user/group.go @@ -2,6 +2,7 @@ package user import ( "log" + "net/http" ) func groupFromString(s string) UserGroup { @@ -59,3 +60,11 @@ func (ug UserGroup) CanAccessRoute(route string) bool { } return true } + +func CanProceed(rq *http.Request, route string) bool { + ug := UserAnon + if u := FromRequest(rq); u != nil { + ug = u.Group + } + return ug.CanAccessRoute(route) +} From 57751d03f4ab43abbdefd92bff97d2d23f475e90 Mon Sep 17 00:00:00 2001 From: bouncepaw Date: Mon, 16 Nov 2020 20:26:03 +0500 Subject: [PATCH 09/16] Change navigation links depending on who the user is --- http_mutators.go | 6 +- http_readers.go | 5 +- templates/common.qtpl | 27 ++++- templates/common.qtpl.go | 183 ++++++++++++++++++++-------- templates/css.qtpl | 1 + templates/css.qtpl.go | 27 +++-- templates/delete.qtpl | 6 +- templates/delete.qtpl.go | 129 ++++++++++---------- templates/http_mutators.qtpl | 27 +++-- templates/http_mutators.qtpl.go | 106 ++++++++-------- templates/http_readers.qtpl | 17 ++- templates/http_readers.qtpl.go | 208 +++++++++++++++++--------------- templates/rename.qtpl | 5 +- templates/rename.qtpl.go | 125 +++++++++---------- user/user.go | 7 ++ 15 files changed, 517 insertions(+), 362 deletions(-) diff --git a/http_mutators.go b/http_mutators.go index 8deaaac..59f464b 100644 --- a/http_mutators.go +++ b/http_mutators.go @@ -31,7 +31,7 @@ func handlerRenameAsk(w http.ResponseWriter, rq *http.Request) { log.Println("Rejected", rq.URL) return } - util.HTTP200Page(w, base("Rename "+hyphaName+"?", templates.RenameAskHTML(hyphaName, isOld))) + util.HTTP200Page(w, base("Rename "+hyphaName+"?", templates.RenameAskHTML(rq, hyphaName, isOld))) } func handlerRenameConfirm(w http.ResponseWriter, rq *http.Request) { @@ -87,7 +87,7 @@ func handlerDeleteAsk(w http.ResponseWriter, rq *http.Request) { log.Println("Rejected", rq.URL) return } - util.HTTP200Page(w, base("Delete "+hyphaName+"?", templates.DeleteAskHTML(hyphaName, isOld))) + util.HTTP200Page(w, base("Delete "+hyphaName+"?", templates.DeleteAskHTML(rq, hyphaName, isOld))) } // handlerDeleteConfirm deletes a hypha for sure @@ -144,7 +144,7 @@ func handlerEdit(w http.ResponseWriter, rq *http.Request) { } else { warning = `

You are creating a new hypha.

` } - util.HTTP200Page(w, base("Edit "+hyphaName, templates.EditHTML(hyphaName, textAreaFill, warning))) + util.HTTP200Page(w, base("Edit "+hyphaName, templates.EditHTML(rq, hyphaName, textAreaFill, warning))) } // handlerUploadText uploads a new text part for the hypha. diff --git a/http_readers.go b/http_readers.go index 7870d3e..b8887e9 100644 --- a/http_readers.go +++ b/http_readers.go @@ -40,6 +40,7 @@ func handlerRevision(w http.ResponseWriter, rq *http.Request) { contents = markup.ToHtml(hyphaName, textContents) } page := templates.RevisionHTML( + rq, hyphaName, naviTitle(hyphaName), contents, @@ -67,7 +68,7 @@ func handlerHistory(w http.ResponseWriter, rq *http.Request) { log.Println("Found", len(revs), "revisions for", hyphaName) util.HTTP200Page(w, - base(hyphaName, templates.HistoryHTML(hyphaName, tbody))) + base(hyphaName, templates.HistoryHTML(rq, hyphaName, tbody))) } // handlerText serves raw source text of the hypha. @@ -110,7 +111,7 @@ func handlerPage(w http.ResponseWriter, rq *http.Request) { contents = binaryHtmlBlock(hyphaName, data) + contents } } - util.HTTP200Page(w, base(hyphaName, templates.PageHTML(hyphaName, + util.HTTP200Page(w, base(hyphaName, templates.PageHTML(rq, hyphaName, naviTitle(hyphaName), contents, tree.TreeAsHtml(hyphaName, IterateHyphaNamesWith)))) diff --git a/templates/common.qtpl b/templates/common.qtpl index c199fa2..321da84 100644 --- a/templates/common.qtpl +++ b/templates/common.qtpl @@ -1,3 +1,7 @@ +{% import "net/http" %} +{% import "github.com/bouncepaw/mycorrhiza/user" %} +{% import "github.com/bouncepaw/mycorrhiza/util" %} + This is the