diff --git a/createboltcard.go b/createboltcard.go new file mode 100644 index 0000000..c2d291d --- /dev/null +++ b/createboltcard.go @@ -0,0 +1,141 @@ +package main + +import ( + "crypto/rand" + "encoding/hex" + "github.com/boltcard/boltcard/db" + "github.com/boltcard/boltcard/resp_err" + log "github.com/sirupsen/logrus" + "net/http" + "strconv" + "strings" +) + +func random_hex() string { + b := make([]byte, 16) + _, err := rand.Read(b) + if err != nil { + log.Warn(err.Error()) + return "" + } + + return hex.EncodeToString(b) +} + +func createboltcard(w http.ResponseWriter, r *http.Request) { + if db.Get_setting("FUNCTION_INTERNAL_API") != "ENABLE" { + msg := "createboltcard: internal API function is not enabled" + log.Debug(msg) + resp_err.Write_message(w, msg) + return + } + + tx_max_str := r.URL.Query().Get("tx_max") + tx_max, err := strconv.Atoi(tx_max_str) + if err != nil { + msg := "createboltcard: tx_max is not a valid integer" + log.Warn(msg) + resp_err.Write_message(w, msg) + return + } + + day_max_str := r.URL.Query().Get("day_max") + day_max, err := strconv.Atoi(day_max_str) + if err != nil { + msg := "createboltcard: day_max is not a valid integer" + log.Warn(msg) + resp_err.Write_message(w, msg) + return + } + + enable_flag_str := r.URL.Query().Get("enable") + enable_flag, err := strconv.ParseBool(enable_flag_str) + if err != nil { + msg := "createboltcard: enable is not a valid boolean" + log.Warn(msg) + resp_err.Write_message(w, msg) + return + } + + card_name := r.URL.Query().Get("card_name") + + uid_privacy_flag_str := r.URL.Query().Get("uid_privacy") + uid_privacy_flag, err := strconv.ParseBool(uid_privacy_flag_str) + if err != nil { + msg := "createboltcard: uid_privacy is not a valid boolean" + log.Warn(msg) + resp_err.Write_message(w, msg) + return + } + + allow_neg_bal_flag_str := r.URL.Query().Get("allow_neg_bal") + allow_neg_bal_flag, err := strconv.ParseBool(allow_neg_bal_flag_str) + if err != nil { + msg := "createboltcard: allow_neg_bal is not a valid boolean" + log.Warn(msg) + resp_err.Write_message(w, msg) + return + } + + // check if card_name already exists + + card_count, err := db.Get_card_name_count(card_name) + if err != nil { + log.Warn(err.Error()) + return + } + + if card_count > 0 { + msg := "createboltcard: the card name already exists in the database" + log.Warn(msg) + resp_err.Write_message(w, msg) + return + } + + // log the request + + log.WithFields(log.Fields{ + "card_name": card_name, "tx_max": tx_max, "day_max": day_max, + "enable": enable_flag, "uid_privacy": uid_privacy_flag, + "allow_neg_bal": allow_neg_bal_flag}).Info("createboltcard API request") + + // create the keys + + one_time_code := random_hex() + k0_auth_key := random_hex() + k2_cmac_key := random_hex() + k3 := random_hex() + k4 := random_hex() + + // create the new card record + + err = db.Insert_card(one_time_code, k0_auth_key, k2_cmac_key, k3, k4, + tx_max, day_max, enable_flag, card_name, + uid_privacy_flag, allow_neg_bal_flag) + if err != nil { + log.Warn(err.Error()) + return + } + + // return the URI + one_time_code + + hostdomain := db.Get_setting("HOST_DOMAIN") + url := "" + if strings.HasSuffix(hostdomain, ".onion") { + url = "http://" + hostdomain + "/new?a=" + one_time_code + } else { + url = "https://" + hostdomain + "/new?a=" + one_time_code + } + + // log the response + + log.WithFields(log.Fields{ + "card_name": card_name, "url": url}).Info("createboltcard API response") + + jsonData := []byte(`{"status":"OK",` + + `"url":"` + url + `"}`) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write(jsonData) +} diff --git a/db/db.go b/db/db.go index 33ed840..4a868fd 100644 --- a/db/db.go +++ b/db/db.go @@ -46,6 +46,16 @@ type Transaction struct { Tx_time string } +type Card_wipe_info struct { + Id int + K0 string + K1 string + K2 string + K3 string + K4 string + Uid string +} + func open() (*sql.DB, error) { // get connection string from environment variables @@ -706,3 +716,121 @@ func Get_card_total_sats(card_id int) (int, error) { return card_total_sats, nil } + +func Get_card_name_count(card_name string) (card_count int, err error) { + + card_count = 0 + + db, err := open() + if err != nil { + return 0, err + } + defer db.Close() + + sqlStatement := `SELECT COUNT(card_id) FROM cards WHERE card_name = $1;` + + row := db.QueryRow(sqlStatement, card_name) + err = row.Scan(&card_count) + if err != nil { + return 0, err + } + + return card_count, nil +} + +func Insert_card(one_time_code string, k0_auth_key string, k2_cmac_key string, k3 string, k4 string, + tx_max_sats int, day_max_sats int, lnurlw_enable bool, card_name string, uid_privacy bool, + allow_neg_bal_ptr bool) error { + + lnurlw_enable_yn := "N" + if lnurlw_enable { + lnurlw_enable_yn = "Y" + } + + uid_privacy_yn := "N" + if uid_privacy { + uid_privacy_yn = "Y" + } + + allow_neg_bal_yn := "N" + if allow_neg_bal_ptr { + allow_neg_bal_yn = "Y" + } + + db, err := open() + if err != nil { + return err + } + defer db.Close() + + // insert a new record into cards + + sqlStatement := `INSERT INTO cards` + + ` (one_time_code, k0_auth_key, k2_cmac_key, k3, k4, uid, last_counter_value,` + + ` lnurlw_request_timeout_sec, tx_limit_sats, day_limit_sats, lnurlw_enable,` + + ` one_time_code_used, card_name, uid_privacy, allow_negative_balance)` + + ` VALUES ($1, $2, $3, $4, $5, '', 0, 60, $6, $7, $8, 'N', $9, $10, $11);` + res, err := db.Exec(sqlStatement, one_time_code, k0_auth_key, k2_cmac_key, k3, k4, + tx_max_sats, day_max_sats, lnurlw_enable_yn, card_name, uid_privacy_yn, + allow_neg_bal_yn) + if err != nil { + return err + } + count, err := res.RowsAffected() + if err != nil { + return err + } + if count != 1 { + return errors.New("not one card record inserted") + } + + return nil +} + +func Wipe_card(card_name string) (*Card_wipe_info, error) { + + card_wipe_info := Card_wipe_info{} + + db, err := open() + if err != nil { + return &card_wipe_info, err + } + defer db.Close() + + // set card as wiped and disabled + + sqlStatement := `UPDATE cards SET` + + ` lnurlw_enable = 'N', lnurlp_enable = 'N', email_enable = 'N', wiped = 'Y'` + + ` WHERE card_name = $1;` + res, err := db.Exec(sqlStatement, card_name) + if err != nil { + return &card_wipe_info, err + } + count, err := res.RowsAffected() + if err != nil { + return &card_wipe_info, err + } + if count != 1 { + return &card_wipe_info, errors.New("not one card record updated") + } + + // get card keys + + sqlStatement = `SELECT card_id, uid, k0_auth_key, k2_cmac_key, k3, k4` + + ` FROM cards WHERE card_name = $1;` + row := db.QueryRow(sqlStatement, card_name) + err = row.Scan( + &card_wipe_info.Id, + &card_wipe_info.Uid, + &card_wipe_info.K0, + &card_wipe_info.K2, + &card_wipe_info.K3, + &card_wipe_info.K4) + if err != nil { + return &card_wipe_info, err + } + + card_wipe_info.K1 = Get_setting("AES_DECRYPT_KEY") + + return &card_wipe_info, nil +} diff --git a/email/email.go b/email/email.go index 2b123e7..8f78ddd 100644 --- a/email/email.go +++ b/email/email.go @@ -6,10 +6,10 @@ import ( "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/ses" + "github.com/boltcard/boltcard/db" log "github.com/sirupsen/logrus" "strconv" "strings" - "github.com/boltcard/boltcard/db" ) func Send_balance_email(recipient_email string, card_id int) { diff --git a/lndhub/lndhub.go b/lndhub/lndhub.go index e0d6153..92780a8 100644 --- a/lndhub/lndhub.go +++ b/lndhub/lndhub.go @@ -1,10 +1,10 @@ package lndhub import ( - log "github.com/sirupsen/logrus" +// log "github.com/sirupsen/logrus" - "github.com/boltcard/boltcard/db" - "github.com/boltcard/boltcard/email" +// "github.com/boltcard/boltcard/db" +// "github.com/boltcard/boltcard/email" ) func Pay_invoice(card_payment_id int, invoice string) { diff --git a/lnurlp/lnurlp_callback.go b/lnurlp/lnurlp_callback.go index 84fb023..1984e8c 100644 --- a/lnurlp/lnurlp_callback.go +++ b/lnurlp/lnurlp_callback.go @@ -2,13 +2,13 @@ package lnurlp import ( "encoding/hex" + "github.com/boltcard/boltcard/db" + "github.com/boltcard/boltcard/lnd" + "github.com/boltcard/boltcard/resp_err" "github.com/gorilla/mux" log "github.com/sirupsen/logrus" "net/http" "strconv" - "github.com/boltcard/boltcard/lnd" - "github.com/boltcard/boltcard/db" - "github.com/boltcard/boltcard/resp_err" ) func Callback(w http.ResponseWriter, r *http.Request) { diff --git a/lnurlp/lnurlp_request.go b/lnurlp/lnurlp_request.go index a7adfbf..6483388 100644 --- a/lnurlp/lnurlp_request.go +++ b/lnurlp/lnurlp_request.go @@ -1,11 +1,11 @@ package lnurlp import ( + "github.com/boltcard/boltcard/db" + "github.com/boltcard/boltcard/resp_err" "github.com/gorilla/mux" log "github.com/sirupsen/logrus" "net/http" - "github.com/boltcard/boltcard/db" - "github.com/boltcard/boltcard/resp_err" ) func Response(w http.ResponseWriter, r *http.Request) { diff --git a/lnurlw/lnurlw_callback.go b/lnurlw/lnurlw_callback.go index 5d31e27..d502290 100644 --- a/lnurlw/lnurlw_callback.go +++ b/lnurlw/lnurlw_callback.go @@ -1,14 +1,14 @@ package lnurlw import ( - decodepay "github.com/fiatjaf/ln-decodepay" - log "github.com/sirupsen/logrus" - "net/http" "bytes" - "io" "github.com/boltcard/boltcard/db" "github.com/boltcard/boltcard/lnd" "github.com/boltcard/boltcard/resp_err" + decodepay "github.com/fiatjaf/ln-decodepay" + log "github.com/sirupsen/logrus" + "io" + "net/http" ) func lndhub_payment(w http.ResponseWriter, p *db.Payment) { @@ -28,7 +28,7 @@ func lndhub_payment(w http.ResponseWriter, p *db.Payment) { //the login JSON is held in the Card_name field body := []byte(c.Card_name) - r, err := http.NewRequest("POST", lndhub_url + "/auth", bytes.NewBuffer(body)) + r, err := http.NewRequest("POST", lndhub_url+"/auth", bytes.NewBuffer(body)) if err != nil { log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id}).Warn(err) resp_err.Write(w) @@ -57,7 +57,7 @@ func lndhub_payment(w http.ResponseWriter, p *db.Payment) { log.Info(string(b)) -// fmt.Println(string(b)) + // fmt.Println(string(b)) //lndhub.payinvoice API call } diff --git a/lnurlw/lnurlw_request.go b/lnurlw/lnurlw_request.go index b0a70b3..c3a25e2 100644 --- a/lnurlw/lnurlw_request.go +++ b/lnurlw/lnurlw_request.go @@ -4,14 +4,14 @@ import ( "encoding/hex" "encoding/json" "errors" + "github.com/boltcard/boltcard/crypto" + "github.com/boltcard/boltcard/db" + "github.com/boltcard/boltcard/resp_err" log "github.com/sirupsen/logrus" "net/http" "os" "strconv" "strings" - "github.com/boltcard/boltcard/db" - "github.com/boltcard/boltcard/crypto" - "github.com/boltcard/boltcard/resp_err" ) type ResponseData struct { diff --git a/main.go b/main.go index 6460e3c..40a0ed0 100644 --- a/main.go +++ b/main.go @@ -1,13 +1,13 @@ package main import ( + "github.com/boltcard/boltcard/db" + "github.com/boltcard/boltcard/lnurlp" + "github.com/boltcard/boltcard/lnurlw" "github.com/gorilla/mux" log "github.com/sirupsen/logrus" "net/http" "time" - "github.com/boltcard/boltcard/db" - "github.com/boltcard/boltcard/lnurlw" - "github.com/boltcard/boltcard/lnurlp" ) var router = mux.NewRouter() @@ -30,26 +30,51 @@ func main() { DisableHTMLEscape: true, }) + var external_router = mux.NewRouter() + var internal_router = mux.NewRouter() + + // external API + + // ping + external_router.Path("/ping").Methods("GET").HandlerFunc(external_ping) // createboltcard - router.Path("/new").Methods("GET").HandlerFunc(new_card_request) + external_router.Path("/new").Methods("GET").HandlerFunc(new_card_request) // lnurlw for pos - router.Path("/ln").Methods("GET").HandlerFunc(lnurlw.Response) - router.Path("/cb").Methods("GET").HandlerFunc(lnurlw.Callback) + external_router.Path("/ln").Methods("GET").HandlerFunc(lnurlw.Response) + external_router.Path("/cb").Methods("GET").HandlerFunc(lnurlw.Callback) // lnurlp for lightning address - router.Path("/.well-known/lnurlp/{name}").Methods("GET").HandlerFunc(lnurlp.Response) - router.Path("/lnurlp/{name}").Methods("GET").HandlerFunc(lnurlp.Callback) + external_router.Path("/.well-known/lnurlp/{name}").Methods("GET").HandlerFunc(lnurlp.Response) + external_router.Path("/lnurlp/{name}").Methods("GET").HandlerFunc(lnurlp.Callback) + + // internal API + // this has no authentication and is not to be exposed publicly + // it exists for use on a private virtual network within a docker container + + internal_router.Path("/ping").Methods("GET").HandlerFunc(internal_ping) + internal_router.Path("/createboltcard").Methods("GET").HandlerFunc(createboltcard) + internal_router.Path("/wipeboltcard").Methods("GET").HandlerFunc(wipeboltcard) port := db.Get_setting("HOST_PORT") if port == "" { port = "9000" } - srv := &http.Server{ - Handler: router, + external_server := &http.Server{ + Handler: external_router, Addr: ":" + port, // consider adding host WriteTimeout: 30 * time.Second, ReadTimeout: 30 * time.Second, } - srv.ListenAndServe() + internal_server := &http.Server{ + Handler: internal_router, + Addr: ":9001", + WriteTimeout: 5 * time.Second, + ReadTimeout: 5 * time.Second, + } + + go external_server.ListenAndServe() + go internal_server.ListenAndServe() + + select {} } diff --git a/new_card_request.go b/new_card_request.go index f74f4e8..5e7e545 100644 --- a/new_card_request.go +++ b/new_card_request.go @@ -3,10 +3,10 @@ package main import ( "database/sql" "encoding/json" - log "github.com/sirupsen/logrus" - "net/http" "github.com/boltcard/boltcard/db" "github.com/boltcard/boltcard/resp_err" + log "github.com/sirupsen/logrus" + "net/http" ) /** diff --git a/ping.go b/ping.go new file mode 100644 index 0000000..d42eabe --- /dev/null +++ b/ping.go @@ -0,0 +1,20 @@ +package main + +import ( + "net/http" +) + +func external_ping(w http.ResponseWriter, req *http.Request) { + ping(w, "external API") +} + +func internal_ping(w http.ResponseWriter, req *http.Request) { + ping(w, "internal API") +} + +func ping(w http.ResponseWriter, message string) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + jsonData := []byte(`{"status":"OK","pong":"` + message + `"}`) + w.Write(jsonData) +} diff --git a/wipeboltcard.go b/wipeboltcard.go new file mode 100644 index 0000000..3fa52fe --- /dev/null +++ b/wipeboltcard.go @@ -0,0 +1,75 @@ +package main + +import ( + "github.com/boltcard/boltcard/db" + "github.com/boltcard/boltcard/resp_err" + log "github.com/sirupsen/logrus" + "net/http" + "strconv" +) + +func wipeboltcard(w http.ResponseWriter, r *http.Request) { + if db.Get_setting("FUNCTION_INTERNAL_API") != "ENABLE" { + msg := "wipeboltcard: internal API function is not enabled" + log.Debug(msg) + resp_err.Write_message(w, msg) + return + } + + card_name := r.URL.Query().Get("card_name") + + // check if card_name has been given + + if card_name == "" { + msg := "wipeboltcard: the card name must be set" + log.Warn(msg) + resp_err.Write_message(w, msg) + return + } + + // check if card_name exists + + card_count, err := db.Get_card_name_count(card_name) + + if card_count == 0 { + msg := "the card name does not exist in the database" + log.Warn(msg) + resp_err.Write_message(w, msg) + return + } + + // set the card as wiped and disabled, get the keys + + card_wipe_info_values, err := db.Wipe_card(card_name) + if err != nil { + log.Warn(err.Error()) + return + } + + // log the request + + log.WithFields(log.Fields{ + "card_name": card_name}).Info("wipeboltcard API request") + + // generate a response + + jsonData := `{"status":"OK",` + + `"action": "wipe",` + + `"id": ` + strconv.Itoa(card_wipe_info_values.Id) + `,` + + `"k0": "` + card_wipe_info_values.K0 + `",` + + `"k1": "` + card_wipe_info_values.K1 + `",` + + `"k2": "` + card_wipe_info_values.K2 + `",` + + `"k3": "` + card_wipe_info_values.K3 + `",` + + `"k4": "` + card_wipe_info_values.K4 + `",` + + `"uid": "` + card_wipe_info_values.Uid + `",` + + `"version": 1}` + + // log the response + + log.WithFields(log.Fields{ + "card_name": card_name, "response": jsonData}).Info("wipeboltcard API response") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(jsonData)) +}