diff --git a/.env-template b/.env-template new file mode 100644 index 0000000..11bdf2e --- /dev/null +++ b/.env-template @@ -0,0 +1,16 @@ +# Copy this file to .env and put in the correct values + +# MySQL info +MYSQL_ROOT_PASSWORD= +MYSQL_USER= +MYSQL_PASSWORD= + +# DB info for Captain +DB= +DB_PORT= + +# Web server info +LISTEN= +PORT= +DB_ATTEMPTS= +DB_CONNECTION_TIMEOUT= \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..d2a6c12 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.pythonPath": "/usr/bin/python" +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4f392d4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM golang:latest +LABEL maintainer="Ryan Stewart " +# USER www-data +WORKDIR /srv/website +EXPOSE 5000 +COPY .env *sql go.mod go.sum ./ +RUN go mod download +COPY src/* src/ +RUN go build -o main src/* +CMD ./main \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..12c6d6e --- /dev/null +++ b/build.sh @@ -0,0 +1,19 @@ +#!/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/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..6a4a240 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,23 @@ +# Use root/example as user/password credentials +version: '3.7' + +services: + db: + container_name: captain_db + image: mariadb + restart: always + env_file: .env + ports: + - "3306:3306" + + captain: + container_name: captain_hook + build: . + restart: unless-stopped + env_file: .env + ports: + - "5000:5000" + depends_on: + - "db" + volumes: + - ./html:/srv/website/html \ No newline at end of file diff --git a/drop.sql b/drop.sql new file mode 100644 index 0000000..e3aaa7a --- /dev/null +++ b/drop.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS HOOKS; \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..20dda46 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module captain + +go 1.14 + +require github.com/go-sql-driver/mysql v1.5.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d314899 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +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= diff --git a/html/css/index.css b/html/css/index.css new file mode 100644 index 0000000..50231ae --- /dev/null +++ b/html/css/index.css @@ -0,0 +1,16 @@ +/* Style page content */ +.main { + margin-left: 160px; /* Same as the width of the sidebar */ + padding: 0px 20px; +} + +.main h2 { + margin-top: 5vh; + margin-bottom: 2vh; +} + +/* Debug */ +.center { + margin-left: 160px; /* Same as the width of the sidebar */ + position: absolute center; +} \ No newline at end of file diff --git a/html/css/sidenav.css b/html/css/sidenav.css new file mode 100644 index 0000000..cc9fa27 --- /dev/null +++ b/html/css/sidenav.css @@ -0,0 +1,32 @@ + /* The sidebar menu */ +.sidenav { + height: 100%; /* Full-height: remove this if you want "auto" height */ + width: 160px; /* Set the width of the sidebar */ + position: fixed; /* Fixed Sidebar (stay in place on scroll) */ + z-index: 1; /* Stay on top */ + top: 0; /* Stay at the top */ + left: 0; + background-color: #111; /* Black */ + overflow-x: hidden; /* Disable horizontal scroll */ + padding-top: 20px; +} + +/* The navigation menu links */ +.sidenav a { + padding: 6px 8px 6px 16px; + text-decoration: none; + font-size: 25px; + color: #818181; + display: block; +} + +/* When you mouse over the navigation links, change their color */ +.sidenav a:hover { + color: #f1f1f1; +} + +/* On smaller screens, where height is less than 450px, change the style of the sidebar (less padding and a smaller font size) */ +@media screen and (max-height: 450px) { + .sidenav {padding-top: 15px;} + .sidenav a {font-size: 18px;} +} diff --git a/html/index.html b/html/index.html deleted file mode 100644 index cf19783..0000000 --- a/html/index.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - -

Test Web Hook Server

- - diff --git a/html/index.tmpl b/html/index.tmpl new file mode 100644 index 0000000..77d99c8 --- /dev/null +++ b/html/index.tmpl @@ -0,0 +1,44 @@ + + + + + + + + + + + + +
+ Create + Delete +
+ +
+
+ + {{if . -}} +

Currently available hooks

+ + {{- else}} +

No hooks have been created

+ {{- end}} +
+
Testing
+ + + + + + + diff --git a/setup.sql b/setup.sql new file mode 100644 index 0000000..1c820ae --- /dev/null +++ b/setup.sql @@ -0,0 +1,4 @@ +CREATE TABLE IF NOT EXISTS HOOKS +( + ID varchar(64) PRIMARY KEY +); \ No newline at end of file diff --git a/src/db.go b/src/db.go new file mode 100644 index 0000000..e75b778 --- /dev/null +++ b/src/db.go @@ -0,0 +1,29 @@ +package main + +import ( + "database/sql" + _ "github.com/go-sql-driver/mysql" + "io/ioutil" +) + +func runSQL(db *sql.DB, name string) error { + b, err := ioutil.ReadFile(name) + if err != nil { + return err + } + statement, err := db.Prepare(string(b)) + if err != nil { + return err + } + statement.Exec() + + return nil +} + +func createDB(db *sql.DB) error { + return runSQL(db, "setup.sql") +} + +func dropDB(db *sql.DB) error { + return runSQL(db, "drop.sql") +} diff --git a/src/hook.go b/src/hook.go index 36a912d..424dcc1 100644 --- a/src/hook.go +++ b/src/hook.go @@ -6,6 +6,7 @@ import ( "net/http" ) + type HookHandler interface { ServeHTTP(http.ResponseWriter, *http.Request) } diff --git a/src/server.go b/src/server.go index 583e58a..63bd30b 100644 --- a/src/server.go +++ b/src/server.go @@ -2,24 +2,62 @@ package main import ( "database/sql" - "fmt" - "io/ioutil" + "fmt" + "time" + + // "github.com/joho/godotenv" + "html/template" "log" "net/http" - // "sort" - "strings" - _ "github.com/go-sql-driver/mysql" + "os" + "strconv" + + _ "github.com/go-sql-driver/mysql" ) var db *sql.DB -//Show the index +// Check errors and fail if they're bad +func check(err error) { + if err != nil { + log.Fatal(err) + } +} + +// Get all the currently available hooks +func getHooks() []Hook { + rows, err := db.Query("SELECT ID FROM HOOKS") + if err != nil { + log.Fatal(err) + } + defer rows.Close() + + var hooks []Hook + for rows.Next() { + var hook Hook + err = rows.Scan(&hook.Name) + if err != nil { + log.Fatal(err) + } + hooks = append(hooks, hook) + } + return hooks +} + +// Show the index func index(w http.ResponseWriter, r *http.Request) { log.Println(r.URL.String()) if r.URL.String() == "/" { - http.ServeFile(w, r, "../html/index.html") + 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()) + if err != nil { + log.Fatal(err) + } } else { - http.ServeFile(w, r, "../html/404.html") + http.ServeFile(w, r, "html/404.html") } } @@ -28,73 +66,63 @@ func runHook(w http.ResponseWriter, r *http.Request, hook Hook) { fmt.Fprintln(w, hook.Name) } -//Handles all of the hooks +// Handles all of the hooks func hookHandler(w http.ResponseWriter, r *http.Request) { log.Println(r.URL.String()) name := r.URL.String()[len("/hook/"):] - row := db.QueryRow("SELECT EXISTS(SELECT id FROM HOOKS WHERE HOOKS.id = ?)", name) + 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") + http.ServeFile(w, r, "html/404.html") return } - hook := Hook{ - Name: name, - params: nil, - actions: nil, - } + hook := Hook{ + Name: name, + params: nil, + actions: nil, + } runHook(w, r, hook) } -//Shows all of the hooks +// 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") + http.ServeFile(w, r, "html/404.html") return } - - rows, err := db.Query("SELECT ID FROM HOOKS") - if err != nil { - log.Fatal(err) - } - defer rows.Close() - hookNames := "" - var name string - for rows.Next() { - err = rows.Scan(&name) - if err != nil { - log.Fatal(err) - } - hookNames += name + "\n" - } - fmt.Fprint(w, hookNames) + hooks := getHooks() + allHooks := "" + for _, h := range hooks { + allHooks += h.Name + "\n" + } + fmt.Fprint(w, allHooks) } -//Creates a new hook +// Creates a new hook func createHook(w http.ResponseWriter, r *http.Request) { log.Println(r.URL.String()) - //Doesn't actually work yet + // Doesn't actually work yet if r.URL.String() == "/hook/create/" { - http.ServeFile(w, r, "../html/create.html") - } else { - //Create a hook - name := r.URL.String()[len("/hook/create/"):] - statement, err := db.Prepare("INSERT IGNORE INTO HOOKS(id) VALUES(?)") - if err != nil { - log.Fatal(err) - fmt.Fprintln(w, err) - return - } - defer statement.Close() - statement.Exec(name) - fmt.Fprintln(w, name) + http.ServeFile(w, r, "html/create.html") + return } + // Create a hook + name := r.URL.String()[len("/hook/create/"):] + statement, err := db.Prepare("INSERT IGNORE INTO HOOKS(id) VALUES(?)") + if err != nil { + log.Fatal(err) + fmt.Fprintln(w, err) + return + } + defer statement.Close() + statement.Exec(name) + fmt.Fprintln(w, name) } -//Deletes all hooks +// Delete one hook func deleteHook(w http.ResponseWriter, r *http.Request) { log.Println(r.URL.String()) name := r.URL.String()[len("/hook/delete/"):] @@ -118,7 +146,7 @@ func deleteHook(w http.ResponseWriter, r *http.Request) { return } if n == 0 { - http.ServeFile(w, r, "../html/404.html") + http.ServeFile(w, r, "html/404.html") return } fmt.Fprintln(w, name) @@ -127,45 +155,108 @@ func deleteHook(w http.ResponseWriter, r *http.Request) { // 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() { - //Get user credentials - password, err := ioutil.ReadFile(".password") - if err != nil { - log.Fatal(err) - } - connectString := strings.TrimSpace(string(password)) - - //Open the db and initialize the table if it doesn't exist - db, err = sql.Open("mysql", connectString + "@tcp(localhost:3306)/captain") - if err != nil { - log.Fatal(err) - } - defer db.Close() - - var statement *sql.Stmt - //// DEBUG: Making sure table is good - // statement, err = db.Prepare("DROP TABLE IF EXISTS HOOKS") - // if err != nil { - // log.Fatal(err) - // } - // statement.Exec() - - statement, err = db.Prepare(`CREATE TABLE IF NOT EXISTS HOOKS( - ID varchar(64) PRIMARY KEY - )`) - if err != nil { - log.Fatal(err) - } - statement.Exec() - - //Handle hooks + // 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 + // Start the server log.Println("Starting server") - log.Fatal(http.ListenAndServe("127.0.0.1:8000", nil)) + log.Fatal(http.ListenAndServe(addr+":"+port, nil)) }