mirror of
https://github.com/PeernetOfficial/core.git
synced 2026-07-17 02:47:51 +01:00
Add webapi. Provides an HTTP API for clients to use Peernet.
This commit is contained in:
113
webapi/API.go
Normal file
113
webapi/API.go
Normal file
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
File Name: API.go
|
||||
Copyright: 2021 Peernet Foundation s.r.o.
|
||||
Author: Peter Kleissner
|
||||
*/
|
||||
|
||||
package webapi
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// Router can be used to register additional API functions
|
||||
var Router *mux.Router
|
||||
|
||||
// WSUpgrader can be used to provide web sockets. It uses all default options and allows all requests.
|
||||
var WSUpgrader = websocket.Upgrader{}
|
||||
|
||||
// Start starts the API. ListenAddresses is a list of IP:Ports
|
||||
func Start(ListenAddresses []string, UseSSL bool, CertificateFile, CertificateKey string, TimeoutRead, TimeoutWrite time.Duration) {
|
||||
if len(ListenAddresses) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
WSUpgrader.CheckOrigin = func(r *http.Request) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
Router = mux.NewRouter()
|
||||
|
||||
Router.HandleFunc("/test", apiTest).Methods("GET")
|
||||
Router.HandleFunc("/status", apiStatus).Methods("GET")
|
||||
Router.HandleFunc("/peer/self", apiPeerSelf).Methods("GET")
|
||||
Router.HandleFunc("/blockchain/self/header", apiBlockchainSelfHeader).Methods("GET")
|
||||
Router.HandleFunc("/blockchain/self/append", apiBlockchainSelfAppend).Methods("POST")
|
||||
Router.HandleFunc("/blockchain/self/read", apiBlockchainSelfRead).Methods("GET")
|
||||
Router.HandleFunc("/blockchain/self/add/file", apiBlockchainSelfAddFile).Methods("POST")
|
||||
Router.HandleFunc("/blockchain/self/list/file", apiBlockchainSelfListFile).Methods("GET")
|
||||
Router.HandleFunc("/profile/list", apiProfileList).Methods("GET")
|
||||
Router.HandleFunc("/profile/read", apiProfileRead).Methods("GET")
|
||||
Router.HandleFunc("/profile/write", apiProfileWrite).Methods("POST")
|
||||
|
||||
for _, listen := range ListenAddresses {
|
||||
go startWebServer(listen, UseSSL, CertificateFile, CertificateKey, Router, "API", TimeoutRead, TimeoutWrite)
|
||||
}
|
||||
}
|
||||
|
||||
// startWebServer starts a web-server with given parameters and logs the status. If may block forever and only returns if there is an error.
|
||||
func startWebServer(WebListen string, UseSSL bool, CertificateFile, CertificateKey string, Handler http.Handler, Info string, ReadTimeout, WriteTimeout time.Duration) {
|
||||
tlsConfig := &tls.Config{MinVersion: tls.VersionTLS12} // for security reasons disable TLS 1.0/1.1
|
||||
|
||||
server := &http.Server{
|
||||
Addr: WebListen,
|
||||
Handler: Handler,
|
||||
ReadTimeout: ReadTimeout, // ReadTimeout is the maximum duration for reading the entire request, including the body.
|
||||
WriteTimeout: WriteTimeout, // WriteTimeout is the maximum duration before timing out writes of the response. This includes processing time and is therefore the max time any HTTP function may take.
|
||||
//IdleTimeout: IdleTimeout, // IdleTimeout is the maximum amount of time to wait for the next request when keep-alives are enabled.
|
||||
TLSConfig: tlsConfig,
|
||||
}
|
||||
|
||||
if UseSSL {
|
||||
// HTTPS
|
||||
if err := server.ListenAndServeTLS(CertificateFile, CertificateKey); err != nil {
|
||||
log.Printf("Error listening on '%s': %v\n", WebListen, err)
|
||||
}
|
||||
} else {
|
||||
// HTTP
|
||||
if err := server.ListenAndServe(); err != nil {
|
||||
log.Printf("Error listening on '%s': %v\n", WebListen, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func apiEncodeJSON(w http.ResponseWriter, r *http.Request, data interface{}) (err error) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
err = json.NewEncoder(w).Encode(data)
|
||||
if err != nil {
|
||||
log.Printf("Error writing data for route '%s': %v\n", r.URL.Path, err)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// apiDecodeJSON decodes input JSON data server side sent either via GET or POST. It does not limit the maximum amount to read.
|
||||
// In case of error it will automatically send an error to the client.
|
||||
func apiDecodeJSON(w http.ResponseWriter, r *http.Request, data interface{}) (err error) {
|
||||
if r.Body == nil {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return errors.New("no data")
|
||||
}
|
||||
|
||||
err = json.NewDecoder(r.Body).Decode(data)
|
||||
if err != nil {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func apiTest(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("ok"))
|
||||
}
|
||||
307
webapi/Blockchain.go
Normal file
307
webapi/Blockchain.go
Normal file
@@ -0,0 +1,307 @@
|
||||
/*
|
||||
File Name: Blockchain.go
|
||||
Copyright: 2021 Peernet Foundation s.r.o.
|
||||
Author: Peter Kleissner
|
||||
*/
|
||||
|
||||
package webapi
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/PeernetOfficial/core"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const dateFormat = "2006-01-02 15:04:05"
|
||||
|
||||
type apiBlockchainHeader struct {
|
||||
PeerID string `json:"peerid"` // Peer ID hex encoded.
|
||||
Version uint64 `json:"version"` // Current version number of the blockchain.
|
||||
Height uint64 `json:"height"` // Height of the blockchain (number of blocks). If 0, no data exists.
|
||||
}
|
||||
|
||||
/*
|
||||
apiBlockchainSelfHeader returns the current blockchain header information
|
||||
|
||||
Request: GET /blockchain/self/header
|
||||
Result: 200 with JSON structure apiResponsePeerSelf
|
||||
*/
|
||||
func apiBlockchainSelfHeader(w http.ResponseWriter, r *http.Request) {
|
||||
publicKey, height, version := core.UserBlockchainHeader()
|
||||
|
||||
apiEncodeJSON(w, r, apiBlockchainHeader{Version: version, Height: height, PeerID: hex.EncodeToString(publicKey.SerializeCompressed())})
|
||||
}
|
||||
|
||||
type apiBlockRecordRaw struct {
|
||||
Type uint8 `json:"type"` // Record Type. See core.RecordTypeX.
|
||||
Data []byte `json:"data"` // Data according to the type.
|
||||
}
|
||||
|
||||
// apiBlockchainBlockRaw contains a raw block of the blockchain via API
|
||||
type apiBlockchainBlockRaw struct {
|
||||
Records []apiBlockRecordRaw `json:"records"` // Block records in encoded raw format.
|
||||
}
|
||||
|
||||
type apiBlockchainBlockStatus struct {
|
||||
Status int `json:"status"` // Status: 0 = Success, 1 = Error invalid data
|
||||
Height uint64 `json:"height"` // New height of the blockchain (number of blocks).
|
||||
}
|
||||
|
||||
/*
|
||||
apiBlockchainSelfAppend appends a block to the blockchain. This is a low-level function for already encoded blocks.
|
||||
Do not use this function. Adding invalid data to the blockchain may corrupt it which might result in blacklisting by other peers.
|
||||
|
||||
Request: POST /blockchain/self/append with JSON structure apiBlockchainBlockRaw
|
||||
Response: 200 with JSON structure apiBlockchainBlockStatus
|
||||
*/
|
||||
func apiBlockchainSelfAppend(w http.ResponseWriter, r *http.Request) {
|
||||
var input apiBlockchainBlockRaw
|
||||
if err := apiDecodeJSON(w, r, &input); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var records []core.BlockRecordRaw
|
||||
|
||||
for _, record := range input.Records {
|
||||
records = append(records, core.BlockRecordRaw{Type: record.Type, Data: record.Data})
|
||||
}
|
||||
|
||||
newHeight, status := core.UserBlockchainAppend(records)
|
||||
|
||||
apiEncodeJSON(w, r, apiBlockchainBlockStatus{Status: status, Height: newHeight})
|
||||
}
|
||||
|
||||
type apiBlockchainBlock struct {
|
||||
Status int `json:"status"` // Status: 0 = Success, 1 = Error block not found, 2 = Error block encoding (indicates that the blockchain is corrupt)
|
||||
PeerID string `json:"peerid"` // Peer ID hex encoded.
|
||||
LastBlockHash []byte `json:"lastblockhash"` // Hash of the last block. Blake3.
|
||||
BlockchainVersion uint64 `json:"blockchainversion"` // Blockchain version
|
||||
Number uint64 `json:"blocknumber"` // Block number
|
||||
RecordsRaw []apiBlockRecordRaw `json:"recordsraw"` // Records raw. Successfully decoded records are parsed into the below fields.
|
||||
RecordsDecoded []interface{} `json:"recordsdecoded"` // Records decoded. The encoding for each record depends on its type.
|
||||
}
|
||||
|
||||
// apiBlockRecordProfile contains end-user information. Any data is treated as untrusted and unverified by default.
|
||||
type apiBlockRecordProfile struct {
|
||||
Fields []apiBlockRecordProfileField `json:"fields"` // All fields
|
||||
Blobs []apiBlockRecordProfileBlob `json:"blobs"` // Blobs
|
||||
}
|
||||
|
||||
// apiBlockRecordProfileField contains a single information about the end user. The data is always UTF8 text encoded.
|
||||
// Note that all profile data is arbitrary and shall be considered untrusted and unverified.
|
||||
// To establish trust, the user must load Certificates into the blockchain that validate certain data.
|
||||
type apiBlockRecordProfileField struct {
|
||||
Type uint16 `json:"type"` // See ProfileFieldX constants.
|
||||
Text string `json:"text"` // The data
|
||||
}
|
||||
|
||||
// apiBlockRecordProfileBlob is similar to apiBlockRecordProfileField but contains binary objects instead of text.
|
||||
// It can be used for example to store a profile picture on the blockchain.
|
||||
type apiBlockRecordProfileBlob struct {
|
||||
Type uint16 `json:"type"` // See ProfileBlobX constants.
|
||||
Data []byte `json:"data"` // The data
|
||||
}
|
||||
|
||||
// apiFileMetadata describes recognized metadata that is decoded into text.
|
||||
type apiFileMetadata struct {
|
||||
Type uint16 `json:"type"` // See core.TagTypeX constants.
|
||||
Name string `json:"name"` // User friendly name of the tag. Use the Type fields to identify the metadata as this name may change.
|
||||
Value string `json:"value"` // Text value of the tag.
|
||||
}
|
||||
|
||||
// apiFileTagRaw describes a raw tag. This allows to support future metadata that is not yet defined in the core library.
|
||||
type apiFileTagRaw struct {
|
||||
Type uint16 `json:"type"` // See core.TagTypeX constants.
|
||||
Data []byte `json:"data"` // Data
|
||||
}
|
||||
|
||||
// apiBlockRecordFile is the metadata of a file published on the blockchain
|
||||
type apiBlockRecordFile struct {
|
||||
ID uuid.UUID `json:"id"` // Unique ID.
|
||||
Hash []byte `json:"hash"` // Blake3 hash of the file data
|
||||
Type uint8 `json:"type"` // Type (low-level)
|
||||
Format uint16 `json:"format"` // Format (high-level)
|
||||
Size uint64 `json:"size"` // Size of the file
|
||||
Folder string `json:"folder"` // Folder, optional
|
||||
Name string `json:"name"` // Name of the file
|
||||
Description string `json:"description"` // Description. This is expected to be multiline and contain hashtags!
|
||||
Metadata []apiFileMetadata `json:"metadata"` // Metadata. These are decoded tags.
|
||||
TagsRaw []apiFileTagRaw `json:"tagsraw"` // All tags encoded that were not recognized as metadata.
|
||||
|
||||
// The following known tags from the core library are decoded into metadata or other fields in above structure; everything else is a raw tag:
|
||||
// TagTypeName, TagTypeFolder, TagTypeDescription, TagTypeDateCreated
|
||||
// The caller can specify its own metadata fields and fill the TagsRaw structure when creating a new file. It will be returned when reading the files' data.
|
||||
}
|
||||
|
||||
/*
|
||||
apiBlockchainSelfRead reads a block and returns the decoded information.
|
||||
|
||||
Request: GET /blockchain/self/read?block=[number]
|
||||
Result: 200 with JSON structure apiBlockchainBlock
|
||||
*/
|
||||
func apiBlockchainSelfRead(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
blockN, err := strconv.Atoi(r.Form.Get("block"))
|
||||
if err != nil || blockN < 0 {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
block, status, _ := core.UserBlockchainRead(uint64(blockN))
|
||||
result := apiBlockchainBlock{Status: status}
|
||||
|
||||
if status == 0 {
|
||||
for _, record := range block.RecordsRaw {
|
||||
result.RecordsRaw = append(result.RecordsRaw, apiBlockRecordRaw{Type: record.Type, Data: record.Data})
|
||||
}
|
||||
|
||||
result.PeerID = hex.EncodeToString(block.OwnerPublicKey.SerializeCompressed())
|
||||
|
||||
for _, record := range block.RecordsDecoded {
|
||||
switch v := record.(type) {
|
||||
case core.BlockRecordFile:
|
||||
result.RecordsDecoded = append(result.RecordsDecoded, blockRecordFileToAPI(v))
|
||||
|
||||
case core.BlockRecordProfile:
|
||||
result.RecordsDecoded = append(result.RecordsDecoded, blockRecordProfileToAPI(v))
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
apiEncodeJSON(w, r, result)
|
||||
}
|
||||
|
||||
// apiBlockAddFiles contains the file metadata to add to the blockchain
|
||||
type apiBlockAddFiles struct {
|
||||
Files []apiBlockRecordFile `json:"files"` // List of files
|
||||
Status int `json:"status"` // Status of the operation, only used when this structure is returned from the API.
|
||||
}
|
||||
|
||||
/*
|
||||
apiBlockchainSelfAddFile adds a file with the provided information to the blockchain.
|
||||
|
||||
Request: POST /blockchain/self/add/file with JSON structure apiBlockAddFiles
|
||||
Response: 200 with JSON structure apiBlockchainBlockStatus
|
||||
*/
|
||||
func apiBlockchainSelfAddFile(w http.ResponseWriter, r *http.Request) {
|
||||
var input apiBlockAddFiles
|
||||
if err := apiDecodeJSON(w, r, &input); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var filesAdd []core.BlockRecordFile
|
||||
|
||||
for _, file := range input.Files {
|
||||
if file.ID == uuid.Nil { // if the ID is not provided by the caller, set it
|
||||
file.ID = uuid.New()
|
||||
}
|
||||
|
||||
filesAdd = append(filesAdd, blockRecordFileFromAPI(file))
|
||||
}
|
||||
|
||||
newHeight, status := core.UserBlockchainAddFiles(filesAdd)
|
||||
|
||||
apiEncodeJSON(w, r, apiBlockchainBlockStatus{Status: status, Height: newHeight})
|
||||
}
|
||||
|
||||
/*
|
||||
apiBlockchainSelfListFile lists all files stored on the blockchain.
|
||||
|
||||
Request: GET /blockchain/self/list/file
|
||||
Response: 200 with JSON structure apiBlockAddFiles
|
||||
*/
|
||||
func apiBlockchainSelfListFile(w http.ResponseWriter, r *http.Request) {
|
||||
files, status := core.UserBlockchainListFiles()
|
||||
|
||||
var result apiBlockAddFiles
|
||||
|
||||
for _, file := range files {
|
||||
result.Files = append(result.Files, blockRecordFileToAPI(file))
|
||||
}
|
||||
|
||||
result.Status = status
|
||||
|
||||
apiEncodeJSON(w, r, result)
|
||||
}
|
||||
|
||||
// --- conversion from core to API data ---
|
||||
|
||||
func isFileTagKnownMetadata(tagType uint16) bool {
|
||||
switch tagType {
|
||||
case core.TagTypeName, core.TagTypeFolder, core.TagTypeDescription, core.TagTypeDateCreated, core.TagTypeDateShared:
|
||||
return true
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func blockRecordFileToAPI(input core.BlockRecordFile) (output apiBlockRecordFile) {
|
||||
output = apiBlockRecordFile{ID: input.ID, Hash: input.Hash, Type: input.Type, Format: input.Format, Size: input.Size, TagsRaw: []apiFileTagRaw{}, Metadata: []apiFileMetadata{}}
|
||||
|
||||
// Copy all raw tags. This allows the API caller to decode any future tags that are not defined yet.
|
||||
for n := range input.TagsRaw {
|
||||
if !isFileTagKnownMetadata(input.TagsRaw[n].Type) {
|
||||
output.TagsRaw = append(output.TagsRaw, apiFileTagRaw{Type: input.TagsRaw[n].Type, Data: input.TagsRaw[n].Data})
|
||||
}
|
||||
}
|
||||
|
||||
// Try to decode tags into known metadata.
|
||||
for _, tagDecoded := range input.TagsDecoded {
|
||||
switch v := tagDecoded.(type) {
|
||||
case core.FileTagName:
|
||||
output.Name = v.Name
|
||||
|
||||
case core.FileTagFolder:
|
||||
output.Folder = v.Name
|
||||
|
||||
case core.FileTagDescription:
|
||||
output.Description = v.Description
|
||||
|
||||
case core.FileTagDateCreated:
|
||||
output.Metadata = append(output.Metadata, apiFileMetadata{Type: core.TagTypeDateCreated, Name: "Date Created", Value: v.Date.Format(dateFormat)})
|
||||
|
||||
case core.FileTagDateShared:
|
||||
output.Metadata = append(output.Metadata, apiFileMetadata{Type: core.TagTypeDateShared, Name: "Date Shared", Value: v.Date.Format(dateFormat)})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func blockRecordFileFromAPI(input apiBlockRecordFile) (output core.BlockRecordFile) {
|
||||
output = core.BlockRecordFile{ID: input.ID, Hash: input.Hash, Type: input.Type, Format: input.Format, Size: input.Size}
|
||||
|
||||
if input.Name != "" {
|
||||
output.TagsDecoded = append(output.TagsDecoded, core.FileTagName{Name: input.Name})
|
||||
}
|
||||
if input.Folder != "" {
|
||||
output.TagsDecoded = append(output.TagsDecoded, core.FileTagFolder{Name: input.Folder})
|
||||
}
|
||||
if input.Description != "" {
|
||||
output.TagsDecoded = append(output.TagsDecoded, core.FileTagDescription{Description: input.Description})
|
||||
}
|
||||
|
||||
for _, tag := range input.Metadata {
|
||||
switch tag.Type {
|
||||
case core.TagTypeDateCreated:
|
||||
if dateF, err := time.Parse(dateFormat, tag.Value); err == nil {
|
||||
output.TagsDecoded = append(output.TagsDecoded, core.FileTagDateCreated{Date: dateF})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for n := range input.TagsRaw {
|
||||
if !isFileTagKnownMetadata(input.TagsRaw[n].Type) {
|
||||
output.TagsRaw = append(output.TagsRaw, core.BlockRecordFileTag{Type: input.TagsRaw[n].Type, Data: input.TagsRaw[n].Data})
|
||||
}
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
126
webapi/Profile.go
Normal file
126
webapi/Profile.go
Normal file
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
File Name: Profile.go
|
||||
Copyright: 2021 Peernet Foundation s.r.o.
|
||||
Author: Peter Kleissner
|
||||
*/
|
||||
|
||||
package webapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/PeernetOfficial/core"
|
||||
)
|
||||
|
||||
// apiProfileData contains profile metadata stored on the blockchain. Any data is treated as untrusted and unverified by default.
|
||||
type apiProfileData struct {
|
||||
Fields []apiBlockRecordProfileField `json:"fields"` // All fields
|
||||
Blobs []apiBlockRecordProfileBlob `json:"blobs"` // All blobs
|
||||
Status int `json:"status"` // Status of the operation, only used when this structure is returned from the API. See core.BlockchainStatusX.
|
||||
}
|
||||
|
||||
/*
|
||||
apiProfileList lists all users profile fields and blobs.
|
||||
|
||||
Request: GET /profile/list
|
||||
Response: 200 with JSON structure apiProfileData
|
||||
*/
|
||||
func apiProfileList(w http.ResponseWriter, r *http.Request) {
|
||||
fields, blobs, status := core.UserProfileList()
|
||||
|
||||
result := apiProfileData{Status: status}
|
||||
for n := range fields {
|
||||
result.Fields = append(result.Fields, apiBlockRecordProfileField{Type: fields[n].Type, Text: fields[n].Text})
|
||||
}
|
||||
for n := range blobs {
|
||||
result.Blobs = append(result.Blobs, apiBlockRecordProfileBlob{Type: blobs[n].Type, Data: blobs[n].Data})
|
||||
}
|
||||
|
||||
apiEncodeJSON(w, r, result)
|
||||
}
|
||||
|
||||
/*
|
||||
apiProfileRead reads a specific users profile field or blob.
|
||||
For the index see core.ProfileFieldX and core.ProfileBlobX constants.
|
||||
|
||||
Request: GET /profile/read?field=[index] or &blob=[index]
|
||||
Response: 200 with JSON structure apiProfileData
|
||||
*/
|
||||
func apiProfileRead(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
fieldN, err1 := strconv.Atoi(r.Form.Get("field"))
|
||||
blobN, err2 := strconv.Atoi(r.Form.Get("blob"))
|
||||
|
||||
if (err1 != nil && err2 != nil) || (err1 == nil && fieldN < 0) || (err2 == nil && blobN < 0) {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var result apiProfileData
|
||||
|
||||
if err1 == nil {
|
||||
var text string
|
||||
if text, result.Status = core.UserProfileReadField(uint16(fieldN)); result.Status == core.BlockchainStatusOK {
|
||||
result.Fields = append(result.Fields, apiBlockRecordProfileField{Type: uint16(fieldN), Text: text})
|
||||
}
|
||||
} else {
|
||||
var data []byte
|
||||
if data, result.Status = core.UserProfileReadBlob(uint16(blobN)); result.Status == core.BlockchainStatusOK {
|
||||
result.Blobs = append(result.Blobs, apiBlockRecordProfileBlob{Type: uint16(blobN), Data: data})
|
||||
}
|
||||
}
|
||||
|
||||
apiEncodeJSON(w, r, result)
|
||||
}
|
||||
|
||||
/*
|
||||
apiProfileWrite writes a specific users profile field or blob.
|
||||
For the index see core.ProfileFieldX and core.ProfileBlobX constants.
|
||||
|
||||
Request: POST /profile/write with JSON structure apiProfileData
|
||||
Response: 200 with JSON structure apiBlockchainBlockStatus
|
||||
*/
|
||||
func apiProfileWrite(w http.ResponseWriter, r *http.Request) {
|
||||
var input apiProfileData
|
||||
if err := apiDecodeJSON(w, r, &input); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var profile core.BlockRecordProfile
|
||||
|
||||
for n := range input.Fields {
|
||||
profile.Fields = append(profile.Fields, core.BlockRecordProfileField{Type: input.Fields[n].Type, Text: input.Fields[n].Text})
|
||||
}
|
||||
for n := range input.Blobs {
|
||||
profile.Blobs = append(profile.Blobs, core.BlockRecordProfileBlob{Type: input.Blobs[n].Type, Data: input.Blobs[n].Data})
|
||||
}
|
||||
|
||||
newHeight, status := core.UserProfileWrite(profile)
|
||||
|
||||
apiEncodeJSON(w, r, apiBlockchainBlockStatus{Status: status, Height: newHeight})
|
||||
}
|
||||
|
||||
// --- conversion from core to API data ---
|
||||
|
||||
func blockRecordProfileToAPI(input core.BlockRecordProfile) (output apiBlockRecordProfile) {
|
||||
for n := range input.Fields {
|
||||
output.Fields = append(output.Fields, apiBlockRecordProfileField{Type: input.Fields[n].Type, Text: input.Fields[n].Text})
|
||||
}
|
||||
for n := range input.Blobs {
|
||||
output.Blobs = append(output.Blobs, apiBlockRecordProfileBlob{Type: input.Blobs[n].Type, Data: input.Blobs[n].Data})
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func blockRecordProfileFromAPI(input apiBlockRecordProfile) (output core.BlockRecordProfile) {
|
||||
for n := range input.Fields {
|
||||
output.Fields = append(output.Fields, core.BlockRecordProfileField{Type: input.Fields[n].Type, Text: input.Fields[n].Text})
|
||||
}
|
||||
for n := range input.Blobs {
|
||||
output.Blobs = append(output.Blobs, core.BlockRecordProfileBlob{Type: input.Blobs[n].Type, Data: input.Blobs[n].Data})
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
60
webapi/Status.go
Normal file
60
webapi/Status.go
Normal file
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
File Name: Status.go
|
||||
Copyright: 2021 Peernet Foundation s.r.o.
|
||||
Author: Peter Kleissner
|
||||
*/
|
||||
|
||||
package webapi
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
|
||||
"github.com/PeernetOfficial/core"
|
||||
)
|
||||
|
||||
type apiResponseStatus struct {
|
||||
Status int `json:"status"` // Status code: 0 = Ok.
|
||||
IsConnected bool `json:"isconnected"` // Whether connected to Peernet.
|
||||
CountPeerList int `json:"countpeerlist"` // Count of peers in the peer list. Note that this contains peers that are considered inactive, but have not yet been removed from the list.
|
||||
CountNetwork int `json:"countnetwork"` // Count of total peers in the network.
|
||||
// This is usually a higher number than CountPeerList, which just represents the current number of connected peers.
|
||||
// The CountNetwork number is going to be queried from root peers which may or may not have a limited view.
|
||||
}
|
||||
|
||||
/*
|
||||
apiStatus returns the current connectivity status to the network
|
||||
Request: GET /status
|
||||
Result: 200 with JSON structure Status
|
||||
*/
|
||||
func apiStatus(w http.ResponseWriter, r *http.Request) {
|
||||
status := apiResponseStatus{Status: 0, CountPeerList: core.PeerlistCount()}
|
||||
status.CountNetwork = status.CountPeerList // For now always same as CountPeerList, until native Statistics message to root peers is available.
|
||||
|
||||
// Connected: If at leat 2 peers.
|
||||
// This metric needs to be improved in the future, as root peers never disconnect.
|
||||
// Instead, the core should keep a count of "active peers".
|
||||
status.IsConnected = status.CountPeerList >= 2
|
||||
|
||||
apiEncodeJSON(w, r, status)
|
||||
}
|
||||
|
||||
type apiResponsePeerSelf struct {
|
||||
PeerID string `json:"peerid"` // Peer ID. This is derived from the public in compressed form.
|
||||
NodeID string `json:"nodeid"` // Node ID. This is the blake3 hash of the peer ID and used in the DHT.
|
||||
}
|
||||
|
||||
/*
|
||||
apiPeerSelf provides information about the self peer details
|
||||
Request: GET /peer/self
|
||||
Result: 200 with JSON structure apiResponsePeerSelf
|
||||
*/
|
||||
func apiPeerSelf(w http.ResponseWriter, r *http.Request) {
|
||||
response := apiResponsePeerSelf{}
|
||||
response.NodeID = hex.EncodeToString(core.SelfNodeID())
|
||||
|
||||
_, publicKey := core.ExportPrivateKey()
|
||||
response.PeerID = hex.EncodeToString(publicKey.SerializeCompressed())
|
||||
|
||||
apiEncodeJSON(w, r, response)
|
||||
}
|
||||
412
webapi/readme.md
Normal file
412
webapi/readme.md
Normal file
@@ -0,0 +1,412 @@
|
||||
# Web API
|
||||
|
||||
The web API provides access to core functions via HTTP.
|
||||
|
||||
It can be used by local client software to connect to the Peernet network and use functions such as share, search, and download files.
|
||||
|
||||
## Use Considerations (when not to use it)
|
||||
|
||||
Do not expose this API to the internet. The web API uses the core library which uses the user's public and private key to connect to the network.
|
||||
It shall only run on a loopback IP such as `127.0.0.1` or `::1`. Special HTTP headers (including the Access-Control headers) are intentionally not set.
|
||||
|
||||
The API is unauthenticated and provides direct access to the users blockchain.
|
||||
|
||||
## Deployment
|
||||
|
||||
The API must be initialized and started before use.
|
||||
|
||||
```go
|
||||
webapi.Start([]string{"127.0.0.1:112"}, false, "", "", 10*time.Second, 10*time.Second)
|
||||
|
||||
// To register an additional API endpoint:
|
||||
webapi.Router.HandleFunc("/newfunction", newFunction).Methods("GET")
|
||||
```
|
||||
|
||||
# Available Functions
|
||||
|
||||
These are the functions provided by the API:
|
||||
|
||||
```
|
||||
/status Provides current connectivity status to the network
|
||||
/peer/self Provides information about the self peer details
|
||||
|
||||
/blockchain/self/header Header of the blockchain
|
||||
/blockchain/self/append Append a block to the blockchain
|
||||
/blockchain/self/read Read a block of the blockchain
|
||||
/blockchain/self/add/file Add file to the blockchain
|
||||
/blockchain/self/list/file List all files stored on the blockchain
|
||||
|
||||
/profile/list List all users profile fields and blobs
|
||||
/profile/read Reads a specific users profile field or blob
|
||||
/profile/write Writes a specific users profile field or blob
|
||||
```
|
||||
|
||||
# API Documentation
|
||||
|
||||
## Informational Functions
|
||||
|
||||
### Status
|
||||
|
||||
This function informs about the current connection status of the client to the network. Additional fields will be added in the future.
|
||||
|
||||
```
|
||||
Request: GET /status
|
||||
Response: 200 with JSON structure apiResponseStatus
|
||||
```
|
||||
|
||||
```go
|
||||
type apiResponseStatus struct {
|
||||
Status int `json:"status"` // Status code: 0 = Ok.
|
||||
IsConnected bool `json:"isconnected"` // Whether connected to Peernet.
|
||||
CountPeerList int `json:"countpeerlist"` // Count of peers in the peer list. Note that this contains peers that are considered inactive, but have not yet been removed from the list.
|
||||
CountNetwork int `json:"countnetwork"` // Count of total peers in the network.
|
||||
// This is usually a higher number than CountPeerList, which just represents the current number of connected peers.
|
||||
// The CountNetwork number is going to be queried from root peers which may or may not have a limited view into the network.
|
||||
}
|
||||
```
|
||||
|
||||
### Self Information
|
||||
|
||||
This function returns information about the current peer.
|
||||
|
||||
```
|
||||
Request: GET /peer/self
|
||||
Response: 200 with JSON structure apiResponsePeerSelf
|
||||
```
|
||||
|
||||
The peer and node IDs are encoded as hex encoded strings.
|
||||
|
||||
```go
|
||||
type apiResponsePeerSelf struct {
|
||||
PeerID string `json:"peerid"` // Peer ID. This is derived from the public in compressed form.
|
||||
NodeID string `json:"nodeid"` // Node ID. This is the blake3 hash of the peer ID and used in the DHT.
|
||||
}
|
||||
```
|
||||
|
||||
## Blockchain Functions
|
||||
|
||||
### Blockchain Self Header
|
||||
|
||||
This function returns information about the current peer. It is not required that a peer has a blockchain. If no data is shared, there are no blocks. The blockchain does not formally have a header as each block has the same structure.
|
||||
|
||||
```
|
||||
Request: GET /blockchain/self/header
|
||||
Response: 200 with JSON structure apiBlockchainHeader
|
||||
```
|
||||
|
||||
```go
|
||||
type apiBlockchainHeader struct {
|
||||
PeerID string `json:"peerid"` // Peer ID hex encoded.
|
||||
Version uint64 `json:"version"` // Current version number of the blockchain.
|
||||
Height uint64 `json:"height"` // Height of the blockchain (number of blocks). If 0, no data exists.
|
||||
}
|
||||
```
|
||||
|
||||
### Blockchain Append Block
|
||||
|
||||
This appends a block to the blockchain. This is a low-level function for already encoded blocks.
|
||||
Do not use this function. Adding invalid data to the blockchain may corrupt it which subsequently might result in blacklisting by other peers.
|
||||
|
||||
```
|
||||
Request: POST /blockchain/self/append with JSON structure apiBlockchainBlockRaw
|
||||
Response: 200 with JSON structure apiBlockchainBlockStatus
|
||||
```
|
||||
|
||||
```go
|
||||
type apiBlockRecordRaw struct {
|
||||
Type uint8 `json:"type"` // Record Type. See core.RecordTypeX.
|
||||
Data []byte `json:"data"` // Data according to the type.
|
||||
}
|
||||
|
||||
type apiBlockchainBlockRaw struct {
|
||||
Records []apiBlockRecordRaw `json:"records"` // Block records in encoded raw format.
|
||||
}
|
||||
|
||||
type apiBlockchainBlockStatus struct {
|
||||
Status int `json:"status"` // Status: 0 = Success, 1 = Error invalid data
|
||||
Version uint64 `json:"version"` // Current version number of the blockchain.
|
||||
Height uint64 `json:"height"` // Height of the blockchain (number of blocks).
|
||||
}
|
||||
```
|
||||
|
||||
### Blockchain Read Block
|
||||
|
||||
This reads a block of the current peer.
|
||||
|
||||
```
|
||||
Request: GET /blockchain/self/read?block=[number]
|
||||
Response: 200 with JSON structure apiBlockchainBlock
|
||||
```
|
||||
|
||||
```go
|
||||
type apiBlockchainBlock struct {
|
||||
Status int `json:"status"` // Status: 0 = Success, 1 = Error block not found, 2 = Error block encoding (indicates that the blockchain is corrupt)
|
||||
PeerID string `json:"peerid"` // Peer ID hex encoded.
|
||||
LastBlockHash []byte `json:"lastblockhash"` // Hash of the last block. Blake3.
|
||||
BlockchainVersion uint64 `json:"blockchainversion"` // Blockchain version
|
||||
Number uint64 `json:"blocknumber"` // Block number
|
||||
RecordsRaw []apiBlockRecordRaw `json:"recordsraw"` // Records raw. Successfully decoded records are parsed into the below fields.
|
||||
RecordsDecoded []interface{} `json:"recordsdecoded"` // Records decoded. The encoding for each record depends on its type.
|
||||
}
|
||||
```
|
||||
|
||||
The array `RecordsDecoded` will contain any present record of the following:
|
||||
* Profile records, see `apiBlockRecordProfile`
|
||||
* File records, see `apiBlockRecordFile`
|
||||
|
||||
```go
|
||||
type apiBlockRecordProfile struct {
|
||||
Fields []apiBlockRecordProfileField `json:"fields"` // All fields
|
||||
Blobs []apiBlockRecordProfileBlob `json:"blobs"` // Blobs
|
||||
}
|
||||
|
||||
type apiBlockRecordProfileField struct {
|
||||
Type uint16 `json:"type"` // See ProfileFieldX constants.
|
||||
Text string `json:"text"` // The data
|
||||
}
|
||||
|
||||
type apiBlockRecordProfileBlob struct {
|
||||
Type uint16 `json:"type"` // See ProfileBlobX constants.
|
||||
Data []byte `json:"data"` // The data
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
type apiBlockRecordFile struct {
|
||||
ID uuid.UUID `json:"id"` // Unique ID.
|
||||
Hash []byte `json:"hash"` // Blake3 hash of the file data
|
||||
Type uint8 `json:"type"` // Type (low-level)
|
||||
Format uint16 `json:"format"` // Format (high-level)
|
||||
Size uint64 `json:"size"` // Size of the file
|
||||
Folder string `json:"folder"` // Folder, optional
|
||||
Name string `json:"name"` // Name of the file
|
||||
Description string `json:"description"` // Description. This is expected to be multiline and contain hashtags!
|
||||
Metadata []apiFileMetadata `json:"metadata"` // Metadata. These are decoded tags.
|
||||
TagsRaw []apiFileTagRaw `json:"tagsraw"` // All tags encoded that were not recognized as metadata.
|
||||
|
||||
// The following known tags from the core library are decoded into metadata or other fields in above structure; everything else is a raw tag:
|
||||
// TagTypeName, TagTypeFolder, TagTypeDescription, TagTypeDateCreated
|
||||
// The caller can specify its own metadata fields and fill the TagsRaw structure when creating a new file. It will be returned when reading the files' data.
|
||||
}
|
||||
|
||||
type apiFileMetadata struct {
|
||||
Type uint16 `json:"type"` // See core.TagTypeX constants.
|
||||
Name string `json:"name"` // User friendly name of the tag. Use the Type fields to identify the metadata as this name may change.
|
||||
Value string `json:"value"` // Text value of the tag.
|
||||
}
|
||||
|
||||
type apiFileTagRaw struct {
|
||||
Type uint16 `json:"type"` // See core.TagTypeX constants.
|
||||
Data []byte `json:"data"` // Data
|
||||
}
|
||||
```
|
||||
|
||||
### Blockchain Add File
|
||||
|
||||
This adds a file with the provided information to the blockchain.
|
||||
|
||||
```
|
||||
Request: POST /blockchain/self/add/file with JSON structure apiBlockAddFiles
|
||||
Response: 200 with JSON structure apiBlockchainBlockStatus
|
||||
```
|
||||
|
||||
```go
|
||||
type apiBlockAddFiles struct {
|
||||
Files []apiBlockRecordFile `json:"files"`
|
||||
}
|
||||
|
||||
type apiBlockchainBlockStatus struct {
|
||||
Status int `json:"status"` // Status: 0 = Success, 1 = Error invalid data
|
||||
Height uint64 `json:"height"` // New height of the blockchain (number of blocks).
|
||||
}
|
||||
```
|
||||
|
||||
Example POST request to `http://127.0.0.1:112/blockchain/self/add/file`:
|
||||
|
||||
```json
|
||||
{
|
||||
"files": [{
|
||||
"id": "236de31d-f402-4389-bdd1-56463abdc309",
|
||||
"hash": "aFad3zRACbk44dsOw5sVGxYmz+Rqh8ORDcGJNqIz+Ss=",
|
||||
"type": 1,
|
||||
"format": 10,
|
||||
"size": 4,
|
||||
"name": "Test.txt",
|
||||
"folder": "sample directory/sub folder",
|
||||
"description": "",
|
||||
"metadata": [],
|
||||
"tagsraw": []
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
Another example to create a new file but with a new arbitrary tag with type number 100 set to "test" and setting the metadata field "Date Created" (which is type 2 = `core.TagTypeDateCreated`):
|
||||
|
||||
```json
|
||||
{
|
||||
"files": [{
|
||||
"id": "bc32cbae-011d-4f0b-80a8-281ca93692e7",
|
||||
"hash": "aFad3zRACbk44dsOw5sVGxYmz+Rqh8ORDcGJNqIz+Ss=",
|
||||
"type": 1,
|
||||
"format": 10,
|
||||
"size": 4,
|
||||
"name": "Test.txt",
|
||||
"folder": "sample directory/sub folder",
|
||||
"description": "Example description\nThis can be any text #newfile #2021.",
|
||||
"metadata": [{
|
||||
"type": 2,
|
||||
"value": "2021-08-28 00:00:00"
|
||||
}],
|
||||
"tagsraw": [{
|
||||
"type": 100,
|
||||
"data": "dGVzdA=="
|
||||
}]
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
### Blockchain List Files
|
||||
|
||||
This lists all files stored on the blockchain.
|
||||
|
||||
```
|
||||
Request: GET /blockchain/self/list/file
|
||||
Response: 200 with JSON structure apiBlockAddFiles
|
||||
```
|
||||
|
||||
Example output:
|
||||
|
||||
```json
|
||||
{
|
||||
"files": [{
|
||||
"id": "a59b6465-fe8c-4a61-9fcc-fe37cf711fd4",
|
||||
"hash": "aFad3zRACbk44dsOw5sVGxYmz+Rqh8ORDcGJNqIz+Ss=",
|
||||
"type": 1,
|
||||
"format": 10,
|
||||
"size": 4,
|
||||
"folder": "sample directory/sub folder",
|
||||
"name": "Test.txt",
|
||||
"description": "",
|
||||
"metadata": [{
|
||||
"type": 4,
|
||||
"name": "Date Shared",
|
||||
"value": "2021-08-27 16:59:13"
|
||||
}],
|
||||
"tagsraw": []
|
||||
}],
|
||||
"status": 0
|
||||
}
|
||||
```
|
||||
|
||||
## Profile Functions
|
||||
|
||||
### Profile List
|
||||
|
||||
```
|
||||
Request: GET /profile/list
|
||||
Response: 200 with JSON structure apiProfileData
|
||||
```
|
||||
|
||||
```go
|
||||
type apiProfileData struct {
|
||||
Fields []apiBlockRecordProfileField `json:"fields"` // All fields
|
||||
Blobs []apiBlockRecordProfileBlob `json:"blobs"` // All blobs
|
||||
Status int `json:"status"` // Status of the operation, only used when this structure is returned from the API. See core.BlockchainStatusX.
|
||||
}
|
||||
|
||||
const (
|
||||
BlockchainStatusOK = 0 // No problems in the blockchain detected.
|
||||
BlockchainStatusBlockNotFound = 1 // Missing block in the blockchain.
|
||||
BlockchainStatusCorruptBlock = 2 // Error block encoding
|
||||
BlockchainStatusCorruptBlockRecord = 3 // Error block record encoding
|
||||
BlockchainStatusDataNotFound = 4 // Requested data not available in the blockchain
|
||||
)
|
||||
```
|
||||
|
||||
Example request: `http://127.0.0.1:112/profile/list`
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"fields": [{
|
||||
"type": 0,
|
||||
"text": "Test Username 2022"
|
||||
}, {
|
||||
"type": 1,
|
||||
"text": "test@example.com"
|
||||
}],
|
||||
"blobs": null,
|
||||
"status": 0
|
||||
}
|
||||
```
|
||||
|
||||
### Profile Read
|
||||
|
||||
This reads a specific users' profile field or blob. For the index see the `ProfileFieldX` and `ProfileBlobX` constants.
|
||||
|
||||
```
|
||||
Request: GET /profile/read?field=[index] or &blob=[index]
|
||||
Response: 200 with JSON structure apiProfileData
|
||||
```
|
||||
|
||||
```go
|
||||
// ProfileFieldX constants define well known profile information
|
||||
const (
|
||||
ProfileFieldName = 0 // Arbitrary username
|
||||
ProfileFieldEmail = 1 // Email address
|
||||
ProfileFieldWebsite = 2 // Website address
|
||||
ProfileFieldTwitter = 3 // Twitter account without the @
|
||||
ProfileFieldYouTube = 4 // YouTube channel URL
|
||||
ProfileFieldAddress = 5 // Physical address
|
||||
)
|
||||
|
||||
// ProfileBlobX constants define well known blobs
|
||||
// Pictures should be in JPEG or PNG format.
|
||||
const (
|
||||
ProfileBlobPicture = 0 // Profile picture, unspecified size
|
||||
)
|
||||
```
|
||||
|
||||
Example request to read the users username: `http://127.0.0.1:112/profile/read?field=0`
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"fields": [{
|
||||
"type": 0,
|
||||
"text": "Test Username 2022"
|
||||
}],
|
||||
"blobs": null,
|
||||
"status": 0
|
||||
}
|
||||
```
|
||||
|
||||
### Profile Write
|
||||
|
||||
```
|
||||
Request: POST /profile/write with JSON structure apiProfileData
|
||||
Response: 200 with JSON structure apiBlockchainBlockStatus
|
||||
```
|
||||
|
||||
Example POST request to `http://127.0.0.1:112/profile/write`:
|
||||
|
||||
```json
|
||||
{
|
||||
"fields": [{
|
||||
"type": 0,
|
||||
"text": "Test Username 2021"
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 0,
|
||||
"height": 1
|
||||
}
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user