Browse Source

Output and handle HTTP errors.

Frederic G. MARAND 9 years ago
parent
commit
5ad81e3612
4 changed files with 362 additions and 0 deletions
  1. 250 0
      ch3/ex6/ex6api_error.go
  2. 20 0
      ch3/ex6/ex6api_error_test.go
  3. 60 0
      ch3/ex6front.html
  4. 32 0
      ch3/ex6script.js

+ 250 - 0
ch3/ex6/ex6api_error.go

@@ -0,0 +1,250 @@
+/*
+Available routes:
+
+/api/users			OPTIONS	Expose available actions
+/api/users			GET			Return user list, possibly filtered
+/api/users			POST		Create user
+/api/users/123	GET			Return user 123 -> not listed in book
+/api/users/123	PUT			Update user 123
+/api/users/123	DELETE	Delete user 123
+
+POST fields:
+	"user" -> name
+	"mail" -> email
+	"first"
+	"last"
+*/
+
+package main
+
+import (
+	"database/sql"
+	"encoding/json"
+	"fmt"
+	"log"
+	"net/http"
+
+	_ "github.com/go-sql-driver/mysql"
+
+	"strconv"
+	"strings"
+
+	"github.com/gorilla/mux"
+)
+
+var database *sql.DB
+
+type Users struct {
+	Users []User `json:"users"`
+}
+
+type User struct {
+	ID    int    `json:"id"`
+	Name  string `json:"username"`
+	Email string `json:"mail"`
+	First string `json:"first"`
+	Last  string `json:"last"`
+}
+
+type CreateResponse struct {
+	Error     string `json:"error"`
+	ErrorCode int    `json:"code"`
+}
+
+// MySQL error message look like Error nn: description
+func dbParseEror(raw_error string) (string, int64) {
+	Parts := strings.Split(raw_error, ": ")
+	errorMessage := Parts[1] // After the ":"
+	Code := strings.Split(Parts[0], "Error ")
+	errorCode, _ := strconv.ParseInt(Code[1], 10, 32)
+	return errorMessage, errorCode
+}
+
+type ErrMsg struct {
+	ErrCode    int
+	StatusCode int
+	Message    string
+}
+
+func ErrorMessages(code int64) ErrMsg {
+	var em ErrMsg = ErrMsg{}
+
+	errorMessage := ""
+	statusCode := 200
+	errorCode := 0
+
+	switch code {
+	case 1062:
+		errorMessage = "Duplicate entry"
+		statusCode = 409
+		errorCode = 10
+	}
+
+	em.ErrCode = errorCode
+	em.Message = errorMessage
+	em.StatusCode = statusCode
+
+	return em
+}
+
+/*
+UserCreate creates a new user and stores it.
+
+The SQL query is vulnerable to injection, so the route matching needs be safe.
+*/
+func UserCreate(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("Access-Control-Allow-Origin", "http://localhost:8000")
+
+	NewUser := User{
+		Name:  r.FormValue("user"),
+		Email: r.FormValue("mail"),
+		First: r.FormValue("first"),
+		Last:  r.FormValue("last"),
+	}
+
+	output, err := json.MarshalIndent(NewUser, "", "  ")
+	fmt.Println(string(output))
+	if err != nil {
+		fmt.Println("Something went wrong!")
+	}
+
+	Response := CreateResponse{}
+	sql := "INSERT INTO users SET user_name='" + NewUser.Name +
+		"', user_first='" + NewUser.First +
+		"', user_last='" + NewUser.Last +
+		"', user_email='" + NewUser.Email + "'"
+	result, err := database.Exec(sql)
+	if err != nil {
+		errorMessage, errorCode := dbParseEror(err.Error())
+		em := ErrorMessages(errorCode)
+		fmt.Println(errorMessage)
+
+		Response.Error = em.Message
+		Response.ErrorCode = em.ErrCode
+		w.WriteHeader(em.StatusCode)
+	}
+
+	var id int64
+	var rows int64
+	if result != nil {
+		id, _ = result.LastInsertId()
+		rows, _ = result.RowsAffected()
+	} else {
+		id = 0
+		rows = 0
+	}
+	fmt.Printf("Id: %d, Rows affected: %d\n", id, rows)
+
+	createOutput, err := json.Marshal(Response)
+	fmt.Fprintln(w, string(createOutput))
+}
+
+func UsersRetrieve(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("Pragma", "no-cache")
+	rows, err := database.Query(`
+SELECT  u.user_id, u.user_name, u.user_email, u.user_first, u.user_last
+FROM users u
+LIMIT 10
+	`)
+	if err != nil {
+		fmt.Fprintln(w, "Something went wrong!")
+		log.Fatal(err)
+	}
+	Response := Users{}
+	for rows.Next() {
+		user := User{}
+		rows.Scan(&user.ID, &user.Name, &user.Email, &user.First, &user.Last)
+		Response.Users = append(Response.Users, user)
+	}
+
+	output, _ := json.MarshalIndent(Response, "", "  ")
+	fmt.Fprintln(w, string(output))
+}
+
+type API struct {
+	Message string `json:"message"`
+}
+
+func UserMethods(w http.ResponseWriter, r *http.Request) {
+
+}
+
+func UserReplace(w http.ResponseWriter, r *http.Request) {
+
+}
+
+func UserDelete(w http.ResponseWriter, r *http.Request) {
+
+}
+
+func UserRetrieve(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("Pragma", "no-cache")
+
+	vars := mux.Vars(r)
+	id := vars["id"]
+	user := User{}
+	var result API
+
+	sqlQuery := `
+SELECT u.user_id, u.user_name, u.user_first, u.user_last, u.user_email
+FROM users u
+WHERE u.user_id = ?
+`
+	stmt, err := database.Prepare(sqlQuery)
+	if err != nil {
+		log.Fatal(err.Error())
+	}
+
+	row := stmt.QueryRow(id)
+	scanErr := row.Scan(&user.ID, &user.Name, &user.First, &user.Last, &user.Email)
+	switch {
+	case scanErr == sql.ErrNoRows:
+		// FIXME XSS
+		result = API{Message: fmt.Sprintf("No such user: %s", id)}
+		json, err := json.MarshalIndent(result, "", "  ")
+		if err != nil {
+			log.Fatal(err.Error())
+		}
+		w.Write(json)
+	case err != nil:
+		// FIXME XSS
+		result = API{Message: fmt.Sprintf("Error reading user: %s", id)}
+		json, errIndent := json.MarshalIndent(result, "", "  ")
+		if errIndent != nil {
+			log.Fatal(errIndent.Error())
+		}
+		w.Write(json)
+		log.Fatal(err.Error())
+	case err == nil:
+		json, errIndent := json.MarshalIndent(user, "", "  ")
+		if errIndent != nil {
+			log.Fatal(errIndent.Error())
+		}
+		w.Write(json)
+	}
+}
+
+func main() {
+	var err error
+
+	db, err := sql.Open("mysql", "goroot:gopass@/goweb_social_network")
+	if err != nil {
+		log.Fatal(err.Error())
+	}
+
+	database = db
+
+	routes := mux.NewRouter()
+	routes.HandleFunc("/api/users", UserCreate).Methods("POST")
+	routes.HandleFunc(`/api/users`, UsersRetrieve).Methods("GET")
+
+	// Not yet implemented.
+	routes.HandleFunc(`/api/users`, UserMethods).Methods("OPTIONS")
+	routes.HandleFunc(`/api/users/{id:[\d]+}`, UserRetrieve).Methods("GET")
+	routes.HandleFunc(`/api/users/{id:[\d]+}`, UserReplace).Methods("PUT")
+	routes.HandleFunc(`/api/users/{id:[\d]+}`, UserDelete).Methods("DELETE")
+	// --------------------
+
+	http.Handle("/", routes)
+	http.ListenAndServe(":8080", nil)
+}

+ 20 - 0
ch3/ex6/ex6api_error_test.go

@@ -0,0 +1,20 @@
+package main
+
+import (
+	"fmt"
+	"testing"
+)
+
+func TestDbParseString(t *testing.T) {
+	expected_message := "duplicate entry"
+	expected_code := int64(1062)
+
+	error_string := fmt.Sprintf("Error %d: %s", expected_code, expected_message)
+	actual_message, actual_code := dbParseEror(error_string)
+	if actual_message != expected_message {
+		t.Errorf("Message: expected [%s], got [%s].\n", expected_message, actual_message)
+	}
+	if actual_code != expected_code {
+		t.Errorf("Code: expected %d, got %d.\n", expected_code, actual_code)
+	}
+}

+ 60 - 0
ch3/ex6front.html

@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>API interface</title>
+    <!-- Online versions -->
+    <!--<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>-->
+    <!--<link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css//bootstrap.min.css" />-->
+    <!--<script src="http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>-->
+
+    <!-- Local versions -->
+    <script src="jquery.min.js"></script>
+    <link rel="stylesheet"href="bootstrap.css" />
+    <script src="bootstrap.js"></script>
+
+    <link rel="stylesheet" href="ex5style.css" />
+    <script src="ex6script.js"></script>
+</head>
+
+<body>
+    <div class="container">
+        <div class="row">
+            <div class="col-12-lg">
+                <h1>API interface</h1>
+                <div class="alert alert-warning" id="api-messages" role="alert"></div>
+                <ul class="nav nav-tabs" role="tablist">
+                    <li class="active">
+                        <a href="#create" role="tab" data-toggle="tab">Create user</a>
+                    </li>
+                </ul>
+
+                <div class="tab-content">
+                    <div class="tab-pane active" id="create">
+                        <div class="form-group">
+                            <label for="createEmail">Email</label>
+                            <input type="email" class="form-control", id="createEmail" placeholder="Enter e-mail address" />
+                        </div>
+                        <div class="form-group">
+                            <label for="createUsername">User name</label>
+                            <input type="text" class="form-control" id="createUsername" placeholder="Enter user nickname" />
+                        </div>
+                        <div class="form-group">
+                            <label for="createFirst">First name</label>
+                            <input type="text" class="form-control" id="createFirst" placeholder="Enter user first name" />
+                        </div>
+                        <div class="form-group">
+                            <label for="createLast">Last name</label>
+                            <input type="text" class="form-control", id="createLast" placeholder="Enter user last name" />
+                        </div>
+
+                        <button type="submit" onclick="javascript:userCreate();" class="btn btn-success">Create</button>
+
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+
+</body>
+</html>

+ 32 - 0
ch3/ex6script.js

@@ -0,0 +1,32 @@
+function userCreate() {
+    var action = "http://localhost:8080/api/users";
+    var postData = {};
+
+    postData.mail = $('#createEmail').val();
+    postData.user = $('#createUsername').val();
+    postData.first = $('#createFirst').val();
+    postData.last = $('#createLast').val();
+
+    $.post(action, postData, function (data) {
+        console.log("In post cb", data)
+        // If there is an error message, but no HTTP error status.
+        if (data.error) {
+            $('.alert').html(data.error);
+            $('.alert').alert();
+        }
+        else {
+            $('.alert').html("All OK");
+            $('.alert').alert();
+        }
+    }, 'json').fail(function (error) {
+      // If there is a HTTP error status
+      console.log('In fail', error.responseJSON)
+      $('.alert').html(error.responseJSON.error)
+      $('.alert').alert()
+    });
+}
+
+$(document).ready(function () {
+//    $('.alert').alert('close');
+});
+