diff --git a/api.go b/api.go new file mode 100644 index 0000000..d6a9ade --- /dev/null +++ b/api.go @@ -0,0 +1,165 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + + "github.com/gorilla/mux" +) + +type Response struct { + Ok bool `json:"ok"` + Message string `json:"message"` + Data interface{} `json:"data"` +} + +type SuccessClaim struct { + Name string `json:"name"` + PIN string `json:"pin"` + Invoice string `json:"invoice"` +} + +// not authenticated, if correct pin is provided call returns the SuccessClaim +func ClaimAddress(w http.ResponseWriter, r *http.Request) { + params := parseParams(r) + pin, inv, err := SaveName(params.Name, params, params.Pin) + if err != nil { + sendError(w, 400, "could not register name: %s", err.Error()) + return + } + + response := Response{ + Ok: true, + Message: fmt.Sprintf("claimed %v@%v", params.Name, s.Domain), + Data: SuccessClaim{params.Name, pin, inv}, + } + + // TODO: middleware for responses that adds this header + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(response) +} + +func GetUser(w http.ResponseWriter, r *http.Request) { + name := mux.Vars(r)["name"] + params, err := GetName(name) + if err != nil { + sendError(w, 400, err.Error()) + return + } + + // add pin to response because sometimes not saved in database; after first call to /api/v1/claim + params.Pin = ComputePIN(name) + + response := Response{ + Ok: true, + Message: fmt.Sprintf("%v@%v found", params.Name, s.Domain), + Data: params, + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(response) +} + +func UpdateUser(w http.ResponseWriter, r *http.Request) { + params := parseParams(r) + name := mux.Vars(r)["name"] + + // if pin not in json request body get it from header + if params.Pin == "" { + // TODO: work with Context()? + params.Pin = r.Header.Get("X-Pin") + } + + if _, _, err := SaveName(name, params, params.Pin); err != nil { + sendError(w, 500, err.Error()) + return + } + + updatedParams, err := GetName(name) + if err != nil { + sendError(w, 500, err.Error()) + return + } + + // return the updated values or just http.StatusCreated? + response := Response{ + Ok: true, + Message: fmt.Sprintf("updated %v@%v parameters", params.Name, s.Domain), + Data: updatedParams, + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(response) +} + +func DeleteUser(w http.ResponseWriter, r *http.Request) { + name := mux.Vars(r)["name"] + if err := DeleteName(name); err != nil { + sendError(w, 500, err.Error()) + return + } + + response := Response{ + Ok: true, + Message: fmt.Sprintf("deleted %v@%v", name, s.Domain), + Data: nil, + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(response) +} + +// authentication middleware +func authenticate(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // exempt /claim from authentication check; + if strings.HasPrefix(r.URL.Path, "/api/v1/claim") { + next.ServeHTTP(w, r) + return + } + + name := mux.Vars(r)["name"] + providedPin := r.Header.Get("X-Pin") + + var err error + + if providedPin == "" { + err = fmt.Errorf("X-Pin header not provided") + // pin should always be passed in header but search in json request body anyways + providedPin = parseParams(r).Pin + } + + if providedPin != ComputePIN(name) { + err = fmt.Errorf("wrong pin") + } + + if err != nil { + sendError(w, 401, "error fetching user: %s", err.Error()) + return + } + + next.ServeHTTP(w, r) + }) +} + +// helpers +func sendError(w http.ResponseWriter, code int, msg string, args ...interface{}) { + b, _ := json.Marshal(Response{false, fmt.Sprintf(msg, args...), nil}) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + w.Write(b) +} + +func parseParams(r *http.Request) *Params { + reqBody, _ := ioutil.ReadAll(r.Body) + var params Params + json.Unmarshal(reqBody, ¶ms) + return ¶ms +} diff --git a/db.go b/db.go index 5b37025..15b8fa4 100644 --- a/db.go +++ b/db.go @@ -13,12 +13,15 @@ import ( ) type Params struct { - Name string - Kind string - Host string - Key string - Pak string - Waki string + Name string `json:"name"` + Kind string `json:"kind"` + Host string `json:"host"` + Key string `json:"key"` + Pak string `json:"pak"` + Waki string `json:"waki"` + Pin string `json:"pin"` + MinSendable string `json:"minSendable"` + MaxSendable string `json:"maxSendable"` } func SaveName( @@ -29,16 +32,17 @@ func SaveName( name = strings.ToLower(name) key := []byte(name) - mac := hmac.New(sha256.New, []byte(s.Secret)) - mac.Write([]byte(name + "@" + s.Domain)) - pin = hex.EncodeToString(mac.Sum(nil)) + pin = ComputePIN(name) if _, closer, err := db.Get(key); err == nil { defer closer.Close() if pin != providedPin { - return "", "", errors.New("name already exists! must provide pin.") + return "", "", errors.New("name already exists! must provide pin") } } + if err != nil { + return "", "", errors.New("that name does not exist") + } params.Name = name @@ -73,3 +77,21 @@ func GetName(name string) (*Params, error) { params.Name = name return ¶ms, nil } + +func DeleteName(name string) error { + name = strings.ToLower(name) + key := []byte(name) + + if err := db.Delete(key, pebble.Sync); err != nil { + return err + } + + return nil +} + +func ComputePIN(name string) string { + name = strings.ToLower(name) + mac := hmac.New(sha256.New, []byte(s.Secret)) + mac.Write([]byte(name + "@" + s.Domain)) + return hex.EncodeToString(mac.Sum(nil)) +} diff --git a/lnurl.go b/lnurl.go index 4428a18..2630eff 100644 --- a/lnurl.go +++ b/lnurl.go @@ -28,11 +28,22 @@ func handleLNURL(w http.ResponseWriter, r *http.Request) { var commentLength int64 = 0 // TODO: support webhook comments + // convert configured sendable amounts to integer + minSendable, err := strconv.ParseInt(params.MinSendable, 10, 64) + // set defaults + if err != nil { + minSendable = 1000 + } + maxSendable, err := strconv.ParseInt(params.MaxSendable, 10, 64) + if err != nil { + maxSendable = 100000000 + } + json.NewEncoder(w).Encode(lnurl.LNURLPayResponse1{ LNURLResponse: lnurl.LNURLResponse{Status: "OK"}, Callback: fmt.Sprintf("https://%s/.well-known/lnurlp/%s", s.Domain, username), - MinSendable: 1000, - MaxSendable: 100000000, + MinSendable: minSendable, + MaxSendable: maxSendable, EncodedMetadata: makeMetadata(params), CommentAllowed: commentLength, Tag: "payRequest", diff --git a/main.go b/main.go index a9da523..14e1573 100644 --- a/main.go +++ b/main.go @@ -95,6 +95,17 @@ func main() { }, ) + api := router.PathPrefix("/api/v1").Subrouter() + api.Use(authenticate) + + // unauthenticated + api.HandleFunc("/claim", ClaimAddress).Methods("POST") + + // authenticated routes; X-Pin in header or in json request body + api.HandleFunc("/users/{name}", GetUser).Methods("GET") + api.HandleFunc("/users/{name}", UpdateUser).Methods("PUT") + api.HandleFunc("/users/{name}", DeleteUser).Methods("DELETE") + srv := &http.Server{ Handler: router, Addr: s.Host + ":" + s.Port,