diff --git a/.gitignore b/.gitignore index eb48986..f042913 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -.password +*.env +!.env-template +bin/ diff --git a/build.sh b/build.sh deleted file mode 100755 index 12c6d6e..0000000 --- a/build.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/sh - -# full path to the image -app="$1" -if [ -z $app ]; then - app="/srv/main" -fi - -# image name -name="$2" -if [ -z $name ]; then - name="compile_container" -fi - -docker build -t $name -f Dockerfile.comp . -docker container create --name temp $name -docker container cp temp:$app bin -docker container rm temp -docker-compose up --build -d \ No newline at end of file diff --git a/go.mod b/go.mod index 20dda46..e5a2ffd 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module captain go 1.14 -require github.com/go-sql-driver/mysql v1.5.0 +require ( + github.com/go-sql-driver/mysql v1.5.0 + github.com/pkg/errors v0.9.1 // indirect +) diff --git a/go.sum b/go.sum index d314899..d3a490e 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,4 @@ github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/html/404.html b/html/error.html similarity index 50% rename from html/404.html rename to html/error.html index f216209..9711b11 100644 --- a/html/404.html +++ b/html/error.html @@ -2,9 +2,9 @@ - 404 - Page Not Found + {{.StatusCode}} Error - {{.Msg}} -

404 - Page Not Found

+

{{.StatusCode}} Error - {{.Msg}}

diff --git a/html/index.tmpl b/html/index.html similarity index 100% rename from html/index.tmpl rename to html/index.html diff --git a/drop.sql b/sql/drop.sql similarity index 100% rename from drop.sql rename to sql/drop.sql diff --git a/setup.sql b/sql/setup.sql similarity index 100% rename from setup.sql rename to sql/setup.sql diff --git a/src/db.go b/src/db.go index e75b778..1759442 100644 --- a/src/db.go +++ b/src/db.go @@ -2,11 +2,110 @@ package main import ( "database/sql" - _ "github.com/go-sql-driver/mysql" + "fmt" "io/ioutil" + "log" + "os" + "strconv" + "strings" + "time" + + _ "github.com/go-sql-driver/mysql" + "github.com/pkg/errors" ) -func runSQL(db *sql.DB, name string) error { +func connectDB() (*sql.DB, error) { + // Get user credentials + user, exists := os.LookupEnv("MYSQL_USER") + checkExists(exists, "Couldn't find database user") + password, exists := os.LookupEnv("MYSQL_PASSWORD") + checkExists(exists, "Couldn't find database password") + + // Get database params + dbServer, exists := os.LookupEnv("MYSQL_SERVER") + checkExists(exists, "Couldn't find database server") + dbPort, exists := os.LookupEnv("MYSQL_PORT") + checkExists(exists, "Couldn't find database port") + dbName, exists := os.LookupEnv("MYSQL_DATABASE") + checkExists(exists, "Couldn't find database name") + connectionString := fmt.Sprintf( + "%s:%s@tcp(%s:%s)/%s", + user, + password, + dbServer, + dbPort, + dbName, + ) + + // Check how many times to try the db before quitting + attemptsStr, exists := os.LookupEnv("DB_ATTEMPTS") + if !exists { + attemptsStr = "5" + } + attempts, err := strconv.Atoi(attemptsStr) + if err != nil { + attempts = 5 + } + + + timeoutStr, exists := os.LookupEnv("DB_CONNECTION_TIMEOUT") + if !exists { + timeoutStr = "5" + } + timeout, err := strconv.Atoi(timeoutStr) + if err != nil { + timeout = 5 + } + + db, err = sql.Open("mysql", connectionString) + if err != nil { + return db, err + } + for i := 1; i <= attempts; i++ { + //// DEBUG: Making sure table is good + // if err := dropDB(db); err != nil { + // log.Fatal(err) + // } + + // Create database if it doesn't exist + err = createDB(db) + if err != nil { + // Check to see if the error is a connection issue + if !strings.HasPrefix(err.Error(), "dial") { + return db, err + } + if i != attempts { + log.Printf( + "WARNING: Could not connect to db on attempt %d. Trying again in %d seconds.\n", + i, + timeout, + ) + } else { + return db, errors.Errorf("Could not connect to db after %d attempts\n", attempts) + } + time.Sleep(time.Duration(timeout) * time.Second) + } else { + // No error to worry about + break + } + } + log.Println("Connection to db succeeded!") + return db, nil +} + +// func queryRow(db *sql.DB, name string) (*sql.Rows, error) { +// b, err := ioutil.ReadFile(name) +// if err != nil { +// return nil, err +// } +// return db.QueryRow(string(b)), nil +// } + +// func query(db *sql.DB, name string) (interface{}, error) { +// db.Qu +// } + +func execSQL(db *sql.DB, name string) error { b, err := ioutil.ReadFile(name) if err != nil { return err @@ -21,9 +120,9 @@ func runSQL(db *sql.DB, name string) error { } func createDB(db *sql.DB) error { - return runSQL(db, "setup.sql") + return execSQL(db, "sql/setup.sql") } func dropDB(db *sql.DB) error { - return runSQL(db, "drop.sql") + return execSQL(db, "sql/drop.sql") } diff --git a/src/helper.go b/src/helper.go new file mode 100644 index 0000000..3071818 --- /dev/null +++ b/src/helper.go @@ -0,0 +1,19 @@ +package main + +import ( + "log" +) + +// Check errors and fail if they're bad +func check(err error) { + if err != nil { + log.Fatal(err) + } +} + +// Check if a value exists and fail if it doesn't +func checkExists(exists bool, msg string) { + if !exists { + log.Fatal(msg) + } +} diff --git a/src/main.go b/src/main.go new file mode 100644 index 0000000..0cd4256 --- /dev/null +++ b/src/main.go @@ -0,0 +1,39 @@ +package main + +import ( + "net/http" + "log" + "os" +) + + +// func init() { +// // Loads values from .env into the system +// if err := godotenv.Load(); err != nil { +// log.Println("No .env file found") +// } +// } + +func main() { + var err error + db, err = connectDB(); + if err != nil { + log.Fatal(err) + } + defer db.Close() + + // Get listen address and port + addr, exists := os.LookupEnv("LISTEN") + checkExists(exists, "Couldn't find listen address") + port, exists := os.LookupEnv("PORT") + checkExists(exists, "Couldn't find port") + + // Handle hooks + http.HandleFunc("/hook/", hookHandler) + http.HandleFunc("/", index) + http.Handle("/css/", http.StripPrefix("/css/", http.FileServer(http.Dir("html/css")))) + + // Start the server + log.Println("Starting server") + log.Fatal(http.ListenAndServe(addr+":"+port, nil)) +} \ No newline at end of file diff --git a/src/server.go b/src/server.go index 63bd30b..104b202 100644 --- a/src/server.go +++ b/src/server.go @@ -3,41 +3,57 @@ package main import ( "database/sql" "fmt" - "time" - - // "github.com/joho/godotenv" "html/template" "log" "net/http" - "os" - "strconv" - _ "github.com/go-sql-driver/mysql" ) +//Global db object var db *sql.DB -// Check errors and fail if they're bad -func check(err error) { +// Used in writing out errors when they occur +type errorMessage struct { + StatusCode int + Msg string +} + +// Writes the given error message in the template. If not, just writes the message raw +func writeError(w http.ResponseWriter, e errorMessage) { + // Try to use error template + w.WriteHeader(e.StatusCode) + t := template.Must(template.New("error.html").ParseFiles("html/error.html")) + err := t.Execute(w, e) + // If something goes wrong just tell them the error in an ugly way if err != nil { - log.Fatal(err) + // Can't tell the user since the writer already wrote out + log.Println(err) } } // Get all the currently available hooks -func getHooks() []Hook { +func getHooks(w http.ResponseWriter) []Hook { + // Get all hooks from db rows, err := db.Query("SELECT ID FROM HOOKS") if err != nil { - log.Fatal(err) + writeError(w, errorMessage { + StatusCode: http.StatusInternalServerError, + Msg: err.Error(), + }) } defer rows.Close() + // Look through hooks var hooks []Hook for rows.Next() { var hook Hook err = rows.Scan(&hook.Name) + // Hook doesn't have a name (This shouldn't really happen) if err != nil { - log.Fatal(err) + writeError(w, errorMessage { + StatusCode: http.StatusInternalServerError, + Msg: err.Error(), + }) } hooks = append(hooks, hook) } @@ -46,75 +62,122 @@ func getHooks() []Hook { // Show the index func index(w http.ResponseWriter, r *http.Request) { + // Log request to this url log.Println(r.URL.String()) if r.URL.String() == "/" { - t := template.Must(template.New("index.tmpl").ParseFiles("html/index.tmpl")) - for i, j := range getHooks() { - log.Printf("%d: %s\n", i, j.Name) - } - err := t.Execute(w, getHooks()) + // Serve index template + t := template.Must(template.New("index.html").ParseFiles("html/index.html")) + err := t.Execute(w, getHooks(w)) if err != nil { - log.Fatal(err) + writeError(w, errorMessage { + StatusCode: http.StatusInternalServerError, + Msg: err.Error(), + }) } + // Can't find the page } else { - http.ServeFile(w, r, "html/404.html") + writeError(w, errorMessage { + StatusCode: http.StatusNotFound, + Msg: "Not found", + }) } } -//Run a particular hook (will probably come up with a better solution) +// Run a particular hook (will probably come up with a better solution) func runHook(w http.ResponseWriter, r *http.Request, hook Hook) { fmt.Fprintln(w, hook.Name) } -// Handles all of the hooks +// Handles requests to /hook/ func hookHandler(w http.ResponseWriter, r *http.Request) { + // Log request to this url log.Println(r.URL.String()) - name := r.URL.String()[len("/hook/"):] - - row := db.QueryRow("SELECT EXISTS(SELECT id FROM HOOKS WHERE HOOKS.id = ?)", name) - var res int - row.Scan(&res) - if res == 0 { - http.ServeFile(w, r, "html/404.html") + if r.URL.String() == "/hook/" { + switch r.Method { + // Display all hooks + case http.MethodGet: + showHooks(w) + // Method unsupported + default: + log.Println("y tho") + writeError(w, errorMessage { + StatusCode: http.StatusMethodNotAllowed, + Msg: "Method Unsupported", + }) + } return } - hook := Hook{ - Name: name, - params: nil, - actions: nil, + + // Hook name + name := r.URL.String()[len("/hook/"):] + + switch r.Method { + // Handle the hook + case http.MethodGet, http.MethodPost: + // See if the hook exists + row := db.QueryRow("SELECT EXISTS(SELECT id FROM HOOKS WHERE HOOKS.id = ?)", name) + var res int + row.Scan(&res) + // Hook doesn't exist + if res == 0 { + writeError(w, errorMessage { + StatusCode: http.StatusNotFound, + Msg: "Not Found", + }) + return + } + + // Construct the hook from the results + hook := Hook { + Name: name, + params: nil, + actions: nil, + } + + // Run the user's hook + runHook(w, r, hook) + + // Create / Update the hook + case http.MethodPut: + createHook(w, name) + + // Delete the hook + case http.MethodDelete: + deleteHook(w, name) + + // Method unsupported + default: + writeError(w, errorMessage { + StatusCode: http.StatusMethodNotAllowed, + Msg: "Method Not Allowed", + }) } - runHook(w, r, hook) } // Shows all of the hooks -func showHooks(w http.ResponseWriter, r *http.Request) { - log.Println(r.URL.String()) - if r.URL.String() != "/hooks/" { - http.ServeFile(w, r, "html/404.html") - return - } - hooks := getHooks() +func showHooks(w http.ResponseWriter) { + hooks := getHooks(w) allHooks := "" - for _, h := range hooks { - allHooks += h.Name + "\n" + if hooks != nil { + for _, h := range hooks { + allHooks += h.Name + "\n" + } + } else { + allHooks = "Nothing Found\n" } fmt.Fprint(w, allHooks) } // Creates a new hook -func createHook(w http.ResponseWriter, r *http.Request) { - log.Println(r.URL.String()) - // Doesn't actually work yet - if r.URL.String() == "/hook/create/" { - http.ServeFile(w, r, "html/create.html") - return - } - // Create a hook - name := r.URL.String()[len("/hook/create/"):] +func createHook(w http.ResponseWriter, name string) { statement, err := db.Prepare("INSERT IGNORE INTO HOOKS(id) VALUES(?)") + // Shouldn't really happen, but incase the insert goes wrong if err != nil { log.Fatal(err) - fmt.Fprintln(w, err) + writeError(w, errorMessage { + StatusCode: http.StatusInternalServerError, + Msg: err.Error(), + }) return } defer statement.Close() @@ -122,141 +185,48 @@ func createHook(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, name) } -// Delete one hook -func deleteHook(w http.ResponseWriter, r *http.Request) { - log.Println(r.URL.String()) - name := r.URL.String()[len("/hook/delete/"):] +// Delete a hook +func deleteHook(w http.ResponseWriter, name string) { statement, err := db.Prepare("DELETE FROM HOOKS where ID=(?)") + //Shouldn't ever error if err != nil { log.Println(err) - fmt.Fprintln(w, err) + writeError(w, errorMessage { + StatusCode: http.StatusInternalServerError, + Msg: err.Error(), + }) return } defer statement.Close() + + // Get results res, err := statement.Exec(name) + // Shouldn't ever error if err != nil { log.Println(err) - fmt.Fprintln(w, err) + writeError(w, errorMessage { + StatusCode: http.StatusInternalServerError, + Msg: err.Error(), + }) return } + + // Probably also shouldn't error n, err := res.RowsAffected() if err != nil { log.Println(err) fmt.Fprintln(w, err) return } + + // Nothing was deleted if n == 0 { - http.ServeFile(w, r, "html/404.html") + writeError(w, errorMessage { + StatusCode: http.StatusInternalServerError, + Msg: "Hook was not deleted", + }) return } - fmt.Fprintln(w, name) - // log.Println(cap(Hooks)) - // //Clear hooks (this is perfectly safe to do) - // Hooks = nil -} - -// func init() { -// // Loads values from .env into the system -// if err := godotenv.Load(); err != nil { -// log.Println("No .env file found") -// } -// } - -func main() { - // Check if a value exists and fail if it doesn't - checkExists := func(exists bool, msg string) { - if !exists { - log.Fatal(msg) - } - } - - // Get user credentials - user, exists := os.LookupEnv("MYSQL_USER") - checkExists(exists, "Couldn't find database user") - password, exists := os.LookupEnv("MYSQL_PASSWORD") - checkExists(exists, "Couldn't find database password") - - // Get database params - dbServer, exists := os.LookupEnv("MYSQL_SERVER") - checkExists(exists, "Couldn't find database server") - dbPort, exists := os.LookupEnv("MYSQL_PORT") - checkExists(exists, "Couldn't find database port") - dbName, exists := os.LookupEnv("MYSQL_DATABASE") - checkExists(exists, "Couldn't find database name") - connectionString := fmt.Sprintf( - "%s:%s@tcp(%s:%s)/%s", - user, - password, - dbServer, - dbPort, - dbName, - ) - - // Check how many times to try the db before quitting - attemptsStr, exists := os.LookupEnv("DB_ATTEMPTS") - if !exists { - attemptsStr = "5" - } - attempts, err := strconv.Atoi(attemptsStr) - if err != nil { - attempts = 5 - } - - - timeoutStr, exists := os.LookupEnv("DB_CONNECTION_TIMEOUT") - if !exists { - timeoutStr = "5" - } - timeout, err := strconv.Atoi(timeoutStr) - if err != nil { - timeout = 5 - } - - for i := 1; i <= attempts; i++ { - db, err = sql.Open("mysql", connectionString) - if err != nil && i != attempts { - log.Printf( - "WARNING: Could not connect to db on attempt %d. Trying again in %d seconds.\n", - attempts, - timeout, - ) - } else if err != nil { - log.Fatalf("Could not connect to db after %d attempts\n", attempts) - } - time.Sleep(time.Duration(timeout) * time.Second) - } - log.Println("Connection to db succeeded!") - defer db.Close() - - - // Open the db and initialize the table if it doesn't exist - db, err = sql.Open("mysql", connectionString) - - //// DEBUG: Making sure table is good - // if err := dropDB(db); err != nil { - // log.Fatal(err) - // } - - // Create database if it doesn't exist - if err := createDB(db); err != nil { - log.Fatal(err) - } - // Get listen address and port - addr, exists := os.LookupEnv("LISTEN") - checkExists(exists, "Couldn't find listen address") - port, exists := os.LookupEnv("PORT") - checkExists(exists, "Couldn't find port") - - // Handle hooks - http.HandleFunc("/hook/delete/", deleteHook) - http.HandleFunc("/hook/create/", createHook) - http.HandleFunc("/hook/", hookHandler) - http.HandleFunc("/hooks/", showHooks) - http.HandleFunc("/", index) - http.Handle("/css/", http.StripPrefix("/css/", http.FileServer(http.Dir("html/css")))) - - // Start the server - log.Println("Starting server") - log.Fatal(http.ListenAndServe(addr+":"+port, nil)) -} + fmt.Fprintln(w, name) +} \ No newline at end of file