diff --git a/.gitignore b/.gitignore index 3fbf3ff..5407dcb 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ log.txt *debug_bin *.yaml +.vscode +self.* \ No newline at end of file diff --git a/API.go b/API.go index ddfc2a9..7226486 100644 --- a/API.go +++ b/API.go @@ -11,8 +11,10 @@ import ( "crypto/tls" "encoding/hex" "encoding/json" + "errors" "log" "net/http" + "strconv" "time" "github.com/PeernetOfficial/core" @@ -36,6 +38,10 @@ func startAPI() { router.HandleFunc("/status", apiStatus).Methods("GET") router.HandleFunc("/peer/self", apiPeerSelf).Methods("GET") router.HandleFunc("/console", apiConsole).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") for _, listen := range config.APIListen { go startWebServer(listen, config.APIUseSSL, config.APICertificateFile, config.APICertificateKey, router, "API", parseDuration(config.APITimeoutRead), parseDuration(config.APITimeoutWrite)) @@ -85,6 +91,23 @@ func apiEncodeJSON(w http.ResponseWriter, r *http.Request, data interface{}) (er 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")) @@ -94,6 +117,9 @@ 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 @@ -102,6 +128,7 @@ 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. @@ -188,3 +215,156 @@ func apiConsole(w http.ResponseWriter, r *http.Request) { bufferR.Write(message) } } + +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"` // Block records raw. Successfully decoded records are parsed into the below fields. + Decoded struct { + User apiBlockRecordUser `json:"user"` // User details + Files []apiBlockRecordFile `json:"files"` // Files + } `json:"decoded"` +} + +// apiBlockRecordUser specifies user information +type apiBlockRecordUser struct { + NameValid bool `json:"namevalid"` // Whether the name is supplied in this structure. + Name string `json:"name"` // Arbitrary name of the user. + NameSanitized string `json:"namesanitized"` // Sanitized version of the name. +} + +// apiBlockRecordFile is the metadata of a file published on the blockchain +type apiBlockRecordFile struct { + 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 + Directory string `json:"directory"` // Directory + Name string `json:"name"` // File name +} + +/* +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()) + + if block.User.Valid { + result.Decoded.User.NameValid = true + result.Decoded.User.Name = block.User.Name + result.Decoded.User.NameSanitized = block.User.NameS + } + + for _, file := range block.Files { + result.Decoded.Files = append(result.Decoded.Files, apiBlockRecordFile{Hash: file.Hash, Type: file.Type, Format: file.Format, Size: file.Size, Directory: file.Directory, Name: file.Name}) + } + } + + apiEncodeJSON(w, r, result) +} + +// apiBlockAddFiles contains the file metadata to add to the blockchain +type apiBlockAddFiles struct { + Files []apiBlockRecordFile `json:"files"` +} + +/* +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 files []core.BlockRecordFile + + for _, file := range input.Files { + files = append(files, core.BlockRecordFile{Hash: file.Hash, Type: file.Type, Format: file.Format, Size: file.Size, Directory: file.Directory, Name: file.Name}) + } + + newHeight, status := core.UserBlockchainAddFiles(files) + + apiEncodeJSON(w, r, apiBlockchainBlockStatus{Status: status, Height: newHeight}) +} diff --git a/API.md b/API.md index 29c6c38..bd710d8 100644 --- a/API.md +++ b/API.md @@ -14,7 +14,7 @@ The API is currently unauthenticated and intentionally provides direct access to The API is still in development and endpoints are subject to change. The API should be currently only used for debugging and early phase development purposes. -## Configuartion +## Configuration The configuration file (default `Config.yaml`) contains settings for the API. To listen on `http://127.0.0.1:112/` add this line: @@ -39,9 +39,10 @@ These are the functions provided by the API: /status/ws Starts a websocket to receive updates on operations immediately (push instead of pull) /console Console provides a websocket to send/receive internal commands -/blockchain/self/header Header of the self peers blockchain -/blockchain/self/read Read the self peers blockchain -/blockchain/self/append Add a record to the blockchain +/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 ``` The `/share` functions are providing high-level functionality to work with files. The `/blockchain` functions provide low-level functionality which is typically not needed. @@ -61,11 +62,12 @@ 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. } ``` -Example response data: Todo - ## Self Information This function returns information about the current peer. @@ -76,7 +78,7 @@ Request: GET /peer/self Response: 200 with JSON structure apiResponsePeerSelf ``` -The peer and node IDs are returned as hex encoded strings. +The peer and node IDs are encoded as hex encoded strings. ```go type apiResponsePeerSelf struct { @@ -85,8 +87,6 @@ type apiResponsePeerSelf struct { } ``` -Example response data: Todo - ## Console The `/console` websocket allows to execute internal commands. This should be only used for debugging purposes by the end-user. The same input and output as raw text as via the command-line is provided through this endpoint. @@ -94,3 +94,110 @@ The `/console` websocket allows to execute internal commands. This should be onl ``` Request: ws://127.0.0.1:112/console ``` + +## 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"` // Block records raw. Successfully decoded records are parsed into the below fields. + decoded struct { + User apiBlockRecordUser `json:"user"` // User details + Files []apiBlockRecordFile `json:"files"` // Files + } `json:"decoded"` +} + +type apiBlockRecordUser struct { + NameValid bool `json:"namevalid"` // Whether the name is supplied in this structure. + Name string `json:"name"` // Arbitrary name of the user. + NameSanitized string `json:"namesanitized"` // Sanitized version of the name. +} + +type apiBlockRecordFile struct { + Hash []byte // Hash of the file data + Type uint8 // Type (low-level) + Format uint16 // Format (high-level) + Size uint64 // Size of the file + Directory string // Directory + Name string // File name +} +``` + +## 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). +} +``` diff --git a/go.mod b/go.mod index 8c9b26d..6bb9a53 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/PeernetOfficial/Cmd go 1.16 require ( - github.com/PeernetOfficial/core v0.0.0-20210731152914-2937154c8d7b + github.com/PeernetOfficial/core v0.0.0-20210819021308-a2420b8468b1 github.com/btcsuite/btcd v0.22.0-beta.0.20210625194946-86a17263b0ff github.com/gorilla/mux v1.8.1-0.20200912192056-d07530f46e1e github.com/gorilla/websocket v1.4.3-0.20210424162022-e8629af678b7 diff --git a/go.sum b/go.sum index b6eb75e..7199165 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ -github.com/PeernetOfficial/core v0.0.0-20210731152914-2937154c8d7b h1:y/Afz6M1vlLpn5nmZmHyK0Tze7hOl9N7p43HfTb578I= -github.com/PeernetOfficial/core v0.0.0-20210731152914-2937154c8d7b/go.mod h1:9aIe6MS6tefJnfmyEZFqaNF4NIeq7wJutV7EU+uowps= +github.com/PeernetOfficial/core v0.0.0-20210819021308-a2420b8468b1 h1:FTI8IWp3NN7ypWz+nTcERpFlBlE6PVzt1bhSTfzPd2Q= +github.com/PeernetOfficial/core v0.0.0-20210819021308-a2420b8468b1/go.mod h1:upfaI2iJJ7lyH+UtOLU7t6oVcLO7PrDF7SsSUWJJ/OY= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/akrylysov/pogreb v0.10.1 h1:FqlR8VR7uCbJdfUob916tPM+idpKgeESDXOA1K0DK4w= +github.com/akrylysov/pogreb v0.10.1/go.mod h1:pNs6QmpQ1UlTJKDezuRWmaqkgUE2TuU0YTWyqJZ7+lI= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.21.0-beta.0.20210401013323-36a96f6a0025/go.mod h1:9n5ntfhhHQBIhUvlhDvD3Qg6fRUj4jkN0VB8L8svzOA= github.com/btcsuite/btcd v0.22.0-beta.0.20210625194946-86a17263b0ff h1:7aWbvIwkXchSCIWgobzJ+a6l87s6WHkJ78sZqxSpQCk=