diff --git a/Connection.go b/Connection.go index 9d08dc0..42fb5f5 100644 --- a/Connection.go +++ b/Connection.go @@ -86,6 +86,14 @@ func (peer *PeerInfo) GetConnections(active bool) (connections []*Connection) { return peer.connectionInactive } +// IsConnectionActive checks if the peer has an active connection that can be used to send and receive messages +func (peer *PeerInfo) IsConnectionActive() bool { + peer.RLock() + defer peer.RUnlock() + + return len(peer.connectionActive) > 0 +} + // IsConnectable checks if the peer is connectable to the given IP parameters. func (peer *PeerInfo) IsConnectable(allowLocal, allowIPv4, allowIPv6 bool) bool { peer.RLock() diff --git a/Peer ID.go b/Peer ID.go index e674ee1..21aea05 100644 --- a/Peer ID.go +++ b/Peer ID.go @@ -8,6 +8,7 @@ package core import ( "encoding/hex" + "errors" "math/rand" "net" "os" @@ -281,14 +282,12 @@ func DeleteAccount() { saveConfig() } -// PublicKeyFromPeerID decodes the peer ID (hex encoded) into a public key. Returns nil if invalid. -func PublicKeyFromPeerID(peerID string) (publicKey *btcec.PublicKey) { +// PublicKeyFromPeerID decodes the peer ID (hex encoded) into a public key. +func PublicKeyFromPeerID(peerID string) (publicKey *btcec.PublicKey, err error) { hash, err := hex.DecodeString(peerID) if err != nil || len(hash) != 33 { - return nil + return nil, errors.New("invalid peer ID length") } - publicKey, _ = btcec.ParsePubKey(hash, btcec.S256()) - - return publicKey + return btcec.ParsePubKey(hash, btcec.S256()) } diff --git a/webapi/API.go b/webapi/API.go index 2e9adf6..bf46a7a 100644 --- a/webapi/API.go +++ b/webapi/API.go @@ -68,6 +68,7 @@ func Start(ListenAddresses []string, UseSSL bool, CertificateFile, CertificateKe Router.HandleFunc("/warehouse/create/path", apiWarehouseCreateFilePath).Methods("GET") Router.HandleFunc("/warehouse/read", apiWarehouseReadFile).Methods("GET") Router.HandleFunc("/warehouse/delete", apiWarehouseDeleteFile).Methods("GET") + Router.HandleFunc("/file/read", apiFileRead).Methods("GET") for _, listen := range ListenAddresses { go startWebServer(listen, UseSSL, CertificateFile, CertificateKey, Router, "API", TimeoutRead, TimeoutWrite) diff --git a/webapi/File IO.go b/webapi/File IO.go new file mode 100644 index 0000000..9365eaa --- /dev/null +++ b/webapi/File IO.go @@ -0,0 +1,166 @@ +/* +File Name: File IO.go +Copyright: 2021 Peernet Foundation s.r.o. +Author: Peter Kleissner +*/ + +package webapi + +import ( + "errors" + "fmt" + "io" + "net/http" + "strconv" + "time" + + "github.com/PeernetOfficial/core" + "github.com/PeernetOfficial/core/protocol" + "github.com/btcsuite/btcd/btcec" +) + +/* +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 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 + } + + // try connecting via node ID or peer ID? + var peer *core.PeerInfo + + if valid2 { + peer, err = PeerConnectNode(nodeID, timeout) + } else if err3 == nil { + peer, err = PeerConnectPublicKey(publicKey, timeout) + } + if err != nil { + w.WriteHeader(http.StatusBadGateway) + return + } + + reader, fileSize, transferSize, err := FileStartReader(peer, fileHash, uint64(offset), uint64(limit)) + if reader != nil { + defer reader.Close() + } + if err != nil || reader == nil { + w.WriteHeader(http.StatusNotFound) + return + } + fmt.Printf("SUCCESS file size is %d transfer size is %d\n", fileSize, transferSize) + + // Set the Content-Length header, always to the actual size of transferred data. + w.Header().Set("Content-Length", strconv.FormatUint(transferSize, 10)) + + // Set the Content-Range header if needed. + if len(ranges) == 1 { + w.Header().Set("Content-Range", "bytes "+strconv.Itoa(offset)+"-"+strconv.Itoa(offset+int(transferSize)-1)+"/"+strconv.FormatUint(fileSize, 10)) + w.WriteHeader(http.StatusPartialContent) + } else { + w.WriteHeader(http.StatusOK) + } + + // 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(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 = core.PeerlistLookup(publicKey); peer != nil { + return peer, nil + } + + // Try to connect via DHT. + nodeID := protocol.PublicKey2NodeID(publicKey) + if _, peer, _ = core.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(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, _ = core.FindNode(nodeID, timeout); peer != nil { + return peer, nil + } + + return nil, nil +} + +// FileStartReader providers a reader to a remote file. The reader must be closed by the caller. +// File Size is the full file size, regardless of the requested offset and limit. +// Transfer Size is the size in bytes that is actually going to be transferred. The reader should be closed after reading that amount. +func FileStartReader(peer *core.PeerInfo, hash []byte, offset, limit uint64) (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, err := peer.FileTransferRequestUDT(hash, offset, limit) + if err != nil { + return nil, 0, 0, err + } + + fileSize, transferSize, err = core.FileTransferReadHeaderUDT(udtConn) + if err != nil { + udtConn.Close() + return nil, 0, 0, err + } + + return udtConn, fileSize, transferSize, nil +} diff --git a/webapi/HTTP Range.go b/webapi/HTTP Range.go new file mode 100644 index 0000000..38ab0be --- /dev/null +++ b/webapi/HTTP Range.go @@ -0,0 +1,94 @@ +/* +File Name: HTTP Range.go +Copyright: 2021 Peernet Foundation s.r.o. +Author: Peter Kleissner +*/ + +package webapi + +import ( + "errors" + "net/textproto" + "strconv" + "strings" +) + +// Fork from https://golang.org/src/net/http/fs.go + +// HTTPRange represents an HTTP range +type HTTPRange struct { + start, length int +} + +// ParseRangeHeader parses a Range header string as per RFC 7233. +func ParseRangeHeader(s string, size int, noMultiRange bool) ([]HTTPRange, error) { + if s == "" { + return nil, nil // header not present + } + const b = "bytes=" + if !strings.HasPrefix(s, b) { + return nil, errors.New("invalid range") + } + var ranges []HTTPRange + for _, ra := range strings.Split(s[len(b):], ",") { + ra = textproto.TrimString(ra) + if ra == "" { + continue + } + i := strings.Index(ra, "-") + if i < 0 { + return nil, errors.New("invalid range") + } + start, end := textproto.TrimString(ra[:i]), textproto.TrimString(ra[i+1:]) + var r HTTPRange + if start == "" { + if size < 0 { + return nil, errors.New("range start relative to end not supported") + } + // If no start is specified, end specifies the + // range start relative to the end of the file. + i, err := strconv.ParseInt(end, 10, 64) + if err != nil { + return nil, errors.New("invalid range") + } + if int(i) > size { + i = int64(size) + } + r.start = size - int(i) + r.length = size - r.start + } else { + i, err := strconv.ParseInt(start, 10, 64) + if err != nil || i < 0 { + return nil, errors.New("invalid range") + } + if size > 0 && int(i) >= size { + // If the range begins after the size of the content, then it does not overlap. -> always return error. + return nil, errors.New("start out of range") + } + r.start = int(i) + if end == "" { + // If no end is specified, range extends to end of the file. + if size < 0 { + //return nil, errors.New("open range not supported") + r.length = -1 + } else { + r.length = size - r.start + } + } else { + i, err := strconv.ParseInt(end, 10, 64) + if err != nil || r.start > int(i) { + return nil, errors.New("invalid range") + } + if size > 0 && int(i) >= size { + i = int64(size - 1) + } + r.length = int(i) - r.start + 1 + } + } + ranges = append(ranges, r) + if noMultiRange && len(ranges) > 1 { + return nil, errors.New("multiple ranges not supported") + } + } + return ranges, nil +}