From ae810f964f6459c69bf587ccbdcf09234a107a28 Mon Sep 17 00:00:00 2001 From: chandrawisesa Date: Tue, 24 Sep 2024 15:05:21 +0700 Subject: [PATCH] Initial Commit. --- .gitignore | 11 +- go_auth.yaml | 21 +++ src/Dockerfile | 13 ++ src/appinfo/appinfo.go | 12 ++ src/db/db.go | 61 +++++++ src/go.mod | 12 ++ src/go.sum | 12 ++ src/handlers/auth.go | 245 +++++++++++++++++++++++++++ src/handlers/greeting.go | 167 +++++++++++++++++++ src/handlers/hello.go | 12 ++ src/helper/crypto.go | 70 ++++++++ src/helper/util.go | 36 ++++ src/logger/logger.go | 102 ++++++++++++ src/main.go | 129 ++++++++++++++ src/moffas/moffas_auth.go | 341 ++++++++++++++++++++++++++++++++++++++ test/test_auth1.http | 29 ++++ 16 files changed, 1268 insertions(+), 5 deletions(-) create mode 100644 go_auth.yaml create mode 100644 src/Dockerfile create mode 100644 src/appinfo/appinfo.go create mode 100644 src/db/db.go create mode 100644 src/go.mod create mode 100644 src/go.sum create mode 100644 src/handlers/auth.go create mode 100644 src/handlers/greeting.go create mode 100644 src/handlers/hello.go create mode 100644 src/helper/crypto.go create mode 100644 src/helper/util.go create mode 100644 src/logger/logger.go create mode 100644 src/main.go create mode 100644 src/moffas/moffas_auth.go create mode 100644 test/test_auth1.http diff --git a/.gitignore b/.gitignore index a304625..b3b2181 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,5 @@ # ---> VisualStudioCode .vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -!.vscode/*.code-snippets # Local History for Visual Studio Code .history/ @@ -35,3 +30,9 @@ # Go workspace file go.work +# compressed files +*.zip +*.gz +*.rar +*.bz2 +*.tar \ No newline at end of file diff --git a/go_auth.yaml b/go_auth.yaml new file mode 100644 index 0000000..193a35d --- /dev/null +++ b/go_auth.yaml @@ -0,0 +1,21 @@ +services: + servoauth: + image: go_auth + container_name: servoauth + ports: + - "48002:48000" + environment: + - "DBHOST=host.docker.internal" + - "DBPORT=5432" + - "DBUSER=asterisk" + - "DBPASS=asterisk" + - "DBNAME=asterisk" + - "DBDRIVER=postgres" + - "DBPOOLSIZE=100" + extra_hosts: + - "host.docker.internal:host-gateway" + networks: + - dbcon +networks: + dbcon: + external: true diff --git a/src/Dockerfile b/src/Dockerfile new file mode 100644 index 0000000..9f36ff5 --- /dev/null +++ b/src/Dockerfile @@ -0,0 +1,13 @@ +FROM golang:alpine + +RUN apk update && apk add --no-cache git + +WORKDIR /app + +COPY . . + +RUN go mod tidy + +RUN go build -o binary + +ENTRYPOINT ["/app/binary"] \ No newline at end of file diff --git a/src/appinfo/appinfo.go b/src/appinfo/appinfo.go new file mode 100644 index 0000000..4e42795 --- /dev/null +++ b/src/appinfo/appinfo.go @@ -0,0 +1,12 @@ +package appinfo + +var appName string = "MOFFAS-AUTH" +var version string = "1.0.0.1" + +func Version() string { + return version +} + +func AppName() string { + return appName +} diff --git a/src/db/db.go b/src/db/db.go new file mode 100644 index 0000000..f1b50ea --- /dev/null +++ b/src/db/db.go @@ -0,0 +1,61 @@ +package db + +import ( + "errors" + "fmt" + "moffas_go/logger" + "sync" + "time" + + _ "github.com/go-sql-driver/mysql" + "github.com/jmoiron/sqlx" + _ "github.com/lib/pq" +) + +type DBPool struct { + db *sqlx.DB + count int + mutex sync.Mutex + poolSize int +} + +var dbpool DBPool = DBPool{} + +func GetConnection() (*sqlx.DB, error) { + dbpool.mutex.Lock() + defer dbpool.mutex.Unlock() + if dbpool.count >= dbpool.poolSize { + dbpool.count = dbpool.poolSize + return nil, errors.New("no connection available in pool") + } else { + dbpool.count++ + return dbpool.db, nil + } +} + +func ReleaseConnection() { + dbpool.mutex.Lock() + defer dbpool.mutex.Unlock() + if dbpool.count > 0 { + dbpool.count-- + } else { + dbpool.count = 0 + } +} + +func InitDB(driver string, host string, port int, user string, password string, dbname string, poolSize int) error { + var err error + connStr := fmt.Sprintf("%s://%s:%s@%s:%d/%s", driver, user, password, host, port, dbname) + logger.Debug("DB", "CONNSTR : ", connStr) + dbpool.db, err = sqlx.Connect(driver, connStr) + if err != nil { + return err + } else { + dbpool.db.SetMaxOpenConns(poolSize) + dbpool.db.SetMaxIdleConns(poolSize) + dbpool.db.SetConnMaxLifetime(5 * time.Minute) + dbpool.poolSize = poolSize + dbpool.count = 0 + return nil + } +} diff --git a/src/go.mod b/src/go.mod new file mode 100644 index 0000000..cd3fb4e --- /dev/null +++ b/src/go.mod @@ -0,0 +1,12 @@ +module moffas_go + +go 1.23.1 + +require ( + github.com/go-sql-driver/mysql v1.8.1 + github.com/jmoiron/sqlx v1.4.0 + github.com/lib/pq v1.10.9 + golang.org/x/crypto v0.27.0 +) + +require filippo.io/edwards25519 v1.1.0 // indirect diff --git a/src/go.sum b/src/go.sum new file mode 100644 index 0000000..352b928 --- /dev/null +++ b/src/go.sum @@ -0,0 +1,12 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= diff --git a/src/handlers/auth.go b/src/handlers/auth.go new file mode 100644 index 0000000..e0a7c67 --- /dev/null +++ b/src/handlers/auth.go @@ -0,0 +1,245 @@ +package handlers + +import ( + "encoding/json" + "fmt" + "io" + "moffas_go/helper" + "moffas_go/logger" + "moffas_go/moffas" + "net/http" +) + +// ====== HANDLER FOR /auth ====== +func Auth(w http.ResponseWriter, r *http.Request) { + var err error + var res string + + var ctxkey HTTPContextKey = "requsetID" + reference_id := r.Context().Value(ctxkey).(string) + defer func() { + logger.Info(reference_id, "Response body: ", res) + }() + + // ---- SET THE RESPONSE TYPE TO application/json ---- + w.Header().Set("Content-Type", "application/json") + response := map[string]any{ + "error_code": "000000000", + "error_message": "", + } + // ---- CHECK REQUEST METHOD ---- + if r.Method == http.MethodPost { + // ---- VERIFY THE CONTENT IS JSON ---- + if contentType := r.Header.Get("Content-Type"); contentType != "application/json" { + // NOT JSON + logger.Error(reference_id, "!!! INVALID CONTENT-TYPE: ", contentType) + response["error_code"] = "400000001" + response["error_message"] = "Bad Request. Not JSON" + + res, _ = helper.JSONencode(response) + http.Error(w, res, http.StatusBadRequest) + return + } + + // ---- READ THE POST BODY ---- + var body []byte + body, err = io.ReadAll(r.Body) + if err != nil { + // ---- FAILED TO READ BODY ---- + logger.Error(reference_id, err) + response["error_code"] = "400000002" + response["error_message"] = "Bad Request. Can't read POST body" + + res, _ = helper.JSONencode(response) + http.Error(w, res, http.StatusBadRequest) + return + } + bodyString := string(body) + logger.Info(reference_id, "-- POST BODY: ", bodyString) + + // ---- EXTRACT DATA FROM JSON BODY ---- + type Request struct { + Half_nonce string `json:"half_nonce"` + Username string `json:"username"` + } + var req Request + err = json.Unmarshal(body, &req) + if err != nil { + // ---- FAILED TO DECODE JSON ---- + logger.Error(reference_id, err) + response["error_code"] = "400000003" + response["error_message"] = "Bad Request. Invalid JSON" + + res, _ = helper.JSONencode(response) + http.Error(w, res, http.StatusBadRequest) + return + } + + // ---- CHECK THE MANDATORY FIELDS ---- + if len(req.Half_nonce) != 8 || req.Username == "" { + // ---- MISSING OR INVALID MANDATORY FIELD ---- + logger.Error(reference_id, "!!! MANDATORY FIELD NOT FOUND OR INVALID") + response["error_code"] = "400000003" + response["error_message"] = "Bad Request. Missing/Invalid Mandatory Fields" + res, _ = helper.JSONencode(response) + http.Error(w, res, http.StatusBadRequest) + return + } + + // ---- GENERATE CHALLENGE ---- + challenge, err := moffas.Generate_challenge(reference_id, req.Username, req.Half_nonce) + if err != nil { + // ---- ERROR QUERYING USER DATA ---- + logger.Error(reference_id, "!!! ERROR GENERATING CHALLENGE") + logger.Error(reference_id, err) + challenge.Salt, _ = helper.GenerateRandomString(16) + challenge.Iterations = 10000 + } + + // ---- PREPARE RESPONSE BODY ---- + response["full_nonce"] = challenge.Full_nonce + response["salt"] = challenge.Salt + response["iterations"] = challenge.Iterations + + res, _ = helper.JSONencode(response) + fmt.Fprint(w, res) + return + + } else { + // ---- METHOD NOT ALLOWED (not POST) ---- + response["error_code"] = "405000001" + response["error_message"] = "Method Not Allowed" + + res, _ = helper.JSONencode(response) + http.Error(w, res, http.StatusMethodNotAllowed) + return + } + +} + +// ====== HANDLER FOR /verify ====== +func Verify(w http.ResponseWriter, r *http.Request) { + var err error + var res string + + var ctxkey HTTPContextKey = "requsetID" + reference_id := r.Context().Value(ctxkey).(string) + defer func() { + logger.Info(reference_id, "Response body: ", res) + }() + + // ---- SET THE RESPONSE TYPE TO application/json ---- + w.Header().Set("Content-Type", "application/json") + response := map[string]any{ + "error_code": "000000000", + "error_message": "", + } + // ---- CHECK REQUEST METHOD ---- + if r.Method == http.MethodPost { + // ---- VERIFY THE CONTENT IS JSON ---- + if contentType := r.Header.Get("Content-Type"); contentType != "application/json" { + // NOT JSON + logger.Error(reference_id, "!!! INVALID CONTENT-TYPE: ", contentType) + response["error_code"] = "400000001" + response["error_message"] = "Bad Request. Not JSON" + + res, _ = helper.JSONencode(response) + http.Error(w, res, http.StatusBadRequest) + return + } + + // ---- READ THE POST BODY ---- + var body []byte + body, err = io.ReadAll(r.Body) + if err != nil { + // FAILED TO READ BODY + logger.Error(reference_id, err) + response["error_code"] = "400000002" + response["error_message"] = "Bad Request. Can't read POST body" + + res, _ = helper.JSONencode(response) + http.Error(w, res, http.StatusBadRequest) + return + } + bodyString := string(body) + logger.Info(reference_id, "-- POST BODY: ", bodyString) + + // ---- EXTRACT DATA FROM JSON BODY ---- + type Request struct { + Full_nonce string `json:"full_nonce"` + Client_hash string `json:"client_hash"` + Next_nonce string `json:"next_nonce"` + } + var req Request + err = json.Unmarshal(body, &req) + if err != nil { + // ---- FAILED TO DECODE JSON ---- + logger.Error(reference_id, "!!! FAILED TO DECODE JSON") + logger.Error(reference_id, err) + response["error_code"] = "400000003" + response["error_message"] = "Bad Request. Invalid JSON" + + res, _ = helper.JSONencode(response) + http.Error(w, res, http.StatusBadRequest) + return + } + + // ---- CHECK MANDATORY FIELDS ---- + if len(req.Full_nonce) != 16 || req.Client_hash == "" || len(req.Next_nonce) != 8 { + // MISSING MANDATORY FIELD + logger.Error(reference_id, "!!! MISSING OR INVALID MANDATORY FIELD") + logger.Error(reference_id, "len(full_nonce) : ", len(req.Full_nonce)) + logger.Error(reference_id, "client_hash : ", req.Client_hash) + logger.Error(reference_id, "len(next_nonce) : ", len(req.Next_nonce)) + response["error_code"] = "400000003" + response["error_message"] = "Bad Request. Missing/Invalid Mandatory Fields" + res, _ = helper.JSONencode(response) + http.Error(w, res, http.StatusBadRequest) + return + } + + // ---- VERIFY CHALLENGE RESPONSE ---- + challenge_data, err := moffas.Verify_challenge(reference_id, req.Full_nonce, req.Client_hash, req.Next_nonce) + if err != nil { + // ---- ERROR QUERYING USER DATA ---- + logger.Error(reference_id, "!!! FAILED TO VERIFY CHALLENGE") + logger.Error(reference_id, err) + response["error_code"] = "401000001" + response["error_message"] = "Unauthorized" + res, _ = helper.JSONencode(response) + http.Error(w, res, http.StatusUnauthorized) + return + } + + // ---- PREPARE RESPONSE BODY ---- + response["session_id"] = challenge_data.Session_id + response["final_nonce"] = challenge_data.Final_nonce + response["server_hash"] = challenge_data.Server_hash + + data := make(map[string]interface{}) + premium_voice, ok := challenge_data.Organization_data["premium_voice"] + if ok { + data["premium_voice"] = premium_voice + } else { + data["premium_voice"] = 0 + } + data["privileges"] = challenge_data.Privileges + data["tier"] = challenge_data.Tier + data["full_name"] = challenge_data.Full_name + response["user_data"] = data + + res, _ = helper.JSONencode(response) + fmt.Fprint(w, res) + return + + } else { + // ---- METHOD NOT ALLOWED (not POST) ---- + response["error_code"] = "405000001" + response["error_message"] = "Method Not Allowed" + + res, _ = helper.JSONencode(response) + http.Error(w, res, http.StatusMethodNotAllowed) + return + } + +} diff --git a/src/handlers/greeting.go b/src/handlers/greeting.go new file mode 100644 index 0000000..04c2465 --- /dev/null +++ b/src/handlers/greeting.go @@ -0,0 +1,167 @@ +package handlers + +import ( + "encoding/json" + "fmt" + "io" + "moffas_go/helper" + "moffas_go/logger" + "net/http" + "net/url" + "strconv" + "strings" +) + +type HTTPContextKey string + +func Greeting(w http.ResponseWriter, r *http.Request) { + // GENERATE RANDOMIZED reference_id TO IDENTIFY REQUESTS UNIQUELY + var err error + var res string + type reqStruct struct { + Name string `json:"name"` + Password string `json:"password"` + Salt string `json:"salt"` + Iterations int `json:"iterations"` + } + + var req reqStruct = reqStruct{} + + var ctxkey HTTPContextKey = "requsetID" + reference_id := r.Context().Value(ctxkey).(string) + + defer func() { + logger.Info(reference_id, "Response body: ", res) + }() + + // SET THE RESPONSE TYPE TO application/json + w.Header().Set("Content-Type", "application/json") + response := map[string]any{ + "error_code": "000000000", + "error_message": "", + } + + // GET THE REQUEST QUERY PARAMS AND THE RAW QUERY STRING + var queryString string = string(r.URL.RawQuery) + var query url.Values = r.URL.Query() + if r.Method == http.MethodGet { + // GET NAME FROM QUERY PARAMS + names := query["name"] + req.Name = strings.Join(names, "") + + // PREPARE THE RESPONSE + if req.Name == "" { + response["message"] = "Hello!" + } else { + response["message"] = "Hello, " + req.Name + "!" + } + response["reference_id"] = reference_id + response["query_string"] = queryString + req.Password = query["password"][0] + req.Salt = query["salt"][0] + req.Iterations, _ = strconv.Atoi(query["iterations"][0]) + + if req.Password == "" { + req.Password = "rahasia" + } + response["password"] = req.Password + + if req.Salt == "" { + req.Salt, _ = helper.GenerateRandomString(16) + } + response["salt"] = req.Salt + + if req.Iterations <= 0 { + req.Iterations = 10000 + } + response["iterations"] = req.Iterations + response["salted_password"] = helper.PBKDF2_SHA256(req.Password, req.Salt, req.Iterations) + + // SEND THE RESPONSE AS JSON + res, _ = helper.JSONencode(response) + fmt.Fprint(w, res) + return + + } else if r.Method == http.MethodPost { + // THE RECEIVED REQUEST METHOD IS POST + logger.Info(reference_id, "Request Method: POST") + + // VERIFY THE CONTENT IS JSON + if contentType := r.Header.Get("Content-Type"); contentType != "application/json" { + // NOT JSON + logger.Error(reference_id, "Invalid content-type: ", contentType) + response["error_code"] = "400000001" + response["error_message"] = "Bad Request. Not JSON" + res, _ = helper.JSONencode(response) + http.Error(w, res, http.StatusBadRequest) + return + } + + // READ THE POST BODY + var body []byte + body, err = io.ReadAll(r.Body) + if err != nil { + // FAILED TO READ BODY + logger.Error(reference_id, err) + response["error_code"] = "400000002" + response["error_message"] = "Bad Request. Can't read POST body" + // logger.E("ERROR : ", err) + res, _ = helper.JSONencode(response) + http.Error(w, res, http.StatusBadRequest) + return + } + bodyString := string(body) + logger.Info(reference_id, "POST BODY: ", bodyString) + + // EXTRACT DATA FROM JSON BODY + err = json.Unmarshal(body, &req) + if err != nil { + // FAILED TO DECODE JSON + logger.Error(reference_id, err) + response["error_code"] = "400000003" + response["error_message"] = "Bad Request. Invalid JSON" + // logger.E("ERROR : ", err) + res, _ = helper.JSONencode(response) + http.Error(w, res, http.StatusBadRequest) + return + } + + if req.Password == "" { + req.Password = "rahasia" + } + response["password"] = req.Password + + if req.Salt == "" { + req.Salt, _ = helper.GenerateRandomString(16) + } + response["salt"] = req.Salt + + if req.Iterations <= 0 { + req.Iterations = 10000 + } + response["iterations"] = req.Iterations + response["salted_password"] = helper.PBKDF2_SHA256(req.Password, req.Salt, req.Iterations) + + // PREPARE THE RESPONSE + if req.Name == "" { + response["message"] = "Hello!" + } else { + response["message"] = "Hello, " + req.Name + "!" + } + response["reference_id"] = reference_id + response["query_string"] = queryString + + // SEND THE RESPONSE AS JSON + res, _ = helper.JSONencode(response) + fmt.Fprint(w, res) + return + + } else { + // METHOD NOT ALLOWED (not GET or POST) + response["error_code"] = "405000001" + response["error_message"] = "Method Not Allowed" + res, _ = helper.JSONencode(response) + http.Error(w, res, http.StatusMethodNotAllowed) + return + } +} diff --git a/src/handlers/hello.go b/src/handlers/hello.go new file mode 100644 index 0000000..86bf081 --- /dev/null +++ b/src/handlers/hello.go @@ -0,0 +1,12 @@ +package handlers + +import ( + "fmt" + "net/http" +) + +func Hello(w http.ResponseWriter, r *http.Request) { + res := "Hello World!" + fmt.Fprint(w, res) + +} diff --git a/src/helper/crypto.go b/src/helper/crypto.go new file mode 100644 index 0000000..d060f71 --- /dev/null +++ b/src/helper/crypto.go @@ -0,0 +1,70 @@ +package helper + +import ( + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "fmt" + + "golang.org/x/crypto/pbkdf2" +) + +func GenerateRandomString(length int) (string, error) { + // Character set untuk random string + charset := "0123456789abcdefghijklmnopqrstuvwxyz" + + // Membuat slice byte untuk menampung hasil random string + result := make([]byte, length) + + // Menggunakan crypto/rand untuk mengisi slice dengan data acak + for i := range result { + // Membuat byte acak untuk digunakan sebagai indeks + randomByte := make([]byte, 1) + _, err := rand.Read(randomByte) + if err != nil { + fmt.Println("Error generating random string:", err) + return "", err + } + + // Menentukan indeks dalam charset menggunakan byte acak + index := int(randomByte[0]) % len(charset) + result[i] = charset[index] + } + + return string(result), nil +} + +func SHA256(data string) string { + shaHash := sha256.Sum256([]byte(data)) + shaHashStr := hex.EncodeToString(shaHash[:]) + return shaHashStr +} + +func HMAC_SHA256(inputData string, hexKey string) (string, error) { + key, err := hex.DecodeString(hexKey) + if err != nil { + return "", err + } + + // Membuat HMAC-SHA256 menggunakan key yang didecode + hmacHash := hmac.New(sha256.New, key) + _, err = hmacHash.Write([]byte(inputData)) + if err != nil { + return "", err + } + + // Menghasilkan hash dalam bentuk hexadecimal string + hmacHashStr := hex.EncodeToString(hmacHash.Sum(nil)) + return hmacHashStr, nil +} + +func PBKDF2_SHA256(password string, salt string, iterations int) string { + // Menghasilkan key 256-bit (32 bytes) + bpassword := []byte(password) + bsalt := []byte(salt) + key := pbkdf2.Key(bpassword, bsalt, iterations, 32, sha256.New) + + // Mengonversi hasil key ke hexadecimal string + return hex.EncodeToString(key) +} diff --git a/src/helper/util.go b/src/helper/util.go new file mode 100644 index 0000000..6717fdb --- /dev/null +++ b/src/helper/util.go @@ -0,0 +1,36 @@ +package helper + +import ( + "bytes" + "encoding/json" + "fmt" + "os" +) + +func JSONencode(data any) (string, error) { + var buffer bytes.Buffer + + // Buat encoder yang menulis ke buffer + encoder := json.NewEncoder(&buffer) + + // Set agar encoder tidak melakukan escaping HTML + encoder.SetEscapeHTML(false) + + // Encode data ke JSON dan simpan ke buffer + if err := encoder.Encode(data); err != nil { + fmt.Println("Error encoding JSON:", err) + return "", err + } + + // Mendapatkan hasil JSON sebagai string + jsonString := buffer.String() + return jsonString, nil +} + +func GetEnv(key, fallback string) string { + value := os.Getenv(key) + if len(value) == 0 { + return fallback + } + return value +} diff --git a/src/logger/logger.go b/src/logger/logger.go new file mode 100644 index 0000000..db7fa2b --- /dev/null +++ b/src/logger/logger.go @@ -0,0 +1,102 @@ +package logger + +import ( + "fmt" + "moffas_go/appinfo" + "runtime" + "strconv" + "strings" + "time" +) + +const ( + CRITICAL = iota + ERROR + WARNING + EVENT + INFO + DEBUG +) + +var debugLevel int = ERROR + +func SetDebugLevel(level int) { + debugLevel = level +} + +func getLogPrefix() string { + timestr := time.Now().Format(time.RFC3339Nano) + // 2024-09-22T15:54:51.665494100 + timestrs := strings.Split(timestr, "+") + tstr := (timestrs[0] + "000000000") + timestrs[0] = tstr[:29] + timestr = strings.Join(timestrs, "+") + + version := appinfo.Version() + + // Mengambil informasi tentang caller satu level di atas (yaitu fungsi main dalam hal ini) + funcName := "" + pc, _, line, ok := runtime.Caller(2) + if !ok { + fmt.Println("No caller information") + } else { + function := runtime.FuncForPC(pc) + funcName = function.Name() + } + funcString := "" + if debugLevel == DEBUG { + funcString = funcName + ":" + strconv.Itoa(line) + " - " + } + + // Mendapatkan nama fungsi dari program counter (pc) + return timestr + " - " + appinfo.AppName() + " - VERSION:" + version + " - " + funcString + +} + +func Debug(id string, v ...any) { + if debugLevel >= DEBUG { + prefix := getLogPrefix() + id + " - DEBUG - " + msgs := fmt.Sprint(v...) + fmt.Println(prefix + msgs) + } +} + +func Info(id string, v ...any) { + if debugLevel >= INFO { + prefix := getLogPrefix() + id + " - INFO - " + msgs := fmt.Sprint(v...) + fmt.Println(prefix + msgs) + } +} + +func Event(id string, v ...any) { + if debugLevel >= EVENT { + prefix := getLogPrefix() + id + " - EVENT - " + msgs := fmt.Sprint(v...) + fmt.Println(prefix + msgs) + } +} + +func Warning(id string, v ...any) { + if debugLevel >= WARNING { + prefix := getLogPrefix() + id + " - WARNING - " + msgs := fmt.Sprint(v...) + fmt.Println(prefix + msgs) + } +} + +func Error(id string, v ...any) { + if debugLevel >= ERROR { + prefix := getLogPrefix() + id + " - ERROR - " + msgs := fmt.Sprint(v...) + fmt.Println(prefix + msgs) + } +} + +func Critical(id string, v ...any) { + if debugLevel >= CRITICAL { + prefix := getLogPrefix() + id + " - CRITICAL - " + msgs := fmt.Sprint(v...) + fmt.Println(prefix + msgs) + } +} diff --git a/src/main.go b/src/main.go new file mode 100644 index 0000000..6e30675 --- /dev/null +++ b/src/main.go @@ -0,0 +1,129 @@ +package main + +import ( + "context" + "moffas_go/db" + "moffas_go/handlers" + "moffas_go/helper" + "moffas_go/logger" + "net/http" + "os" + "strconv" + "time" +) + +func generate_reference_id(timer int64) string { + timeBase36 := strconv.FormatUint(uint64(timer), 36) + nonce8, _ := helper.GenerateRandomString(8) + reference_id := timeBase36 + "." + nonce8 + return reference_id + +} + +// Middleware untuk menangani CORS +func corsMiddleware(next http.Handler) http.Handler { + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") // Mengizinkan semua asal + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") // Metode yang diizinkan + w.Header().Set("Access-Control-Allow-Headers", "Content-Type") // Header yang diizinkan + + if r.Method == "OPTIONS" { + // Jika request adalah OPTIONS (preflight), kembalikan status 204 tanpa body + w.WriteHeader(http.StatusNoContent) + return + } + // GENERATE UNIQUE REQUEST ID FOR REFERENCE + start := time.Now() + request_id := generate_reference_id(start.UnixNano()) + + // LOG START, METHOD AND PATH + logger.Event(request_id, "--- Handle Request Started", r.Method, " ", r.URL.Path) + + // LOG THE REQUEST RAW QUERY STRING + logger.Info(request_id, "--- Query String: ", string(r.URL.RawQuery)) + + // LOG HEADERS + logger.Info(request_id, "--- Headers: ") + for name, values := range r.Header { + // Loop over all values for the name. + for _, value := range values { + logger.Info(request_id, "[", name, "] : ", value) + } + } + + // CALL THE REAL HANDLER + var ctxkey handlers.HTTPContextKey = "requsetID" + ctx := context.WithValue(r.Context(), ctxkey, request_id) + next.ServeHTTP(w, r.WithContext(ctx)) + + // LOG COMPLETEION AND DURATION + duration := time.Since(start) + logger.Event(request_id, "--- Handle Request Completed in : ", duration) + + }) +} + +func main() { + // CONFIGURE LOGGER + dblv := helper.GetEnv("DEBUG_LEVEL", "INFO") + + if dblv == "DEBUG" { + logger.SetDebugLevel(logger.DEBUG) + } else if dblv == "INFO" { + logger.SetDebugLevel(logger.INFO) + } else if dblv == "EVENT" { + logger.SetDebugLevel(logger.EVENT) + } else if dblv == "WARNING" { + logger.SetDebugLevel(logger.WARNING) + } else if dblv == "ERROR" { + logger.SetDebugLevel(logger.ERROR) + } else { + logger.SetDebugLevel(logger.INFO) + } + + // CONFIGURE AND INITIALIZE DB + DBDRIVER := helper.GetEnv("DBDRIVER", "postgres") + DBUSER := helper.GetEnv("DBUSER", "magang") + DBPASS := helper.GetEnv("DBPASS", "magang") + DBHOST := helper.GetEnv("DBHOST", "ningning.cayangqu.com") + DBPORT, err := strconv.Atoi(helper.GetEnv("DBPORT", "5432")) + if err != nil { + DBPORT = 5432 + } + DBPOOLSIZE, err := strconv.Atoi(helper.GetEnv("DBPOOLSIZE", "20")) + if err != nil { + DBPOOLSIZE = 20 + } + DBNAME := helper.GetEnv("DBNAME", "magang") + logger.Info("MAIN", "DBDRIVER : ", DBDRIVER) + logger.Info("MAIN", "DBHOST : ", DBHOST) + logger.Info("MAIN", "DBPORT : ", DBPORT) + logger.Debug("MAIN", "DBUSER : ", DBUSER) + logger.Debug("MAIN", "DBPASS : ", DBPASS) + logger.Info("MAIN", "DBNAME : ", DBNAME) + + err = db.InitDB(DBDRIVER, DBHOST, DBPORT, DBUSER, DBPASS, DBNAME, DBPOOLSIZE) + if err != nil { + logger.Error("MAIN", "!!! FAILED TO INITIATE DB POOL..", err) + os.Exit(1) + } else { + logger.Info("MAIN", "Database Connection Pool Initated.") + } + + // CONFIGURE ENDPOINTS + paths := make(map[string]func(http.ResponseWriter, *http.Request)) + paths["/"] = handlers.Greeting + paths["/hello"] = handlers.Hello + paths["/auth"] = handlers.Auth + paths["/verify"] = handlers.Verify + + mux := http.NewServeMux() + for path, handler := range paths { + mux.HandleFunc(path, handler) + } + + // START SERVER + logger.Info("MAIN", "Server started on Port :48000") + http.ListenAndServe(":48000", corsMiddleware(mux)) +} diff --git a/src/moffas/moffas_auth.go b/src/moffas/moffas_auth.go new file mode 100644 index 0000000..4fe08bc --- /dev/null +++ b/src/moffas/moffas_auth.go @@ -0,0 +1,341 @@ +package moffas + +import ( + "encoding/json" + "errors" + "moffas_go/db" + "moffas_go/helper" + "moffas_go/logger" + "strconv" + "strings" + "time" +) + +type GeneratedChallenge struct { + Full_nonce string + User_id int64 + Salt string + Iterations int +} + +type VerificationResult struct { + Server_hash string + Final_nonce string + Session_id string + Privileges map[string]interface{} + Tier string + Full_name string + Organization_data map[string]interface{} +} + +func Generate_challenge(reference_id, username string, half_nonce string) (GeneratedChallenge, error) { + logger.Debug(reference_id, " -- start generate_challenge") + startTime := time.Now() + defer func() { + dur := time.Since(startTime) + logger.Debug(reference_id, " -- generate_challenge done in ", dur) + }() + + conn, err := db.GetConnection() + if err != nil { + return GeneratedChallenge{}, err + } + defer db.ReleaseConnection() + + current_time := time.Now().Unix() + nonce, err := helper.GenerateRandomString(8) + if err != nil { + return GeneratedChallenge{}, err + } + + full_nonce := half_nonce + nonce + + logger.Info(reference_id, "HALF NONCE : ", half_nonce) + logger.Info(reference_id, "USERNAME : ", username) + logger.Info(reference_id, "FULL NONCE : ", full_nonce) + + type DBResult struct { + User_id int64 `db:"id"` + Salt string `db:"salt"` + Iterations int `db:"iterations"` + Timestamp int64 `db:"tstamp"` + SaltedPasword string `db:"saltedpassword"` + } + + query := ` + SELECT + a.id,a.salt,a.saltedpassword,a.iterations, + COALESCE(b.tstamp,0) as tstamp + FROM servouser.user a + LEFT JOIN servouser.challenge_response b ON b.user_id = a.id + WHERE a.username = $1 AND a.st=1 LIMIT 1` + + sql := strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(query, "\t", " "), "\n", " "), " ", " ") + logger.Debug(reference_id, "SQL : ", sql) + + dbresult := DBResult{} + err = conn.Get(&dbresult, sql, username) + if err != nil { + return GeneratedChallenge{}, err + } + + logger.Info(reference_id, "USER ID : ", dbresult.User_id) + waittime, err := strconv.Atoi(helper.GetEnv("AUTH_WAIT_TIME", "10")) + if err != nil { + waittime = 10 + } + wait_until := dbresult.Timestamp + int64(waittime) + logger.Debug(reference_id, "LAST AUTH REQUEST : ", dbresult.Timestamp) + logger.Debug(reference_id, "WAIT TIME : ", waittime) + logger.Debug(reference_id, "WAIT UNTIL : ", wait_until) + logger.Debug(reference_id, "CURRENT TIME : ", current_time) + + if wait_until > current_time { + logger.Info(reference_id, "!!! WAIT TIME FOR THE USER ID HAS NOT ELAPSED") + err = errors.New("wait time has not elapsed") + return GeneratedChallenge{}, err + } + + //----- CONTEKAN DOANG ----- + sharedSecret := helper.GetEnv("SHARED_SECRET", "Winter is coming") + logger.Info(reference_id, "SHARED SECRET : ", sharedSecret) + logger.Info(reference_id, "SALTED PASSWORD : ", dbresult.SaltedPasword) + + hash1, err := helper.HMAC_SHA256(sharedSecret, dbresult.SaltedPasword) + if err != nil { + return GeneratedChallenge{}, err + } + logger.Info(reference_id, "HASH1 : ", hash1) + + calculated_client_hash, err := helper.HMAC_SHA256(full_nonce, hash1) + if err != nil { + return GeneratedChallenge{}, err + } + logger.Info(reference_id, "CALCULATED CLIENT HASH : ", calculated_client_hash) + //----- CONTEKAN DOANG ----- + + err = upsert_challenge(reference_id, full_nonce, dbresult.User_id) + if err != nil { + return GeneratedChallenge{}, err + } + return GeneratedChallenge{ + User_id: dbresult.User_id, + Salt: dbresult.Salt, + Iterations: dbresult.Iterations, + Full_nonce: full_nonce, + }, nil +} + +func Verify_challenge(reference_id string, full_nonce string, client_hash string, next_nonce string) (VerificationResult, error) { + logger.Debug(reference_id, " start verify_challenge") + startTime := time.Now() + defer func() { + dur := time.Since(startTime) + logger.Debug(reference_id, " -- verify_challenge done in ", dur) + }() + + conn, err := db.GetConnection() + if err != nil { + return VerificationResult{}, err + } + defer db.ReleaseConnection() + + current_time := time.Now().Unix() + nonce, err := helper.GenerateRandomString(8) + if err != nil { + return VerificationResult{}, err + } + + logger.Info(reference_id, "FULL NONCE : ", full_nonce) + + type DBResult struct { + User_id int64 `db:"user_id"` + Timestamp int64 `db:"tstamp"` + Fullname string `db:"full_name"` + Privileges string `db:"privileges"` + Tier string `db:"tier"` + SaltedPasword string `db:"saltedpassword"` + Organization_data string `db:"organization_data"` + } + dbresult := DBResult{} + + query := ` + SELECT + a.user_id, a.tstamp, + b.full_name, b.saltedpassword, + c.privileges, + d.tier, d.data organization_data + FROM servouser.challenge_response a + LEFT JOIN servouser.user b ON b.id = a.user_id + LEFT JOIN servouser.role c ON c.name = b.role + LEFT JOIN servouser.organization d ON d.id = b.organization_id + WHERE a.full_nonce = $1 AND b.id IS NOT NULL AND c.privileges IS NOT NULL AND b.st = 1 + LIMIT 1` + + sql := strings.ReplaceAll(strings.ReplaceAll(query, "\t", " "), "\n", " ") + logger.Debug(reference_id, "SQL : ", sql) + err = conn.Get(&dbresult, sql, full_nonce) + if err != nil { + return VerificationResult{}, err + } + rep_nonce := full_nonce[0:15] + logger.Debug(reference_id, "replacement nonce : ", rep_nonce) + + query = "UPDATE servouser.challenge_response SET full_nonce = $1 WHERE full_nonce = $2" + logger.Debug(reference_id, "SQL : ", query) + _, err = conn.Exec(query, rep_nonce, full_nonce) + if err != nil { + return VerificationResult{}, err + } + + timeout, err := strconv.Atoi(helper.GetEnv("AUTH_TIMEOUT", "300")) + if err != nil { + timeout = 300 + } + logger.Info(reference_id, "AUTH TIMEOUT : ", timeout) + + expire_limit := dbresult.Timestamp + int64(timeout) + logger.Info(reference_id, "AUTH EXPIRATION TIME : ", expire_limit) + logger.Info(reference_id, "CURRENT TIME : ", current_time) + if current_time > expire_limit { + logger.Error(reference_id, "!!! CHALLENGE EXPIRED") + err = errors.New("client expired") + return VerificationResult{}, err + } + + sharedSecret := helper.GetEnv("SHARED_SECRET", "Winter is coming") + logger.Info(reference_id, "SHARED SECRET : ", sharedSecret) + logger.Info(reference_id, "SALTED PASSWORD : ", dbresult.SaltedPasword) + + hash1, err := helper.HMAC_SHA256(sharedSecret, dbresult.SaltedPasword) + if err != nil { + return VerificationResult{}, err + } + logger.Info(reference_id, "HASH1 : ", hash1) + + calculated_client_hash, err := helper.HMAC_SHA256(full_nonce, hash1) + if err != nil { + return VerificationResult{}, err + } + logger.Info(reference_id, "CALCULATED CLIENT HASH : ", calculated_client_hash) + logger.Info(reference_id, "CLIENT HASH : ", client_hash) + if calculated_client_hash != client_hash { + logger.Warning(reference_id, "!!! CLIENT HASH DOES NOT MATCH STORED CHALLENGE RESPONSE") + err = errors.New("client hash does not match stored challenge response") + return VerificationResult{}, err + } + + logger.Info(reference_id, "NEXT NONCE : ", next_nonce) + final_nonce := next_nonce + nonce + logger.Info(reference_id, "FINAL NONCE : ", final_nonce) + + server_hash, err := helper.HMAC_SHA256(final_nonce, hash1) + if err != nil { + return VerificationResult{}, err + } + logger.Info(reference_id, "SERVER HASH : ", server_hash) + + session_secret, err := helper.HMAC_SHA256(full_nonce+final_nonce, hash1) + if err != nil { + return VerificationResult{}, err + } + logger.Info(reference_id, "SESSION SECRET : ", session_secret) + session_id, _ := helper.GenerateRandomString(16) + logger.Info(reference_id, "SESSION ID : ", session_id) + + err = upsert_session(reference_id, session_id, dbresult.User_id, session_secret) + if err != nil { + return VerificationResult{}, err + } + + privileges := make(map[string]interface{}) + organization_data := make(map[string]interface{}) + + err = json.Unmarshal([]byte(dbresult.Privileges), &privileges) + if err != nil { + return VerificationResult{}, err + } + + err = json.Unmarshal([]byte(dbresult.Organization_data), &organization_data) + if err != nil { + return VerificationResult{}, err + } + + return VerificationResult{ + Final_nonce: final_nonce, + Server_hash: server_hash, + Session_id: session_id, + Privileges: privileges, + Tier: dbresult.Tier, + Full_name: dbresult.Fullname, + Organization_data: organization_data, + }, nil +} + +func upsert_challenge(reference_id, full_nonce string, user_id int64) error { + logger.Debug(reference_id, " start upsert_challenge") + startTime := time.Now() + defer func() { + dur := time.Since(startTime) + logger.Debug(reference_id, " -- upsert_challenge done in ", dur) + }() + + conn, err := db.GetConnection() + if err != nil { + return err + } + defer db.ReleaseConnection() + + query := "DELETE FROM servouser.challenge_response WHERE full_nonce = $1 OR user_id = $2" + sql := strings.ReplaceAll(strings.ReplaceAll(query, "\t", " "), "\n", " ") + logger.Debug(reference_id, "SQL : ", sql) + _, err = conn.Exec(query, full_nonce, user_id) + if err != nil { + return err + } + + query = ` + INSERT INTO + servouser.challenge_response (full_nonce,user_id,challenge_response,tstamp) + VALUES + ($1,$2,'0123456789abcdefghijklmnopqrstuv',$3)` + sql = strings.ReplaceAll(strings.ReplaceAll(query, "\t", " "), "\n", " ") + logger.Debug(reference_id, "SQL : ", sql) + _, err = conn.Exec(sql, full_nonce, user_id, time.Now().Unix()) + + return err +} + +func upsert_session(reference_id, session_id string, user_id int64, session_secret string) error { + logger.Debug(reference_id, " start upsert_challenge") + + startTime := time.Now() + defer func() { + dur := time.Since(startTime) + logger.Debug(reference_id, " -- upsert_session done in ", dur) + }() + + conn, err := db.GetConnection() + if err != nil { + return err + } + defer db.ReleaseConnection() + + query := "DELETE FROM servouser.session WHERE session_id = $1 OR user_id = $2" + sql := strings.ReplaceAll(strings.ReplaceAll(query, "\t", " "), "\n", " ") + logger.Debug(reference_id, "SQL : ", sql) + _, err = conn.Exec(query, session_id, user_id) + if err != nil { + return err + } + query = ` + INSERT INTO + servouser.session (session_id,user_id,session_secret,tstamp,st,last_ms_tstamp,last_sequence) + VALUES + ($1,$2,$3,$4,1,0,0)` + + sql = strings.ReplaceAll(strings.ReplaceAll(query, "\t", " "), "\n", " ") + logger.Debug(reference_id, "SQL : ", sql) + _, err = conn.Exec(query, session_id, user_id, session_secret, time.Now().Unix()) + return err +} diff --git a/test/test_auth1.http b/test/test_auth1.http new file mode 100644 index 0000000..3504da8 --- /dev/null +++ b/test/test_auth1.http @@ -0,0 +1,29 @@ +### AUTH +POST https://servobot.ai/servoauth/auth +Content-Type: application/json + +{ + "username":"zadahouse", + "half_nonce":"01234567" +} + + +### VERIFY +POST https://servobot.ai/servoauth/verify +Content-Type: application/json + +{ + "full_nonce":"01234567eko42163", + "client_hash":"3ec5cf7ea34a41ca4675f24b8f9cc1696cd1c60479020c083ba724bf6e93b705", + "next_nonce":"76543210" +} + + +### GREETING +POST https://servobot.ai/servoauth/ +Content-Type: application/json + +{ + "name":"emil", + "password":"spider-man" +} \ No newline at end of file