mirror of
https://github.com/PeernetOfficial/Cmd.git
synced 2026-07-16 18:37:51 +01:00
Refactor API into the core package.
This commit is contained in:
545
API.go
545
API.go
@@ -8,18 +8,10 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/PeernetOfficial/core"
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/PeernetOfficial/core/webapi"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
@@ -28,55 +20,8 @@ func startAPI() {
|
||||
return
|
||||
}
|
||||
|
||||
// by default allow all requests
|
||||
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("/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")
|
||||
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 config.APIListen {
|
||||
go startWebServer(listen, config.APIUseSSL, config.APICertificateFile, config.APICertificateKey, router, "API", parseDuration(config.APITimeoutRead), parseDuration(config.APITimeoutWrite))
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
webapi.Start(config.APIListen, config.APIUseSSL, config.APICertificateFile, config.APICertificateKey, parseDuration(config.APITimeoutRead), parseDuration(config.APITimeoutWrite))
|
||||
webapi.Router.HandleFunc("/console", apiConsole).Methods("GET")
|
||||
}
|
||||
|
||||
// parseDuration is the same as time.ParseDuration without returning an error. Valid units are ms, s, m, h. For example "10s".
|
||||
@@ -85,91 +30,13 @@ func parseDuration(input string) (result time.Duration) {
|
||||
return
|
||||
}
|
||||
|
||||
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"))
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
var wsUpgrader = websocket.Upgrader{} // use default options
|
||||
|
||||
/* apiConsole provides a websocket to send/receive internal commands
|
||||
/*
|
||||
apiConsole provides a websocket to send/receive internal commands
|
||||
Request: GET /console
|
||||
Result: 200 with JSON structure apiResponsePeerSelf
|
||||
*/
|
||||
func apiConsole(w http.ResponseWriter, r *http.Request) {
|
||||
c, err := wsUpgrader.Upgrade(w, r, nil)
|
||||
c, err := webapi.WSUpgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
// May happen if request is simple HTTP request.
|
||||
return
|
||||
@@ -220,403 +87,3 @@ 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"` // 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)
|
||||
}
|
||||
|
||||
// --- Profile API ---
|
||||
|
||||
// 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 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
418
API.md
418
API.md
@@ -1,96 +1,13 @@
|
||||
# API
|
||||
|
||||
The API is intended to be used by a local full standalone client that provides a frontend for the end-user. The API allows to use Peernet effectively; it provides functions to share files, search for content and download files.
|
||||
Please see https://github.com/PeernetOfficial/core/webapi for the API specification.
|
||||
|
||||
Note: This API code is likely to be moved into the core soon.
|
||||
|
||||
## Use considerations
|
||||
|
||||
It is not intended to be directly used in a legacy web browser, and shall not be exposed to the internet. 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 currently unauthenticated and intentionally provides direct access to the users blockchain.
|
||||
|
||||
### Notes
|
||||
|
||||
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.
|
||||
|
||||
## Configuration
|
||||
|
||||
The configuration file (default `Config.yaml`) contains settings for the API. To listen on `http://127.0.0.1:112/` add this line:
|
||||
|
||||
```yaml
|
||||
APIListen: ["127.0.0.1:112"]
|
||||
```
|
||||
|
||||
## Overview
|
||||
|
||||
These are the functions provided by the API:
|
||||
This code extends the API by these function:
|
||||
|
||||
```
|
||||
/status Provides current connectivity status to the network
|
||||
/peer/self Provides information about the self peer details
|
||||
/share/list List all files and directories that are shared
|
||||
/share/file Share a file via the peers blockchain
|
||||
|
||||
/search Search Peernet for a file based on keywords or hash
|
||||
/download/start Download a file based on a hash
|
||||
/download/status Get the status of a download
|
||||
|
||||
/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 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
|
||||
```
|
||||
|
||||
The `/share` functions are providing high-level functionality to work with files. The `/blockchain` functions provide low-level functionality which is typically not needed.
|
||||
|
||||
## 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.
|
||||
}
|
||||
```
|
||||
|
||||
## Console
|
||||
|
||||
@@ -99,334 +16,3 @@ 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"` // 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 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
|
||||
}
|
||||
```
|
||||
|
||||
4
go.mod
4
go.mod
@@ -3,9 +3,7 @@ module github.com/PeernetOfficial/Cmd
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/PeernetOfficial/core v0.0.0-20210903002426-928c16c492cf
|
||||
github.com/PeernetOfficial/core v0.0.0-20210910114007-19e29b3a910a
|
||||
github.com/btcsuite/btcd v0.22.0-beta.0.20210625194946-86a17263b0ff
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/gorilla/mux v1.8.1-0.20200912192056-d07530f46e1e
|
||||
github.com/gorilla/websocket v1.4.3-0.20210424162022-e8629af678b7
|
||||
)
|
||||
|
||||
9
go.sum
9
go.sum
@@ -1,5 +1,5 @@
|
||||
github.com/PeernetOfficial/core v0.0.0-20210903002426-928c16c492cf h1:Hp+32gIAaXZBMPxVmHVY9JhHF9deLmmrnxtruWObP8s=
|
||||
github.com/PeernetOfficial/core v0.0.0-20210903002426-928c16c492cf/go.mod h1:GCG3qjbCPEY+/PGIO76Dj/Dp6JvpMYy0B6T+CbmSk1A=
|
||||
github.com/PeernetOfficial/core v0.0.0-20210910114007-19e29b3a910a h1:1ebpb3L6PGLI8lX5xkeXKOxoATOoedMUamAnl0elJ04=
|
||||
github.com/PeernetOfficial/core v0.0.0-20210910114007-19e29b3a910a/go.mod h1:c2AGQxju2HdM8mrsgy6nFX3mW2TGs0/00kggbBqLB+o=
|
||||
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=
|
||||
@@ -26,8 +26,9 @@ 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.1-0.20200912192056-d07530f46e1e h1:8+CIGbDW29nl9niCoMrpF8+ACFz3rLRpIjuqnx4rNKg=
|
||||
github.com/gorilla/mux v1.8.1-0.20200912192056-d07530f46e1e/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
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/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.4.3-0.20210424162022-e8629af678b7 h1:L89uC9ATI61/V2eNgZYtQHyjjyjEplemB+aky4HdyzQ=
|
||||
github.com/gorilla/websocket v1.4.3-0.20210424162022-e8629af678b7/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
|
||||
Reference in New Issue
Block a user