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 +}