Skip to content

Instantly share code, notes, and snippets.

@amlwwalker
Last active March 18, 2023 16:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save amlwwalker/407a04c9b21915eee067a5531394f542 to your computer and use it in GitHub Desktop.
Save amlwwalker/407a04c9b21915eee067a5531394f542 to your computer and use it in GitHub Desktop.
helpers for greenfinch api
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")
}
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
}
<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>
@amlwwalker
Copy link
Author

amlwwalker commented Mar 18, 2023

NOTE

This is out of date. There is more modern approaches to creating a container. This probably will not work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment