forked from heierbtc/satdress-mirror
JSON API (#28)
Co-authored-by: Raphael <raphael.schleithoff@tum.de> Co-authored-by: Raphael <raphjaph@protonmail.com>
This commit is contained in:
parent
3102708352
commit
a8fd253bc3
165
api.go
Normal file
165
api.go
Normal file
|
@ -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
|
||||||
|
}
|
42
db.go
42
db.go
|
@ -13,12 +13,15 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Params struct {
|
type Params struct {
|
||||||
Name string
|
Name string `json:"name"`
|
||||||
Kind string
|
Kind string `json:"kind"`
|
||||||
Host string
|
Host string `json:"host"`
|
||||||
Key string
|
Key string `json:"key"`
|
||||||
Pak string
|
Pak string `json:"pak"`
|
||||||
Waki string
|
Waki string `json:"waki"`
|
||||||
|
Pin string `json:"pin"`
|
||||||
|
MinSendable string `json:"minSendable"`
|
||||||
|
MaxSendable string `json:"maxSendable"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func SaveName(
|
func SaveName(
|
||||||
|
@ -29,16 +32,17 @@ func SaveName(
|
||||||
name = strings.ToLower(name)
|
name = strings.ToLower(name)
|
||||||
key := []byte(name)
|
key := []byte(name)
|
||||||
|
|
||||||
mac := hmac.New(sha256.New, []byte(s.Secret))
|
pin = ComputePIN(name)
|
||||||
mac.Write([]byte(name + "@" + s.Domain))
|
|
||||||
pin = hex.EncodeToString(mac.Sum(nil))
|
|
||||||
|
|
||||||
if _, closer, err := db.Get(key); err == nil {
|
if _, closer, err := db.Get(key); err == nil {
|
||||||
defer closer.Close()
|
defer closer.Close()
|
||||||
if pin != providedPin {
|
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
|
params.Name = name
|
||||||
|
|
||||||
|
@ -73,3 +77,21 @@ func GetName(name string) (*Params, error) {
|
||||||
params.Name = name
|
params.Name = name
|
||||||
return ¶ms, nil
|
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))
|
||||||
|
}
|
||||||
|
|
15
lnurl.go
15
lnurl.go
|
@ -28,11 +28,22 @@ func handleLNURL(w http.ResponseWriter, r *http.Request) {
|
||||||
var commentLength int64 = 0
|
var commentLength int64 = 0
|
||||||
// TODO: support webhook comments
|
// 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{
|
json.NewEncoder(w).Encode(lnurl.LNURLPayResponse1{
|
||||||
LNURLResponse: lnurl.LNURLResponse{Status: "OK"},
|
LNURLResponse: lnurl.LNURLResponse{Status: "OK"},
|
||||||
Callback: fmt.Sprintf("https://%s/.well-known/lnurlp/%s", s.Domain, username),
|
Callback: fmt.Sprintf("https://%s/.well-known/lnurlp/%s", s.Domain, username),
|
||||||
MinSendable: 1000,
|
MinSendable: minSendable,
|
||||||
MaxSendable: 100000000,
|
MaxSendable: maxSendable,
|
||||||
EncodedMetadata: makeMetadata(params),
|
EncodedMetadata: makeMetadata(params),
|
||||||
CommentAllowed: commentLength,
|
CommentAllowed: commentLength,
|
||||||
Tag: "payRequest",
|
Tag: "payRequest",
|
||||||
|
|
11
main.go
11
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{
|
srv := &http.Server{
|
||||||
Handler: router,
|
Handler: router,
|
||||||
Addr: s.Host + ":" + s.Port,
|
Addr: s.Host + ":" + s.Port,
|
||||||
|
|
Loading…
Reference in New Issue
Block a user