mirror of
https://github.com/PeernetOfficial/Abstraction.git
synced 2026-07-18 19:37:50 +01:00
added abstraction to search for a file
This commit is contained in:
394
webapi/API.go
394
webapi/API.go
@@ -1,197 +1,197 @@
|
||||
/*
|
||||
File Name: API.go
|
||||
Copyright: 2021 Peernet Foundation s.r.o.
|
||||
Author: Peter Kleissner
|
||||
*/
|
||||
|
||||
package webapi
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/IncSW/geoip2"
|
||||
"github.com/PeernetOfficial/core"
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type WebapiInstance struct {
|
||||
backend *core.Backend
|
||||
geoipCityReader *geoip2.CityReader
|
||||
|
||||
// Router can be used to register additional API functions
|
||||
Router *mux.Router
|
||||
AllowKeyInParam []string // List of paths that accept the API key as &k= parameter
|
||||
|
||||
// search jobs
|
||||
allJobs map[uuid.UUID]*SearchJob
|
||||
allJobsMutex sync.RWMutex
|
||||
|
||||
// download info
|
||||
downloads map[uuid.UUID]*downloadInfo
|
||||
downloadsMutex sync.RWMutex
|
||||
}
|
||||
|
||||
// WSUpgrader is used for websocket functionality. It allows all requests.
|
||||
var WSUpgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
// allow all connections by default
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
// Start starts the API. ListenAddresses is a list of IP:Ports.
|
||||
// The certificate file and key are only used if SSL is enabled. The read and write timeout may be 0 for no timeout.
|
||||
// The API key may be uuid.Nil to disable it although this is not recommended for security reasons.
|
||||
func Start(Backend *core.Backend, ListenAddresses []string, UseSSL bool, CertificateFile, CertificateKey string, TimeoutRead, TimeoutWrite time.Duration, APIKey uuid.UUID) (api *WebapiInstance) {
|
||||
if len(ListenAddresses) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
api = &WebapiInstance{
|
||||
backend: Backend,
|
||||
Router: mux.NewRouter(),
|
||||
AllowKeyInParam: []string{"/file/read", "/file/view"},
|
||||
allJobs: make(map[uuid.UUID]*SearchJob),
|
||||
downloads: make(map[uuid.UUID]*downloadInfo),
|
||||
}
|
||||
|
||||
if APIKey != uuid.Nil {
|
||||
api.Router.Use(api.authenticateMiddleware(APIKey))
|
||||
}
|
||||
|
||||
api.Router.HandleFunc("/test", apiTest).Methods("GET")
|
||||
api.Router.HandleFunc("/status", api.apiStatus).Methods("GET")
|
||||
api.Router.HandleFunc("/status/peers", api.apiStatusPeers).Methods("GET")
|
||||
api.Router.HandleFunc("/account/info", api.apiAccountInfo).Methods("GET")
|
||||
api.Router.HandleFunc("/account/delete", api.apiAccountDelete).Methods("GET")
|
||||
api.Router.HandleFunc("/blockchain/header", api.apiBlockchainHeaderFunc).Methods("GET")
|
||||
api.Router.HandleFunc("/blockchain/append", api.apiBlockchainAppend).Methods("POST")
|
||||
api.Router.HandleFunc("/blockchain/read", api.apiBlockchainRead).Methods("GET")
|
||||
api.Router.HandleFunc("/blockchain/file/add", api.apiBlockchainFileAdd).Methods("POST")
|
||||
api.Router.HandleFunc("/blockchain/file/list", api.apiBlockchainFileList).Methods("GET")
|
||||
api.Router.HandleFunc("/blockchain/file/delete", api.apiBlockchainFileDelete).Methods("POST")
|
||||
api.Router.HandleFunc("/blockchain/file/update", api.apiBlockchainFileUpdate).Methods("POST")
|
||||
api.Router.HandleFunc("/profile/list", api.apiProfileList).Methods("GET")
|
||||
api.Router.HandleFunc("/profile/read", api.apiProfileRead).Methods("GET")
|
||||
api.Router.HandleFunc("/profile/write", api.apiProfileWrite).Methods("POST")
|
||||
api.Router.HandleFunc("/profile/delete", api.apiProfileDelete).Methods("POST")
|
||||
api.Router.HandleFunc("/search", api.apiSearch).Methods("POST")
|
||||
api.Router.HandleFunc("/search/result", api.apiSearchResult).Methods("GET")
|
||||
api.Router.HandleFunc("/search/result/ws", api.apiSearchResultStream).Methods("GET")
|
||||
api.Router.HandleFunc("/search/statistic", api.apiSearchStatistic).Methods("GET")
|
||||
api.Router.HandleFunc("/search/terminate", api.apiSearchTerminate).Methods("GET")
|
||||
api.Router.HandleFunc("/explore", api.apiExplore).Methods("GET")
|
||||
api.Router.HandleFunc("/file/format", api.apiFileFormat).Methods("GET")
|
||||
api.Router.HandleFunc("/download/start", api.apiDownloadStart).Methods("GET")
|
||||
api.Router.HandleFunc("/download/status", api.apiDownloadStatus).Methods("GET")
|
||||
api.Router.HandleFunc("/download/action", api.apiDownloadAction).Methods("GET")
|
||||
api.Router.HandleFunc("/warehouse/create", api.apiWarehouseCreateFile).Methods("POST")
|
||||
api.Router.HandleFunc("/warehouse/create/path", api.apiWarehouseCreateFilePath).Methods("GET")
|
||||
api.Router.HandleFunc("/warehouse/read", api.apiWarehouseReadFile).Methods("GET")
|
||||
api.Router.HandleFunc("/warehouse/read/path", api.apiWarehouseReadFilePath).Methods("GET")
|
||||
api.Router.HandleFunc("/warehouse/delete", api.apiWarehouseDeleteFile).Methods("GET")
|
||||
api.Router.HandleFunc("/file/read", api.apiFileRead).Methods("GET")
|
||||
api.Router.HandleFunc("/file/view", api.apiFileView).Methods("GET")
|
||||
|
||||
for _, listen := range ListenAddresses {
|
||||
go startWebAPI(Backend, listen, UseSSL, CertificateFile, CertificateKey, api.Router, "API", TimeoutRead, TimeoutWrite)
|
||||
}
|
||||
|
||||
return api
|
||||
}
|
||||
|
||||
// startWebAPI starts a web-server with given parameters and logs the status. If may block forever and only returns if there is an error.
|
||||
// The certificate file and key are only used if SSL is enabled. The read and write timeout may be 0 for no timeout.
|
||||
func startWebAPI(Backend *core.Backend, WebListen string, UseSSL bool, CertificateFile, CertificateKey string, Handler http.Handler, Info string, ReadTimeout, WriteTimeout time.Duration) {
|
||||
Backend.LogError("startWebAPI", "Start API at '%s'\n", WebListen)
|
||||
|
||||
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 {
|
||||
Backend.LogError("startWebAPI", "Error listening on '%s': %v\n", WebListen, err)
|
||||
}
|
||||
} else {
|
||||
// HTTP
|
||||
if err := server.ListenAndServe(); err != nil {
|
||||
Backend.LogError("startWebAPI", "Error listening on '%s': %v\n", WebListen, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// EncodeJSON encodes the data as JSON
|
||||
func EncodeJSON(Backend *core.Backend, 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 {
|
||||
Backend.LogError("EncodeJSON", "Error writing data for route '%s': %v\n", r.URL.Path, err)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// DecodeJSON 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 DecodeJSON(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
|
||||
}
|
||||
|
||||
// authenticateMiddleware returns a middleware function to be used with mux.Router.Use(). It handles all authentication functionality.
|
||||
func (api *WebapiInstance) authenticateMiddleware(APIKey uuid.UUID) func(http.Handler) http.Handler {
|
||||
return (func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
keyID, err := uuid.Parse(r.Header.Get("x-api-key"))
|
||||
if err != nil { // special case for some paths
|
||||
for _, exceptPath := range api.AllowKeyInParam {
|
||||
if exceptPath == r.URL.Path {
|
||||
r.ParseForm()
|
||||
keyID, err = uuid.Parse(r.Form.Get("k"))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil { // Invalid key format
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if keyID != APIKey {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
})
|
||||
}
|
||||
/*
|
||||
File Name: API.go
|
||||
Copyright: 2021 Peernet Foundation s.r.o.
|
||||
Author: Peter Kleissner
|
||||
*/
|
||||
|
||||
package webapi
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/IncSW/geoip2"
|
||||
"github.com/PeernetOfficial/core"
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type WebapiInstance struct {
|
||||
Backend *core.Backend
|
||||
geoipCityReader *geoip2.CityReader
|
||||
|
||||
// Router can be used to register additional API functions
|
||||
Router *mux.Router
|
||||
AllowKeyInParam []string // List of paths that accept the API key as &k= parameter
|
||||
|
||||
// search jobs
|
||||
allJobs map[uuid.UUID]*SearchJob
|
||||
allJobsMutex sync.RWMutex
|
||||
|
||||
// download info
|
||||
downloads map[uuid.UUID]*downloadInfo
|
||||
downloadsMutex sync.RWMutex
|
||||
}
|
||||
|
||||
// WSUpgrader is used for websocket functionality. It allows all requests.
|
||||
var WSUpgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
// allow all connections by default
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
// Start starts the API. ListenAddresses is a list of IP:Ports.
|
||||
// The certificate file and key are only used if SSL is enabled. The read and write timeout may be 0 for no timeout.
|
||||
// The API key may be uuid.Nil to disable it although this is not recommended for security reasons.
|
||||
func Start(Backend *core.Backend, ListenAddresses []string, UseSSL bool, CertificateFile, CertificateKey string, TimeoutRead, TimeoutWrite time.Duration, APIKey uuid.UUID) (api *WebapiInstance) {
|
||||
if len(ListenAddresses) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
api = &WebapiInstance{
|
||||
Backend: Backend,
|
||||
Router: mux.NewRouter(),
|
||||
AllowKeyInParam: []string{"/file/read", "/file/view"},
|
||||
allJobs: make(map[uuid.UUID]*SearchJob),
|
||||
downloads: make(map[uuid.UUID]*downloadInfo),
|
||||
}
|
||||
|
||||
if APIKey != uuid.Nil {
|
||||
api.Router.Use(api.authenticateMiddleware(APIKey))
|
||||
}
|
||||
|
||||
api.Router.HandleFunc("/test", apiTest).Methods("GET")
|
||||
api.Router.HandleFunc("/status", api.apiStatus).Methods("GET")
|
||||
api.Router.HandleFunc("/status/peers", api.apiStatusPeers).Methods("GET")
|
||||
api.Router.HandleFunc("/account/info", api.apiAccountInfo).Methods("GET")
|
||||
api.Router.HandleFunc("/account/delete", api.apiAccountDelete).Methods("GET")
|
||||
api.Router.HandleFunc("/blockchain/header", api.apiBlockchainHeaderFunc).Methods("GET")
|
||||
api.Router.HandleFunc("/blockchain/append", api.apiBlockchainAppend).Methods("POST")
|
||||
api.Router.HandleFunc("/blockchain/read", api.apiBlockchainRead).Methods("GET")
|
||||
api.Router.HandleFunc("/blockchain/file/add", api.apiBlockchainFileAdd).Methods("POST")
|
||||
api.Router.HandleFunc("/blockchain/file/list", api.apiBlockchainFileList).Methods("GET")
|
||||
api.Router.HandleFunc("/blockchain/file/delete", api.apiBlockchainFileDelete).Methods("POST")
|
||||
api.Router.HandleFunc("/blockchain/file/update", api.apiBlockchainFileUpdate).Methods("POST")
|
||||
api.Router.HandleFunc("/profile/list", api.apiProfileList).Methods("GET")
|
||||
api.Router.HandleFunc("/profile/read", api.apiProfileRead).Methods("GET")
|
||||
api.Router.HandleFunc("/profile/write", api.apiProfileWrite).Methods("POST")
|
||||
api.Router.HandleFunc("/profile/delete", api.apiProfileDelete).Methods("POST")
|
||||
api.Router.HandleFunc("/search", api.apiSearch).Methods("POST")
|
||||
api.Router.HandleFunc("/search/result", api.apiSearchResult).Methods("GET")
|
||||
api.Router.HandleFunc("/search/result/ws", api.apiSearchResultStream).Methods("GET")
|
||||
api.Router.HandleFunc("/search/statistic", api.apiSearchStatistic).Methods("GET")
|
||||
api.Router.HandleFunc("/search/terminate", api.apiSearchTerminate).Methods("GET")
|
||||
api.Router.HandleFunc("/explore", api.apiExplore).Methods("GET")
|
||||
api.Router.HandleFunc("/file/format", api.apiFileFormat).Methods("GET")
|
||||
api.Router.HandleFunc("/download/start", api.apiDownloadStart).Methods("GET")
|
||||
api.Router.HandleFunc("/download/status", api.apiDownloadStatus).Methods("GET")
|
||||
api.Router.HandleFunc("/download/action", api.apiDownloadAction).Methods("GET")
|
||||
api.Router.HandleFunc("/warehouse/create", api.apiWarehouseCreateFile).Methods("POST")
|
||||
api.Router.HandleFunc("/warehouse/create/path", api.apiWarehouseCreateFilePath).Methods("GET")
|
||||
api.Router.HandleFunc("/warehouse/read", api.apiWarehouseReadFile).Methods("GET")
|
||||
api.Router.HandleFunc("/warehouse/read/path", api.apiWarehouseReadFilePath).Methods("GET")
|
||||
api.Router.HandleFunc("/warehouse/delete", api.apiWarehouseDeleteFile).Methods("GET")
|
||||
api.Router.HandleFunc("/file/read", api.apiFileRead).Methods("GET")
|
||||
api.Router.HandleFunc("/file/view", api.apiFileView).Methods("GET")
|
||||
|
||||
for _, listen := range ListenAddresses {
|
||||
go startWebAPI(Backend, listen, UseSSL, CertificateFile, CertificateKey, api.Router, "API", TimeoutRead, TimeoutWrite)
|
||||
}
|
||||
|
||||
return api
|
||||
}
|
||||
|
||||
// startWebAPI starts a web-server with given parameters and logs the status. If may block forever and only returns if there is an error.
|
||||
// The certificate file and key are only used if SSL is enabled. The read and write timeout may be 0 for no timeout.
|
||||
func startWebAPI(Backend *core.Backend, WebListen string, UseSSL bool, CertificateFile, CertificateKey string, Handler http.Handler, Info string, ReadTimeout, WriteTimeout time.Duration) {
|
||||
Backend.LogError("startWebAPI", "Start API at '%s'\n", WebListen)
|
||||
|
||||
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 {
|
||||
Backend.LogError("startWebAPI", "Error listening on '%s': %v\n", WebListen, err)
|
||||
}
|
||||
} else {
|
||||
// HTTP
|
||||
if err := server.ListenAndServe(); err != nil {
|
||||
Backend.LogError("startWebAPI", "Error listening on '%s': %v\n", WebListen, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// EncodeJSON encodes the data as JSON
|
||||
func EncodeJSON(Backend *core.Backend, 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 {
|
||||
Backend.LogError("EncodeJSON", "Error writing data for route '%s': %v\n", r.URL.Path, err)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// DecodeJSON 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 DecodeJSON(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
|
||||
}
|
||||
|
||||
// authenticateMiddleware returns a middleware function to be used with mux.Router.Use(). It handles all authentication functionality.
|
||||
func (api *WebapiInstance) authenticateMiddleware(APIKey uuid.UUID) func(http.Handler) http.Handler {
|
||||
return (func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
keyID, err := uuid.Parse(r.Header.Get("x-api-key"))
|
||||
if err != nil { // special case for some paths
|
||||
for _, exceptPath := range api.AllowKeyInParam {
|
||||
if exceptPath == r.URL.Path {
|
||||
r.ParseForm()
|
||||
keyID, err = uuid.Parse(r.Form.Get("k"))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil { // Invalid key format
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if keyID != APIKey {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,122 +1,122 @@
|
||||
/*
|
||||
File Name: Blockchain.go
|
||||
Copyright: 2021 Peernet Foundation s.r.o.
|
||||
Author: Peter Kleissner
|
||||
*/
|
||||
|
||||
package webapi
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/PeernetOfficial/core/blockchain"
|
||||
)
|
||||
|
||||
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.
|
||||
}
|
||||
|
||||
/*
|
||||
apiBlockchainHeaderFunc returns the current blockchain header information
|
||||
|
||||
Request: GET /blockchain/header
|
||||
Result: 200 with JSON structure apiResponsePeerSelf
|
||||
*/
|
||||
func (api *WebapiInstance) apiBlockchainHeaderFunc(w http.ResponseWriter, r *http.Request) {
|
||||
publicKey, height, version := api.backend.UserBlockchain.Header()
|
||||
|
||||
EncodeJSON(api.backend, 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"` // See blockchain.StatusX.
|
||||
Height uint64 `json:"height"` // Height of the blockchain (number of blocks).
|
||||
Version uint64 `json:"version"` // Version of the blockchain.
|
||||
}
|
||||
|
||||
/*
|
||||
apiBlockchainAppend 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/append with JSON structure apiBlockchainBlockRaw
|
||||
Response: 200 with JSON structure apiBlockchainBlockStatus
|
||||
*/
|
||||
func (api *WebapiInstance) apiBlockchainAppend(w http.ResponseWriter, r *http.Request) {
|
||||
var input apiBlockchainBlockRaw
|
||||
if err := DecodeJSON(w, r, &input); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var records []blockchain.BlockRecordRaw
|
||||
|
||||
for _, record := range input.Records {
|
||||
records = append(records, blockchain.BlockRecordRaw{Type: record.Type, Data: record.Data})
|
||||
}
|
||||
|
||||
newHeight, newVersion, status := api.backend.UserBlockchain.Append(records)
|
||||
|
||||
EncodeJSON(api.backend, w, r, apiBlockchainBlockStatus{Status: status, Height: newHeight, Version: newVersion})
|
||||
}
|
||||
|
||||
type apiBlockchainBlock struct {
|
||||
Status int `json:"status"` // See blockchain.StatusX.
|
||||
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.
|
||||
}
|
||||
|
||||
/*
|
||||
apiBlockchainRead reads a block and returns the decoded information.
|
||||
|
||||
Request: GET /blockchain/read?block=[number]
|
||||
Result: 200 with JSON structure apiBlockchainBlock
|
||||
*/
|
||||
func (api *WebapiInstance) apiBlockchainRead(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, _ := api.backend.UserBlockchain.Read(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 blockchain.BlockRecordFile:
|
||||
result.RecordsDecoded = append(result.RecordsDecoded, blockRecordFileToAPI(v))
|
||||
|
||||
case blockchain.BlockRecordProfile:
|
||||
result.RecordsDecoded = append(result.RecordsDecoded, blockRecordProfileToAPI(v))
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EncodeJSON(api.backend, w, r, result)
|
||||
}
|
||||
/*
|
||||
File Name: Blockchain.go
|
||||
Copyright: 2021 Peernet Foundation s.r.o.
|
||||
Author: Peter Kleissner
|
||||
*/
|
||||
|
||||
package webapi
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/PeernetOfficial/core/blockchain"
|
||||
)
|
||||
|
||||
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.
|
||||
}
|
||||
|
||||
/*
|
||||
apiBlockchainHeaderFunc returns the current blockchain header information
|
||||
|
||||
Request: GET /blockchain/header
|
||||
Result: 200 with JSON structure apiResponsePeerSelf
|
||||
*/
|
||||
func (api *WebapiInstance) apiBlockchainHeaderFunc(w http.ResponseWriter, r *http.Request) {
|
||||
publicKey, height, version := api.Backend.UserBlockchain.Header()
|
||||
|
||||
EncodeJSON(api.Backend, 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"` // See blockchain.StatusX.
|
||||
Height uint64 `json:"height"` // Height of the blockchain (number of blocks).
|
||||
Version uint64 `json:"version"` // Version of the blockchain.
|
||||
}
|
||||
|
||||
/*
|
||||
apiBlockchainAppend 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/append with JSON structure apiBlockchainBlockRaw
|
||||
Response: 200 with JSON structure apiBlockchainBlockStatus
|
||||
*/
|
||||
func (api *WebapiInstance) apiBlockchainAppend(w http.ResponseWriter, r *http.Request) {
|
||||
var input apiBlockchainBlockRaw
|
||||
if err := DecodeJSON(w, r, &input); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var records []blockchain.BlockRecordRaw
|
||||
|
||||
for _, record := range input.Records {
|
||||
records = append(records, blockchain.BlockRecordRaw{Type: record.Type, Data: record.Data})
|
||||
}
|
||||
|
||||
newHeight, newVersion, status := api.Backend.UserBlockchain.Append(records)
|
||||
|
||||
EncodeJSON(api.Backend, w, r, apiBlockchainBlockStatus{Status: status, Height: newHeight, Version: newVersion})
|
||||
}
|
||||
|
||||
type apiBlockchainBlock struct {
|
||||
Status int `json:"status"` // See blockchain.StatusX.
|
||||
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.
|
||||
}
|
||||
|
||||
/*
|
||||
apiBlockchainRead reads a block and returns the decoded information.
|
||||
|
||||
Request: GET /blockchain/read?block=[number]
|
||||
Result: 200 with JSON structure apiBlockchainBlock
|
||||
*/
|
||||
func (api *WebapiInstance) apiBlockchainRead(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, _ := api.Backend.UserBlockchain.Read(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 blockchain.BlockRecordFile:
|
||||
result.RecordsDecoded = append(result.RecordsDecoded, blockRecordFileToAPI(v))
|
||||
|
||||
case blockchain.BlockRecordProfile:
|
||||
result.RecordsDecoded = append(result.RecordsDecoded, blockRecordProfileToAPI(v))
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EncodeJSON(api.Backend, w, r, result)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
|
||||
type apiResponseDownloadStatus struct {
|
||||
APIStatus int `json:"apistatus"` // Status of the API call. See DownloadResponseX.
|
||||
ID uuid.UUID `json:"id"` // Download ID. This can be used to query the latest status and take actions.
|
||||
ID uuid.UUID `json:"ID"` // Download ID. This can be used to query the latest status and take actions.
|
||||
DownloadStatus int `json:"downloadstatus"` // Status of the download. See DownloadX.
|
||||
File ApiFile `json:"file"` // File information. Only available for status >= DownloadWaitSwarm.
|
||||
Progress struct {
|
||||
@@ -76,11 +76,11 @@ func (api *WebapiInstance) apiDownloadStart(w http.ResponseWriter, r *http.Reque
|
||||
return
|
||||
}
|
||||
|
||||
info := &downloadInfo{backend: api.backend, api: api, id: uuid.New(), created: time.Now(), hash: hash, nodeID: nodeID}
|
||||
info := &downloadInfo{backend: api.Backend, api: api, id: uuid.New(), created: time.Now(), hash: hash, nodeID: nodeID}
|
||||
|
||||
// create the file immediately
|
||||
if info.initDiskFile(filePath) != nil {
|
||||
EncodeJSON(api.backend, w, r, apiResponseDownloadStatus{APIStatus: DownloadResponseFileInvalid})
|
||||
EncodeJSON(api.Backend, w, r, apiResponseDownloadStatus{APIStatus: DownloadResponseFileInvalid})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -90,18 +90,18 @@ func (api *WebapiInstance) apiDownloadStart(w http.ResponseWriter, r *http.Reque
|
||||
// start the download!
|
||||
go info.Start()
|
||||
|
||||
EncodeJSON(api.backend, w, r, apiResponseDownloadStatus{APIStatus: DownloadResponseSuccess, ID: info.id, DownloadStatus: DownloadWaitMetadata})
|
||||
EncodeJSON(api.Backend, w, r, apiResponseDownloadStatus{APIStatus: DownloadResponseSuccess, ID: info.id, DownloadStatus: DownloadWaitMetadata})
|
||||
}
|
||||
|
||||
/*
|
||||
apiDownloadStatus returns the status of an active download.
|
||||
|
||||
Request: GET /download/status?id=[download ID]
|
||||
Request: GET /download/status?ID=[download ID]
|
||||
Result: 200 with JSON structure apiResponseDownloadStatus
|
||||
*/
|
||||
func (api *WebapiInstance) apiDownloadStatus(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
id, err := uuid.Parse(r.Form.Get("id"))
|
||||
id, err := uuid.Parse(r.Form.Get("ID"))
|
||||
if err != nil {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
@@ -109,7 +109,7 @@ func (api *WebapiInstance) apiDownloadStatus(w http.ResponseWriter, r *http.Requ
|
||||
|
||||
info := api.downloadLookup(id)
|
||||
if info == nil {
|
||||
EncodeJSON(api.backend, w, r, apiResponseDownloadStatus{APIStatus: DownloadResponseIDNotFound})
|
||||
EncodeJSON(api.Backend, w, r, apiResponseDownloadStatus{APIStatus: DownloadResponseIDNotFound})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ func (api *WebapiInstance) apiDownloadStatus(w http.ResponseWriter, r *http.Requ
|
||||
|
||||
info.RUnlock()
|
||||
|
||||
EncodeJSON(api.backend, w, r, response)
|
||||
EncodeJSON(api.Backend, w, r, response)
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -140,12 +140,12 @@ apiDownloadAction pauses, resumes, and cancels a download. Once canceled, a new
|
||||
Only active downloads can be paused. While a download is in discovery phase (querying metadata, joining swarm), it can only be canceled.
|
||||
Action: 0 = Pause, 1 = Resume, 2 = Cancel.
|
||||
|
||||
Request: GET /download/action?id=[download ID]&action=[action]
|
||||
Request: GET /download/action?ID=[download ID]&action=[action]
|
||||
Result: 200 with JSON structure apiResponseDownloadStatus (using APIStatus and DownloadStatus)
|
||||
*/
|
||||
func (api *WebapiInstance) apiDownloadAction(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
id, err := uuid.Parse(r.Form.Get("id"))
|
||||
id, err := uuid.Parse(r.Form.Get("ID"))
|
||||
action, err2 := strconv.Atoi(r.Form.Get("action"))
|
||||
if err != nil || err2 != nil || action < 0 || action > 2 {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
@@ -154,7 +154,7 @@ func (api *WebapiInstance) apiDownloadAction(w http.ResponseWriter, r *http.Requ
|
||||
|
||||
info := api.downloadLookup(id)
|
||||
if info == nil {
|
||||
EncodeJSON(api.backend, w, r, apiResponseDownloadStatus{APIStatus: DownloadResponseIDNotFound})
|
||||
EncodeJSON(api.Backend, w, r, apiResponseDownloadStatus{APIStatus: DownloadResponseIDNotFound})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ func (api *WebapiInstance) apiDownloadAction(w http.ResponseWriter, r *http.Requ
|
||||
apiStatus = info.Cancel()
|
||||
}
|
||||
|
||||
EncodeJSON(api.backend, w, r, apiResponseDownloadStatus{APIStatus: apiStatus, ID: info.id, DownloadStatus: info.status})
|
||||
EncodeJSON(api.Backend, w, r, apiResponseDownloadStatus{APIStatus: apiStatus, ID: info.id, DownloadStatus: info.status})
|
||||
}
|
||||
|
||||
// ---- download tracking ----
|
||||
|
||||
@@ -1,221 +1,221 @@
|
||||
/*
|
||||
File Name: File Detection.go
|
||||
Copyright: 2021 Peernet Foundation s.r.o.
|
||||
Author: Peter Kleissner
|
||||
*/
|
||||
|
||||
package webapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/PeernetOfficial/core"
|
||||
)
|
||||
|
||||
// PathToExtension translates a path to a file extension, if possible. It also returns the second file extension if there is one (relevant for files like "test.tar.gz").
|
||||
func PathToExtension(Path string) (extension, extension2 string, valid bool) {
|
||||
_, fileA := path.Split(Path)
|
||||
|
||||
parts := strings.Split(fileA, ".")
|
||||
if len(parts) <= 1 {
|
||||
return "", "", false
|
||||
}
|
||||
extension = parts[len(parts)-1]
|
||||
extension = strings.ToLower(extension)
|
||||
|
||||
if len(parts) >= 3 {
|
||||
extension2 = parts[len(parts)-2]
|
||||
extension2 = strings.ToLower(extension2)
|
||||
}
|
||||
|
||||
return extension, extension2, true
|
||||
}
|
||||
|
||||
// FileTranslateExtension translates the extension to a File Type and File Format. If invalid, types are 0.
|
||||
func FileTranslateExtension(extension string) (fileType, fileFormat uint16) {
|
||||
switch extension {
|
||||
case "txt", "log", "ini", "json", "md":
|
||||
return core.TypeText, core.FormatText
|
||||
|
||||
case "csv", "tsv":
|
||||
return core.TypeText, core.FormatCSV
|
||||
|
||||
case "html", "htm":
|
||||
return core.TypeText, core.FormatHTML
|
||||
|
||||
case "doc", "docx", "rtf", "odt":
|
||||
return core.TypeDocument, core.FormatWord
|
||||
|
||||
case "pdf":
|
||||
return core.TypeDocument, core.FormatPDF
|
||||
|
||||
case "xls", "xlsx", "ods":
|
||||
return core.TypeDocument, core.FormatExcel
|
||||
|
||||
case "gif", "jpg", "jpeg", "png", "svg", "bmp", "tif", "tiff", "jfif", "webp":
|
||||
return core.TypePicture, core.FormatPicture
|
||||
|
||||
case "mp4", "flv", "avi", "mov", "mpg", "mpeg", "h264", "3g2", "3gp", "mkv", "wmv", "webm", "ts":
|
||||
return core.TypeVideo, core.FormatVideo
|
||||
|
||||
case "mp3", "ogg", "flac":
|
||||
return core.TypeAudio, core.FormatAudio
|
||||
|
||||
case "zip", "rar", "7z", "tar":
|
||||
return core.TypeContainer, core.FormatContainer
|
||||
|
||||
case "ppt", "pptx", "odp":
|
||||
return core.TypeDocument, core.FormatPowerpoint
|
||||
|
||||
case "epub", "mobi", "prc":
|
||||
return core.TypeEbook, core.FormatEbook
|
||||
|
||||
case "gz", "bz", "bz2", "xz":
|
||||
return core.TypeCompressed, core.FormatCompressed
|
||||
|
||||
case "sql":
|
||||
return core.TypeText, core.FormatDatabase
|
||||
|
||||
case "eml", "mbox":
|
||||
return core.TypeText, core.FormatEmail
|
||||
|
||||
case "exe", "sys", "dll", "cmd", "bat":
|
||||
return core.TypeExecutable, core.FormatExecutable
|
||||
|
||||
case "msi":
|
||||
return core.TypeExecutable, core.FormatInstaller
|
||||
|
||||
case "apk":
|
||||
return core.TypeExecutable, core.FormatAPK
|
||||
|
||||
case "iso":
|
||||
return core.TypeContainer, core.FormatISO
|
||||
|
||||
default:
|
||||
return core.TypeBinary, core.FormatBinary
|
||||
}
|
||||
}
|
||||
|
||||
// HTTPContentTypeToCore translates the HTTP content type to the File Type and File Format used by the core package.
|
||||
func HTTPContentTypeToCore(httpContentType string) (fileType, fileFormat uint16) {
|
||||
switch httpContentType {
|
||||
case "text/html", "application/xhtml+xml", "application/xml":
|
||||
return core.TypeText, core.FormatHTML
|
||||
|
||||
case "text/plain":
|
||||
return core.TypeText, core.FormatText
|
||||
|
||||
case "text/csv", "text/tsv", "text/tab-separated-values", "text/x-csv", "application/csv", "application/x-csv", "text/x-comma-separated-values":
|
||||
return core.TypeText, core.FormatCSV
|
||||
|
||||
case "application/pdf", "application/x-pdf":
|
||||
return core.TypeDocument, core.FormatPDF
|
||||
|
||||
case "application/msword", "application/rtf", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.oasis.opendocument.text":
|
||||
return core.TypeDocument, core.FormatWord
|
||||
|
||||
case "application/excel", "application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "application/vnd.oasis.opendocument.spreadsheet":
|
||||
return core.TypeDocument, core.FormatExcel
|
||||
|
||||
case "application/vnd.ms-powerpoint", "application/vnd.openxmlformats-officedocument.presentationml.presentation", "application/vnd.oasis.opendocument.presentation":
|
||||
return core.TypeDocument, core.FormatPowerpoint
|
||||
|
||||
case "image/png", "image/jpeg", "image/gif", "image/svg+xml", "image/tiff", "image/webp", "image/bmp", "image/x-bmp", "image/x-windows-bmp": // image/x-icon excluded
|
||||
return core.TypePicture, core.FormatPicture
|
||||
|
||||
case "audio/aac", "audio/midi", "audio/ogg", "audio/x-wav", "audio/webm", "audio/3gpp", "audio/3gpp2", "audio/mpeg", "audio/vorbis":
|
||||
return core.TypeAudio, core.FormatAudio
|
||||
|
||||
case "video/x-msvideo", "video/x-ms-wmv", "video/mpeg", "video/ogg", "video/webm", "video/3gpp", "video/3gpp2", "video/x-flv", "video/mp4":
|
||||
return core.TypeVideo, core.FormatVideo
|
||||
|
||||
case "application/zip", "application/x-rar-compressed", "application/x-tar", "application/x-bzip", "application/x-bzip2", "application/x-7z-compressed":
|
||||
return core.TypeContainer, core.FormatContainer
|
||||
|
||||
case "application/epub+zip", "application/x-mobipocket-ebook":
|
||||
return core.TypeEbook, core.FormatEbook
|
||||
|
||||
default:
|
||||
return core.TypeBinary, core.FormatBinary
|
||||
}
|
||||
}
|
||||
|
||||
// FileDataToHTTPContentType returns the HTTP content type based on the initial file data. It reads the first 512 bytes of the file.
|
||||
func FileDataToHTTPContentType(Path string) (httpContentType string, err error) {
|
||||
file, err := os.Open(Path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Read up to 512 bytes. This specific number comes from http.DetectContentType which specifies it as constant "sniffLen".
|
||||
buff := make([]byte, 512)
|
||||
|
||||
if _, err := file.Read(buff); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := file.Close(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
httpContentType = http.DetectContentType(buff)
|
||||
|
||||
// sanitize it first
|
||||
httpContentType = strings.ToLower(strings.TrimSpace(httpContentType))
|
||||
if indexD := strings.IndexAny(httpContentType, ";"); indexD >= 0 {
|
||||
httpContentType = httpContentType[:indexD]
|
||||
}
|
||||
|
||||
return httpContentType, nil
|
||||
}
|
||||
|
||||
// FileDetectType detects the File Type and File Format of a file. It uses the extension if available, otherwise the file data, for detection.
|
||||
func FileDetectType(Path string) (fileType, fileFormat uint16, err error) {
|
||||
// If a file extension is available, use that to detect the file type and file format.
|
||||
// Otherwise, use the initial file data for detection.
|
||||
if ext1, _, valid := PathToExtension(Path); valid {
|
||||
fileType, fileFormat = FileTranslateExtension(ext1)
|
||||
return fileType, fileFormat, nil
|
||||
}
|
||||
|
||||
httpContentType, err := FileDataToHTTPContentType(Path)
|
||||
if err != nil {
|
||||
return core.TypeBinary, core.FormatBinary, err
|
||||
}
|
||||
|
||||
fileType, fileFormat = HTTPContentTypeToCore(httpContentType)
|
||||
return fileType, fileFormat, nil
|
||||
}
|
||||
|
||||
type apiResponseFileFormat struct {
|
||||
Status int `json:"status"` // Status: 0 = Success, 1 = Error reading file
|
||||
FileType uint16 `json:"filetype"` // File Type.
|
||||
FileFormat uint16 `json:"fileformat"` // File Format.
|
||||
}
|
||||
|
||||
/*
|
||||
apiFileFormat detects the file type and file format of the specified file.
|
||||
It will primarily use the file extension for detection. If unavailable, it uses the first 512 bytes of the file data to detect the type.
|
||||
|
||||
Request: GET /file/format?path=[file path on disk]
|
||||
Result: 200 with JSON structure apiResponseFileFormat
|
||||
*/
|
||||
func (api *WebapiInstance) apiFileFormat(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
filePath := r.Form.Get("path")
|
||||
if filePath == "" {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
fileType, fileFormat, err := FileDetectType(filePath)
|
||||
if err != nil {
|
||||
EncodeJSON(api.backend, w, r, apiResponseFileFormat{Status: 1})
|
||||
return
|
||||
}
|
||||
|
||||
EncodeJSON(api.backend, w, r, apiResponseFileFormat{Status: 0, FileType: fileType, FileFormat: fileFormat})
|
||||
}
|
||||
/*
|
||||
File Name: File Detection.go
|
||||
Copyright: 2021 Peernet Foundation s.r.o.
|
||||
Author: Peter Kleissner
|
||||
*/
|
||||
|
||||
package webapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/PeernetOfficial/core"
|
||||
)
|
||||
|
||||
// PathToExtension translates a path to a file extension, if possible. It also returns the second file extension if there is one (relevant for files like "test.tar.gz").
|
||||
func PathToExtension(Path string) (extension, extension2 string, valid bool) {
|
||||
_, fileA := path.Split(Path)
|
||||
|
||||
parts := strings.Split(fileA, ".")
|
||||
if len(parts) <= 1 {
|
||||
return "", "", false
|
||||
}
|
||||
extension = parts[len(parts)-1]
|
||||
extension = strings.ToLower(extension)
|
||||
|
||||
if len(parts) >= 3 {
|
||||
extension2 = parts[len(parts)-2]
|
||||
extension2 = strings.ToLower(extension2)
|
||||
}
|
||||
|
||||
return extension, extension2, true
|
||||
}
|
||||
|
||||
// FileTranslateExtension translates the extension to a File Type and File Format. If invalid, types are 0.
|
||||
func FileTranslateExtension(extension string) (fileType, fileFormat uint16) {
|
||||
switch extension {
|
||||
case "txt", "log", "ini", "json", "md":
|
||||
return core.TypeText, core.FormatText
|
||||
|
||||
case "csv", "tsv":
|
||||
return core.TypeText, core.FormatCSV
|
||||
|
||||
case "html", "htm":
|
||||
return core.TypeText, core.FormatHTML
|
||||
|
||||
case "doc", "docx", "rtf", "odt":
|
||||
return core.TypeDocument, core.FormatWord
|
||||
|
||||
case "pdf":
|
||||
return core.TypeDocument, core.FormatPDF
|
||||
|
||||
case "xls", "xlsx", "ods":
|
||||
return core.TypeDocument, core.FormatExcel
|
||||
|
||||
case "gif", "jpg", "jpeg", "png", "svg", "bmp", "tif", "tiff", "jfif", "webp":
|
||||
return core.TypePicture, core.FormatPicture
|
||||
|
||||
case "mp4", "flv", "avi", "mov", "mpg", "mpeg", "h264", "3g2", "3gp", "mkv", "wmv", "webm", "ts":
|
||||
return core.TypeVideo, core.FormatVideo
|
||||
|
||||
case "mp3", "ogg", "flac":
|
||||
return core.TypeAudio, core.FormatAudio
|
||||
|
||||
case "zip", "rar", "7z", "tar":
|
||||
return core.TypeContainer, core.FormatContainer
|
||||
|
||||
case "ppt", "pptx", "odp":
|
||||
return core.TypeDocument, core.FormatPowerpoint
|
||||
|
||||
case "epub", "mobi", "prc":
|
||||
return core.TypeEbook, core.FormatEbook
|
||||
|
||||
case "gz", "bz", "bz2", "xz":
|
||||
return core.TypeCompressed, core.FormatCompressed
|
||||
|
||||
case "sql":
|
||||
return core.TypeText, core.FormatDatabase
|
||||
|
||||
case "eml", "mbox":
|
||||
return core.TypeText, core.FormatEmail
|
||||
|
||||
case "exe", "sys", "dll", "cmd", "bat":
|
||||
return core.TypeExecutable, core.FormatExecutable
|
||||
|
||||
case "msi":
|
||||
return core.TypeExecutable, core.FormatInstaller
|
||||
|
||||
case "apk":
|
||||
return core.TypeExecutable, core.FormatAPK
|
||||
|
||||
case "iso":
|
||||
return core.TypeContainer, core.FormatISO
|
||||
|
||||
default:
|
||||
return core.TypeBinary, core.FormatBinary
|
||||
}
|
||||
}
|
||||
|
||||
// HTTPContentTypeToCore translates the HTTP content type to the File Type and File Format used by the core package.
|
||||
func HTTPContentTypeToCore(httpContentType string) (fileType, fileFormat uint16) {
|
||||
switch httpContentType {
|
||||
case "text/html", "application/xhtml+xml", "application/xml":
|
||||
return core.TypeText, core.FormatHTML
|
||||
|
||||
case "text/plain":
|
||||
return core.TypeText, core.FormatText
|
||||
|
||||
case "text/csv", "text/tsv", "text/tab-separated-values", "text/x-csv", "application/csv", "application/x-csv", "text/x-comma-separated-values":
|
||||
return core.TypeText, core.FormatCSV
|
||||
|
||||
case "application/pdf", "application/x-pdf":
|
||||
return core.TypeDocument, core.FormatPDF
|
||||
|
||||
case "application/msword", "application/rtf", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.oasis.opendocument.text":
|
||||
return core.TypeDocument, core.FormatWord
|
||||
|
||||
case "application/excel", "application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "application/vnd.oasis.opendocument.spreadsheet":
|
||||
return core.TypeDocument, core.FormatExcel
|
||||
|
||||
case "application/vnd.ms-powerpoint", "application/vnd.openxmlformats-officedocument.presentationml.presentation", "application/vnd.oasis.opendocument.presentation":
|
||||
return core.TypeDocument, core.FormatPowerpoint
|
||||
|
||||
case "image/png", "image/jpeg", "image/gif", "image/svg+xml", "image/tiff", "image/webp", "image/bmp", "image/x-bmp", "image/x-windows-bmp": // image/x-icon excluded
|
||||
return core.TypePicture, core.FormatPicture
|
||||
|
||||
case "audio/aac", "audio/midi", "audio/ogg", "audio/x-wav", "audio/webm", "audio/3gpp", "audio/3gpp2", "audio/mpeg", "audio/vorbis":
|
||||
return core.TypeAudio, core.FormatAudio
|
||||
|
||||
case "video/x-msvideo", "video/x-ms-wmv", "video/mpeg", "video/ogg", "video/webm", "video/3gpp", "video/3gpp2", "video/x-flv", "video/mp4":
|
||||
return core.TypeVideo, core.FormatVideo
|
||||
|
||||
case "application/zip", "application/x-rar-compressed", "application/x-tar", "application/x-bzip", "application/x-bzip2", "application/x-7z-compressed":
|
||||
return core.TypeContainer, core.FormatContainer
|
||||
|
||||
case "application/epub+zip", "application/x-mobipocket-ebook":
|
||||
return core.TypeEbook, core.FormatEbook
|
||||
|
||||
default:
|
||||
return core.TypeBinary, core.FormatBinary
|
||||
}
|
||||
}
|
||||
|
||||
// FileDataToHTTPContentType returns the HTTP content type based on the initial file data. It reads the first 512 bytes of the file.
|
||||
func FileDataToHTTPContentType(Path string) (httpContentType string, err error) {
|
||||
file, err := os.Open(Path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Read up to 512 bytes. This specific number comes from http.DetectContentType which specifies it as constant "sniffLen".
|
||||
buff := make([]byte, 512)
|
||||
|
||||
if _, err := file.Read(buff); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := file.Close(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
httpContentType = http.DetectContentType(buff)
|
||||
|
||||
// sanitize it first
|
||||
httpContentType = strings.ToLower(strings.TrimSpace(httpContentType))
|
||||
if indexD := strings.IndexAny(httpContentType, ";"); indexD >= 0 {
|
||||
httpContentType = httpContentType[:indexD]
|
||||
}
|
||||
|
||||
return httpContentType, nil
|
||||
}
|
||||
|
||||
// FileDetectType detects the File Type and File Format of a file. It uses the extension if available, otherwise the file data, for detection.
|
||||
func FileDetectType(Path string) (fileType, fileFormat uint16, err error) {
|
||||
// If a file extension is available, use that to detect the file type and file format.
|
||||
// Otherwise, use the initial file data for detection.
|
||||
if ext1, _, valid := PathToExtension(Path); valid {
|
||||
fileType, fileFormat = FileTranslateExtension(ext1)
|
||||
return fileType, fileFormat, nil
|
||||
}
|
||||
|
||||
httpContentType, err := FileDataToHTTPContentType(Path)
|
||||
if err != nil {
|
||||
return core.TypeBinary, core.FormatBinary, err
|
||||
}
|
||||
|
||||
fileType, fileFormat = HTTPContentTypeToCore(httpContentType)
|
||||
return fileType, fileFormat, nil
|
||||
}
|
||||
|
||||
type apiResponseFileFormat struct {
|
||||
Status int `json:"status"` // Status: 0 = Success, 1 = Error reading file
|
||||
FileType uint16 `json:"filetype"` // File Type.
|
||||
FileFormat uint16 `json:"fileformat"` // File Format.
|
||||
}
|
||||
|
||||
/*
|
||||
apiFileFormat detects the file type and file format of the specified file.
|
||||
It will primarily use the file extension for detection. If unavailable, it uses the first 512 bytes of the file data to detect the type.
|
||||
|
||||
Request: GET /file/format?path=[file path on disk]
|
||||
Result: 200 with JSON structure apiResponseFileFormat
|
||||
*/
|
||||
func (api *WebapiInstance) apiFileFormat(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
filePath := r.Form.Get("path")
|
||||
if filePath == "" {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
fileType, fileFormat, err := FileDetectType(filePath)
|
||||
if err != nil {
|
||||
EncodeJSON(api.Backend, w, r, apiResponseFileFormat{Status: 1})
|
||||
return
|
||||
}
|
||||
|
||||
EncodeJSON(api.Backend, w, r, apiResponseFileFormat{Status: 0, FileType: fileType, FileFormat: fileFormat})
|
||||
}
|
||||
|
||||
@@ -1,319 +1,319 @@
|
||||
/*
|
||||
File Name: File IO.go
|
||||
Copyright: 2021 Peernet Foundation s.r.o.
|
||||
Author: Peter Kleissner
|
||||
*/
|
||||
|
||||
package webapi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/PeernetOfficial/core"
|
||||
"github.com/PeernetOfficial/core/btcec"
|
||||
"github.com/PeernetOfficial/core/protocol"
|
||||
"github.com/PeernetOfficial/core/warehouse"
|
||||
)
|
||||
|
||||
/*
|
||||
apiFileRead reads a file immediately from a remote peer. Use the /download functions to download a file.
|
||||
This endpoint supports the Range, Content-Range and Content-Length headers. Multipart ranges are not supported and result in HTTP 400.
|
||||
Instead of providing the node ID, the peer ID is also accepted in the &node= parameter.
|
||||
The default timeout for connecting to the peer is 10 seconds.
|
||||
|
||||
Request: GET /file/read?hash=[hash]&node=[node ID]
|
||||
Optional: &offset=[offset]&limit=[limit] or via Range header.
|
||||
Optional: &timeout=[seconds]
|
||||
Response: 200 with the content
|
||||
206 with partial content
|
||||
400 if the parameters are invalid
|
||||
404 if the file was not found or other error on transfer initiate
|
||||
502 if unable to find or connect to the remote peer in time
|
||||
*/
|
||||
func (api *WebapiInstance) apiFileRead(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
var err error
|
||||
|
||||
// validate hashes (must be blake3) and other input
|
||||
fileHash, valid1 := DecodeBlake3Hash(r.Form.Get("hash"))
|
||||
nodeID, valid2 := DecodeBlake3Hash(r.Form.Get("node"))
|
||||
publicKey, err3 := core.PublicKeyFromPeerID(r.Form.Get("node"))
|
||||
if !valid1 || (!valid2 && err3 != nil) {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
timeoutSeconds, _ := strconv.Atoi(r.Form.Get("timeout"))
|
||||
if timeoutSeconds == 0 {
|
||||
timeoutSeconds = 10
|
||||
}
|
||||
timeout := time.Duration(timeoutSeconds) * time.Second
|
||||
|
||||
offset, _ := strconv.Atoi(r.Form.Get("offset"))
|
||||
limit, _ := strconv.Atoi(r.Form.Get("limit"))
|
||||
|
||||
// Range header?
|
||||
var ranges []HTTPRange
|
||||
if ranges, err = ParseRangeHeader(r.Header.Get("Range"), -1, true); err != nil || len(ranges) > 1 {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
} else if len(ranges) == 1 {
|
||||
if ranges[0].length != -1 { // if length is not specified, limit remains 0 which is maximum
|
||||
limit = ranges[0].length
|
||||
}
|
||||
offset = ranges[0].start
|
||||
}
|
||||
|
||||
// Is the file available in the local warehouse? In that case requesting it from the remote is unnecessary.
|
||||
if serveFileFromWarehouse(api.backend, w, fileHash, uint64(offset), uint64(limit), ranges) {
|
||||
return
|
||||
}
|
||||
|
||||
// try connecting via node ID or peer ID?
|
||||
var peer *core.PeerInfo
|
||||
|
||||
if valid2 {
|
||||
peer, err = PeerConnectNode(api.backend, nodeID, timeout)
|
||||
} else if err3 == nil {
|
||||
peer, err = PeerConnectPublicKey(api.backend, publicKey, timeout)
|
||||
}
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
|
||||
// Start the reader. If this HTTP request is canceled, r.Context().Done() acts as cancellation signal to the underlying UDT connection.
|
||||
reader, fileSize, transferSize, err := FileStartReader(peer, fileHash, uint64(offset), uint64(limit), r.Context().Done())
|
||||
if reader != nil {
|
||||
defer reader.Close()
|
||||
}
|
||||
if err != nil || reader == nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// set the right headers
|
||||
setContentLengthRangeHeader(w, uint64(offset), transferSize, fileSize, ranges)
|
||||
|
||||
// Start sending the data!
|
||||
io.Copy(w, io.LimitReader(reader, int64(transferSize)))
|
||||
}
|
||||
|
||||
// serveFileFromWarehouse serves the file from the warehouse. If it is not available, it returns false and does not use the writer.
|
||||
// Limit is optional, 0 means the entire file.
|
||||
func serveFileFromWarehouse(backend *core.Backend, w http.ResponseWriter, fileHash []byte, offset, limit uint64, ranges []HTTPRange) (valid bool) {
|
||||
// Check if the file is available in the local warehouse.
|
||||
_, fileSize, status, _ := backend.UserWarehouse.FileExists(fileHash)
|
||||
if status != warehouse.StatusOK {
|
||||
return false
|
||||
}
|
||||
|
||||
// validate offset and limit
|
||||
if limit > 0 && offset+limit > fileSize {
|
||||
http.Error(w, "invalid limit", http.StatusBadRequest)
|
||||
return true
|
||||
} else if offset > fileSize {
|
||||
http.Error(w, "invalid offset", http.StatusBadRequest)
|
||||
return true
|
||||
} else if limit == 0 {
|
||||
limit = fileSize - offset
|
||||
}
|
||||
|
||||
setContentLengthRangeHeader(w, offset, limit, fileSize, ranges)
|
||||
|
||||
status, _, _ = backend.UserWarehouse.ReadFile(fileHash, int64(offset), int64(limit), w)
|
||||
|
||||
// StatusErrorReadFile must be considered success, since parts of the file may have been transferred already and recovery is not possible.
|
||||
return status == warehouse.StatusErrorReadFile || status == warehouse.StatusOK
|
||||
}
|
||||
|
||||
/*
|
||||
apiFileView is similar to /file/read but but provides a format parameter. It sets the Content-Type and Accept-Ranges headers.
|
||||
This endpoint supports the Range, Content-Range and Content-Length headers. Multipart ranges are not supported and result in HTTP 400.
|
||||
Instead of providing the node ID, the peer ID is also accepted in the &node= parameter.
|
||||
The default timeout for connecting to the peer is 10 seconds.
|
||||
Formats: 14 = Video
|
||||
|
||||
Request: GET /file/view?hash=[hash]&node=[node ID]&format=[format]
|
||||
Optional: &offset=[offset]&limit=[limit] or via Range header.
|
||||
Optional: &timeout=[seconds]
|
||||
Response: 200 with the content
|
||||
206 with partial content
|
||||
400 if the parameters are invalid
|
||||
404 if the file was not found or other error on transfer initiate
|
||||
502 if unable to find or connect to the remote peer in time
|
||||
*/
|
||||
func (api *WebapiInstance) apiFileView(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
var err error
|
||||
|
||||
// validate hashes (must be blake3) and other input
|
||||
fileHash, valid1 := DecodeBlake3Hash(r.Form.Get("hash"))
|
||||
nodeID, valid2 := DecodeBlake3Hash(r.Form.Get("node"))
|
||||
publicKey, err3 := core.PublicKeyFromPeerID(r.Form.Get("node"))
|
||||
if !valid1 || (!valid2 && err3 != nil) {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
timeoutSeconds, _ := strconv.Atoi(r.Form.Get("timeout"))
|
||||
if timeoutSeconds == 0 {
|
||||
timeoutSeconds = 10
|
||||
}
|
||||
timeout := time.Duration(timeoutSeconds) * time.Second
|
||||
|
||||
offset, _ := strconv.Atoi(r.Form.Get("offset"))
|
||||
limit, _ := strconv.Atoi(r.Form.Get("limit"))
|
||||
format, _ := strconv.Atoi(r.Form.Get("format"))
|
||||
localCacheDisable, _ := strconv.ParseBool(r.Form.Get("nocache"))
|
||||
|
||||
// Range header?
|
||||
var ranges []HTTPRange
|
||||
if ranges, err = ParseRangeHeader(r.Header.Get("Range"), -1, true); err != nil || len(ranges) > 1 {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
} else if len(ranges) == 1 {
|
||||
if ranges[0].length != -1 { // if length is not specified, limit remains 0 which is maximum
|
||||
limit = ranges[0].length
|
||||
}
|
||||
offset = ranges[0].start
|
||||
}
|
||||
|
||||
w.Header().Set("Accept-Ranges", "bytes") // always indicate accepting of Range header
|
||||
|
||||
switch format {
|
||||
case 14:
|
||||
// Video: Indicate MP4 always. There are tons of other MIME types that could be used.
|
||||
w.Header().Set("Content-Type", "video/mp4")
|
||||
}
|
||||
|
||||
// Is the file available in the local warehouse? In that case requesting it from the remote is unnecessary.
|
||||
if !localCacheDisable {
|
||||
if serveFileFromWarehouse(api.backend, w, fileHash, uint64(offset), uint64(limit), ranges) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// try connecting via node ID or peer ID?
|
||||
var peer *core.PeerInfo
|
||||
|
||||
if valid2 {
|
||||
peer, err = PeerConnectNode(api.backend, nodeID, timeout)
|
||||
} else if err3 == nil {
|
||||
peer, err = PeerConnectPublicKey(api.backend, publicKey, timeout)
|
||||
}
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
|
||||
// start the reader
|
||||
reader, fileSize, transferSize, err := FileStartReader(peer, fileHash, uint64(offset), uint64(limit), r.Context().Done())
|
||||
if reader != nil {
|
||||
defer reader.Close()
|
||||
}
|
||||
if err != nil || reader == nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// set the right headers
|
||||
setContentLengthRangeHeader(w, uint64(offset), transferSize, fileSize, ranges)
|
||||
|
||||
// Start sending the data!
|
||||
io.Copy(w, io.LimitReader(reader, int64(transferSize)))
|
||||
}
|
||||
|
||||
// PeerConnectPublicKey attempts to connect to the peer specified by its public key (= peer ID).
|
||||
func PeerConnectPublicKey(backend *core.Backend, publicKey *btcec.PublicKey, timeout time.Duration) (peer *core.PeerInfo, err error) {
|
||||
if publicKey == nil {
|
||||
return nil, errors.New("invalid public key")
|
||||
}
|
||||
|
||||
// First look up in the peer list.
|
||||
if peer = backend.PeerlistLookup(publicKey); peer != nil {
|
||||
return peer, nil
|
||||
}
|
||||
|
||||
// Try to connect via DHT.
|
||||
nodeID := protocol.PublicKey2NodeID(publicKey)
|
||||
if _, peer, _ = backend.FindNode(nodeID, timeout); peer != nil {
|
||||
return peer, nil
|
||||
}
|
||||
|
||||
// otherwise not found :(
|
||||
return nil, errors.New("peer not found")
|
||||
}
|
||||
|
||||
// PeerConnectNode tries to connect via the node ID
|
||||
func PeerConnectNode(backend *core.Backend, nodeID []byte, timeout time.Duration) (peer *core.PeerInfo, err error) {
|
||||
if len(nodeID) != 256/8 {
|
||||
return nil, errors.New("invalid node ID")
|
||||
}
|
||||
|
||||
// Try to connect via DHT.
|
||||
if _, peer, _ = backend.FindNode(nodeID, timeout); peer != nil {
|
||||
return peer, nil
|
||||
}
|
||||
|
||||
// otherwise not found :(
|
||||
return nil, errors.New("peer not found")
|
||||
}
|
||||
|
||||
// FileStartReader providers a reader to a remote file. The reader must be closed by the caller.
|
||||
// File Size is the full file size reported by the remote peer, regardless of the requested offset and limit. Limit is optional (0 means the entire file).
|
||||
// Transfer Size is the size in bytes that is actually going to be transferred. The reader should be closed after reading that amount.
|
||||
// The optional cancelChan can be used to stop the file transfer at any point.
|
||||
func FileStartReader(peer *core.PeerInfo, hash []byte, offset, limit uint64, cancelChan <-chan struct{}) (reader io.ReadCloser, fileSize, transferSize uint64, err error) {
|
||||
if peer == nil {
|
||||
return nil, 0, 0, errors.New("peer not provided")
|
||||
} else if !peer.IsConnectionActive() {
|
||||
return nil, 0, 0, errors.New("no valid connection to peer")
|
||||
}
|
||||
|
||||
udtConn, virtualConn, err := peer.FileTransferRequestUDT(hash, offset, limit)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
if cancelChan != nil {
|
||||
go func() {
|
||||
<-cancelChan
|
||||
udtConn.Close()
|
||||
}()
|
||||
}
|
||||
|
||||
fileSize, transferSize, err = protocol.FileTransferReadHeader(udtConn)
|
||||
if err != nil {
|
||||
udtConn.Close()
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
virtualConn.Stats.(*core.FileTransferStats).FileSize = fileSize
|
||||
|
||||
return udtConn, fileSize, transferSize, nil
|
||||
}
|
||||
|
||||
// FileReadAll downloads the file from the peer.
|
||||
// This function should only be used for testing or as a basis to fork. The caller should develop a custom download function that handles timeouts and excessive file sizes.
|
||||
// It allocates whatever size is reported by the remote peer. This could lead to an out of memory crash.
|
||||
// This function is blocking and may take a long time depending on the remote peer and the network connection.
|
||||
func FileReadAll(peer *core.PeerInfo, hash []byte) (data []byte, err error) {
|
||||
reader, _, transferSize, err := FileStartReader(peer, hash, 0, 0, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
// read all data
|
||||
data = make([]byte, transferSize) // Warning: This could lead to an out of memory crash.
|
||||
_, err = reader.Read(data)
|
||||
|
||||
// Note: This function does not verify if the returned data matches the hash and expected size.
|
||||
|
||||
return data, err
|
||||
}
|
||||
/*
|
||||
File Name: File IO.go
|
||||
Copyright: 2021 Peernet Foundation s.r.o.
|
||||
Author: Peter Kleissner
|
||||
*/
|
||||
|
||||
package webapi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/PeernetOfficial/core"
|
||||
"github.com/PeernetOfficial/core/btcec"
|
||||
"github.com/PeernetOfficial/core/protocol"
|
||||
"github.com/PeernetOfficial/core/warehouse"
|
||||
)
|
||||
|
||||
/*
|
||||
apiFileRead reads a file immediately from a remote peer. Use the /download functions to download a file.
|
||||
This endpoint supports the Range, Content-Range and Content-Length headers. Multipart ranges are not supported and result in HTTP 400.
|
||||
Instead of providing the node ID, the peer ID is also accepted in the &node= parameter.
|
||||
The default timeout for connecting to the peer is 10 seconds.
|
||||
|
||||
Request: GET /file/read?hash=[hash]&node=[node ID]
|
||||
Optional: &offset=[offset]&limit=[limit] or via Range header.
|
||||
Optional: &timeout=[seconds]
|
||||
Response: 200 with the content
|
||||
206 with partial content
|
||||
400 if the parameters are invalid
|
||||
404 if the file was not found or other error on transfer initiate
|
||||
502 if unable to find or connect to the remote peer in time
|
||||
*/
|
||||
func (api *WebapiInstance) apiFileRead(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
var err error
|
||||
|
||||
// validate hashes (must be blake3) and other input
|
||||
fileHash, valid1 := DecodeBlake3Hash(r.Form.Get("hash"))
|
||||
nodeID, valid2 := DecodeBlake3Hash(r.Form.Get("node"))
|
||||
publicKey, err3 := core.PublicKeyFromPeerID(r.Form.Get("node"))
|
||||
if !valid1 || (!valid2 && err3 != nil) {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
timeoutSeconds, _ := strconv.Atoi(r.Form.Get("timeout"))
|
||||
if timeoutSeconds == 0 {
|
||||
timeoutSeconds = 10
|
||||
}
|
||||
timeout := time.Duration(timeoutSeconds) * time.Second
|
||||
|
||||
offset, _ := strconv.Atoi(r.Form.Get("offset"))
|
||||
limit, _ := strconv.Atoi(r.Form.Get("limit"))
|
||||
|
||||
// Range header?
|
||||
var ranges []HTTPRange
|
||||
if ranges, err = ParseRangeHeader(r.Header.Get("Range"), -1, true); err != nil || len(ranges) > 1 {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
} else if len(ranges) == 1 {
|
||||
if ranges[0].length != -1 { // if length is not specified, limit remains 0 which is maximum
|
||||
limit = ranges[0].length
|
||||
}
|
||||
offset = ranges[0].start
|
||||
}
|
||||
|
||||
// Is the file available in the local warehouse? In that case requesting it from the remote is unnecessary.
|
||||
if serveFileFromWarehouse(api.Backend, w, fileHash, uint64(offset), uint64(limit), ranges) {
|
||||
return
|
||||
}
|
||||
|
||||
// try connecting via node ID or peer ID?
|
||||
var peer *core.PeerInfo
|
||||
|
||||
if valid2 {
|
||||
peer, err = PeerConnectNode(api.Backend, nodeID, timeout)
|
||||
} else if err3 == nil {
|
||||
peer, err = PeerConnectPublicKey(api.Backend, publicKey, timeout)
|
||||
}
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
|
||||
// Start the reader. If this HTTP request is canceled, r.Context().Done() acts as cancellation signal to the underlying UDT connection.
|
||||
reader, fileSize, transferSize, err := FileStartReader(peer, fileHash, uint64(offset), uint64(limit), r.Context().Done())
|
||||
if reader != nil {
|
||||
defer reader.Close()
|
||||
}
|
||||
if err != nil || reader == nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// set the right headers
|
||||
setContentLengthRangeHeader(w, uint64(offset), transferSize, fileSize, ranges)
|
||||
|
||||
// Start sending the data!
|
||||
io.Copy(w, io.LimitReader(reader, int64(transferSize)))
|
||||
}
|
||||
|
||||
// serveFileFromWarehouse serves the file from the warehouse. If it is not available, it returns false and does not use the writer.
|
||||
// Limit is optional, 0 means the entire file.
|
||||
func serveFileFromWarehouse(backend *core.Backend, w http.ResponseWriter, fileHash []byte, offset, limit uint64, ranges []HTTPRange) (valid bool) {
|
||||
// Check if the file is available in the local warehouse.
|
||||
_, fileSize, status, _ := backend.UserWarehouse.FileExists(fileHash)
|
||||
if status != warehouse.StatusOK {
|
||||
return false
|
||||
}
|
||||
|
||||
// validate offset and limit
|
||||
if limit > 0 && offset+limit > fileSize {
|
||||
http.Error(w, "invalid limit", http.StatusBadRequest)
|
||||
return true
|
||||
} else if offset > fileSize {
|
||||
http.Error(w, "invalid offset", http.StatusBadRequest)
|
||||
return true
|
||||
} else if limit == 0 {
|
||||
limit = fileSize - offset
|
||||
}
|
||||
|
||||
setContentLengthRangeHeader(w, offset, limit, fileSize, ranges)
|
||||
|
||||
status, _, _ = backend.UserWarehouse.ReadFile(fileHash, int64(offset), int64(limit), w)
|
||||
|
||||
// StatusErrorReadFile must be considered success, since parts of the file may have been transferred already and recovery is not possible.
|
||||
return status == warehouse.StatusErrorReadFile || status == warehouse.StatusOK
|
||||
}
|
||||
|
||||
/*
|
||||
apiFileView is similar to /file/read but but provides a format parameter. It sets the Content-Type and Accept-Ranges headers.
|
||||
This endpoint supports the Range, Content-Range and Content-Length headers. Multipart ranges are not supported and result in HTTP 400.
|
||||
Instead of providing the node ID, the peer ID is also accepted in the &node= parameter.
|
||||
The default timeout for connecting to the peer is 10 seconds.
|
||||
Formats: 14 = Video
|
||||
|
||||
Request: GET /file/view?hash=[hash]&node=[node ID]&format=[format]
|
||||
Optional: &offset=[offset]&limit=[limit] or via Range header.
|
||||
Optional: &timeout=[seconds]
|
||||
Response: 200 with the content
|
||||
206 with partial content
|
||||
400 if the parameters are invalid
|
||||
404 if the file was not found or other error on transfer initiate
|
||||
502 if unable to find or connect to the remote peer in time
|
||||
*/
|
||||
func (api *WebapiInstance) apiFileView(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
var err error
|
||||
|
||||
// validate hashes (must be blake3) and other input
|
||||
fileHash, valid1 := DecodeBlake3Hash(r.Form.Get("hash"))
|
||||
nodeID, valid2 := DecodeBlake3Hash(r.Form.Get("node"))
|
||||
publicKey, err3 := core.PublicKeyFromPeerID(r.Form.Get("node"))
|
||||
if !valid1 || (!valid2 && err3 != nil) {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
timeoutSeconds, _ := strconv.Atoi(r.Form.Get("timeout"))
|
||||
if timeoutSeconds == 0 {
|
||||
timeoutSeconds = 10
|
||||
}
|
||||
timeout := time.Duration(timeoutSeconds) * time.Second
|
||||
|
||||
offset, _ := strconv.Atoi(r.Form.Get("offset"))
|
||||
limit, _ := strconv.Atoi(r.Form.Get("limit"))
|
||||
format, _ := strconv.Atoi(r.Form.Get("format"))
|
||||
localCacheDisable, _ := strconv.ParseBool(r.Form.Get("nocache"))
|
||||
|
||||
// Range header?
|
||||
var ranges []HTTPRange
|
||||
if ranges, err = ParseRangeHeader(r.Header.Get("Range"), -1, true); err != nil || len(ranges) > 1 {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
} else if len(ranges) == 1 {
|
||||
if ranges[0].length != -1 { // if length is not specified, limit remains 0 which is maximum
|
||||
limit = ranges[0].length
|
||||
}
|
||||
offset = ranges[0].start
|
||||
}
|
||||
|
||||
w.Header().Set("Accept-Ranges", "bytes") // always indicate accepting of Range header
|
||||
|
||||
switch format {
|
||||
case 14:
|
||||
// Video: Indicate MP4 always. There are tons of other MIME types that could be used.
|
||||
w.Header().Set("Content-Type", "video/mp4")
|
||||
}
|
||||
|
||||
// Is the file available in the local warehouse? In that case requesting it from the remote is unnecessary.
|
||||
if !localCacheDisable {
|
||||
if serveFileFromWarehouse(api.Backend, w, fileHash, uint64(offset), uint64(limit), ranges) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// try connecting via node ID or peer ID?
|
||||
var peer *core.PeerInfo
|
||||
|
||||
if valid2 {
|
||||
peer, err = PeerConnectNode(api.Backend, nodeID, timeout)
|
||||
} else if err3 == nil {
|
||||
peer, err = PeerConnectPublicKey(api.Backend, publicKey, timeout)
|
||||
}
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
|
||||
// start the reader
|
||||
reader, fileSize, transferSize, err := FileStartReader(peer, fileHash, uint64(offset), uint64(limit), r.Context().Done())
|
||||
if reader != nil {
|
||||
defer reader.Close()
|
||||
}
|
||||
if err != nil || reader == nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// set the right headers
|
||||
setContentLengthRangeHeader(w, uint64(offset), transferSize, fileSize, ranges)
|
||||
|
||||
// Start sending the data!
|
||||
io.Copy(w, io.LimitReader(reader, int64(transferSize)))
|
||||
}
|
||||
|
||||
// PeerConnectPublicKey attempts to connect to the peer specified by its public key (= peer ID).
|
||||
func PeerConnectPublicKey(backend *core.Backend, publicKey *btcec.PublicKey, timeout time.Duration) (peer *core.PeerInfo, err error) {
|
||||
if publicKey == nil {
|
||||
return nil, errors.New("invalid public key")
|
||||
}
|
||||
|
||||
// First look up in the peer list.
|
||||
if peer = backend.PeerlistLookup(publicKey); peer != nil {
|
||||
return peer, nil
|
||||
}
|
||||
|
||||
// Try to connect via DHT.
|
||||
nodeID := protocol.PublicKey2NodeID(publicKey)
|
||||
if _, peer, _ = backend.FindNode(nodeID, timeout); peer != nil {
|
||||
return peer, nil
|
||||
}
|
||||
|
||||
// otherwise not found :(
|
||||
return nil, errors.New("peer not found")
|
||||
}
|
||||
|
||||
// PeerConnectNode tries to connect via the node ID
|
||||
func PeerConnectNode(backend *core.Backend, nodeID []byte, timeout time.Duration) (peer *core.PeerInfo, err error) {
|
||||
if len(nodeID) != 256/8 {
|
||||
return nil, errors.New("invalid node ID")
|
||||
}
|
||||
|
||||
// Try to connect via DHT.
|
||||
if _, peer, _ = backend.FindNode(nodeID, timeout); peer != nil {
|
||||
return peer, nil
|
||||
}
|
||||
|
||||
// otherwise not found :(
|
||||
return nil, errors.New("peer not found")
|
||||
}
|
||||
|
||||
// FileStartReader providers a reader to a remote file. The reader must be closed by the caller.
|
||||
// File Size is the full file size reported by the remote peer, regardless of the requested offset and limit. Limit is optional (0 means the entire file).
|
||||
// Transfer Size is the size in bytes that is actually going to be transferred. The reader should be closed after reading that amount.
|
||||
// The optional cancelChan can be used to stop the file transfer at any point.
|
||||
func FileStartReader(peer *core.PeerInfo, hash []byte, offset, limit uint64, cancelChan <-chan struct{}) (reader io.ReadCloser, fileSize, transferSize uint64, err error) {
|
||||
if peer == nil {
|
||||
return nil, 0, 0, errors.New("peer not provided")
|
||||
} else if !peer.IsConnectionActive() {
|
||||
return nil, 0, 0, errors.New("no valid connection to peer")
|
||||
}
|
||||
|
||||
udtConn, virtualConn, err := peer.FileTransferRequestUDT(hash, offset, limit)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
if cancelChan != nil {
|
||||
go func() {
|
||||
<-cancelChan
|
||||
udtConn.Close()
|
||||
}()
|
||||
}
|
||||
|
||||
fileSize, transferSize, err = protocol.FileTransferReadHeader(udtConn)
|
||||
if err != nil {
|
||||
udtConn.Close()
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
virtualConn.Stats.(*core.FileTransferStats).FileSize = fileSize
|
||||
|
||||
return udtConn, fileSize, transferSize, nil
|
||||
}
|
||||
|
||||
// FileReadAll downloads the file from the peer.
|
||||
// This function should only be used for testing or as a basis to fork. The caller should develop a custom download function that handles timeouts and excessive file sizes.
|
||||
// It allocates whatever size is reported by the remote peer. This could lead to an out of memory crash.
|
||||
// This function is blocking and may take a long time depending on the remote peer and the network connection.
|
||||
func FileReadAll(peer *core.PeerInfo, hash []byte) (data []byte, err error) {
|
||||
reader, _, transferSize, err := FileStartReader(peer, hash, 0, 0, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
// read all data
|
||||
data = make([]byte, transferSize) // Warning: This could lead to an out of memory crash.
|
||||
_, err = reader.Read(data)
|
||||
|
||||
// Note: This function does not verify if the returned data matches the hash and expected size.
|
||||
|
||||
return data, err
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ type ApiFileMetadata struct {
|
||||
|
||||
// ApiFile is the metadata of a file published on the blockchain
|
||||
type ApiFile struct {
|
||||
ID uuid.UUID `json:"id"` // Unique ID.
|
||||
ID uuid.UUID `json:"ID"` // Unique ID.
|
||||
Hash []byte `json:"hash"` // Blake3 hash of the file data
|
||||
Type uint8 `json:"type"` // File Type. For example audio or document. See TypeX.
|
||||
Format uint16 `json:"format"` // File Format. This is more granular, for example PDF or Word file. See FormatX.
|
||||
@@ -154,8 +154,8 @@ func (api *WebapiInstance) apiBlockchainFileAdd(w http.ResponseWriter, r *http.R
|
||||
if _, err := warehouse.ValidateHash(file.Hash); err != nil {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
} else if _, fileSize, status, _ := api.backend.UserWarehouse.FileExists(file.Hash); status != warehouse.StatusOK {
|
||||
EncodeJSON(api.backend, w, r, apiBlockchainBlockStatus{Status: blockchain.StatusNotInWarehouse})
|
||||
} else if _, fileSize, status, _ := api.Backend.UserWarehouse.FileExists(file.Hash); status != warehouse.StatusOK {
|
||||
EncodeJSON(api.Backend, w, r, apiBlockchainBlockStatus{Status: blockchain.StatusNotInWarehouse})
|
||||
return
|
||||
} else {
|
||||
file.Size = fileSize
|
||||
@@ -168,17 +168,17 @@ func (api *WebapiInstance) apiBlockchainFileAdd(w http.ResponseWriter, r *http.R
|
||||
blockRecord := BlockRecordFileFromAPI(file)
|
||||
|
||||
// Set the merkle tree info as appropriate.
|
||||
if !SetFileMerkleInfo(api.backend, &blockRecord) {
|
||||
EncodeJSON(api.backend, w, r, apiBlockchainBlockStatus{Status: blockchain.StatusNotInWarehouse})
|
||||
if !SetFileMerkleInfo(api.Backend, &blockRecord) {
|
||||
EncodeJSON(api.Backend, w, r, apiBlockchainBlockStatus{Status: blockchain.StatusNotInWarehouse})
|
||||
return
|
||||
}
|
||||
|
||||
filesAdd = append(filesAdd, blockRecord)
|
||||
}
|
||||
|
||||
newHeight, newVersion, status := api.backend.UserBlockchain.AddFiles(filesAdd)
|
||||
newHeight, newVersion, status := api.Backend.UserBlockchain.AddFiles(filesAdd)
|
||||
|
||||
EncodeJSON(api.backend, w, r, apiBlockchainBlockStatus{Status: status, Height: newHeight, Version: newVersion})
|
||||
EncodeJSON(api.Backend, w, r, apiBlockchainBlockStatus{Status: status, Height: newHeight, Version: newVersion})
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -188,7 +188,7 @@ Request: GET /blockchain/file/list
|
||||
Response: 200 with JSON structure ApiBlockAddFiles
|
||||
*/
|
||||
func (api *WebapiInstance) apiBlockchainFileList(w http.ResponseWriter, r *http.Request) {
|
||||
files, status := api.backend.UserBlockchain.ListFiles()
|
||||
files, status := api.Backend.UserBlockchain.ListFiles()
|
||||
|
||||
var result ApiBlockAddFiles
|
||||
|
||||
@@ -198,7 +198,7 @@ func (api *WebapiInstance) apiBlockchainFileList(w http.ResponseWriter, r *http.
|
||||
|
||||
result.Status = status
|
||||
|
||||
EncodeJSON(api.backend, w, r, result)
|
||||
EncodeJSON(api.Backend, w, r, result)
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -220,18 +220,18 @@ func (api *WebapiInstance) apiBlockchainFileDelete(w http.ResponseWriter, r *htt
|
||||
deleteIDs = append(deleteIDs, input.Files[n].ID)
|
||||
}
|
||||
|
||||
newHeight, newVersion, deletedFiles, status := api.backend.UserBlockchain.DeleteFiles(deleteIDs)
|
||||
newHeight, newVersion, deletedFiles, status := api.Backend.UserBlockchain.DeleteFiles(deleteIDs)
|
||||
|
||||
// If successfully deleted from the blockchain, delete from the Warehouse in case there are no other references.
|
||||
if status == blockchain.StatusOK {
|
||||
for n := range deletedFiles {
|
||||
if files, status := api.backend.UserBlockchain.FileExists(deletedFiles[n].Hash); status == blockchain.StatusOK && len(files) == 0 {
|
||||
api.backend.UserWarehouse.DeleteFile(deletedFiles[n].Hash)
|
||||
if files, status := api.Backend.UserBlockchain.FileExists(deletedFiles[n].Hash); status == blockchain.StatusOK && len(files) == 0 {
|
||||
api.Backend.UserWarehouse.DeleteFile(deletedFiles[n].Hash)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EncodeJSON(api.backend, w, r, apiBlockchainBlockStatus{Status: status, Height: newHeight, Version: newVersion})
|
||||
EncodeJSON(api.Backend, w, r, apiBlockchainBlockStatus{Status: status, Height: newHeight, Version: newVersion})
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -263,8 +263,8 @@ func (api *WebapiInstance) apiBlockchainFileUpdate(w http.ResponseWriter, r *htt
|
||||
if _, err := warehouse.ValidateHash(file.Hash); err != nil {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
} else if _, fileSize, status, _ := api.backend.UserWarehouse.FileExists(file.Hash); status != warehouse.StatusOK {
|
||||
EncodeJSON(api.backend, w, r, apiBlockchainBlockStatus{Status: blockchain.StatusNotInWarehouse})
|
||||
} else if _, fileSize, status, _ := api.Backend.UserWarehouse.FileExists(file.Hash); status != warehouse.StatusOK {
|
||||
EncodeJSON(api.Backend, w, r, apiBlockchainBlockStatus{Status: blockchain.StatusNotInWarehouse})
|
||||
return
|
||||
} else {
|
||||
file.Size = fileSize
|
||||
@@ -277,17 +277,17 @@ func (api *WebapiInstance) apiBlockchainFileUpdate(w http.ResponseWriter, r *htt
|
||||
blockRecord := BlockRecordFileFromAPI(file)
|
||||
|
||||
// Set the merkle tree info as appropriate.
|
||||
if !SetFileMerkleInfo(api.backend, &blockRecord) {
|
||||
EncodeJSON(api.backend, w, r, apiBlockchainBlockStatus{Status: blockchain.StatusNotInWarehouse})
|
||||
if !SetFileMerkleInfo(api.Backend, &blockRecord) {
|
||||
EncodeJSON(api.Backend, w, r, apiBlockchainBlockStatus{Status: blockchain.StatusNotInWarehouse})
|
||||
return
|
||||
}
|
||||
|
||||
filesAdd = append(filesAdd, blockRecord)
|
||||
}
|
||||
|
||||
newHeight, newVersion, status := api.backend.UserBlockchain.ReplaceFiles(filesAdd)
|
||||
newHeight, newVersion, status := api.Backend.UserBlockchain.ReplaceFiles(filesAdd)
|
||||
|
||||
EncodeJSON(api.backend, w, r, apiBlockchainBlockStatus{Status: status, Height: newHeight, Version: newVersion})
|
||||
EncodeJSON(api.Backend, w, r, apiBlockchainBlockStatus{Status: status, Height: newHeight, Version: newVersion})
|
||||
}
|
||||
|
||||
// ---- metadata functions ----
|
||||
|
||||
@@ -1,153 +1,153 @@
|
||||
/*
|
||||
File Name: Profile.go
|
||||
Copyright: 2021 Peernet Foundation s.r.o.
|
||||
Author: Peter Kleissner
|
||||
*/
|
||||
|
||||
package webapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/PeernetOfficial/core/blockchain"
|
||||
)
|
||||
|
||||
// apiProfileData contains profile metadata stored on the blockchain. Any data is treated as untrusted and unverified by default.
|
||||
type apiProfileData struct {
|
||||
Fields []apiBlockRecordProfile `json:"fields"` // All fields
|
||||
Status int `json:"status"` // Status of the operation, only used when this structure is returned from the API. See blockchain.StatusX.
|
||||
}
|
||||
|
||||
// apiBlockRecordProfile provides information about the end user. 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 apiBlockRecordProfile struct {
|
||||
Type uint16 `json:"type"` // See ProfileX constants.
|
||||
// Depending on the exact type, one of the below fields is used for proper encoding:
|
||||
Text string `json:"text"` // Text value. UTF-8 encoding.
|
||||
Blob []byte `json:"blob"` // Binary data
|
||||
}
|
||||
|
||||
/*
|
||||
apiProfileList lists all users profile fields.
|
||||
|
||||
Request: GET /profile/list
|
||||
Response: 200 with JSON structure apiProfileData
|
||||
*/
|
||||
func (api *WebapiInstance) apiProfileList(w http.ResponseWriter, r *http.Request) {
|
||||
fields, status := api.backend.UserBlockchain.ProfileList()
|
||||
|
||||
result := apiProfileData{Status: status}
|
||||
for n := range fields {
|
||||
result.Fields = append(result.Fields, blockRecordProfileToAPI(fields[n]))
|
||||
}
|
||||
|
||||
EncodeJSON(api.backend, w, r, result)
|
||||
}
|
||||
|
||||
/*
|
||||
apiProfileRead reads a specific users profile field. See core.ProfileX for recognized fields.
|
||||
|
||||
Request: GET /profile/read?field=[index]
|
||||
Response: 200 with JSON structure apiProfileData
|
||||
*/
|
||||
func (api *WebapiInstance) apiProfileRead(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
fieldN, err1 := strconv.Atoi(r.Form.Get("field"))
|
||||
|
||||
if err1 != nil || fieldN < 0 {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var result apiProfileData
|
||||
|
||||
var data []byte
|
||||
if data, result.Status = api.backend.UserBlockchain.ProfileReadField(uint16(fieldN)); result.Status == blockchain.StatusOK {
|
||||
result.Fields = append(result.Fields, blockRecordProfileToAPI(blockchain.BlockRecordProfile{Type: uint16(fieldN), Data: data}))
|
||||
}
|
||||
|
||||
EncodeJSON(api.backend, w, r, result)
|
||||
}
|
||||
|
||||
/*
|
||||
apiProfileWrite writes profile fields. See core.ProfileX for recognized fields.
|
||||
|
||||
Request: POST /profile/write with JSON structure apiProfileData
|
||||
Response: 200 with JSON structure apiBlockchainBlockStatus
|
||||
*/
|
||||
func (api *WebapiInstance) apiProfileWrite(w http.ResponseWriter, r *http.Request) {
|
||||
var input apiProfileData
|
||||
if err := DecodeJSON(w, r, &input); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var fields []blockchain.BlockRecordProfile
|
||||
|
||||
for n := range input.Fields {
|
||||
fields = append(fields, blockRecordProfileFromAPI(input.Fields[n]))
|
||||
}
|
||||
|
||||
newHeight, newVersion, status := api.backend.UserBlockchain.ProfileWrite(fields)
|
||||
|
||||
EncodeJSON(api.backend, w, r, apiBlockchainBlockStatus{Status: status, Height: newHeight, Version: newVersion})
|
||||
}
|
||||
|
||||
/*
|
||||
apiProfileDelete deletes profile fields identified by the types. See core.ProfileX for recognized fields.
|
||||
|
||||
Request: POST /profile/delete with JSON structure apiProfileData
|
||||
Response: 200 with JSON structure apiBlockchainBlockStatus
|
||||
*/
|
||||
func (api *WebapiInstance) apiProfileDelete(w http.ResponseWriter, r *http.Request) {
|
||||
var input apiProfileData
|
||||
if err := DecodeJSON(w, r, &input); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var fields []uint16
|
||||
|
||||
for n := range input.Fields {
|
||||
fields = append(fields, input.Fields[n].Type)
|
||||
}
|
||||
|
||||
newHeight, newVersion, status := api.backend.UserBlockchain.ProfileDelete(fields)
|
||||
|
||||
EncodeJSON(api.backend, w, r, apiBlockchainBlockStatus{Status: status, Height: newHeight, Version: newVersion})
|
||||
}
|
||||
|
||||
// --- conversion from core to API data ---
|
||||
|
||||
func blockRecordProfileToAPI(input blockchain.BlockRecordProfile) (output apiBlockRecordProfile) {
|
||||
output.Type = input.Type
|
||||
|
||||
switch input.Type {
|
||||
case blockchain.ProfileName, blockchain.ProfileEmail, blockchain.ProfileWebsite, blockchain.ProfileTwitter, blockchain.ProfileYouTube, blockchain.ProfileAddress:
|
||||
output.Text = input.Text()
|
||||
|
||||
case blockchain.ProfilePicture:
|
||||
output.Blob = input.Data
|
||||
|
||||
default:
|
||||
output.Blob = input.Data
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func blockRecordProfileFromAPI(input apiBlockRecordProfile) (output blockchain.BlockRecordProfile) {
|
||||
output.Type = input.Type
|
||||
|
||||
switch input.Type {
|
||||
case blockchain.ProfileName, blockchain.ProfileEmail, blockchain.ProfileWebsite, blockchain.ProfileTwitter, blockchain.ProfileYouTube, blockchain.ProfileAddress:
|
||||
output.Data = []byte(input.Text)
|
||||
|
||||
case blockchain.ProfilePicture:
|
||||
output.Data = input.Blob
|
||||
|
||||
default:
|
||||
output.Data = input.Blob
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
/*
|
||||
File Name: Profile.go
|
||||
Copyright: 2021 Peernet Foundation s.r.o.
|
||||
Author: Peter Kleissner
|
||||
*/
|
||||
|
||||
package webapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/PeernetOfficial/core/blockchain"
|
||||
)
|
||||
|
||||
// apiProfileData contains profile metadata stored on the blockchain. Any data is treated as untrusted and unverified by default.
|
||||
type apiProfileData struct {
|
||||
Fields []apiBlockRecordProfile `json:"fields"` // All fields
|
||||
Status int `json:"status"` // Status of the operation, only used when this structure is returned from the API. See blockchain.StatusX.
|
||||
}
|
||||
|
||||
// apiBlockRecordProfile provides information about the end user. 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 apiBlockRecordProfile struct {
|
||||
Type uint16 `json:"type"` // See ProfileX constants.
|
||||
// Depending on the exact type, one of the below fields is used for proper encoding:
|
||||
Text string `json:"text"` // Text value. UTF-8 encoding.
|
||||
Blob []byte `json:"blob"` // Binary data
|
||||
}
|
||||
|
||||
/*
|
||||
apiProfileList lists all users profile fields.
|
||||
|
||||
Request: GET /profile/list
|
||||
Response: 200 with JSON structure apiProfileData
|
||||
*/
|
||||
func (api *WebapiInstance) apiProfileList(w http.ResponseWriter, r *http.Request) {
|
||||
fields, status := api.Backend.UserBlockchain.ProfileList()
|
||||
|
||||
result := apiProfileData{Status: status}
|
||||
for n := range fields {
|
||||
result.Fields = append(result.Fields, blockRecordProfileToAPI(fields[n]))
|
||||
}
|
||||
|
||||
EncodeJSON(api.Backend, w, r, result)
|
||||
}
|
||||
|
||||
/*
|
||||
apiProfileRead reads a specific users profile field. See core.ProfileX for recognized fields.
|
||||
|
||||
Request: GET /profile/read?field=[index]
|
||||
Response: 200 with JSON structure apiProfileData
|
||||
*/
|
||||
func (api *WebapiInstance) apiProfileRead(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
fieldN, err1 := strconv.Atoi(r.Form.Get("field"))
|
||||
|
||||
if err1 != nil || fieldN < 0 {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var result apiProfileData
|
||||
|
||||
var data []byte
|
||||
if data, result.Status = api.Backend.UserBlockchain.ProfileReadField(uint16(fieldN)); result.Status == blockchain.StatusOK {
|
||||
result.Fields = append(result.Fields, blockRecordProfileToAPI(blockchain.BlockRecordProfile{Type: uint16(fieldN), Data: data}))
|
||||
}
|
||||
|
||||
EncodeJSON(api.Backend, w, r, result)
|
||||
}
|
||||
|
||||
/*
|
||||
apiProfileWrite writes profile fields. See core.ProfileX for recognized fields.
|
||||
|
||||
Request: POST /profile/write with JSON structure apiProfileData
|
||||
Response: 200 with JSON structure apiBlockchainBlockStatus
|
||||
*/
|
||||
func (api *WebapiInstance) apiProfileWrite(w http.ResponseWriter, r *http.Request) {
|
||||
var input apiProfileData
|
||||
if err := DecodeJSON(w, r, &input); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var fields []blockchain.BlockRecordProfile
|
||||
|
||||
for n := range input.Fields {
|
||||
fields = append(fields, blockRecordProfileFromAPI(input.Fields[n]))
|
||||
}
|
||||
|
||||
newHeight, newVersion, status := api.Backend.UserBlockchain.ProfileWrite(fields)
|
||||
|
||||
EncodeJSON(api.Backend, w, r, apiBlockchainBlockStatus{Status: status, Height: newHeight, Version: newVersion})
|
||||
}
|
||||
|
||||
/*
|
||||
apiProfileDelete deletes profile fields identified by the types. See core.ProfileX for recognized fields.
|
||||
|
||||
Request: POST /profile/delete with JSON structure apiProfileData
|
||||
Response: 200 with JSON structure apiBlockchainBlockStatus
|
||||
*/
|
||||
func (api *WebapiInstance) apiProfileDelete(w http.ResponseWriter, r *http.Request) {
|
||||
var input apiProfileData
|
||||
if err := DecodeJSON(w, r, &input); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var fields []uint16
|
||||
|
||||
for n := range input.Fields {
|
||||
fields = append(fields, input.Fields[n].Type)
|
||||
}
|
||||
|
||||
newHeight, newVersion, status := api.Backend.UserBlockchain.ProfileDelete(fields)
|
||||
|
||||
EncodeJSON(api.Backend, w, r, apiBlockchainBlockStatus{Status: status, Height: newHeight, Version: newVersion})
|
||||
}
|
||||
|
||||
// --- conversion from core to API data ---
|
||||
|
||||
func blockRecordProfileToAPI(input blockchain.BlockRecordProfile) (output apiBlockRecordProfile) {
|
||||
output.Type = input.Type
|
||||
|
||||
switch input.Type {
|
||||
case blockchain.ProfileName, blockchain.ProfileEmail, blockchain.ProfileWebsite, blockchain.ProfileTwitter, blockchain.ProfileYouTube, blockchain.ProfileAddress:
|
||||
output.Text = input.Text()
|
||||
|
||||
case blockchain.ProfilePicture:
|
||||
output.Blob = input.Data
|
||||
|
||||
default:
|
||||
output.Blob = input.Data
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func blockRecordProfileFromAPI(input apiBlockRecordProfile) (output blockchain.BlockRecordProfile) {
|
||||
output.Type = input.Type
|
||||
|
||||
switch input.Type {
|
||||
case blockchain.ProfileName, blockchain.ProfileEmail, blockchain.ProfileWebsite, blockchain.ProfileTwitter, blockchain.ProfileYouTube, blockchain.ProfileAddress:
|
||||
output.Data = []byte(input.Text)
|
||||
|
||||
case blockchain.ProfilePicture:
|
||||
output.Data = input.Blob
|
||||
|
||||
default:
|
||||
output.Data = input.Blob
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
@@ -1,83 +1,83 @@
|
||||
/*
|
||||
File Name: Search Dispatch.go
|
||||
Copyright: 2021 Peernet Foundation s.r.o.
|
||||
Author: Peter Kleissner
|
||||
*/
|
||||
|
||||
package webapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/PeernetOfficial/core/blockchain"
|
||||
)
|
||||
|
||||
func (api *WebapiInstance) dispatchSearch(input SearchRequest) (job *SearchJob) {
|
||||
Timeout := input.Parse()
|
||||
Filter := input.ToSearchFilter()
|
||||
|
||||
// create the search job
|
||||
job = api.CreateSearchJob(Timeout, input.MaxResults, Filter)
|
||||
|
||||
// todo: create actual search clients!
|
||||
job.Status = SearchStatusLive
|
||||
|
||||
go job.localSearch(api, input.Term)
|
||||
|
||||
api.RemoveJobDefer(job, job.timeout+time.Minute*10)
|
||||
|
||||
return job
|
||||
}
|
||||
|
||||
func (job *SearchJob) localSearch(api *WebapiInstance, term string) {
|
||||
if api.backend.SearchIndex == nil {
|
||||
job.Status = SearchStatusNoIndex
|
||||
return
|
||||
}
|
||||
|
||||
results := api.backend.SearchIndex.Search(term)
|
||||
|
||||
job.ResultSync.Lock()
|
||||
|
||||
resultLoop:
|
||||
for _, result := range results {
|
||||
file, _, found, err := api.backend.ReadFile(result.PublicKey, result.BlockchainVersion, result.BlockNumber, result.FileID)
|
||||
if err != nil || !found {
|
||||
continue
|
||||
}
|
||||
|
||||
// Deduplicate based on file hash from the same peer.
|
||||
for n := range job.AllFiles {
|
||||
if bytes.Equal(job.AllFiles[n].Hash, file.Hash) && bytes.Equal(job.AllFiles[n].NodeID, file.NodeID) {
|
||||
continue resultLoop
|
||||
}
|
||||
}
|
||||
|
||||
if bytes.Equal(file.NodeID, api.backend.SelfNodeID()) {
|
||||
// Indicates data from the current user.
|
||||
file.Tags = append(file.Tags, blockchain.TagFromNumber(blockchain.TagSharedByCount, 1))
|
||||
} else if peer := api.backend.NodelistLookup(file.NodeID); peer != nil {
|
||||
// add the tags 'Shared By Count' and 'Shared By GeoIP'
|
||||
file.Tags = append(file.Tags, blockchain.TagFromNumber(blockchain.TagSharedByCount, 1))
|
||||
if latitude, longitude, valid := api.Peer2GeoIP(peer); valid {
|
||||
sharedByGeoIP := fmt.Sprintf("%.4f", latitude) + "," + fmt.Sprintf("%.4f", longitude)
|
||||
file.Tags = append(file.Tags, blockchain.TagFromText(blockchain.TagSharedByGeoIP, sharedByGeoIP))
|
||||
}
|
||||
}
|
||||
|
||||
// new result
|
||||
newFile := blockRecordFileToAPI(file)
|
||||
|
||||
job.Files = append(job.Files, &newFile)
|
||||
job.AllFiles = append(job.AllFiles, &newFile)
|
||||
job.requireSort = true
|
||||
job.statsAdd(&newFile)
|
||||
}
|
||||
|
||||
job.Status = SearchStatusTerminated
|
||||
|
||||
job.ResultSync.Unlock()
|
||||
job.Terminate()
|
||||
}
|
||||
/*
|
||||
File Name: Search Dispatch.go
|
||||
Copyright: 2021 Peernet Foundation s.r.o.
|
||||
Author: Peter Kleissner
|
||||
*/
|
||||
|
||||
package webapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/PeernetOfficial/core/blockchain"
|
||||
)
|
||||
|
||||
func (api *WebapiInstance) DispatchSearch(input SearchRequest) (job *SearchJob) {
|
||||
Timeout := input.Parse()
|
||||
Filter := input.ToSearchFilter()
|
||||
|
||||
// create the search job
|
||||
job = api.CreateSearchJob(Timeout, input.MaxResults, Filter)
|
||||
|
||||
// todo: create actual search clients!
|
||||
job.Status = SearchStatusLive
|
||||
|
||||
go job.localSearch(api, input.Term)
|
||||
|
||||
api.RemoveJobDefer(job, job.timeout+time.Minute*10)
|
||||
|
||||
return job
|
||||
}
|
||||
|
||||
func (job *SearchJob) localSearch(api *WebapiInstance, term string) {
|
||||
if api.Backend.SearchIndex == nil {
|
||||
job.Status = SearchStatusNoIndex
|
||||
return
|
||||
}
|
||||
|
||||
results := api.Backend.SearchIndex.Search(term)
|
||||
|
||||
job.ResultSync.Lock()
|
||||
|
||||
resultLoop:
|
||||
for _, result := range results {
|
||||
file, _, found, err := api.Backend.ReadFile(result.PublicKey, result.BlockchainVersion, result.BlockNumber, result.FileID)
|
||||
if err != nil || !found {
|
||||
continue
|
||||
}
|
||||
|
||||
// Deduplicate based on file hash from the same peer.
|
||||
for n := range job.AllFiles {
|
||||
if bytes.Equal(job.AllFiles[n].Hash, file.Hash) && bytes.Equal(job.AllFiles[n].NodeID, file.NodeID) {
|
||||
continue resultLoop
|
||||
}
|
||||
}
|
||||
|
||||
if bytes.Equal(file.NodeID, api.Backend.SelfNodeID()) {
|
||||
// Indicates data from the current user.
|
||||
file.Tags = append(file.Tags, blockchain.TagFromNumber(blockchain.TagSharedByCount, 1))
|
||||
} else if peer := api.Backend.NodelistLookup(file.NodeID); peer != nil {
|
||||
// add the tags 'Shared By Count' and 'Shared By GeoIP'
|
||||
file.Tags = append(file.Tags, blockchain.TagFromNumber(blockchain.TagSharedByCount, 1))
|
||||
if latitude, longitude, valid := api.Peer2GeoIP(peer); valid {
|
||||
sharedByGeoIP := fmt.Sprintf("%.4f", latitude) + "," + fmt.Sprintf("%.4f", longitude)
|
||||
file.Tags = append(file.Tags, blockchain.TagFromText(blockchain.TagSharedByGeoIP, sharedByGeoIP))
|
||||
}
|
||||
}
|
||||
|
||||
// new result
|
||||
newFile := blockRecordFileToAPI(file)
|
||||
|
||||
job.Files = append(job.Files, &newFile)
|
||||
job.AllFiles = append(job.AllFiles, &newFile)
|
||||
job.requireSort = true
|
||||
job.statsAdd(&newFile)
|
||||
}
|
||||
|
||||
job.Status = SearchStatusTerminated
|
||||
|
||||
job.ResultSync.Unlock()
|
||||
job.Terminate()
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ type SearchFilter struct {
|
||||
// SearchJob is a collection of search jobs
|
||||
type SearchJob struct {
|
||||
// input settings
|
||||
id uuid.UUID // The job id
|
||||
ID uuid.UUID // The job ID
|
||||
timeout time.Duration // timeout set for all searches
|
||||
maxResult int // max results user-facing.
|
||||
|
||||
@@ -82,7 +82,7 @@ const (
|
||||
func (api *WebapiInstance) CreateSearchJob(Timeout time.Duration, MaxResults int, Filter SearchFilter) (job *SearchJob) {
|
||||
job = &SearchJob{}
|
||||
job.Status = SearchStatusNotStarted
|
||||
job.id = uuid.New()
|
||||
job.ID = uuid.New()
|
||||
job.timeout = Timeout
|
||||
job.maxResult = MaxResults
|
||||
job.filtersStart = Filter
|
||||
@@ -94,7 +94,7 @@ func (api *WebapiInstance) CreateSearchJob(Timeout time.Duration, MaxResults int
|
||||
|
||||
// add to the list of jobs
|
||||
api.allJobsMutex.Lock()
|
||||
api.allJobs[job.id] = job
|
||||
api.allJobs[job.ID] = job
|
||||
api.allJobsMutex.Unlock()
|
||||
|
||||
return
|
||||
@@ -319,7 +319,7 @@ func (job *SearchJob) isFileReceived(id uuid.UUID) (exists bool) {
|
||||
// RemoveJob removes the job structure from the list. Terminate should be called before. Unless the search is manually removed, it stays forever in the list.
|
||||
func (api *WebapiInstance) RemoveJob(job *SearchJob) {
|
||||
api.allJobsMutex.Lock()
|
||||
delete(api.allJobs, job.id) // delete is safe to call multiple times, so auto-removal and manual one are fine and need no syncing
|
||||
delete(api.allJobs, job.ID) // delete is safe to call multiple times, so auto-removal and manual one are fine and need no syncing
|
||||
api.allJobsMutex.Unlock()
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ const (
|
||||
|
||||
// SearchRequestResponse is the result to the initial search request
|
||||
type SearchRequestResponse struct {
|
||||
ID uuid.UUID `json:"id"` // ID of the search job. This is used to get the results.
|
||||
ID uuid.UUID `json:"ID"` // ID of the search job. This is used to get the results.
|
||||
Status int `json:"status"` // Status of the search: 0 = Success (ID valid), 1 = Invalid Term, 2 = Error Max Concurrent Searches
|
||||
}
|
||||
|
||||
@@ -104,16 +104,16 @@ func (api *WebapiInstance) apiSearch(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
job := api.dispatchSearch(input)
|
||||
job := api.DispatchSearch(input)
|
||||
|
||||
EncodeJSON(api.backend, w, r, SearchRequestResponse{Status: 0, ID: job.id})
|
||||
EncodeJSON(api.Backend, w, r, SearchRequestResponse{Status: 0, ID: job.ID})
|
||||
}
|
||||
|
||||
/*
|
||||
apiSearchResult returns results. The default limit is 100.
|
||||
If reset is set, all results will be filtered and sorted according to the settings. This means that the new first result will be returned again and internal result offset is set to 0.
|
||||
|
||||
Request: GET /search/result?id=[UUID]&limit=[max records]
|
||||
Request: GET /search/result?ID=[UUID]&limit=[max records]
|
||||
Optional parameters:
|
||||
&reset=[0|1] to reset the filters or sort orders with any of the below parameters (all required):
|
||||
&filetype=[File Type]
|
||||
@@ -127,7 +127,7 @@ Result: 200 with JSON structure SearchResult. Check the field status.
|
||||
*/
|
||||
func (api *WebapiInstance) apiSearchResult(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
jobID, err := uuid.Parse(r.Form.Get("id"))
|
||||
jobID, err := uuid.Parse(r.Form.Get("ID"))
|
||||
if err != nil {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
@@ -141,7 +141,7 @@ func (api *WebapiInstance) apiSearchResult(w http.ResponseWriter, r *http.Reques
|
||||
// find the job ID
|
||||
job := api.JobLookup(jobID)
|
||||
if job == nil {
|
||||
EncodeJSON(api.backend, w, r, SearchResult{Status: 2})
|
||||
EncodeJSON(api.Backend, w, r, SearchResult{Status: 2})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -201,19 +201,19 @@ func (api *WebapiInstance) apiSearchResult(w http.ResponseWriter, r *http.Reques
|
||||
result.Statistic = job.Statistics()
|
||||
}
|
||||
|
||||
EncodeJSON(api.backend, w, r, result)
|
||||
EncodeJSON(api.Backend, w, r, result)
|
||||
}
|
||||
|
||||
/*
|
||||
apiSearchResultStream provides a websocket to receive results as stream.
|
||||
|
||||
Request: GET /search/result/ws?id=[UUID]&limit=[optional max records]
|
||||
Request: GET /search/result/ws?ID=[UUID]&limit=[optional max records]
|
||||
Result: If successful, upgrades to a websocket and sends JSON structure SearchResult messages.
|
||||
Limit is optional. Not used if ommitted or 0.
|
||||
*/
|
||||
func (api *WebapiInstance) apiSearchResultStream(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
jobID, err := uuid.Parse(r.Form.Get("id"))
|
||||
jobID, err := uuid.Parse(r.Form.Get("ID"))
|
||||
if err != nil {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
@@ -224,7 +224,7 @@ func (api *WebapiInstance) apiSearchResultStream(w http.ResponseWriter, r *http.
|
||||
// look up the job
|
||||
job := api.JobLookup(jobID)
|
||||
if job == nil {
|
||||
EncodeJSON(api.backend, w, r, SearchResult{Status: 2})
|
||||
EncodeJSON(api.Backend, w, r, SearchResult{Status: 2})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -291,7 +291,7 @@ func (api *WebapiInstance) apiSearchResultStream(w http.ResponseWriter, r *http.
|
||||
/*
|
||||
apiSearchTerminate terminates a search
|
||||
|
||||
Request: GET /search/terminate?id=[UUID]
|
||||
Request: GET /search/terminate?ID=[UUID]
|
||||
Response: 204 Empty
|
||||
400 Invalid input
|
||||
404 ID not found
|
||||
@@ -299,7 +299,7 @@ Response: 204 Empty
|
||||
func (api *WebapiInstance) apiSearchTerminate(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
r.ParseForm()
|
||||
jobID, err := uuid.Parse(r.Form.Get("id"))
|
||||
jobID, err := uuid.Parse(r.Form.Get("ID"))
|
||||
if err != nil {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
@@ -322,12 +322,12 @@ func (api *WebapiInstance) apiSearchTerminate(w http.ResponseWriter, r *http.Req
|
||||
/*
|
||||
apiSearchStatistic returns search result statistics. Statistics are always calculated over all results, regardless of any applied runtime filters.
|
||||
|
||||
Request: GET /search/result?id=[UUID]
|
||||
Request: GET /search/result?ID=[UUID]
|
||||
Result: 200 with JSON structure SearchStatistic. Check the field status (0 = Success, 2 = ID not found).
|
||||
*/
|
||||
func (api *WebapiInstance) apiSearchStatistic(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
jobID, err := uuid.Parse(r.Form.Get("id"))
|
||||
jobID, err := uuid.Parse(r.Form.Get("ID"))
|
||||
if err != nil {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
@@ -336,13 +336,13 @@ func (api *WebapiInstance) apiSearchStatistic(w http.ResponseWriter, r *http.Req
|
||||
// find the job ID
|
||||
job := api.JobLookup(jobID)
|
||||
if job == nil {
|
||||
EncodeJSON(api.backend, w, r, SearchStatistic{Status: 2})
|
||||
EncodeJSON(api.Backend, w, r, SearchStatistic{Status: 2})
|
||||
return
|
||||
}
|
||||
|
||||
stats := job.Statistics()
|
||||
|
||||
EncodeJSON(api.backend, w, r, SearchStatistic{SearchStatisticData: stats, Status: 0, IsTerminated: job.IsTerminated()})
|
||||
EncodeJSON(api.Backend, w, r, SearchStatistic{SearchStatisticData: stats, Status: 0, IsTerminated: job.IsTerminated()})
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -364,7 +364,7 @@ func (api *WebapiInstance) apiExplore(w http.ResponseWriter, r *http.Request) {
|
||||
fileType = -1
|
||||
}
|
||||
|
||||
resultFiles := api.queryRecentShared(api.backend, fileType, uint64(limit*20/100), uint64(offset), uint64(limit))
|
||||
resultFiles := api.queryRecentShared(api.Backend, fileType, uint64(limit*20/100), uint64(offset), uint64(limit))
|
||||
|
||||
var result SearchResult
|
||||
result.Files = []ApiFile{}
|
||||
@@ -380,7 +380,7 @@ func (api *WebapiInstance) apiExplore(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
result.Status = 1 // No more results to expect
|
||||
|
||||
EncodeJSON(api.backend, w, r, result)
|
||||
EncodeJSON(api.Backend, w, r, result)
|
||||
}
|
||||
|
||||
func (input *SearchRequest) Parse() (Timeout time.Duration) {
|
||||
|
||||
@@ -1,89 +1,89 @@
|
||||
/*
|
||||
File Name: Shared Recent.go
|
||||
Copyright: 2021 Peernet Foundation s.r.o.
|
||||
Author: Peter Kleissner
|
||||
*/
|
||||
package webapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/PeernetOfficial/core"
|
||||
"github.com/PeernetOfficial/core/blockchain"
|
||||
)
|
||||
|
||||
// queryRecentShared returns recently shared files on the network from random peers until the limit is reached.
|
||||
func (api *WebapiInstance) queryRecentShared(backend *core.Backend, fileType int, limitPeer, offsetTotal, limitTotal uint64) (files []blockchain.BlockRecordFile) {
|
||||
if limitPeer == 0 {
|
||||
limitPeer = 1
|
||||
}
|
||||
|
||||
// Use the peer list to know about active peers. Random order!
|
||||
peerList := api.backend.PeerlistGet()
|
||||
|
||||
// Files from peers exceeding the limit. It is used if from all peers the total limit is not reached.
|
||||
var filesSeconday []blockchain.BlockRecordFile
|
||||
|
||||
for _, peer := range peerList {
|
||||
if peer.BlockchainHeight == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var filesFromPeer uint64
|
||||
|
||||
// decode blocks from top down
|
||||
blockLoop:
|
||||
for blockN := peer.BlockchainHeight - 1; blockN > 0; blockN-- {
|
||||
blockDecoded, _, found, _ := backend.ReadBlock(peer.PublicKey, peer.BlockchainVersion, blockN)
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, record := range blockDecoded.RecordsDecoded {
|
||||
if file, ok := record.(blockchain.BlockRecordFile); ok && isFileTypeMatchBlock(&file, fileType) {
|
||||
// add the tags 'Shared By Count' and 'Shared By GeoIP'
|
||||
file.Tags = append(file.Tags, blockchain.TagFromNumber(blockchain.TagSharedByCount, 1))
|
||||
if latitude, longitude, valid := api.Peer2GeoIP(peer); valid {
|
||||
sharedByGeoIP := fmt.Sprintf("%.4f", latitude) + "," + fmt.Sprintf("%.4f", longitude)
|
||||
file.Tags = append(file.Tags, blockchain.TagFromText(blockchain.TagSharedByGeoIP, sharedByGeoIP))
|
||||
}
|
||||
|
||||
// found a new file! append.
|
||||
if filesFromPeer < limitPeer {
|
||||
filesFromPeer++
|
||||
|
||||
if offsetTotal > 0 {
|
||||
offsetTotal--
|
||||
continue
|
||||
}
|
||||
|
||||
files = append(files, file)
|
||||
|
||||
if uint64(len(files)) >= limitTotal {
|
||||
return
|
||||
}
|
||||
} else if uint64(len(filesSeconday)) < limitTotal-uint64(len(files)) {
|
||||
filesSeconday = append(filesSeconday, file)
|
||||
} else {
|
||||
break blockLoop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
files = append(files, filesSeconday...)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// isFileTypeMatchBlock checks if the file type matches. -1 = accept any. -2 = core.TypeBinary, core.TypeCompressed, core.TypeContainer, core.TypeExecutable.
|
||||
func isFileTypeMatchBlock(file *blockchain.BlockRecordFile, fileType int) bool {
|
||||
if fileType == -1 {
|
||||
return true
|
||||
} else if fileType == -2 {
|
||||
return file.Type == core.TypeBinary || file.Type == core.TypeCompressed || file.Type == core.TypeContainer || file.Type == core.TypeExecutable
|
||||
}
|
||||
|
||||
return file.Type == uint8(fileType)
|
||||
}
|
||||
/*
|
||||
File Name: Shared Recent.go
|
||||
Copyright: 2021 Peernet Foundation s.r.o.
|
||||
Author: Peter Kleissner
|
||||
*/
|
||||
package webapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/PeernetOfficial/core"
|
||||
"github.com/PeernetOfficial/core/blockchain"
|
||||
)
|
||||
|
||||
// queryRecentShared returns recently shared files on the network from random peers until the limit is reached.
|
||||
func (api *WebapiInstance) queryRecentShared(backend *core.Backend, fileType int, limitPeer, offsetTotal, limitTotal uint64) (files []blockchain.BlockRecordFile) {
|
||||
if limitPeer == 0 {
|
||||
limitPeer = 1
|
||||
}
|
||||
|
||||
// Use the peer list to know about active peers. Random order!
|
||||
peerList := api.Backend.PeerlistGet()
|
||||
|
||||
// Files from peers exceeding the limit. It is used if from all peers the total limit is not reached.
|
||||
var filesSeconday []blockchain.BlockRecordFile
|
||||
|
||||
for _, peer := range peerList {
|
||||
if peer.BlockchainHeight == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var filesFromPeer uint64
|
||||
|
||||
// decode blocks from top down
|
||||
blockLoop:
|
||||
for blockN := peer.BlockchainHeight - 1; blockN > 0; blockN-- {
|
||||
blockDecoded, _, found, _ := backend.ReadBlock(peer.PublicKey, peer.BlockchainVersion, blockN)
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, record := range blockDecoded.RecordsDecoded {
|
||||
if file, ok := record.(blockchain.BlockRecordFile); ok && isFileTypeMatchBlock(&file, fileType) {
|
||||
// add the tags 'Shared By Count' and 'Shared By GeoIP'
|
||||
file.Tags = append(file.Tags, blockchain.TagFromNumber(blockchain.TagSharedByCount, 1))
|
||||
if latitude, longitude, valid := api.Peer2GeoIP(peer); valid {
|
||||
sharedByGeoIP := fmt.Sprintf("%.4f", latitude) + "," + fmt.Sprintf("%.4f", longitude)
|
||||
file.Tags = append(file.Tags, blockchain.TagFromText(blockchain.TagSharedByGeoIP, sharedByGeoIP))
|
||||
}
|
||||
|
||||
// found a new file! append.
|
||||
if filesFromPeer < limitPeer {
|
||||
filesFromPeer++
|
||||
|
||||
if offsetTotal > 0 {
|
||||
offsetTotal--
|
||||
continue
|
||||
}
|
||||
|
||||
files = append(files, file)
|
||||
|
||||
if uint64(len(files)) >= limitTotal {
|
||||
return
|
||||
}
|
||||
} else if uint64(len(filesSeconday)) < limitTotal-uint64(len(files)) {
|
||||
filesSeconday = append(filesSeconday, file)
|
||||
} else {
|
||||
break blockLoop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
files = append(files, filesSeconday...)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// isFileTypeMatchBlock checks if the file type matches. -1 = accept any. -2 = core.TypeBinary, core.TypeCompressed, core.TypeContainer, core.TypeExecutable.
|
||||
func isFileTypeMatchBlock(file *blockchain.BlockRecordFile, fileType int) bool {
|
||||
if fileType == -1 {
|
||||
return true
|
||||
} else if fileType == -2 {
|
||||
return file.Type == core.TypeBinary || file.Type == core.TypeCompressed || file.Type == core.TypeContainer || file.Type == core.TypeExecutable
|
||||
}
|
||||
|
||||
return file.Type == uint8(fileType)
|
||||
}
|
||||
|
||||
252
webapi/Status.go
252
webapi/Status.go
@@ -1,126 +1,126 @@
|
||||
/*
|
||||
File Name: Status.go
|
||||
Copyright: 2021 Peernet Foundation s.r.o.
|
||||
Author: Peter Kleissner
|
||||
*/
|
||||
|
||||
package webapi
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
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 (api *WebapiInstance) apiStatus(w http.ResponseWriter, r *http.Request) {
|
||||
status := apiResponseStatus{Status: 0, CountPeerList: api.backend.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
|
||||
|
||||
EncodeJSON(api.backend, 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.
|
||||
}
|
||||
|
||||
/*
|
||||
apiAccountInfo provides information about the current account.
|
||||
Request: GET /account/info
|
||||
Result: 200 with JSON structure apiResponsePeerSelf
|
||||
*/
|
||||
func (api *WebapiInstance) apiAccountInfo(w http.ResponseWriter, r *http.Request) {
|
||||
response := apiResponsePeerSelf{}
|
||||
response.NodeID = hex.EncodeToString(api.backend.SelfNodeID())
|
||||
|
||||
_, publicKey := api.backend.ExportPrivateKey()
|
||||
response.PeerID = hex.EncodeToString(publicKey.SerializeCompressed())
|
||||
|
||||
EncodeJSON(api.backend, w, r, response)
|
||||
}
|
||||
|
||||
/*
|
||||
apiAccountDelete deletes the current account. The confirm parameter must include the user's choice.
|
||||
Request: GET /account/delete?confirm=[0 or 1]
|
||||
Result: 204 if the user choses not to delete the account
|
||||
|
||||
200 if successfully deleted
|
||||
*/
|
||||
func (api *WebapiInstance) apiAccountDelete(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
if confirm, _ := strconv.ParseBool(r.Form.Get("confirm")); !confirm {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
api.backend.DeleteAccount()
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
/*
|
||||
apiStatusPeers returns the information about peers currently connected.
|
||||
The GeoIP information may not alawys be available, for example if the GeoIP file is not available or the mapping from IP address to location is not available.
|
||||
Peers that are connected only via local network will not have a geo location.
|
||||
|
||||
Request: GET /status/peers
|
||||
Result: 200 with JSON array apiResponsePeerInfo
|
||||
*/
|
||||
func (api *WebapiInstance) apiStatusPeers(w http.ResponseWriter, r *http.Request) {
|
||||
var peers []apiResponsePeerInfo
|
||||
|
||||
// query all nodes
|
||||
for _, peer := range api.backend.PeerlistGet() {
|
||||
peerInfo := apiResponsePeerInfo{
|
||||
PeerID: peer.PublicKey.SerializeCompressed(),
|
||||
NodeID: peer.NodeID,
|
||||
UserAgent: peer.UserAgent,
|
||||
IsRoot: peer.IsRootPeer,
|
||||
BlockchainHeight: peer.BlockchainHeight,
|
||||
BlockchainVersion: peer.BlockchainVersion,
|
||||
}
|
||||
|
||||
if latitude, longitude, valid := api.Peer2GeoIP(peer); valid {
|
||||
peerInfo.GeoIP = fmt.Sprintf("%.4f", latitude) + "," + fmt.Sprintf("%.4f", longitude)
|
||||
}
|
||||
|
||||
peers = append(peers, peerInfo)
|
||||
}
|
||||
|
||||
EncodeJSON(api.backend, w, r, peers)
|
||||
}
|
||||
|
||||
type apiResponsePeerInfo struct {
|
||||
PeerID []byte `json:"peerid"` // Peer ID. This is derived from the public in compressed form.
|
||||
NodeID []byte `json:"nodeid"` // Node ID. This is the blake3 hash of the peer ID and used in the DHT.
|
||||
GeoIP string `json:"geoip"` // GeoIP location as "Latitude,Longitude" CSV format. Empty if location not available.
|
||||
UserAgent string `json:"useragent"` // User Agent.
|
||||
IsRoot bool `json:"isroot"` // If the peer is a root peer.
|
||||
BlockchainHeight uint64 `json:"blockchainheight"` // Blockchain height
|
||||
BlockchainVersion uint64 `json:"blockchainversion"` // Blockchain version
|
||||
}
|
||||
/*
|
||||
File Name: Status.go
|
||||
Copyright: 2021 Peernet Foundation s.r.o.
|
||||
Author: Peter Kleissner
|
||||
*/
|
||||
|
||||
package webapi
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
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 (api *WebapiInstance) apiStatus(w http.ResponseWriter, r *http.Request) {
|
||||
status := apiResponseStatus{Status: 0, CountPeerList: api.Backend.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
|
||||
|
||||
EncodeJSON(api.Backend, 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.
|
||||
}
|
||||
|
||||
/*
|
||||
apiAccountInfo provides information about the current account.
|
||||
Request: GET /account/info
|
||||
Result: 200 with JSON structure apiResponsePeerSelf
|
||||
*/
|
||||
func (api *WebapiInstance) apiAccountInfo(w http.ResponseWriter, r *http.Request) {
|
||||
response := apiResponsePeerSelf{}
|
||||
response.NodeID = hex.EncodeToString(api.Backend.SelfNodeID())
|
||||
|
||||
_, publicKey := api.Backend.ExportPrivateKey()
|
||||
response.PeerID = hex.EncodeToString(publicKey.SerializeCompressed())
|
||||
|
||||
EncodeJSON(api.Backend, w, r, response)
|
||||
}
|
||||
|
||||
/*
|
||||
apiAccountDelete deletes the current account. The confirm parameter must include the user's choice.
|
||||
Request: GET /account/delete?confirm=[0 or 1]
|
||||
Result: 204 if the user choses not to delete the account
|
||||
|
||||
200 if successfully deleted
|
||||
*/
|
||||
func (api *WebapiInstance) apiAccountDelete(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
if confirm, _ := strconv.ParseBool(r.Form.Get("confirm")); !confirm {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
api.Backend.DeleteAccount()
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
/*
|
||||
apiStatusPeers returns the information about peers currently connected.
|
||||
The GeoIP information may not alawys be available, for example if the GeoIP file is not available or the mapping from IP address to location is not available.
|
||||
Peers that are connected only via local network will not have a geo location.
|
||||
|
||||
Request: GET /status/peers
|
||||
Result: 200 with JSON array apiResponsePeerInfo
|
||||
*/
|
||||
func (api *WebapiInstance) apiStatusPeers(w http.ResponseWriter, r *http.Request) {
|
||||
var peers []apiResponsePeerInfo
|
||||
|
||||
// query all nodes
|
||||
for _, peer := range api.Backend.PeerlistGet() {
|
||||
peerInfo := apiResponsePeerInfo{
|
||||
PeerID: peer.PublicKey.SerializeCompressed(),
|
||||
NodeID: peer.NodeID,
|
||||
UserAgent: peer.UserAgent,
|
||||
IsRoot: peer.IsRootPeer,
|
||||
BlockchainHeight: peer.BlockchainHeight,
|
||||
BlockchainVersion: peer.BlockchainVersion,
|
||||
}
|
||||
|
||||
if latitude, longitude, valid := api.Peer2GeoIP(peer); valid {
|
||||
peerInfo.GeoIP = fmt.Sprintf("%.4f", latitude) + "," + fmt.Sprintf("%.4f", longitude)
|
||||
}
|
||||
|
||||
peers = append(peers, peerInfo)
|
||||
}
|
||||
|
||||
EncodeJSON(api.Backend, w, r, peers)
|
||||
}
|
||||
|
||||
type apiResponsePeerInfo struct {
|
||||
PeerID []byte `json:"peerid"` // Peer ID. This is derived from the public in compressed form.
|
||||
NodeID []byte `json:"nodeid"` // Node ID. This is the blake3 hash of the peer ID and used in the DHT.
|
||||
GeoIP string `json:"geoip"` // GeoIP location as "Latitude,Longitude" CSV format. Empty if location not available.
|
||||
UserAgent string `json:"useragent"` // User Agent.
|
||||
IsRoot bool `json:"isroot"` // If the peer is a root peer.
|
||||
BlockchainHeight uint64 `json:"blockchainheight"` // Blockchain height
|
||||
BlockchainVersion uint64 `json:"blockchainversion"` // Blockchain version
|
||||
}
|
||||
|
||||
@@ -1,151 +1,151 @@
|
||||
/*
|
||||
File Name: Warehouse.go
|
||||
Copyright: 2021 Peernet Foundation s.r.o.
|
||||
Author: Peter Kleissner
|
||||
*/
|
||||
|
||||
package webapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/PeernetOfficial/core/warehouse"
|
||||
)
|
||||
|
||||
// WarehouseResult is the response to creating a new file in the warehouse
|
||||
type WarehouseResult struct {
|
||||
Status int `json:"status"` // See warehouse.StatusX.
|
||||
Hash []byte `json:"hash"` // Hash of the file.
|
||||
}
|
||||
|
||||
/*
|
||||
apiWarehouseCreateFile creates a file in the warehouse.
|
||||
|
||||
Request: POST /warehouse/create with raw data to create as new file
|
||||
Response: 200 with JSON structure WarehouseResult
|
||||
*/
|
||||
func (api *WebapiInstance) apiWarehouseCreateFile(w http.ResponseWriter, r *http.Request) {
|
||||
hash, status, err := api.backend.UserWarehouse.CreateFile(r.Body, 0)
|
||||
|
||||
if err != nil {
|
||||
api.backend.LogError("warehouse.CreateFile", "status %d error: %v", status, err)
|
||||
}
|
||||
|
||||
EncodeJSON(api.backend, w, r, WarehouseResult{Status: status, Hash: hash})
|
||||
}
|
||||
|
||||
/*
|
||||
apiWarehouseCreateFilePath creates a file in the warehouse by copying it from an existing file.
|
||||
Warning: An attacker could supply any local file using this function, put them into storage and read them! No input path verification or limitation is done.
|
||||
In the future the API should be secured using a random API key and setting the CORS header prohibiting regular browsers to access the API.
|
||||
|
||||
Request: GET /warehouse/create/path?path=[target path on disk]
|
||||
Response: 200 with JSON structure WarehouseResult
|
||||
*/
|
||||
func (api *WebapiInstance) apiWarehouseCreateFilePath(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
filePath := r.Form.Get("path")
|
||||
if filePath == "" {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
hash, status, err := api.backend.UserWarehouse.CreateFileFromPath(filePath)
|
||||
|
||||
if err != nil {
|
||||
api.backend.LogError("warehouse.CreateFile", "status %d error: %v", status, err)
|
||||
}
|
||||
|
||||
EncodeJSON(api.backend, w, r, WarehouseResult{Status: status, Hash: hash})
|
||||
}
|
||||
|
||||
/*
|
||||
apiWarehouseReadFile reads a file in the warehouse.
|
||||
|
||||
Request: GET /warehouse/read?hash=[hash]
|
||||
Optional parameters &offset=[file offset]&limit=[read limit in bytes]
|
||||
Response: 200 with the raw file data
|
||||
404 if file was not found
|
||||
500 in case of internal error opening the file
|
||||
*/
|
||||
func (api *WebapiInstance) apiWarehouseReadFile(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
hash, valid1 := DecodeBlake3Hash(r.Form.Get("hash"))
|
||||
if !valid1 {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
offset, _ := strconv.Atoi(r.Form.Get("offset"))
|
||||
limit, _ := strconv.Atoi(r.Form.Get("limit"))
|
||||
|
||||
status, bytesRead, err := api.backend.UserWarehouse.ReadFile(hash, int64(offset), int64(limit), w)
|
||||
|
||||
switch status {
|
||||
case warehouse.StatusFileNotFound:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
case warehouse.StatusInvalidHash, warehouse.StatusErrorOpenFile, warehouse.StatusErrorSeekFile:
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
// Cannot catch warehouse.StatusErrorReadFile since data may have been already returned.
|
||||
// In the future a special header indicating the expected file length could be sent (would require a callback in ReadFile), although the caller should already know the file size based on metadata.
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
api.backend.LogError("warehouse.ReadFile", "status %d read %d error: %v", status, bytesRead, err)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
apiWarehouseDeleteFile deletes a file in the warehouse.
|
||||
|
||||
Request: GET /warehouse/delete?hash=[hash]
|
||||
Response: 200 with JSON structure WarehouseResult
|
||||
*/
|
||||
func (api *WebapiInstance) apiWarehouseDeleteFile(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
hash, valid1 := DecodeBlake3Hash(r.Form.Get("hash"))
|
||||
if !valid1 {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
status, err := api.backend.UserWarehouse.DeleteFile(hash)
|
||||
|
||||
if err != nil {
|
||||
api.backend.LogError("warehouse.DeleteFile", "status %d error: %v", status, err)
|
||||
}
|
||||
|
||||
EncodeJSON(api.backend, w, r, WarehouseResult{Status: status, Hash: hash})
|
||||
}
|
||||
|
||||
/*
|
||||
apiWarehouseReadFilePath reads a file from the warehouse and stores it to the target file. It fails with StatusErrorTargetExists if the target file already exists.
|
||||
The path must include the full directory and file name.
|
||||
|
||||
Request: GET /warehouse/read/path?hash=[hash]&path=[target path on disk]
|
||||
Optional parameters &offset=[file offset]&limit=[read limit in bytes]
|
||||
Response: 200 with JSON structure WarehouseResult
|
||||
*/
|
||||
func (api *WebapiInstance) apiWarehouseReadFilePath(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
hash, valid1 := DecodeBlake3Hash(r.Form.Get("hash"))
|
||||
if !valid1 {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
targetFile := r.Form.Get("path")
|
||||
offset, _ := strconv.Atoi(r.Form.Get("offset"))
|
||||
limit, _ := strconv.Atoi(r.Form.Get("limit"))
|
||||
|
||||
status, bytesRead, err := api.backend.UserWarehouse.ReadFileToDisk(hash, int64(offset), int64(limit), targetFile)
|
||||
|
||||
if err != nil {
|
||||
api.backend.LogError("warehouse.ReadFileToDisk", "status %d read %d error: %v", status, bytesRead, err)
|
||||
}
|
||||
|
||||
EncodeJSON(api.backend, w, r, WarehouseResult{Status: status, Hash: hash})
|
||||
}
|
||||
/*
|
||||
File Name: Warehouse.go
|
||||
Copyright: 2021 Peernet Foundation s.r.o.
|
||||
Author: Peter Kleissner
|
||||
*/
|
||||
|
||||
package webapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/PeernetOfficial/core/warehouse"
|
||||
)
|
||||
|
||||
// WarehouseResult is the response to creating a new file in the warehouse
|
||||
type WarehouseResult struct {
|
||||
Status int `json:"status"` // See warehouse.StatusX.
|
||||
Hash []byte `json:"hash"` // Hash of the file.
|
||||
}
|
||||
|
||||
/*
|
||||
apiWarehouseCreateFile creates a file in the warehouse.
|
||||
|
||||
Request: POST /warehouse/create with raw data to create as new file
|
||||
Response: 200 with JSON structure WarehouseResult
|
||||
*/
|
||||
func (api *WebapiInstance) apiWarehouseCreateFile(w http.ResponseWriter, r *http.Request) {
|
||||
hash, status, err := api.Backend.UserWarehouse.CreateFile(r.Body, 0)
|
||||
|
||||
if err != nil {
|
||||
api.Backend.LogError("warehouse.CreateFile", "status %d error: %v", status, err)
|
||||
}
|
||||
|
||||
EncodeJSON(api.Backend, w, r, WarehouseResult{Status: status, Hash: hash})
|
||||
}
|
||||
|
||||
/*
|
||||
apiWarehouseCreateFilePath creates a file in the warehouse by copying it from an existing file.
|
||||
Warning: An attacker could supply any local file using this function, put them into storage and read them! No input path verification or limitation is done.
|
||||
In the future the API should be secured using a random API key and setting the CORS header prohibiting regular browsers to access the API.
|
||||
|
||||
Request: GET /warehouse/create/path?path=[target path on disk]
|
||||
Response: 200 with JSON structure WarehouseResult
|
||||
*/
|
||||
func (api *WebapiInstance) apiWarehouseCreateFilePath(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
filePath := r.Form.Get("path")
|
||||
if filePath == "" {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
hash, status, err := api.Backend.UserWarehouse.CreateFileFromPath(filePath)
|
||||
|
||||
if err != nil {
|
||||
api.Backend.LogError("warehouse.CreateFile", "status %d error: %v", status, err)
|
||||
}
|
||||
|
||||
EncodeJSON(api.Backend, w, r, WarehouseResult{Status: status, Hash: hash})
|
||||
}
|
||||
|
||||
/*
|
||||
apiWarehouseReadFile reads a file in the warehouse.
|
||||
|
||||
Request: GET /warehouse/read?hash=[hash]
|
||||
Optional parameters &offset=[file offset]&limit=[read limit in bytes]
|
||||
Response: 200 with the raw file data
|
||||
404 if file was not found
|
||||
500 in case of internal error opening the file
|
||||
*/
|
||||
func (api *WebapiInstance) apiWarehouseReadFile(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
hash, valid1 := DecodeBlake3Hash(r.Form.Get("hash"))
|
||||
if !valid1 {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
offset, _ := strconv.Atoi(r.Form.Get("offset"))
|
||||
limit, _ := strconv.Atoi(r.Form.Get("limit"))
|
||||
|
||||
status, bytesRead, err := api.Backend.UserWarehouse.ReadFile(hash, int64(offset), int64(limit), w)
|
||||
|
||||
switch status {
|
||||
case warehouse.StatusFileNotFound:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
case warehouse.StatusInvalidHash, warehouse.StatusErrorOpenFile, warehouse.StatusErrorSeekFile:
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
// Cannot catch warehouse.StatusErrorReadFile since data may have been already returned.
|
||||
// In the future a special header indicating the expected file length could be sent (would require a callback in ReadFile), although the caller should already know the file size based on metadata.
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
api.Backend.LogError("warehouse.ReadFile", "status %d read %d error: %v", status, bytesRead, err)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
apiWarehouseDeleteFile deletes a file in the warehouse.
|
||||
|
||||
Request: GET /warehouse/delete?hash=[hash]
|
||||
Response: 200 with JSON structure WarehouseResult
|
||||
*/
|
||||
func (api *WebapiInstance) apiWarehouseDeleteFile(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
hash, valid1 := DecodeBlake3Hash(r.Form.Get("hash"))
|
||||
if !valid1 {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
status, err := api.Backend.UserWarehouse.DeleteFile(hash)
|
||||
|
||||
if err != nil {
|
||||
api.Backend.LogError("warehouse.DeleteFile", "status %d error: %v", status, err)
|
||||
}
|
||||
|
||||
EncodeJSON(api.Backend, w, r, WarehouseResult{Status: status, Hash: hash})
|
||||
}
|
||||
|
||||
/*
|
||||
apiWarehouseReadFilePath reads a file from the warehouse and stores it to the target file. It fails with StatusErrorTargetExists if the target file already exists.
|
||||
The path must include the full directory and file name.
|
||||
|
||||
Request: GET /warehouse/read/path?hash=[hash]&path=[target path on disk]
|
||||
Optional parameters &offset=[file offset]&limit=[read limit in bytes]
|
||||
Response: 200 with JSON structure WarehouseResult
|
||||
*/
|
||||
func (api *WebapiInstance) apiWarehouseReadFilePath(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
hash, valid1 := DecodeBlake3Hash(r.Form.Get("hash"))
|
||||
if !valid1 {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
targetFile := r.Form.Get("path")
|
||||
offset, _ := strconv.Atoi(r.Form.Get("offset"))
|
||||
limit, _ := strconv.Atoi(r.Form.Get("limit"))
|
||||
|
||||
status, bytesRead, err := api.Backend.UserWarehouse.ReadFileToDisk(hash, int64(offset), int64(limit), targetFile)
|
||||
|
||||
if err != nil {
|
||||
api.Backend.LogError("warehouse.ReadFileToDisk", "status %d read %d error: %v", status, bytesRead, err)
|
||||
}
|
||||
|
||||
EncodeJSON(api.Backend, w, r, WarehouseResult{Status: status, Hash: hash})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user