Last active
March 18, 2023 16:27
-
-
Save amlwwalker/407a04c9b21915eee067a5531394f542 to your computer and use it in GitHub Desktop.
helpers for greenfinch api
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"bytes" | |
"context" | |
"encoding/json" | |
"flag" | |
"fmt" | |
client2 "github.com/configwizard/gaspump-api/pkg/client" | |
container2 "github.com/configwizard/gaspump-api/pkg/container" | |
eacl2 "github.com/configwizard/gaspump-api/pkg/eacl" | |
"github.com/configwizard/gaspump-api/pkg/wallet" | |
"github.com/nspcc-dev/neofs-sdk-go/acl" | |
"github.com/nspcc-dev/neofs-sdk-go/client" | |
"github.com/nspcc-dev/neofs-sdk-go/container" | |
"io/ioutil" | |
"log" | |
"os" | |
"time" | |
) | |
const usage = `Example | |
$ ./createContainer -wallets ./sample_wallets/wallet.json | |
password is password | |
` | |
var ( | |
walletPath = flag.String("wallets", "", "path to JSON wallets file") | |
walletAddr = flag.String("address", "", "wallets address [optional]") | |
createWallet = flag.Bool("create", false, "create a wallets") | |
password = flag.String("password", "", "wallet password") | |
permission = flag.String("permission", "", "permissions on container (public)") | |
) | |
func main() { | |
flag.Usage = func() { | |
_, _ = fmt.Fprintf(os.Stderr, usage) | |
flag.PrintDefaults() | |
} | |
flag.Parse() | |
ctx := context.Background() | |
if *createWallet { | |
secureWallet, err := wallet.GenerateNewSecureWallet(*walletPath, "some account label", *password) | |
if err != nil { | |
log.Fatal("error generating wallets", err) | |
} | |
file, _ := json.MarshalIndent(secureWallet, "", " ") | |
_ = ioutil.WriteFile(*walletPath, file, 0644) | |
log.Printf("created new wallets\r\n%+v\r\n", file) | |
os.Exit(0) | |
} | |
// First obtain client credentials: private key of request owner | |
key, err := wallet.GetCredentialsFromPath(*walletPath, *walletAddr, *password) | |
if err != nil { | |
log.Fatal("can't read credentials:", err) | |
} | |
w := wallet.GetWalletFromPrivateKey(key) | |
log.Println("using account ", w.Address) | |
cli, err := client2.NewClient(key, client2.TESTNET) | |
if err != nil { | |
log.Fatal("can't create NeoFS client:", err) | |
} | |
var attributes []*container.Attribute | |
placementPolicy := `REP 2 IN X | |
CBF 2 | |
SELECT 2 FROM * AS X | |
` | |
id, err := container2.Create(ctx, cli, key, placementPolicy, acl.EACLPublicBasicRule, attributes) | |
if err != nil { | |
log.Fatal(err) | |
} | |
await30Seconds(func() bool { | |
var prmContainerGet client.PrmContainerGet | |
prmContainerGet.SetContainer(*id) | |
_, err = cli.ContainerGet(ctx, prmContainerGet) | |
return err == nil | |
}) | |
fmt.Printf("Container %s has been persisted in side chain\n", id) | |
// Step 2: set restrictive extended ACL | |
table := eacl2.PutAllowDenyOthersEACL(*id, nil) | |
var prmContainerSetEACL client.PrmContainerSetEACL | |
prmContainerSetEACL.SetTable(table) | |
_, err = cli.ContainerSetEACL(ctx, prmContainerSetEACL) | |
if err != nil { | |
log.Fatal("eacl was not set") | |
} | |
await30Seconds(func() bool { | |
var prmContainerEACL client.PrmContainerEACL | |
prmContainerEACL.SetContainer(*id) | |
r, err := cli.ContainerEACL(ctx, prmContainerEACL) | |
if err != nil { | |
return false | |
} | |
expected, _ := table.Marshal() | |
got, _ := r.Table().Marshal() | |
return bytes.Equal(expected, got) | |
}) | |
} | |
func await30Seconds(f func() bool) { | |
for i := 1; i <= 30; i++ { | |
if f() { | |
return | |
} | |
time.Sleep(time.Second) | |
} | |
log.Fatal("timeout") | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
async function decryptAccountData(data) { | |
const myAccount = new Neon.wallet.Account( | |
JSON.parse(data) | |
); | |
await myAccount.decrypt("password") //the wallet password | |
console.log("privateKey", myAccount.privateKey) | |
console.log("privateKey", myAccount.publicKey) | |
console.log("WIF", myAccount.WIF); | |
return myAccount | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<html> | |
<head> | |
<script src="https://unpkg.com/@cityofzion/neon-js@next"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-sha512/0.8.0/sha512.min.js" integrity="sha512-KUrAWA1oxsWKHBaA2mlZyRuR8zzzHHYgpDfkfPrT3FhlZ4YdXbXyE89VHI6WmWradSHtuZjLyLAMP2F7IWK4JQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> | |
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.0.0/dist/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous"> | |
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script> | |
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.12.9/dist/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script> | |
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.0.0/dist/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script> | |
<style> | |
pre { | |
white-space: pre-wrap; /* Since CSS 2.1 */ | |
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ | |
white-space: -pre-wrap; /* Opera 4-6 */ | |
white-space: -o-pre-wrap; /* Opera 7 */ | |
word-wrap: break-word; /* Internet Explorer 5.5+ */ | |
} | |
</style> | |
<body> | |
<div class="container"> | |
<!-- Content here --> | |
<div class="row"> | |
<div class="col-sm-12"> | |
<div class="card"> | |
<div class="card-body"> | |
<h3 class="card-title">Greenfinch API.</h3> | |
<h5 class="card-text">This is an HTTP API into the NeoFS file system</h5> | |
<p>Currently this has initial support for object management. Please see:</p> | |
<ul> | |
<li><a href="https://developers.neo.org/docs/n3/neofs/introduction/Overview">The Neo developer documentation for an overview</a></li> | |
<li><a href="https://gist.github.com/amlwwalker/407a04c9b21915eee067a5531394f542#file-index-html">The Gist here to see how to interact from javascript</a></li> | |
<li><a href="https://gist.github.com/amlwwalker/407a04c9b21915eee067a5531394f542#file-createcontainer-go">Code to create a container</a></li> | |
<li><a href="https://gist.github.com/amlwwalker/407a04c9b21915eee067a5531394f542#file-decryptaccount-js">Helper decrypting wallets for public and private key</a></li> | |
<li><a href="https://greenfinch-api.onrender.com/swagger/">The swagger documentation to explain how the API works</a></li> | |
</ul> | |
<p>In short, this API allows a container owner to interact with their container and objects over HTTP without sharing their private key. <br />You need to have | |
<ul> | |
<li>A containerID and the public key of the container owner. Send these to the API to receive a bearer token.</li> | |
<li>Use this token and your private key to sign the bearer token.</li> | |
<li>Now you can upload an object</li> | |
<li>Or you can retrieve details of an object</li> | |
<li>Or you can list all objects</li> | |
<li>Or you can delete an object</li> | |
</ul></p> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div class="row"> | |
<div class="col-sm-6"> | |
<div class="card"> | |
<div class="card-body"> | |
<h5 class="card-title">1. Request new bearer token</h5> | |
<p class="card-text">before any request you will need a bearer token. You can specify how long a token should last.</p> | |
<div class="input-group mb-3"> | |
<div class="input-group-prepend"> | |
<span class="input-group-text">public key</span> | |
</div> | |
<input type="text" class="form-control" id="publicKey" aria-describedby="basic-addon3" value=""> | |
</div> | |
<div class="input-group mb-3"> | |
<div class="input-group-prepend"> | |
<span class="input-group-text">container ID</span> | |
</div> | |
<input type="text" class="form-control" id="containerID" aria-describedby="basic-addon3" value=""> | |
</div> | |
<button class="btn btn-primary" id="getBearerToken" >Request Token</button> | |
<span> | |
<pre id="token"></pre> | |
</span> | |
</div> | |
</div> | |
</div> | |
<div class="col-sm-6"> | |
<div class="card"> | |
<div class="card-body"> | |
<h5 class="card-title">2. Sign the bearer token</h5> | |
<p class="card-text">Now, with your private key, you need to sign the bearer token. This will respond with two integers, r and s</p> | |
<div class="input-group mb-3"> | |
<div class="input-group-prepend"> | |
<span class="input-group-text">private key</span> | |
</div> | |
<input type="password" class="form-control" id="privateKey" aria-describedby="basic-addon3" value=""> | |
</div> | |
<button class="btn btn-primary" id="signToken">Sign Token</button> | |
<span> | |
<pre id="signature"></pre> | |
<!-- <button onClick="getBearerToken()">Click me</button>--> | |
</span> | |
</div> | |
</div> | |
</div> | |
<div class="col-sm-12"> | |
<div class="card"> | |
<div class="card-body"> | |
<h5 class="card-title">3. Use the signature to upload</h5> | |
<p class="card-text">Use the signature to upload some data to the contain</p> | |
<div class="input-group mb-3"> | |
<div class="dropdown"> | |
<button class="btn btn-secondary dropdown-toggle" type="button" id="uploadDropDown" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> | |
Select File type | |
</button> | |
<div class="dropdown-menu" aria-labelledby="uploadDropDownButton" id="uploadDropDownButton"> | |
<a class="dropdown-item upload" href="#">multipart/form-data</a> | |
<a class="dropdown-item upload" href="#">text/plain</a> | |
</div> | |
<span id="content-type-selector"></span> | |
</div> | |
</div> | |
<div id="multipart-upload" style="display: none;"> | |
<label for="file">choose a file to upload</label> | |
<input type="file" id="file" name="file"> | |
</div> | |
<div id="rawContent-upload" style="display: none;"> | |
<label for="rawContent-attributes">set the name for the content</label><br /> | |
<input type="text" class="form-control" id="rawContentFileName" aria-describedby="basic-addon3" value="shakespeare-content.json"> | |
<label for="rawContent-attributes">set the attributes for the upload</label><br /> | |
<textarea rows="4" cols="50" id="rawContent-attributes"> | |
{ | |
"Author Type":"Poetry/fiction" | |
} | |
</textarea><br /> | |
<label for="raw-content">set the data to upload</label><br /> | |
<textarea rows="4" cols="50" id="raw-content"> | |
{ | |
"title":"The complete works of William Shakespeare", | |
"birth": "26.04.1564", | |
"died": "23.04.1616" | |
} | |
</textarea> | |
</div> | |
<button class="btn btn-primary" id="uploadButton">Upload</button> | |
<span> | |
<pre id="uploadResponse"></pre> | |
</span> | |
</div> | |
</div> | |
</div> | |
<div class="col-sm-12"> | |
<div class="card"> | |
<div class="card-body"> | |
<h5 class="card-title">4. Use the signature to retrieve</h5> | |
<p class="card-text">The signature can now be used to make a request. <b>Note, you do need to have selected the file type above first</b></p> | |
<div class="input-group mb-3"> | |
<div class="dropdown"> | |
<button class="btn btn-secondary dropdown-toggle" type="button" id="methodDropDown" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> | |
Select Method | |
</button> | |
<div class="dropdown-menu" aria-labelledby="methodDropDownButton" id="methodDropDownButton"> | |
<a class="dropdown-item method" href="#">HEAD</a> | |
<a class="dropdown-item method" href="#">GET</a> | |
<a class="dropdown-item method" href="#">LIST</a> | |
<a class="dropdown-item method" href="#">DELETE</a> | |
</div> | |
<span id="method-type-selector"></span> | |
</div> | |
</div> | |
<div class="input-group mb-3"> | |
<div class="input-group-prepend"> | |
<span class="input-group-text">objectID</span> | |
</div> | |
<input type="text" class="form-control" id="objectID" aria-describedby="basic-addon3" value=""> | |
</div> | |
<button class="btn btn-primary" id="requestResponse">Make request</button> | |
<button class="btn btn-primary" id="decodeContent">Decode</button> | |
<span> | |
<pre id="metadata"></pre> | |
<pre id="decodedContent"></pre> | |
<div id="displayImage"></div> | |
</span> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</body> | |
<script> | |
const domain = "http://localhost:9000"//"https://greenfinch-api.onrender.com" | |
let contentTypeUpload, contentTypeDownload, methodTypeUpload = null | |
// // Size should be given in 'bytes' | |
// function generate_random_data(size) { | |
// return new Blob([new ArrayBuffer(size)], {type: 'application/octet-stream'}); | |
// }; | |
//consider using multipart https://stackoverflow.com/questions/50784370/javascript-create-a-file-out-of-json-object-and-use-it-in-a-formdata | |
function rawContentUpload() { | |
const p = document.getElementById("publicKey") | |
let containerID = document.getElementById("containerID") | |
console.log(containerID.value) | |
let signatureArea = document.getElementById("signature").innerText | |
let signature = JSON.parse(signatureArea) | |
let xhttp = new XMLHttpRequest(); | |
xhttp.open("POST", `${domain}/api/v1/object/${containerID.value}`, true); | |
if (contentTypeUpload == null) { | |
alert("no content type set") | |
return false | |
} | |
xhttp.setRequestHeader("X-r", signature.r) | |
xhttp.setRequestHeader("X-s", signature.s) | |
xhttp.setRequestHeader("publicKey", p.value) | |
const filename = document.getElementById("rawContentFileName").value | |
//add attributes to the request | |
const attributes = document.getElementById("rawContent-attributes").value | |
console.log(attributes) | |
xhttp.setRequestHeader("NEOFS-ATTRIBUTES", attributes.replace(/(\r\n|\n|\r)/gm, "")) | |
// xhttp.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); | |
const rawContent = document.getElementById("raw-content").value.replace(/(\r\n|\n|\r)/gm, "") | |
const blob = new Blob([rawContent], { type: 'text/plain' }); | |
const file = new File([ blob ], filename); | |
const formData = new FormData(); | |
formData.append('file', file, filename); | |
xhttp.send(formData); | |
xhttp.onreadystatechange = function() { | |
if (this.readyState == 4 && this.status == 200) { | |
// Typical action to be performed when the document is ready: | |
console.log("response from content upload ", xhttp.responseText) | |
document.getElementById("uploadResponse").innerHTML = xhttp.responseText; | |
} | |
}; | |
} | |
function multipartFileUpload() { | |
const p = document.getElementById("publicKey") | |
let containerID = document.getElementById("containerID") | |
console.log(containerID.value) | |
let signatureArea = document.getElementById("signature").innerText | |
let signature = JSON.parse(signatureArea) | |
let formData = new FormData(); | |
let f = $('input[type=file]')[0].files[0] | |
// HTML file input, chosen by user | |
formData.append("file", f); //fileInputElement.files[0] | |
var xhttp = new XMLHttpRequest(); | |
xhttp.open("POST", `${domain}/api/v1/object/${containerID.value}`, true); | |
if (contentTypeUpload == null) { | |
alert("no content type set") | |
return false | |
} | |
xhttp.setRequestHeader("X-r", signature.r) | |
xhttp.setRequestHeader("X-s", signature.s) | |
xhttp.setRequestHeader("publicKey", p.value) | |
// xhttp.setRequestHeader("Content-Type", "multipart/form-data"); | |
xhttp.send(formData); | |
xhttp.onreadystatechange = function() { | |
if (this.readyState == 4 && this.status == 200) { | |
// Typical action to be performed when the document is ready: | |
console.log("response from content upload ", xhttp.responseText) | |
document.getElementById("uploadResponse").innerHTML = xhttp.responseText; | |
} | |
}; | |
} | |
function getBearerToken() { | |
const p = document.getElementById("publicKey") | |
var xhttp = new XMLHttpRequest(); | |
xhttp.onreadystatechange = function() { | |
if (this.readyState == 4 && this.status == 200) { | |
// Typical action to be performed when the document is ready: | |
document.getElementById("token").innerHTML = xhttp.responseText; | |
} | |
}; | |
xhttp.open("GET", `${domain}/api/v1/bearer/${containerID.value}`, true); | |
xhttp.setRequestHeader("publicKey", p.value) | |
xhttp.send(); | |
} | |
function decodeMeta() { | |
const content = document.getElementById("metadata").innerText | |
const decodedContent = window.atob(content) | |
document.getElementById("decodedContent").innerText = decodedContent | |
} | |
function requestData() { | |
if (methodTypeUpload == null) { | |
console.log("no method set") | |
return | |
} | |
const p = document.getElementById("publicKey") | |
let containerID = document.getElementById("containerID") | |
let objectID = document.getElementById("objectID") | |
console.log(objectID.value) | |
let signatureArea = document.getElementById("signature").innerText | |
let signature = JSON.parse(signatureArea) | |
let xhttp = new XMLHttpRequest(); | |
xhttp.responseType = "blob"; | |
xhttp.onreadystatechange = function() { | |
console.log("state change", this.readyState, this.HEADERS_RECEIVED) | |
if (this.readyState == this.HEADERS_RECEIVED) { | |
// Get the raw header string | |
var headers = xhttp.getAllResponseHeaders(); | |
console.log("headers", headers) | |
var arr = headers.trim().split(/[\r\n]+/); | |
// Create a map of header names to values | |
var headerMap = {}; | |
arr.forEach(function (line) { | |
var parts = line.split(': '); | |
var header = parts.shift(); | |
var value = parts.join(': '); | |
headerMap[header] = value; | |
}); | |
console.log("metadata", headerMap["neofs-meta"]) | |
document.getElementById("metadata").innerText = headerMap["neofs-meta"] | |
} | |
}; | |
xhttp.onload = function(e) { | |
if (this.status == 200) { | |
if (contentTypeUpload == "multipart/form-data") { | |
console.log("assuming a blob") | |
// xhttp.responseType = "blob"; | |
var blob = this.response; | |
console.log("blob", blob) | |
var src = document.getElementById("displayImage"); | |
var img = document.createElement("img"); | |
img.src = window.URL.createObjectURL(blob); | |
src.appendChild(img); | |
} else if (contentTypeUpload == "text/plain") { | |
var blob = this.response; | |
blob.text().then(text => { | |
let blobText = text | |
console.log("blob", blobText) | |
var src = document.getElementById("decodedContent"); | |
src.innerHTML = blobText | |
}) | |
} | |
} | |
}; | |
let requestUrl = `${domain}/api/v1/object/${containerID.value}/${objectID.value}` | |
if (methodTypeUpload == "LIST") { | |
methodTypeUpload = "GET" | |
requestUrl = `${domain}/api/v1/object/${containerID.value}` | |
} | |
console.log("final request url", requestUrl) | |
xhttp.open(methodTypeUpload, requestUrl, true); | |
xhttp.setRequestHeader("publicKey", p.value) | |
xhttp.setRequestHeader("X-r", signature.r) | |
xhttp.setRequestHeader("X-s", signature.s) | |
xhttp.send(); | |
} | |
function _base64ToArrayBuffer(base64) { | |
var binary_string = window.atob(base64); | |
var len = binary_string.length; | |
var bytes = new Uint8Array(len); | |
for (var i = 0; i < len; i++) { | |
bytes[i] = binary_string.charCodeAt(i); | |
} | |
return bytes.buffer; | |
} | |
function toHexString(byteArray) { | |
return Array.from(byteArray, function(byte) { | |
return ('0' + (byte & 0xFF).toString(16)).slice(-2); | |
}).join('') | |
} | |
function signToken() { | |
const msgStr = document.getElementById("token").innerText | |
const msg = JSON.parse(msgStr).token | |
const privKey = document.getElementById("privateKey") | |
const account = decryptAccountData(privKey.value) | |
const decoededMessage = _base64ToArrayBuffer(msg) | |
const curve = Neon.u.getCurve(Neon.u.EllipticCurvePreset.SECP256R1); | |
const sig = curve.sign(sha512.digest(decoededMessage), account.privateKey); | |
curve.verify(sha512.digest(decoededMessage), sig, Neon.wallet.getPublicKeyFromPrivateKey(account.privateKey)) ? null : console.error("warning, not able to verify signature") | |
const signatureBytes = JSON.stringify(sig.toString()) | |
let signatureArea = document.getElementById("signature") | |
signatureArea.innerText = JSON.stringify(sig) | |
return sig; | |
} | |
function decryptAccountData(privateKey) { | |
//perhaps this should require your password | |
const myAccount = new Neon.wallet.Account( | |
privateKey | |
); | |
console.log("privateKey", myAccount.privateKey) | |
console.log("publickey", Neon.wallet.getPublicKeyFromPrivateKey(myAccount.privateKey)); | |
console.log("WIF", myAccount.WIF); | |
return myAccount | |
} | |
document.getElementById("getBearerToken").onclick = function() {getBearerToken()}; | |
document.getElementById("signToken").onclick = function() {signToken()}; | |
document.getElementById("uploadButton").onclick = function() { | |
if (contentTypeUpload == null) { | |
alert("no content type selected") | |
return false | |
} | |
//now based on the content type and the method type, we make a decision | |
if (contentTypeUpload == "multipart/form-data") { | |
console.log("handling multipart") | |
multipartFileUpload() | |
} else if (contentTypeUpload == "text/plain") { | |
console.log("handling json") | |
rawContentUpload() | |
} | |
}; | |
document.getElementById("requestResponse").onclick = function() { | |
requestData() | |
} | |
document.getElementById("decodeContent").onclick = function() {decodeMeta()}; | |
const uploadTypeSelector = document.querySelector("#uploadDropDownButton"); | |
uploadTypeSelector.addEventListener("click", (e) => { | |
e.preventDefault(); | |
contentTypeUpload = e.target.innerText | |
if (contentTypeUpload == "multipart/form-data") { | |
document.getElementById("multipart-upload").style.display = "inline"; | |
document.getElementById("rawContent-upload").style.display = "none"; | |
} else if (contentTypeUpload == "text/plain") { | |
document.getElementById("rawContent-upload").style.display = "inline"; | |
document.getElementById("multipart-upload").style.display = "none"; | |
} | |
document.getElementById("content-type-selector").textContent = e.target.innerText | |
}); | |
const methodTypeSelector = document.querySelector("#methodDropDownButton"); | |
methodTypeSelector.addEventListener("click", (e) => { | |
e.preventDefault(); | |
methodTypeUpload = e.target.innerText | |
document.getElementById("method-type-selector").textContent = e.target.innerText | |
}); | |
</script> | |
</html> | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
NOTE
This is out of date. There is more modern approaches to creating a container. This probably will not work.