From 19e29b3a910a100f61fd07bb5ad1d722aaf7d539 Mon Sep 17 00:00:00 2001 From: Kleissner Date: Fri, 10 Sep 2021 13:40:07 +0200 Subject: [PATCH] Add webapi. Provides an HTTP API for clients to use Peernet. --- File Formats.go | 34 ++-- go.mod | 2 + go.sum | 4 + webapi/API.go | 113 ++++++++++++ webapi/Blockchain.go | 307 ++++++++++++++++++++++++++++++++ webapi/Profile.go | 126 +++++++++++++ webapi/Status.go | 60 +++++++ webapi/readme.md | 412 +++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 1041 insertions(+), 17 deletions(-) create mode 100644 webapi/API.go create mode 100644 webapi/Blockchain.go create mode 100644 webapi/Profile.go create mode 100644 webapi/Status.go create mode 100644 webapi/readme.md diff --git a/File Formats.go b/File Formats.go index 02e3fb8..c85933f 100644 --- a/File Formats.go +++ b/File Formats.go @@ -24,23 +24,23 @@ const ( // High-level file types. New ones may be added as required. const ( - FormatUnspecified = iota // Unspecified - FormatPDF // PDF document - FormatWord // Word document - FormatExcel // Excel - FormatPowerpoint // Powerpoint - FormatPicture // Pictures (including GIF, excluding icons) - FormatAudio // Audio files - FormatVideo // Video files - FormatContainer // Compressed files including ZIP, RAR, TAR and others - FormatHTML // HTML file - FormatText // Text file - FormatEbook // Ebook file - FormatCompressed // Compressed file. - FormatDatabase // Database file - FormatEmail // Single email - FormatCSV // CSV file - FormatFolder // Virtual folder + FormatBinary = iota // Binary/unspecified + FormatPDF // PDF document + FormatWord // Word document + FormatExcel // Excel + FormatPowerpoint // Powerpoint + FormatPicture // Pictures (including GIF, excluding icons) + FormatAudio // Audio files + FormatVideo // Video files + FormatContainer // Compressed files including ZIP, RAR, TAR and others + FormatHTML // HTML file + FormatText // Text file + FormatEbook // Ebook file + FormatCompressed // Compressed file. + FormatDatabase // Database file + FormatEmail // Single email + FormatCSV // CSV file + FormatFolder // Virtual folder ) // Future tags to be defined for audio/video: Artist, Album, Title, Length, Bitrate, Codec diff --git a/go.mod b/go.mod index b96ef3e..1c9c5d3 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,8 @@ require ( github.com/akrylysov/pogreb v0.10.1 github.com/btcsuite/btcd v0.21.0-beta.0.20210401013323-36a96f6a0025 github.com/google/uuid v1.3.0 + github.com/gorilla/mux v1.8.0 + github.com/gorilla/websocket v1.4.2 github.com/pkg/errors v0.9.1 golang.org/x/crypto v0.0.0-20210415154028-4f45737414dc golang.org/x/net v0.0.0-20210420210106-798c2154c571 diff --git a/go.sum b/go.sum index 519acea..8c18b9d 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,10 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= diff --git a/webapi/API.go b/webapi/API.go new file mode 100644 index 0000000..5163f18 --- /dev/null +++ b/webapi/API.go @@ -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")) +} diff --git a/webapi/Blockchain.go b/webapi/Blockchain.go new file mode 100644 index 0000000..4943dcf --- /dev/null +++ b/webapi/Blockchain.go @@ -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 +} diff --git a/webapi/Profile.go b/webapi/Profile.go new file mode 100644 index 0000000..eee1179 --- /dev/null +++ b/webapi/Profile.go @@ -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 +} diff --git a/webapi/Status.go b/webapi/Status.go new file mode 100644 index 0000000..71bd855 --- /dev/null +++ b/webapi/Status.go @@ -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) +} diff --git a/webapi/readme.md b/webapi/readme.md new file mode 100644 index 0000000..c27e950 --- /dev/null +++ b/webapi/readme.md @@ -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 +} +``` +