Skip to content

Commit

Permalink
Merge pull request #29 from mikkelhegn/mikkelhegn/issue21
Browse files Browse the repository at this point in the history
Doesn't support keys with a leading forward slash `/`' for GET
  • Loading branch information
mikkelhegn authored Jan 23, 2024
2 parents 012bd85 + 7e20afe commit 1243d8e
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 38 deletions.
30 changes: 24 additions & 6 deletions explorer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,23 @@ <h5 class="modal-title" id="viewModalLabel">View Value</h5>
$("#kv > tbody:first").empty();
}

// Encode a string, so that it can safely be used as part of the URI on GET and DELETE methods
// A base64 encoded string representation (using `-` instead of `/`) of the UTF-8 encoded byte representation of the string
// That should support most character sets - including this guy: 🎅🏻
// Similar methods needs to be implemented by the receiving api
function encodeSafeKey(key) {
let encoder = new TextEncoder();
let encodedKey = encoder.encode(key)
let binString = String.fromCodePoint(...encodedKey)
let base64Key = btoa(binString)
return base64Key.replaceAll("/", "-")
}

function base64ToBytes(base64) {
const binString = atob(base64);
return Uint8Array.from(binString, (m) => m.codePointAt(0));
}

function insert(key) {
let hashedKey = simpleHash(key);
$("#kv > tbody:first").append(`
Expand All @@ -347,18 +364,19 @@ <h5 class="modal-title" id="viewModalLabel">View Value</h5>
</tr>`);

$(`#${hashedKey}View`).click(function () {
var key = $(this).data("key");
fetch(`{{.SpinRoute}}api/stores/${storeLabel}/keys/${encodeURIComponent(key)}`).then((response) => response.json()).then((data) => {
let value = atob(data.value);
let key = $(this).data("key");
let safeKey = encodeSafeKey(key);
fetch(`{{.SpinRoute}}api/stores/${storeLabel}/keys/${safeKey}`).then((response) => response.json()).then((data) => {
let value = new TextDecoder().decode(base64ToBytes(data.value));
$("#valueContent").text(value);
$("#viewModal").modal("show");
});
});


$(`#${hashedKey}Delete`).click(function () {
var key = $(this).data("key");
fetch(`{{.SpinRoute}}api/stores/${storeLabel}/keys/${encodeURIComponent(key)}`, {
let key = $(this).data("key");
let safeKey = encodeSafeKey(key);
fetch(`{{.SpinRoute}}api/stores/${storeLabel}/keys/${safeKey}`, {
method: 'DELETE',
})
.then(() => {
Expand Down
77 changes: 45 additions & 32 deletions explorer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,19 @@ package main
import (
"crypto/subtle"
_ "embed"
"encoding/base64"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"path"
"strings"
"time"

spinhttp "github.com/fermyon/spin/sdk/go/v2/http"
kv "github.com/fermyon/spin/sdk/go/v2/kv"
"github.com/fermyon/spin/sdk/go/v2/variables"

spin "github.com/fermyon/spin/sdk/go/v2/http"
"github.com/fermyon/spin/sdk/go/v2/kv"
)

var KV_STORE_CREDENTIALS_KEY string = "kv_credentials"
Expand Down Expand Up @@ -45,9 +46,12 @@ type ListResult struct {
Keys []string `json:"keys"`
}

var logger *log.Logger

func init() {
// The entry point to a Spin HTTP request using the Go SDK.
spin.Handle(func(w http.ResponseWriter, r *http.Request) {
spinhttp.Handle(func(w http.ResponseWriter, r *http.Request) {
logger = log.New(os.Stderr, "", log.LstdFlags)
serve(w, r)
})
}
Expand Down Expand Up @@ -81,7 +85,7 @@ func serve(w http.ResponseWriter, r *http.Request) {
}

spinRoute = getBasePath(r.Header)
router := spin.NewRouter()
router := spinhttp.NewRouter()

// Access to the list, get, create, and delete KV pairs endpoints is behind basic auth,
// with the credentials stored in the config store.
Expand All @@ -98,29 +102,28 @@ func serve(w http.ResponseWriter, r *http.Request) {
}

// UIHandler is the HTTP handler for the UI of the application.
func UIHandler(w http.ResponseWriter, _ *http.Request, _ spin.Params) {
func UIHandler(w http.ResponseWriter, _ *http.Request, _ spinhttp.Params) {
out := strings.ReplaceAll(HTMLTemplate, "{{.SpinRoute}}", spinRoute)
w.Write([]byte(out))

}

// ListKeysHandler is the HTTP handler for a list keys request.
func ListKeysHandler(w http.ResponseWriter, _ *http.Request, p spin.Params) {
func ListKeysHandler(w http.ResponseWriter, _ *http.Request, p spinhttp.Params) {
storeName := p.ByName("store")

store, err := kv.OpenStore(storeName)
if err != nil {
fmt.Printf("ERROR: cannot open store: %v\n", err)
http.Error(w, err.Error(), http.StatusUnauthorized)
logger.Printf("ERROR: cannot open store: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer store.Close()

start := time.Now()
keys, err := store.GetKeys()
fmt.Printf("LIST operation took: %s\n", time.Since(start))
logger.Printf("LIST operation took: %s\n", time.Since(start))
if err != nil {
fmt.Printf("ERROR: cannot list keys: %v\n", err)
logger.Printf("ERROR: cannot list keys: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
Expand All @@ -130,23 +133,23 @@ func ListKeysHandler(w http.ResponseWriter, _ *http.Request, p spin.Params) {
}

// GetKeyHandler is the HTTP handler for a get key request.
func GetKeyHandler(w http.ResponseWriter, _ *http.Request, p spin.Params) {
func GetKeyHandler(w http.ResponseWriter, _ *http.Request, p spinhttp.Params) {
storeName := p.ByName("store")
key := p.ByName("key")
safeKey := DecodeSafeKey(key)

store, err := kv.OpenStore(storeName)
if err != nil {
fmt.Printf("ERROR: cannot open store: %v\n", err)
logger.Printf("ERROR: cannot open store: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer store.Close()

start := time.Now()
value, err := store.Get(key)
fmt.Printf("GET operation took %s\n", time.Since(start))
value, err := store.Get(string(safeKey))
logger.Printf("GET operation took %s\n", time.Since(start))
if err != nil {
fmt.Printf("ERROR: cannot get key: %v\n", err)
logger.Printf("ERROR: cannot get key: %v\n", err)
w.WriteHeader(http.StatusNotFound)
}

Expand All @@ -156,29 +159,29 @@ func GetKeyHandler(w http.ResponseWriter, _ *http.Request, p spin.Params) {
}

// DeleteKeyHandler is the HTTP handler for a delete key request.
func DeleteKeyHandler(w http.ResponseWriter, _ *http.Request, p spin.Params) {
func DeleteKeyHandler(w http.ResponseWriter, _ *http.Request, p spinhttp.Params) {
storeName := p.ByName("store")
key := p.ByName("key")
safeKey := DecodeSafeKey(key)

store, err := kv.OpenStore(storeName)
if err != nil {
fmt.Printf("ERROR: cannot open store: %v\n", err)
logger.Printf("ERROR: cannot open store: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer store.Close()

start := time.Now()
err = store.Delete(key)
fmt.Printf("DELETE operation took %s\n", time.Since(start))
err = store.Delete(string(safeKey))
logger.Printf("DELETE operation took %s\n", time.Since(start))
if err != nil {
fmt.Printf("ERROR: cannot delete key: %v\n", err)
logger.Printf("ERROR: cannot delete key: %v\n", err)
w.WriteHeader(http.StatusNotFound)
}
}

// AddKeyHandler is the HTTP handler for an add key/value pair request.
func AddKeyHandler(w http.ResponseWriter, r *http.Request, p spin.Params) {
func AddKeyHandler(w http.ResponseWriter, r *http.Request, p spinhttp.Params) {
storeName := p.ByName("store")

var input SetRequest
Expand All @@ -189,17 +192,16 @@ func AddKeyHandler(w http.ResponseWriter, r *http.Request, p spin.Params) {

store, err := kv.OpenStore(storeName)
if err != nil {
fmt.Printf("ERROR: cannot open store: %v\n", err)
logger.Printf("ERROR: cannot open store: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer store.Close()

start := time.Now()
err = store.Set(input.Key, []byte(input.Value))
fmt.Printf("SET operation took %s\n", time.Since(start))
logger.Printf("SET operation took %s\n", time.Since(start))
if err != nil {
fmt.Printf("ERROR: cannot add key: %v\n", err)
logger.Printf("ERROR: cannot add key: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
Expand All @@ -210,8 +212,8 @@ func ShouldSkipAuth() bool {
}

// BasicAuth is a middleware that checks for basic auth credentials in a request.
func BasicAuth(h spin.RouterHandle, requiredUser, requiredPassword string) spin.RouterHandle {
return func(w http.ResponseWriter, r *http.Request, ps spin.Params) {
func BasicAuth(h spinhttp.RouterHandle, requiredUser, requiredPassword string) spinhttp.RouterHandle {
return func(w http.ResponseWriter, r *http.Request, ps spinhttp.Params) {
// This scenario is only intended for the local scenario, and skips basic authentication
// when the environment variable is set.
if ShouldSkipAuth() {
Expand All @@ -220,14 +222,15 @@ func BasicAuth(h spin.RouterHandle, requiredUser, requiredPassword string) spin.
return
}

logger.Printf("Authenticating")
// Get the Basic Authentication credentials
user, password, hasAuth := r.BasicAuth()

if hasAuth && subtle.ConstantTimeCompare([]byte(user), []byte(requiredUser)) == 1 && subtle.ConstantTimeCompare([]byte(password), []byte(requiredPassword)) == 1 {
// Delegate request to the given handle
h(w, r, ps)
} else {
fmt.Printf("ERROR: Unauthenticated request\n")
logger.Printf("ERROR: Unauthenticated request\n")
// Request Basic Authentication otherwise
w.Header().Set("WWW-Authenticate", "Basic realm=Restricted")
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
Expand All @@ -250,4 +253,14 @@ func GetCredentials() (string, string, error) {
return split[0], split[1], nil
}

// Decodes the URI safe code, sent by the client, for GET and DELETE operations
func DecodeSafeKey(key string) []byte {
base64Key := strings.Replace(key, "-", "/", -1)
keyAsBytes, err := base64.StdEncoding.DecodeString(base64Key)
if err != nil {
logger.Printf("Error decoding key:", err)
}
return keyAsBytes
}

func main() {}

0 comments on commit 1243d8e

Please sign in to comment.