pushed download gateway with information about the file metadata
This commit is contained in:
333
Gateway.go
333
Gateway.go
@@ -7,208 +7,237 @@ Author: Peter Kleissner
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
"crypto/tls"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/PeernetOfficial/core"
|
||||
"github.com/PeernetOfficial/core/btcec"
|
||||
"github.com/PeernetOfficial/core/webapi"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/PeernetOfficial/core"
|
||||
"github.com/PeernetOfficial/core/btcec"
|
||||
"github.com/PeernetOfficial/core/webapi"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func startWebGateway(backend *core.Backend) {
|
||||
router := mux.NewRouter()
|
||||
router.PathPrefix("/").Handler(http.HandlerFunc(webGatewayHandler(backend))).Methods("GET")
|
||||
router := mux.NewRouter()
|
||||
|
||||
for _, listen := range config.WebListen {
|
||||
go startWebServer(backend, listen, config.WebUseSSL, config.WebCertificateFile, config.WebCertificateKey, router, "Web Listen", parseDuration(config.WebTimeoutRead), parseDuration(config.WebTimeoutWrite))
|
||||
}
|
||||
router.PathPrefix("/static/").Handler(
|
||||
http.StripPrefix("/static/",
|
||||
http.FileServer(
|
||||
http.Dir("./html"),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
if config.Redirect80 != "" {
|
||||
go webRedirect80(config.Redirect80)
|
||||
}
|
||||
router.PathPrefix("/static").Handler(http.FileServer(http.Dir("./html/")))
|
||||
router.PathPrefix("/").Handler(http.HandlerFunc(webGatewayHandler(backend))).Methods("GET")
|
||||
|
||||
// wait forever
|
||||
select {}
|
||||
for _, listen := range config.WebListen {
|
||||
go startWebServer(backend, listen, config.WebUseSSL, config.WebCertificateFile, config.WebCertificateKey, router, "Web Listen", parseDuration(config.WebTimeoutRead), parseDuration(config.WebTimeoutWrite))
|
||||
}
|
||||
|
||||
if config.Redirect80 != "" {
|
||||
go webRedirect80(config.Redirect80)
|
||||
}
|
||||
|
||||
// wait forever
|
||||
select {}
|
||||
}
|
||||
|
||||
// startWebServer starts the web-server and may block forever
|
||||
func startWebServer(backend *core.Backend, WebListen string, UseSSL bool, CertificateFile, CertificateKey string, Handler http.Handler, Info string, ReadTimeout, WriteTimeout time.Duration) {
|
||||
//func startWebServer(backend *core.Backend, webListen string, useSSL bool, certificateFile, certificateKey string, server *http.Server) {
|
||||
backend.LogError("startWebServer", "Web Gateway to listen on '%s'", WebListen)
|
||||
//func startWebServer(backend *core.Backend, webListen string, useSSL bool, certificateFile, certificateKey string, server *http.Server) {
|
||||
backend.LogError("startWebServer", "Web Gateway to listen on '%s'", WebListen)
|
||||
|
||||
tlsConfig := &tls.Config{MinVersion: tls.VersionTLS12} // for security reasons disable TLS 1.0/1.1
|
||||
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,
|
||||
}
|
||||
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("startWebServer", "Error listening on '%s': %v\n", WebListen, err)
|
||||
}
|
||||
} else {
|
||||
// HTTP
|
||||
if err := server.ListenAndServe(); err != nil {
|
||||
backend.LogError("startWebServer", "Error listening on '%s': %v\n", WebListen, err)
|
||||
}
|
||||
}
|
||||
if UseSSL {
|
||||
// HTTPS
|
||||
if err := server.ListenAndServeTLS(CertificateFile, CertificateKey); err != nil {
|
||||
backend.LogError("startWebServer", "Error listening on '%s': %v\n", WebListen, err)
|
||||
}
|
||||
} else {
|
||||
// HTTP
|
||||
if err := server.ListenAndServe(); err != nil {
|
||||
backend.LogError("startWebServer", "Error listening on '%s': %v\n", WebListen, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parseDuration is the same as time.ParseDuration without returning an error. Valid units are ms, s, m, h. For example "10s".
|
||||
func parseDuration(input string) (result time.Duration) {
|
||||
result, _ = time.ParseDuration(input)
|
||||
return
|
||||
result, _ = time.ParseDuration(input)
|
||||
return
|
||||
}
|
||||
|
||||
func redirect(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "https://"+r.Host+r.URL.String(), http.StatusMovedPermanently)
|
||||
http.Redirect(w, r, "https://"+r.Host+r.URL.String(), http.StatusMovedPermanently)
|
||||
}
|
||||
|
||||
func webRedirect80(listen80 string) {
|
||||
// redirect HTTP -> HTTPS
|
||||
http.ListenAndServe(net.JoinHostPort(listen80, "80"), http.HandlerFunc(redirect))
|
||||
// redirect HTTP -> HTTPS
|
||||
http.ListenAndServe(net.JoinHostPort(listen80, "80"), http.HandlerFunc(redirect))
|
||||
}
|
||||
|
||||
func webGatewayHandler(backend *core.Backend) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// For security and simplicity reasons, below paths are hard-coded.
|
||||
// Using arbitrary user input is an avoidable security risk here.
|
||||
switch r.URL.Path {
|
||||
case "/", "/index.html":
|
||||
http.ServeFile(w, r, path.Join(config.WebFiles, "index.html"))
|
||||
return
|
||||
case "/favicon.ico":
|
||||
http.ServeFile(w, r, path.Join(config.WebFiles, "favicon.ico"))
|
||||
return
|
||||
}
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// For security and simplicity reasons, below paths are hard-coded.
|
||||
// Using arbitrary user input is an avoidable security risk here.
|
||||
switch r.URL.Path {
|
||||
case "/", "/index.html":
|
||||
http.ServeFile(w, r, path.Join(config.WebFiles, "index.html"))
|
||||
return
|
||||
case "/favicon.ico":
|
||||
http.ServeFile(w, r, path.Join(config.WebFiles, "favicon.ico"))
|
||||
return
|
||||
case "/download":
|
||||
http.ServeFile(w, r, path.Join(config.WebFiles, "download.html"))
|
||||
return
|
||||
}
|
||||
|
||||
// Assume it is in the format "/[blockchain public key]/[file hash]".
|
||||
// Assume it is in the format "/[blockchain public key]/[file hash]".
|
||||
|
||||
// Remove slash prefix and suffix.
|
||||
pathA := strings.TrimPrefix(strings.TrimSuffix(r.URL.Path, "/"), "/")
|
||||
// Remove slash prefix and suffix.
|
||||
pathA := strings.TrimPrefix(strings.TrimSuffix(r.URL.Path, "/"), "/")
|
||||
|
||||
pathParts := strings.Split(pathA, "/")
|
||||
if len(pathParts) != 1 && len(pathParts) != 2 {
|
||||
http.Error(w, "404 not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
pathParts := strings.Split(pathA, "/")
|
||||
if len(pathParts) != 1 && len(pathParts) != 2 {
|
||||
http.Error(w, "404 not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Default timeout for connection is 10 seconds. This will be an optional parameter in the future.
|
||||
timeout := 10 * time.Second
|
||||
// Default timeout for connection is 10 seconds. This will be an optional parameter in the future.
|
||||
timeout := 10 * time.Second
|
||||
|
||||
// First part must be the public key as peer ID or node ID, hex encoded. Form: "/[blockchain public key]"
|
||||
nodeIDA := pathParts[0]
|
||||
nodeID, validNodeID := webapi.DecodeBlake3Hash(nodeIDA)
|
||||
publicKey, errPK := core.PublicKeyFromPeerID(nodeIDA)
|
||||
// First part must be the public key as peer ID or node ID, hex encoded. Form: "/[blockchain public key]"
|
||||
nodeIDA := pathParts[0]
|
||||
nodeID, validNodeID := webapi.DecodeBlake3Hash(nodeIDA)
|
||||
publicKey, errPK := core.PublicKeyFromPeerID(nodeIDA)
|
||||
|
||||
if !validNodeID && errPK != nil {
|
||||
http.Error(w, "404 not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if !validNodeID {
|
||||
nodeID = []byte{}
|
||||
}
|
||||
if !validNodeID && errPK != nil {
|
||||
http.Error(w, "404 not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if !validNodeID {
|
||||
nodeID = []byte{}
|
||||
}
|
||||
|
||||
// Check if a blockchain is requested.
|
||||
// The format must be "/[blockchain public key]". Part 1 = hex encoding, peer ID or node ID.
|
||||
if len(pathParts) == 1 {
|
||||
webGatewayShowBlockchain(backend, w, r, nodeID, publicKey, timeout)
|
||||
} else if len(pathParts) == 2 {
|
||||
// Check if a specific file on a specific blockchain is requested.
|
||||
// The format must be "/[blockchain public key]/[file hash]". Part 2 = hex encoding, blake3 hash.
|
||||
hash, valid := webapi.DecodeBlake3Hash(pathParts[1])
|
||||
if !valid {
|
||||
http.Error(w, "Invalid file hash.", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// Check if a blockchain is requested.
|
||||
// The format must be "/[blockchain public key]". Part 1 = hex encoding, peer ID or node ID.
|
||||
if len(pathParts) == 1 {
|
||||
webGatewayShowBlockchain(backend, w, r, nodeID, publicKey, timeout)
|
||||
} else if len(pathParts) == 2 {
|
||||
// Check if a specific file on a specific blockchain is requested.
|
||||
// The format must be "/[blockchain public key]/[file hash]". Part 2 = hex encoding, blake3 hash.
|
||||
hash, valid := webapi.DecodeBlake3Hash(pathParts[1])
|
||||
if !valid {
|
||||
http.Error(w, "Invalid file hash.", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
webGatewayShowFile(backend, w, r, nodeID, publicKey, hash, timeout)
|
||||
}
|
||||
webGatewayShowFile(backend, w, r, nodeID, publicKey, hash, timeout)
|
||||
}
|
||||
|
||||
// Check if an arbitrary directory on a specific blockchain is requested.
|
||||
// Directories are identified by name.
|
||||
// TODO
|
||||
// Check if an arbitrary directory on a specific blockchain is requested.
|
||||
// Directories are identified by name.
|
||||
// TODO
|
||||
|
||||
//http.Error(w, "test handler", http.StatusOK)
|
||||
}
|
||||
//http.Error(w, "test handler", http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func webGatewayShowBlockchain(backend *core.Backend, w http.ResponseWriter, r *http.Request, nodeID []byte, publicKey *btcec.PublicKey, timeout time.Duration) {
|
||||
var err error
|
||||
var peer *core.PeerInfo
|
||||
if len(nodeID) != 0 {
|
||||
peer, err = webapi.PeerConnectNode(backend, nodeID, timeout)
|
||||
} else {
|
||||
peer, err = webapi.PeerConnectPublicKey(backend, publicKey, timeout)
|
||||
}
|
||||
if err != nil {
|
||||
http.Error(w, "Could not connect to remote peer. 😢", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
var err error
|
||||
var peer *core.PeerInfo
|
||||
if len(nodeID) != 0 {
|
||||
peer, err = webapi.PeerConnectNode(backend, nodeID, timeout)
|
||||
} else {
|
||||
peer, err = webapi.PeerConnectPublicKey(backend, publicKey, timeout)
|
||||
}
|
||||
if err != nil {
|
||||
http.Error(w, "Could not connect to remote peer. 😢", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// connection established!
|
||||
text := fmt.Sprintf("Peer %s blockchain height %d version %d\nUser Agent: %s\n", hex.EncodeToString(peer.NodeID), peer.BlockchainHeight, peer.BlockchainVersion, peer.UserAgent)
|
||||
http.Error(w, text, http.StatusOK)
|
||||
// connection established!
|
||||
text := fmt.Sprintf("Peer %s blockchain height %d version %d\nUser Agent: %s\n", hex.EncodeToString(peer.NodeID), peer.BlockchainHeight, peer.BlockchainVersion, peer.UserAgent)
|
||||
http.Error(w, text, http.StatusOK)
|
||||
}
|
||||
|
||||
// MetaDataResponse meta data struct
|
||||
type MetaDataResponse struct {
|
||||
Hash []byte
|
||||
Size uint64
|
||||
}
|
||||
|
||||
func webGatewayShowFile(backend *core.Backend, w http.ResponseWriter, r *http.Request, nodeID []byte, publicKey *btcec.PublicKey, fileHash []byte, timeout time.Duration) {
|
||||
var err error
|
||||
var peer *core.PeerInfo
|
||||
if len(nodeID) != 0 {
|
||||
peer, err = webapi.PeerConnectNode(backend, nodeID, timeout)
|
||||
} else {
|
||||
peer, err = webapi.PeerConnectPublicKey(backend, publicKey, timeout)
|
||||
}
|
||||
if err != nil {
|
||||
http.Error(w, "Could not connect to remote peer. 😢", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
var err error
|
||||
var peer *core.PeerInfo
|
||||
if len(nodeID) != 0 {
|
||||
peer, err = webapi.PeerConnectNode(backend, nodeID, timeout)
|
||||
} else {
|
||||
peer, err = webapi.PeerConnectPublicKey(backend, publicKey, timeout)
|
||||
}
|
||||
if err != nil {
|
||||
http.Error(w, "Could not connect to remote peer. 😢", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Todo: Try webapi.serveFileFromWarehouse
|
||||
// Todo: Try webapi.serveFileFromWarehouse
|
||||
|
||||
// Todo: Cache.
|
||||
// Todo: Cache.
|
||||
|
||||
offset := 0
|
||||
limit := 0
|
||||
offset := 0
|
||||
limit := 0
|
||||
|
||||
// start the reader
|
||||
//reader, fileSize, transferSize, err := webapi.FileStartReader(peer, fileHash, uint64(offset), uint64(limit), r.Context().Done())
|
||||
reader, _, transferSize, err := webapi.FileStartReader(peer, fileHash, uint64(offset), uint64(limit), r.Context().Done())
|
||||
if reader != nil {
|
||||
defer reader.Close()
|
||||
}
|
||||
if err != nil || reader == nil {
|
||||
http.Error(w, "File not found.", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
// start the reader
|
||||
//reader, fileSize, transferSize, err := webapi.FileStartReader(peer, fileHash, uint64(offset), uint64(limit), r.Context().Done())
|
||||
reader, fileSize, transferSize, err := webapi.FileStartReader(peer, fileHash, uint64(offset), uint64(limit), r.Context().Done())
|
||||
if reader != nil {
|
||||
defer reader.Close()
|
||||
}
|
||||
if err != nil || reader == nil {
|
||||
http.Error(w, "File not found.", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// if get request ?download=true
|
||||
// trigger download of the file
|
||||
download := r.URL.Query().Get("download")
|
||||
if download == "true" {
|
||||
io.Copy(w, io.LimitReader(reader, int64(transferSize)))
|
||||
return
|
||||
} else {
|
||||
// if get request ?download=true
|
||||
// trigger download of the file
|
||||
download := r.URL.Query().Get("download")
|
||||
metadata := r.URL.Query().Get("metadata")
|
||||
if download == "true" {
|
||||
// JSON response with meta data instead
|
||||
io.Copy(w, io.LimitReader(reader, int64(transferSize)))
|
||||
return
|
||||
} else if metadata == "true" {
|
||||
// JSON response of the file meta-data
|
||||
var responseMetadata MetaDataResponse
|
||||
responseMetadata.Size = fileSize
|
||||
responseMetadata.Hash = fileHash
|
||||
|
||||
}
|
||||
webapi.EncodeJSON(backend, w, r, responseMetadata)
|
||||
return
|
||||
}
|
||||
|
||||
// set the right headers
|
||||
//webapi.setContentLengthRangeHeader(w, uint64(offset), transferSize, fileSize, ranges)
|
||||
// set the right headers
|
||||
//webapi.setContentLengthRangeHeader(w, uint64(offset), transferSize, fileSize, ranges)
|
||||
|
||||
// Start sending the data!
|
||||
io.Copy(w, io.LimitReader(reader, int64(transferSize)))
|
||||
http.ServeFile(w, r, path.Join(config.WebFiles, "download.html"))
|
||||
|
||||
// Start sending the data!
|
||||
//io.Copy(w, io.LimitReader(reader, int64(transferSize)))
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||
|
||||
<link rel="stylesheet" href="static/css/style.css">
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
|
||||
</head>
|
||||
|
||||
@@ -23,10 +23,10 @@
|
||||
<div class="text-wrap p-4 p-lg-5 text-center d-flex align-items-center order-md-last">
|
||||
<div class="text w-100">
|
||||
<h2 class="Peernetblue">Download this file from Peernet</h2>
|
||||
<a href="#" style="color:#3ac4e2">Download from the browser</a>
|
||||
<a href="#" style="color:#3ac4e2" id="Download">Download from the browser</a>
|
||||
<br>
|
||||
<br>
|
||||
<a href="#" class="btn btn-white btn-outline-white">Download from Peernet Browser</a>
|
||||
<a href="https://peernet.org" class="btn btn-white btn-outline-white">Download from Peernet Browser</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="login-wrap p-4 p-lg-5">
|
||||
@@ -41,15 +41,15 @@
|
||||
<form action="#" class="signin-form">
|
||||
<div class="form-group mb-3">
|
||||
<label class="label" for="name">File Name:</label>
|
||||
<p>Test.txt</p>
|
||||
<p id="FileName">Test.txt</p>
|
||||
</div>
|
||||
<div class="form-group mb-3">
|
||||
<label class="label" for="name">File Size:</label>
|
||||
<p>15 Mb</p>
|
||||
<p id="FileSize">15 Mb</p>
|
||||
</div>
|
||||
<div class="form-group mb-3">
|
||||
<label class="label" for="name">File Hash:</label>
|
||||
<p>blablablablabla</p>
|
||||
<p id="FileHash">blablablablabla</p>
|
||||
</div>
|
||||
<!-- <div class="form-group mb-3">
|
||||
<label class="label" for="password">Password</label>
|
||||
@@ -78,10 +78,41 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script src="static/js/jquery.min.js"></script>
|
||||
<script src="static/js/popper.js"></script>
|
||||
<script src="static/js/bootstrap.min.js"></script>
|
||||
<script src="static/js/main.js"></script>
|
||||
<script src="/static/js/jquery.min.js"></script>
|
||||
<script src="/static/js/popper.js"></script>
|
||||
<script src="/static/js/bootstrap.min.js"></script>
|
||||
<script src="/static/js/main.js"></script>
|
||||
|
||||
<script>
|
||||
// call a certain api and parse JSON and populate the
|
||||
// the data in the HTML page
|
||||
|
||||
var FileSize = document.getElementById('FileSize');
|
||||
var FileHash = document.getElementById('FileHash');
|
||||
var Download = document.getElementById('Download');
|
||||
|
||||
var promise1 = new Promise(function(resolve, reject) {
|
||||
var xhr = new XMLHttpRequest(),
|
||||
method = "GET",
|
||||
url = window.location + "?metadata=true";
|
||||
|
||||
xhr.open(method, url, true);
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState === 4 && xhr.status === 200) {
|
||||
resolve(JSON.parse(xhr.responseText));
|
||||
}
|
||||
};
|
||||
xhr.send();
|
||||
});
|
||||
|
||||
promise1.then(function(value) {
|
||||
FileSize.innerText = value.Size + " Bytes";
|
||||
FileHash.innerText = value.Hash;
|
||||
Download.href = window.location + "?download=true"
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user