diff --git a/token-validator/.gitignore b/token-validator/.gitignore new file mode 100644 index 0000000..b0b676c --- /dev/null +++ b/token-validator/.gitignore @@ -0,0 +1 @@ +token-validator diff --git a/token-validator/db.go b/token-validator/db.go new file mode 100644 index 0000000..161a66e --- /dev/null +++ b/token-validator/db.go @@ -0,0 +1,109 @@ +package main + +import ( + "database/sql" + _ "github.com/go-sql-driver/mysql" + "log" + "os" + "time" +) + +var db *sql.DB + +func DSNGenerator() string { + db_user := "adlin" + db_password := "adlin" + db_host := "" + db_db := "adlin" + + if v, exists := os.LookupEnv("MYSQL_HOST"); exists { + db_host = v + } + if v, exists := os.LookupEnv("MYSQL_PASSWORD"); exists { + db_password = v + } else if v, exists := os.LookupEnv("MYSQL_ROOT_PASSWORD"); exists { + db_user = "root" + db_password = v + } + if v, exists := os.LookupEnv("MYSQL_USER"); exists { + db_user = v + } + if v, exists := os.LookupEnv("MYSQL_DATABASE"); exists { + db_db = v + } + + return db_user + ":" + db_password + "@" + db_host + "/" + db_db +} + +func DBInit(dsn string) (err error) { + if db, err = sql.Open("mysql", dsn+"?parseTime=true&foreign_key_checks=1"); err != nil { + return + } + + _, err = db.Exec(`SET SESSION sql_mode = 'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION';`) + for i := 0; err != nil && i < 5; i += 1 { + if _, err = db.Exec(`SET SESSION sql_mode = 'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION';`); err != nil && i <= 5 { + log.Println("An error occurs when trying to connect to DB, will retry in 2 seconds: ", err) + time.Sleep(2 * time.Second) + } + } + + return +} + +func DBCreate() (err error) { + if _, err = db.Exec(` +CREATE TABLE IF NOT EXISTS students( + id_student INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, + login VARCHAR(255) NOT NULL UNIQUE, + time TIMESTAMP NOT NULL +) DEFAULT CHARACTER SET = utf8 COLLATE = utf8_bin; +`); err != nil { + return + } + if _, err := db.Exec(` +CREATE TABLE IF NOT EXISTS student_login( + id_student INTEGER, + ip VARCHAR(255) NOT NULL, + mac VARCHAR(255) NOT NULL, + time TIMESTAMP NOT NULL, + FOREIGN KEY(id_student) REFERENCES students(id_student) +) DEFAULT CHARACTER SET = utf8 COLLATE = utf8_bin; +`); err != nil { + return err + } + if _, err := db.Exec(` +CREATE TABLE IF NOT EXISTS student_challenges( + id_st INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, + id_student INTEGER NOT NULL, + challenge INTEGER NOT NULL, + time TIMESTAMP NOT NULL, + value VARCHAR(255) NOT NULL, + CONSTRAINT token_found UNIQUE (id_student,challenge), + FOREIGN KEY(id_student) REFERENCES students(id_student) +) DEFAULT CHARACTER SET = utf8 COLLATE = utf8_bin; +`); err != nil { + return err + } + return +} + +func DBClose() error { + return db.Close() +} + +func DBPrepare(query string) (*sql.Stmt, error) { + return db.Prepare(query) +} + +func DBQuery(query string, args ...interface{}) (*sql.Rows, error) { + return db.Query(query, args...) +} + +func DBExec(query string, args ...interface{}) (sql.Result, error) { + return db.Exec(query, args...) +} + +func DBQueryRow(query string, args ...interface{}) *sql.Row { + return db.QueryRow(query, args...) +} diff --git a/token-validator/handler.go b/token-validator/handler.go new file mode 100644 index 0000000..c5c617f --- /dev/null +++ b/token-validator/handler.go @@ -0,0 +1,80 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net/http" + + "github.com/julienschmidt/httprouter" +) + +var router = httprouter.New() + +func Router() *httprouter.Router { + return router +} + +type DispatchFunction func(httprouter.Params, []byte) (interface{}, error) + +func apiHandler(f DispatchFunction) func(http.ResponseWriter, *http.Request, httprouter.Params) { + return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + if addr := r.Header.Get("X-Forwarded-For"); addr != "" { + r.RemoteAddr = addr + } + log.Printf("%s \"%s %s\" [%s]\n", r.RemoteAddr, r.Method, r.URL.Path, r.UserAgent()) + + w.Header().Set("Content-Type", "application/json") + + var ret interface{} + var err error = nil + + // Read the body + if r.ContentLength < 0 || r.ContentLength > 6553600 { + http.Error(w, fmt.Sprintf("{errmsg:\"Request too large or request size unknown\"}", err), http.StatusRequestEntityTooLarge) + return + } + var body []byte + if r.ContentLength > 0 { + tmp := make([]byte, 1024) + for { + n, err := r.Body.Read(tmp) + for j := 0; j < n; j++ { + body = append(body, tmp[j]) + } + if err != nil || n <= 0 { + break + } + } + } + + ret, err = f(ps, body) + + // Format response + resStatus := http.StatusOK + if err != nil { + ret = map[string]string{"errmsg": err.Error()} + resStatus = http.StatusBadRequest + log.Println(r.RemoteAddr, resStatus, err.Error()) + } + + if ret == nil { + ret = map[string]string{"errmsg": "Page not found"} + resStatus = http.StatusNotFound + } + + if str, found := ret.(string); found { + w.WriteHeader(resStatus) + io.WriteString(w, str) + } else if bts, found := ret.([]byte); found { + w.WriteHeader(resStatus) + w.Write(bts) + } else if j, err := json.Marshal(ret); err != nil { + http.Error(w, fmt.Sprintf("{\"errmsg\":%q}", err), http.StatusInternalServerError) + } else { + w.WriteHeader(resStatus) + w.Write(j) + } + } +} diff --git a/token-validator/htdocs/index.html b/token-validator/htdocs/index.html new file mode 100644 index 0000000..e69de29 diff --git a/token-validator/main.go b/token-validator/main.go new file mode 100644 index 0000000..3a8f4a3 --- /dev/null +++ b/token-validator/main.go @@ -0,0 +1,95 @@ +package main + +import ( + "flag" + "log" + "net/http" + "net/url" + "path" + "path/filepath" + "strings" +) + +var sharedSecret string +var StaticDir string + +type ResponseWriterPrefix struct { + real http.ResponseWriter + prefix string +} + +func (r ResponseWriterPrefix) Header() http.Header { + return r.real.Header() +} + +func (r ResponseWriterPrefix) WriteHeader(s int) { + if v, exists := r.real.Header()["Location"]; exists { + r.real.Header().Set("Location", r.prefix+v[0]) + } + r.real.WriteHeader(s) +} + +func (r ResponseWriterPrefix) Write(z []byte) (int, error) { + return r.real.Write(z) +} + +func StripPrefix(prefix string, h http.Handler) http.Handler { + if prefix == "" { + return h + } + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if prefix != "/" && r.URL.Path == "/" { + http.Redirect(w, r, prefix+"/", http.StatusFound) + } else if p := strings.TrimPrefix(r.URL.Path, prefix); len(p) < len(r.URL.Path) { + r2 := new(http.Request) + *r2 = *r + r2.URL = new(url.URL) + *r2.URL = *r.URL + r2.URL.Path = p + h.ServeHTTP(ResponseWriterPrefix{w, prefix}, r2) + } else { + h.ServeHTTP(w, r) + } + }) +} + +func main() { + var bind = flag.String("bind", ":8081", "Bind port/socket") + var dsn = flag.String("dsn", DSNGenerator(), "DSN to connect to the MySQL server") + var baseURL = flag.String("baseurl", "/", "URL prepended to each URL") + flag.StringVar(&StaticDir, "static", "./htdocs/", "Directory containing static files") + flag.StringVar(&sharedSecret, "sharedsecret", "adelina", "secret used to communicate with remote validator") + flag.Parse() + + // Sanitize options + var err error + log.Println("Checking paths...") + if StaticDir, err = filepath.Abs(StaticDir); err != nil { + log.Fatal(err) + } + if *baseURL != "/" { + tmp := path.Clean(*baseURL) + baseURL = &tmp + } else { + tmp := "" + baseURL = &tmp + } + + // Initialize contents + log.Println("Opening database...") + if err := DBInit(*dsn); err != nil { + log.Fatal("Cannot open the database: ", err) + } + defer DBClose() + + log.Println("Creating database...") + if err := DBCreate(); err != nil { + log.Fatal("Cannot create database: ", err) + } + + // Serve content + log.Println("Ready, listening on", *bind) + if err := http.ListenAndServe(*bind, StripPrefix(*baseURL, Router())); err != nil { + log.Fatal("Unable to listen and serve: ", err) + } +} diff --git a/token-validator/static.go b/token-validator/static.go new file mode 100644 index 0000000..65c6e60 --- /dev/null +++ b/token-validator/static.go @@ -0,0 +1,29 @@ +package main + +import ( + "net/http" + "path" + + "github.com/julienschmidt/httprouter" +) + +func init() { + router.GET("/", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + http.ServeFile(w, r, path.Join(StaticDir, "index.html")) + }) + router.GET("/css/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + http.ServeFile(w, r, path.Join(StaticDir, r.URL.Path)) + }) + router.GET("/fonts/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + http.ServeFile(w, r, path.Join(StaticDir, r.URL.Path)) + }) + router.GET("/img/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + http.ServeFile(w, r, path.Join(StaticDir, r.URL.Path)) + }) + router.GET("/js/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + http.ServeFile(w, r, path.Join(StaticDir, r.URL.Path)) + }) + router.GET("/views/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + http.ServeFile(w, r, path.Join(StaticDir, r.URL.Path)) + }) +} diff --git a/token-validator/students.go b/token-validator/students.go new file mode 100644 index 0000000..1cc1cec --- /dev/null +++ b/token-validator/students.go @@ -0,0 +1,99 @@ +package main + +import ( + "encoding/json" + "strings" + "time" + + "github.com/julienschmidt/httprouter" +) + +func init() { + router.GET("/api/students/", apiHandler( + func(httprouter.Params,[]byte) (interface{}, error) { + return getStudents() })) + router.POST("/api/students/", apiHandler(createStudent)) +} + + +type Student struct { + Id int64 `json:"id"` + Login string `json:"login"` + Time time.Time `json:"time"` +} + +func getStudents() (students []Student, err error) { + if rows, errr := DBQuery("SELECT id_student, login, time FROM students"); errr != nil { + return nil, errr + } else { + defer rows.Close() + + for rows.Next() { + var s Student + if err = rows.Scan(&s.Id, &s.Login, &s.Time); err != nil { + return + } + students = append(students, s) + } + if err = rows.Err(); err != nil { + return + } + + return + } +} + +func getStudent(id int64) (s Student, err error) { + err = DBQueryRow("SELECT id_student, login, time FROM students WHERE id_student=?", id).Scan(&s.Id, &s.Login, &s.Time) + return +} + +func NewStudent(login string) (Student, error) { + if res, err := DBExec("INSERT INTO students (login, time) VALUES (?, ?)", login, time.Now()); err != nil { + return Student{}, err + } else if sid, err := res.LastInsertId(); err != nil { + return Student{}, err + } else { + return Student{sid, login, time.Now()}, nil + } +} + +func (s Student) Update() (int64, error) { + if res, err := DBExec("UPDATE students SET login = ?, time = ? WHERE id_student = ?", s.Login, s.Time, s.Id); err != nil { + return 0, err + } else if nb, err := res.RowsAffected(); err != nil { + return 0, err + } else { + return nb, err + } +} + +func (s Student) Delete() (int64, error) { + if res, err := DBExec("DELETE FROM students WHERE id_student = ?", s.Id); err != nil { + return 0, err + } else if nb, err := res.RowsAffected(); err != nil { + return 0, err + } else { + return nb, err + } +} + +func ClearStudents() (int64, error) { + if res, err := DBExec("DELETE FROM students"); err != nil { + return 0, err + } else if nb, err := res.RowsAffected(); err != nil { + return 0, err + } else { + return nb, err + } +} + + +func createStudent(_ httprouter.Params, body []byte) (interface{}, error) { + var std Student + if err := json.Unmarshal(body, &std); err != nil { + return nil, err + } + + return NewStudent(strings.TrimSpace(std.Login)) +}