From 3ba0b8dc261b493f8ec2ab0b5cc8d5e63e762573 Mon Sep 17 00:00:00 2001 From: Aaron Lindsay Date: Fri, 23 Dec 2016 06:19:10 -0500 Subject: [PATCH] More work --- Makefile | 14 ++++ db.go | 28 +++++++ errors.go | 36 +++++++++ main.go | 84 ++++++++++++++++++++ package.json | 36 +++++++++ sessions.go | 124 +++++++++++++++++++++++++++++ users.go | 216 +++++++++++++++++++++++++++++++++++++++++++++++++++ util.go | 23 ++++++ 8 files changed, 561 insertions(+) create mode 100644 Makefile create mode 100644 db.go create mode 100644 errors.go create mode 100644 main.go create mode 100644 package.json create mode 100644 sessions.go create mode 100644 users.go create mode 100644 util.go diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..550a295 --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +JS_SOURCES = $(wildcard js/*.js) $(wildcard js/*/*.js) + +all: static/bundle.js static/react-widgets + +node_modules: + npm install + +static/bundle.js: $(JS_SOURCES) node_modules + browserify -t [ babelify --presets [ react ] ] js/main.js -o static/bundle.js + +static/react-widgets: node_modules/react-widgets/dist node_modules + rsync -a node_modules/react-widgets/dist/ static/react-widgets/ + +.PHONY = all diff --git a/db.go b/db.go new file mode 100644 index 0000000..178d4ee --- /dev/null +++ b/db.go @@ -0,0 +1,28 @@ +package main + +import ( + "database/sql" + _ "github.com/mattn/go-sqlite3" + "gopkg.in/gorp.v1" + "log" +) + +var DB *gorp.DbMap = initDB() + +func initDB() *gorp.DbMap { + db, err := sql.Open("sqlite3", "file:lunch.sqlite?cache=shared&mode=rwc") + if err != nil { + log.Fatal(err) + } + + dbmap := &gorp.DbMap{Db: db, Dialect: gorp.SqliteDialect{}} + dbmap.AddTableWithName(User{}, "users").SetKeys(true, "UserId") + dbmap.AddTableWithName(Session{}, "sessions").SetKeys(true, "SessionId") + + err = dbmap.CreateTablesIfNotExists() + if err != nil { + log.Fatal(err) + } + + return dbmap +} diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..206404b --- /dev/null +++ b/errors.go @@ -0,0 +1,36 @@ +package main + +import ( + "encoding/json" + "log" + "net/http" +) + +type Error struct { + ErrorId int + ErrorString string +} + +var error_codes = map[int]string{ + 1: "Not Signed In", + 2: "Unauthorized Access", + 3: "Invalid Request", + 4: "User Exists", + // 5: "Connection Failed", //client-side error + 999: "Internal Error", +} + +func WriteError(w http.ResponseWriter, error_code int) { + msg, ok := error_codes[error_code] + if !ok { + log.Printf("Error: WriteError received error code of %d", error_code) + msg = error_codes[999] + } + e := Error{error_code, msg} + + enc := json.NewEncoder(w) + err := enc.Encode(e) + if err != nil { + log.Fatal(err) + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..d494504 --- /dev/null +++ b/main.go @@ -0,0 +1,84 @@ +package main + +//go:generate make + +import ( + "flag" + "github.com/gorilla/context" + "log" + "net" + "net/http" + "net/http/fcgi" + "os" + "path" + "strconv" +) + +var serveFcgi bool +var baseDir string +var tmpDir string +var port int +var smtpServer string +var smtpPort int +var smtpUsername string +var smtpPassword string +var reminderEmail string + +func init() { + flag.StringVar(&baseDir, "base", "./", "Base directory for server") + flag.StringVar(&tmpDir, "tmp", "/tmp", "Directory to create temporary files in") + flag.IntVar(&port, "port", 80, "Port to serve API/files on") + flag.StringVar(&smtpServer, "smtp.server", "smtp.example.com", "SMTP server to send reminder emails from.") + flag.IntVar(&smtpPort, "smtp.port", 587, "SMTP server port to connect to") + flag.StringVar(&smtpUsername, "smtp.username", "moneygo", "SMTP username") + flag.StringVar(&smtpPassword, "smtp.password", "password", "SMTP password") + flag.StringVar(&reminderEmail, "email", "moneygo@example.com", "Email address to send reminder emails as.") + flag.BoolVar(&serveFcgi, "fcgi", false, "Serve via fcgi rather than http.") + flag.Parse() + + static_path := path.Join(baseDir, "static") + + // Ensure base directory is valid + dir_err_str := "The base directory doesn't look like it contains the " + + "'static' directory. Check to make sure you're passing the right " + + "value to the -base argument." + static_dir, err := os.Stat(static_path) + if err != nil { + log.Print(err) + log.Fatal(dir_err_str) + } + if !static_dir.IsDir() { + log.Fatal(dir_err_str) + } + + // Setup the logging flags to be printed + log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) +} + +func rootHandler(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, path.Join(baseDir, "static/index.html")) +} + +func staticHandler(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, path.Join(baseDir, r.URL.Path)) +} + +func main() { + servemux := http.NewServeMux() + servemux.HandleFunc("/", rootHandler) + servemux.HandleFunc("/static/", staticHandler) + servemux.HandleFunc("/session/", SessionHandler) + servemux.HandleFunc("/user/", UserHandler) + + listener, err := net.Listen("tcp", ":"+strconv.Itoa(port)) + if err != nil { + log.Fatal(err) + } + + log.Printf("Serving on port %d out of directory: %s", port, baseDir) + if serveFcgi { + fcgi.Serve(listener, context.ClearHandler(servemux)) + } else { + http.Serve(listener, context.ClearHandler(servemux)) + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..5f84e84 --- /dev/null +++ b/package.json @@ -0,0 +1,36 @@ +{ + "name": "lunch", + "version": "0.0.1", + "description": "A lunch management and tracking web application", + "main": "js/main.js", + "dependencies": { + "babel-preset-react": "^6.16.0", + "babelify": "^7.3.0", + "big.js": "^3.1.3", + "browserify": "^13.1.0", + "cldr-data": "^29.0.2", + "globalize": "^1.1.1", + "keymirror": "^0.1.1", + "react": "^15.3.2", + "react-addons-update": "^15.3.2", + "react-bootstrap": "^0.30.5", + "react-dom": "^15.3.2", + "react-redux": "^4.4.5", + "react-widgets": "^3.4.4", + "redux": "^3.6.0", + "redux-thunk": "^2.1.0" + }, + "devDependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://git.noughth.com/aclindsa/lunch.git" + }, + "author": "Aaron Lindsay", + "bugs": { + "url": "https://git.noughth.com/aclindsa/lunch/issues" + }, + "homepage": "https://git.noughth.com/aclindsa/lunch" +} diff --git a/sessions.go b/sessions.go new file mode 100644 index 0000000..d3e23f8 --- /dev/null +++ b/sessions.go @@ -0,0 +1,124 @@ +package main + +import ( + "encoding/json" + "fmt" + "github.com/gorilla/securecookie" + "github.com/gorilla/sessions" + "log" + "net/http" +) + +var cookie_store = sessions.NewCookieStore(securecookie.GenerateRandomKey(64)) + +type Session struct { + SessionId int64 + SessionSecret string `json:"-"` + UserId int64 +} + +func (s *Session) Write(w http.ResponseWriter) error { + enc := json.NewEncoder(w) + return enc.Encode(s) +} + +func GetSession(r *http.Request) (*Session, error) { + var s Session + + session, _ := cookie_store.Get(r, "moneygo") + _, ok := session.Values["session-secret"] + if !ok { + return nil, fmt.Errorf("session-secret cookie not set") + } + s.SessionSecret = session.Values["session-secret"].(string) + + err := DB.SelectOne(&s, "SELECT * from sessions where SessionSecret=?", s.SessionSecret) + if err != nil { + return nil, err + } + return &s, nil +} + +func DeleteSessionIfExists(r *http.Request) { + session, err := GetSession(r) + if err == nil { + DB.Delete(session) + } +} + +func NewSession(w http.ResponseWriter, r *http.Request, userid int64) (*Session, error) { + s := Session{} + + session, _ := cookie_store.Get(r, "moneygo") + + session.Values["session-secret"] = string(securecookie.GenerateRandomKey(64)) + s.SessionSecret = session.Values["session-secret"].(string) + s.UserId = userid + + err := DB.Insert(&s) + if err != nil { + return nil, err + } + + err = session.Save(r, w) + if err != nil { + return nil, err + } else { + return &s, nil + } +} + +func SessionHandler(w http.ResponseWriter, r *http.Request) { + if r.Method == "POST" || r.Method == "PUT" { + user_json := r.PostFormValue("user") + if user_json == "" { + WriteError(w, 3 /*Invalid Request*/) + return + } + + user := User{} + err := user.Read(user_json) + if err != nil { + WriteError(w, 3 /*Invalid Request*/) + return + } + + dbuser, err := GetUserByUsername(user.Username) + if err != nil { + WriteError(w, 2 /*Unauthorized Access*/) + return + } + + user.HashPassword() + if user.PasswordHash != dbuser.PasswordHash { + WriteError(w, 2 /*Unauthorized Access*/) + return + } + + DeleteSessionIfExists(r) + + session, err := NewSession(w, r, dbuser.UserId) + if err != nil { + WriteError(w, 999 /*Internal Error*/) + return + } + + err = session.Write(w) + if err != nil { + WriteError(w, 999 /*Internal Error*/) + log.Print(err) + return + } + } else if r.Method == "GET" { + s, err := GetSession(r) + if err != nil { + WriteError(w, 1 /*Not Signed In*/) + return + } + + s.Write(w) + } else if r.Method == "DELETE" { + DeleteSessionIfExists(r) + WriteSuccess(w) + } +} diff --git a/users.go b/users.go new file mode 100644 index 0000000..1799db2 --- /dev/null +++ b/users.go @@ -0,0 +1,216 @@ +package main + +import ( + "crypto/sha256" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "strings" +) + +type User struct { + UserId int64 + Name string + Username string + Password string `db:"-"` + PasswordHash string `json:"-"` + Email string +} + +const BogusPassword = "password" + +type UserExistsError struct{} + +func (ueu UserExistsError) Error() string { + return "User exists" +} + +func (u *User) Write(w http.ResponseWriter) error { + enc := json.NewEncoder(w) + return enc.Encode(u) +} + +func (u *User) Read(json_str string) error { + dec := json.NewDecoder(strings.NewReader(json_str)) + return dec.Decode(u) +} + +func (u *User) HashPassword() { + password_hasher := sha256.New() + io.WriteString(password_hasher, u.Password) + u.PasswordHash = fmt.Sprintf("%x", password_hasher.Sum(nil)) + u.Password = "" +} + +func GetUser(userid int64) (*User, error) { + var u User + + err := DB.SelectOne(&u, "SELECT * from users where UserId=?", userid) + if err != nil { + return nil, err + } + return &u, nil +} + +func GetUserByUsername(username string) (*User, error) { + var u User + + err := DB.SelectOne(&u, "SELECT * from users where Username=?", username) + if err != nil { + return nil, err + } + return &u, nil +} + +func InsertUser(u *User) error { + transaction, err := DB.Begin() + if err != nil { + return err + } + + existing, err := transaction.SelectInt("SELECT count(*) from users where Username=?", u.Username) + if err != nil { + transaction.Rollback() + return err + } + if existing > 0 { + transaction.Rollback() + return UserExistsError{} + } + + err = transaction.Insert(u) + if err != nil { + transaction.Rollback() + return err + } + + err = transaction.Commit() + if err != nil { + transaction.Rollback() + return err + } + + return nil +} + +func GetUserFromSession(r *http.Request) (*User, error) { + s, err := GetSession(r) + if err != nil { + return nil, err + } + return GetUser(s.UserId) +} + +func UserHandler(w http.ResponseWriter, r *http.Request) { + if r.Method == "POST" { + user_json := r.PostFormValue("user") + if user_json == "" { + WriteError(w, 3 /*Invalid Request*/) + return + } + + var user User + err := user.Read(user_json) + if err != nil { + WriteError(w, 3 /*Invalid Request*/) + return + } + user.UserId = -1 + user.HashPassword() + + err = InsertUser(&user) + if err != nil { + if _, ok := err.(UserExistsError); ok { + WriteError(w, 4 /*User Exists*/) + } else { + WriteError(w, 999 /*Internal Error*/) + log.Print(err) + } + return + } + + w.WriteHeader(201 /*Created*/) + err = user.Write(w) + if err != nil { + WriteError(w, 999 /*Internal Error*/) + log.Print(err) + return + } + } else { + user, err := GetUserFromSession(r) + if err != nil { + WriteError(w, 1 /*Not Signed In*/) + return + } + + userid, err := GetURLID(r.URL.Path) + if err != nil { + WriteError(w, 3 /*Invalid Request*/) + return + } + + if userid != user.UserId { + WriteError(w, 2 /*Unauthorized Access*/) + return + } + + if r.Method == "GET" { + err = user.Write(w) + if err != nil { + WriteError(w, 999 /*Internal Error*/) + log.Print(err) + return + } + } else if r.Method == "PUT" { + user_json := r.PostFormValue("user") + if user_json == "" { + WriteError(w, 3 /*Invalid Request*/) + return + } + + // Save old PWHash in case the new password is bogus + old_pwhash := user.PasswordHash + + err = user.Read(user_json) + if err != nil || user.UserId != userid { + WriteError(w, 3 /*Invalid Request*/) + return + } + + // If the user didn't create a new password, keep their old one + if user.Password != BogusPassword { + user.HashPassword() + } else { + user.Password = "" + user.PasswordHash = old_pwhash + } + + count, err := DB.Update(user) + if count != 1 || err != nil { + WriteError(w, 999 /*Internal Error*/) + log.Print(err) + return + } + + err = user.Write(w) + if err != nil { + WriteError(w, 999 /*Internal Error*/) + log.Print(err) + return + } + } else if r.Method == "DELETE" { + //TODO delete everything else too? + //TODO how to handle making sure users really meant to delete everything? + count, err := DB.Delete(&user) + if count != 1 || err != nil { + WriteError(w, 999 /*Internal Error*/) + log.Print(err) + return + } + + WriteSuccess(w) + } + } +} diff --git a/util.go b/util.go new file mode 100644 index 0000000..1627920 --- /dev/null +++ b/util.go @@ -0,0 +1,23 @@ +package main + +import ( + "fmt" + "net/http" + "strconv" + "strings" +) + +func GetURLID(url string) (int64, error) { + pieces := strings.Split(strings.Trim(url, "/"), "/") + return strconv.ParseInt(pieces[len(pieces)-1], 10, 0) +} + +func GetURLPieces(url string, format string, a ...interface{}) (int, error) { + url = strings.Replace(url, "/", " ", -1) + format = strings.Replace(format, "/", " ", -1) + return fmt.Sscanf(url, format, a...) +} + +func WriteSuccess(w http.ResponseWriter) { + fmt.Fprint(w, "{}") +}