The API Gateway for Micro-services is a system that allows developers of distributed systems to aggregate their micro-service
+ API endpoints under a single point of entry. It facilitates consistent application of security policies across groups of endpoints,
+ routes traffic to the correct API endpoints, and gives detailed feedback on the behaviour of changes to a developers gateway
+ configuration.
+
+ The gateway provides a suite of core features, including request routing, Basic Auth authentication, rate limiting, logging and
+ observability through a Grafana dashboard. A management UI was developed to allow developers to configure routes and policies
+ associated with their micro-services. A key difference between this API Gateway and existing solutions is how the gateway handles
+ errors using semantics analysis. Changes are analysed by the system and presented to the user as feedback on how the changes are
+ expected to affect the system, as well as what errors are likely to occur.
+
Below is an explanation of each part of the gateway configuration file
+
+
connections: defines the root of the yaml file
+
routes: defines a set of routes to a micro-service
+
path: the name of the microservice as references by the API Gateway. Should not include a leading "/". If added, it will be removed and replaced with the owner's username.
+
url: the public URL to the micro-service the API Gateway will route to e.g. api.example.com
+
rate-limit: defines the values set by the user for rate limiting
+
+
zone: The amount of memory you wish to allocate to your rate limit queue. Default is 10MB. Upper and Lower bounds have been added.
+
rate: The number of requests per sec allowed by the system to your micro-service. Default is 5r/s. Upper and Lower bounds have been added.
+
+
auth: a boolean value that determines whether the owner would like the micro-service to enforce Basic Auth.
+
+
+
+
\ No newline at end of file
diff --git a/management/static/index.html b/management/static/index.html
index 677b794..40fba28 100644
--- a/management/static/index.html
+++ b/management/static/index.html
@@ -12,6 +12,7 @@
Signup/LoginConfig FileDashboard
+ Docs
diff --git a/management/static/styles.css b/management/static/styles.css
index 0eabd58..76f5de4 100644
--- a/management/static/styles.css
+++ b/management/static/styles.css
@@ -432,3 +432,42 @@ aside section:nth-child(3) {
.modal-list li.error::before { content: '✕'; }
.modal-list li.warning { background: var(--warn-bg); color: var(--warn); }
.modal-list li.warning::before { content: '!'; font-weight: 700; }
+
+/* Docs */
+#docs h1{
+ font-size: 3em;
+ margin-bottom: 0.5em;
+}
+
+#docs p{
+ margin-bottom: 2em;
+}
+
+code{
+ font-family: 'Courier New', monospace;
+ font-size: 0.875em;
+ background: #f5f5f5;
+ padding: 0.1em 0.3em;
+ border-radius: 2px;
+}
+
+pre, code:not(li code) {
+ display: block;
+ background: #f5f5f5;
+ padding: 1rem 1.25rem;
+ margin: 1rem 0;
+ white-space: pre;
+ overflow-x: auto;
+ line-height: 1.6;
+ border-left: 3px solid #ddd;
+}
+
+ul {
+ padding-left: 1.5rem;
+ margin: 0 0 1.25rem;
+}
+
+li {
+ margin-bottom: 0.4rem;
+ color: #333;
+}
\ No newline at end of file
diff --git a/src/config/db.go b/src/config/db.go
index 6ae1845..53eeeb6 100644
--- a/src/config/db.go
+++ b/src/config/db.go
@@ -1,326 +1,326 @@
-package config
-
-import (
- "context"
- "database/sql"
- "encoding/json"
- "errors"
- "fyp-api-gateway/src/utils"
- "log/slog"
- "net/http"
- "os"
- "time"
-
- uuid "github.com/google/uuid"
- _ "github.com/jackc/pgx/v5/stdlib"
- "golang.org/x/crypto/bcrypt"
-)
-
-type Database struct {
- Conn *sql.DB
-}
-
-type Server struct {
- DB *Database
-}
-
-type LoginInfo struct {
- Name string `json:"name"`
- Password string `json:"password"`
-}
-
-func NewDatabase(dsn string) (*Database, error) {
- db, err := sql.Open("pgx", dsn)
- if err != nil {
- slog.Error("error opening database connection:", "error", err)
- return nil, err
- }
-
- db.SetMaxOpenConns(25)
- db.SetMaxIdleConns(25)
- db.SetConnMaxLifetime(5 * time.Minute)
-
- ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancel()
-
- if err = db.PingContext(ctx); err != nil {
- return nil, err
- }
-
- return &Database{Conn: db}, nil
-}
-
-func (d *Database) StartDB(path string) error {
- content, err := os.ReadFile(path)
- if err != nil {
- slog.Error("error reading database file", "error", err)
- return err
- }
-
- if _, err = d.Conn.Exec(string(content)); err != nil {
- slog.Error("error executing database statement", "error", err)
- return err
- }
-
- return nil
-}
-
-func (s *Server) Signup(w http.ResponseWriter, r *http.Request) {
- slog.Info("attempting to sign up new user...")
- loginInfo := &LoginInfo{}
-
- if r.Method != http.MethodPost {
- slog.Error("invalid method", "method", r.Method)
- http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
- return
- }
-
- if err := json.NewDecoder(r.Body).Decode(&loginInfo); err != nil {
- slog.Error("error decoding loginInfo", "error", err)
- http.Error(w, "invalid request body", http.StatusBadRequest)
- return
- }
- defer r.Body.Close()
-
- var id string
- err := s.DB.Conn.QueryRow(
- "SELECT id FROM users WHERE username = $1",
- loginInfo.Name,
- ).Scan(&id)
-
- if err == nil || id != "" {
- slog.Error("error querying user", "error", err)
- http.Error(w, "internal server error", http.StatusInternalServerError)
- return
- }
-
- hashed_password, err := bcrypt.GenerateFromPassword([]byte(loginInfo.Password), 14)
- _, err = s.DB.Conn.Exec(`
- INSERT INTO users (username, password, config_yaml)
- VALUES ($1, $2, $3);`,
- loginInfo.Name, string(hashed_password), utils.DefaultConfigContent,
- )
- if err != nil {
- slog.Error("error inserting user", "error", err)
- http.Error(w, "internal server error", http.StatusInternalServerError)
- return
- }
-
- err = InitUserNGINX(loginInfo.Name)
- if err != nil {
- slog.Error("error initializing user", "error", err)
- http.Error(w, "internal server error", http.StatusInternalServerError)
- return
- }
-
- w.WriteHeader(http.StatusOK)
-}
-
-/*
-Receive the login info from the management plane and decode it
-Check the password used is the same as the one in the database, also check the username is there
-If the user has no session, create one and send it back, otherwise return the existing session
-*/
-func (s *Server) VerifyLoginInfo(w http.ResponseWriter, r *http.Request) {
- slog.Info("validating login information")
- loginInfo := &LoginInfo{}
-
- if r.Method != http.MethodPost {
- slog.Error("invalid method", "method", r.Method)
- http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
- return
- }
-
- if err := json.NewDecoder(r.Body).Decode(&loginInfo); err != nil {
- slog.Error("error decoding loginInfo", "error", err)
- http.Error(w, "invalid request body", http.StatusBadRequest)
- return
- }
- defer r.Body.Close()
-
- var storedHash string
- err := s.DB.Conn.QueryRow(
- "SELECT password FROM users WHERE username = $1",
- loginInfo.Name,
- ).Scan(&storedHash)
-
- if errors.Is(err, sql.ErrNoRows) {
- slog.Error("user not found", "username", loginInfo.Name)
- http.Error(w, "invalid credentials", http.StatusUnauthorized)
- return
- }
-
- err = bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(loginInfo.Password))
- if err != nil {
- slog.Error("error verifying loginInfo", "error", err)
- http.Error(w, "invalid credentials", http.StatusUnauthorized)
- return
- }
-
- // check is the user has a session already
- var sessionId string
- sessionId, isSession := s.sessionExists(loginInfo.Name)
-
- if !isSession {
- sessionId, err = s.createSession(loginInfo.Name)
- if err != nil {
- slog.Error("error creating session", "error", err)
- http.Error(w, "internal server error", http.StatusInternalServerError)
- return
- }
- }
-
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(map[string]interface{}{
- "sessionId": sessionId,
- })
-}
-
-func (s *Server) sessionExists(username string) (string, bool) {
- var sessionId string
- err := s.DB.Conn.QueryRow(
- "SELECT id FROM sessions WHERE username=$1",
- username,
- ).Scan(&sessionId)
-
- if err != nil {
- return "", false
- }
- return sessionId, true
-}
-
-func (s *Server) createSession(name string) (string, error) {
- sessionId := uuid.New().String()
- expires := time.Now().Add(24 * time.Hour)
-
- _, err := s.DB.Conn.Exec(
- "INSERT INTO sessions(id, username, expires) VALUES ($1, $2, $3)",
- sessionId, name, expires,
- )
-
- if err != nil {
- slog.Error("error creating session", "error", err)
- return "", err
- }
-
- return sessionId, nil
-}
-
-func (s *Server) ValidateSession(w http.ResponseWriter, r *http.Request) {
- slog.Info("validating user session")
- sessionId := r.Header.Get("X-Session-ID")
-
- var username string
- err := s.DB.Conn.QueryRow(
- "SELECT username FROM sessions WHERE id=$1 AND expires > NOW()",
- sessionId,
- ).Scan(&username)
-
- if err != nil {
- slog.Error("error querying session", "error", err)
- http.Error(w, "unauthorized", http.StatusUnauthorized)
- return
- }
-
- w.WriteHeader(http.StatusOK)
-}
-
-func (s *Server) UserConfig(w http.ResponseWriter, r *http.Request) {
- slog.Info("received request for user config")
-
- cookie, err := r.Cookie("session")
- if err != nil {
- slog.Error("failed getting session id", "error", err)
- w.WriteHeader(http.StatusBadRequest)
- return
- }
-
- sessionId := cookie.Value
-
- var gatewayCfg string
- err = s.DB.Conn.QueryRow(`
- SELECT u.config_yaml
- FROM users AS u
- JOIN sessions AS s ON u.username = s.username
- WHERE s.id = $1 AND s.expires > NOW()`,
- sessionId).Scan(&gatewayCfg)
-
- if err != nil {
- slog.Error("error querying session", "error", err)
- w.WriteHeader(http.StatusBadRequest)
- return
- }
-
- w.Header().Set("Content-Type", "text/yaml")
- w.WriteHeader(http.StatusOK)
- _, err = w.Write([]byte(gatewayCfg))
- if err != nil {
- slog.Error("error writing response", "error", err)
- return
- }
-}
-
-func RetrieveUserBySessionId(sessionId string) string {
- dsn := os.Getenv("DATABASE_URL")
-
- db, err := sql.Open("pgx", dsn)
- if err != nil {
- slog.Error("error opening database connection:", "error", err)
- return ""
- }
-
- var username string
- err = db.QueryRow(`
- SELECT username
- FROM sessions
- WHERE id = $1`,
- sessionId).Scan(&username)
-
- return username
-}
-
-func RetrieveUserConfig(username string) string {
- dsn := os.Getenv("DATABASE_URL")
-
- db, err := sql.Open("pgx", dsn)
- if err != nil {
- slog.Error("error opening database connection:", "error", err)
- return ""
- }
-
- var gatewayCfg string
- err = db.QueryRow(`
- SELECT config_yaml
- FROM users
- WHERE username = $1`,
- username,
- ).Scan(&gatewayCfg)
-
- return gatewayCfg
-}
-
-func InsertNewConfig(sessionId, gatewayCfg string) error {
- dsn := os.Getenv("DATABASE_URL")
-
- db, err := sql.Open("pgx", dsn)
- if err != nil {
- return err
- }
-
- _, err = db.Exec(`
- UPDATE users AS u
- SET config_yaml = $1
- FROM sessions AS s
- WHERE s.id = $2
- AND s.expires > NOW()
- AND u.username = s.username`,
- gatewayCfg, sessionId,
- )
- slog.Info("inserted new config to database")
-
- if err != nil {
- return err
- }
-
- return nil
-}
+package config
+
+import (
+ "context"
+ "database/sql"
+ "encoding/json"
+ "errors"
+ "fyp-api-gateway/src/utils"
+ "log/slog"
+ "net/http"
+ "os"
+ "time"
+
+ uuid "github.com/google/uuid"
+ _ "github.com/jackc/pgx/v5/stdlib"
+ "golang.org/x/crypto/bcrypt"
+)
+
+type Database struct {
+ Conn *sql.DB
+}
+
+type Server struct {
+ DB *Database
+}
+
+type LoginInfo struct {
+ Name string `json:"name"`
+ Password string `json:"password"`
+}
+
+func NewDatabase(dsn string) (*Database, error) {
+ db, err := sql.Open("pgx", dsn)
+ if err != nil {
+ slog.Error("error opening database connection:", "error", err)
+ return nil, err
+ }
+
+ db.SetMaxOpenConns(25)
+ db.SetMaxIdleConns(25)
+ db.SetConnMaxLifetime(5 * time.Minute)
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ if err = db.PingContext(ctx); err != nil {
+ return nil, err
+ }
+
+ return &Database{Conn: db}, nil
+}
+
+func (d *Database) StartDB(path string) error {
+ content, err := os.ReadFile(path)
+ if err != nil {
+ slog.Error("error reading database file", "error", err)
+ return err
+ }
+
+ if _, err = d.Conn.Exec(string(content)); err != nil {
+ slog.Error("error executing database statement", "error", err)
+ return err
+ }
+
+ return nil
+}
+
+func (s *Server) Signup(w http.ResponseWriter, r *http.Request) {
+ slog.Info("attempting to sign up new user...")
+ loginInfo := &LoginInfo{}
+
+ if r.Method != http.MethodPost {
+ slog.Error("invalid method", "method", r.Method)
+ http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+
+ if err := json.NewDecoder(r.Body).Decode(&loginInfo); err != nil {
+ slog.Error("error decoding loginInfo", "error", err)
+ http.Error(w, "invalid request body", http.StatusBadRequest)
+ return
+ }
+ defer r.Body.Close()
+
+ var id string
+ err := s.DB.Conn.QueryRow(
+ "SELECT id FROM users WHERE username = $1",
+ loginInfo.Name,
+ ).Scan(&id)
+
+ if err == nil || id != "" {
+ slog.Error("error querying user", "error", err)
+ http.Error(w, "internal server error", http.StatusInternalServerError)
+ return
+ }
+
+ hashed_password, err := bcrypt.GenerateFromPassword([]byte(loginInfo.Password), 14)
+ _, err = s.DB.Conn.Exec(`
+ INSERT INTO users (username, password, config_yaml)
+ VALUES ($1, $2, $3);`,
+ loginInfo.Name, string(hashed_password), utils.DefaultConfigContent,
+ )
+ if err != nil {
+ slog.Error("error inserting user", "error", err)
+ http.Error(w, "internal server error", http.StatusInternalServerError)
+ return
+ }
+
+ err = InitUserNGINX(loginInfo.Name)
+ if err != nil {
+ slog.Error("error initializing user", "error", err)
+ http.Error(w, "internal server error", http.StatusInternalServerError)
+ return
+ }
+
+ w.WriteHeader(http.StatusOK)
+}
+
+/*
+Receive the login info from the management plane and decode it
+Check the password used is the same as the one in the database, also check the username is there
+If the user has no session, create one and send it back, otherwise return the existing session
+*/
+func (s *Server) VerifyLoginInfo(w http.ResponseWriter, r *http.Request) {
+ slog.Info("validating login information")
+ loginInfo := &LoginInfo{}
+
+ if r.Method != http.MethodPost {
+ slog.Error("invalid method", "method", r.Method)
+ http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+
+ if err := json.NewDecoder(r.Body).Decode(&loginInfo); err != nil {
+ slog.Error("error decoding loginInfo", "error", err)
+ http.Error(w, "invalid request body", http.StatusBadRequest)
+ return
+ }
+ defer r.Body.Close()
+
+ var storedHash string
+ err := s.DB.Conn.QueryRow(
+ "SELECT password FROM users WHERE username = $1",
+ loginInfo.Name,
+ ).Scan(&storedHash)
+
+ if errors.Is(err, sql.ErrNoRows) {
+ slog.Error("user not found", "username", loginInfo.Name)
+ http.Error(w, "invalid credentials", http.StatusUnauthorized)
+ return
+ }
+
+ err = bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(loginInfo.Password))
+ if err != nil {
+ slog.Error("error verifying loginInfo", "error", err)
+ http.Error(w, "invalid credentials", http.StatusUnauthorized)
+ return
+ }
+
+ // check is the user has a session already
+ var sessionId string
+ sessionId, isSession := s.sessionExists(loginInfo.Name)
+
+ if !isSession {
+ sessionId, err = s.createSession(loginInfo.Name)
+ if err != nil {
+ slog.Error("error creating session", "error", err)
+ http.Error(w, "internal server error", http.StatusInternalServerError)
+ return
+ }
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]interface{}{
+ "sessionId": sessionId,
+ })
+}
+
+func (s *Server) sessionExists(username string) (string, bool) {
+ var sessionId string
+ err := s.DB.Conn.QueryRow(
+ "SELECT id FROM sessions WHERE username=$1",
+ username,
+ ).Scan(&sessionId)
+
+ if err != nil {
+ return "", false
+ }
+ return sessionId, true
+}
+
+func (s *Server) createSession(name string) (string, error) {
+ sessionId := uuid.New().String()
+ expires := time.Now().Add(24 * time.Hour)
+
+ _, err := s.DB.Conn.Exec(
+ "INSERT INTO sessions(id, username, expires) VALUES ($1, $2, $3)",
+ sessionId, name, expires,
+ )
+
+ if err != nil {
+ slog.Error("error creating session", "error", err)
+ return "", err
+ }
+
+ return sessionId, nil
+}
+
+func (s *Server) ValidateSession(w http.ResponseWriter, r *http.Request) {
+ slog.Info("validating user session")
+ sessionId := r.Header.Get("X-Session-ID")
+
+ var username string
+ err := s.DB.Conn.QueryRow(
+ "SELECT username FROM sessions WHERE id=$1 AND expires > NOW()",
+ sessionId,
+ ).Scan(&username)
+
+ if err != nil {
+ slog.Error("error querying session", "error", err)
+ http.Error(w, "unauthorized", http.StatusUnauthorized)
+ return
+ }
+
+ w.WriteHeader(http.StatusOK)
+}
+
+func (s *Server) UserConfig(w http.ResponseWriter, r *http.Request) {
+ slog.Info("received request for user config")
+
+ cookie, err := r.Cookie("session")
+ if err != nil {
+ slog.Error("failed getting session id", "error", err)
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ sessionId := cookie.Value
+
+ var gatewayCfg string
+ err = s.DB.Conn.QueryRow(`
+ SELECT u.config_yaml
+ FROM users AS u
+ JOIN sessions AS s ON u.username = s.username
+ WHERE s.id = $1 AND s.expires > NOW()`,
+ sessionId).Scan(&gatewayCfg)
+
+ if err != nil {
+ slog.Error("error querying session", "error", err)
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ w.Header().Set("Content-Type", "text/yaml")
+ w.WriteHeader(http.StatusOK)
+ _, err = w.Write([]byte(gatewayCfg))
+ if err != nil {
+ slog.Error("error writing response", "error", err)
+ return
+ }
+}
+
+func RetrieveUserBySessionId(sessionId string) string {
+ dsn := os.Getenv("DATABASE_URL")
+
+ db, err := sql.Open("pgx", dsn)
+ if err != nil {
+ slog.Error("error opening database connection:", "error", err)
+ return ""
+ }
+
+ var username string
+ err = db.QueryRow(`
+ SELECT username
+ FROM sessions
+ WHERE id = $1`,
+ sessionId).Scan(&username)
+
+ return username
+}
+
+func RetrieveUserConfig(username string) string {
+ dsn := os.Getenv("DATABASE_URL")
+
+ db, err := sql.Open("pgx", dsn)
+ if err != nil {
+ slog.Error("error opening database connection:", "error", err)
+ return ""
+ }
+
+ var gatewayCfg string
+ err = db.QueryRow(`
+ SELECT config_yaml
+ FROM users
+ WHERE username = $1`,
+ username,
+ ).Scan(&gatewayCfg)
+
+ return gatewayCfg
+}
+
+func InsertNewConfig(sessionId, gatewayCfg string) error {
+ dsn := os.Getenv("DATABASE_URL")
+
+ db, err := sql.Open("pgx", dsn)
+ if err != nil {
+ return err
+ }
+
+ _, err = db.Exec(`
+ UPDATE users AS u
+ SET config_yaml = $1
+ FROM sessions AS s
+ WHERE s.id = $2
+ AND s.expires > NOW()
+ AND u.username = s.username`,
+ gatewayCfg, sessionId,
+ )
+ slog.Info("inserted new config to database")
+
+ if err != nil {
+ return err
+ }
+
+ return nil
+}