aboutsummaryrefslogblamecommitdiffstats
path: root/swarm/api/http/server.go
blob: ae113750ff1d4f213be29cac6fe774c35813bbcf (plain) (tree)






















                                                                                  
             


                  
                 



                                                
                                             
                                                   
                                                       
                            





















                                                                                 






                                                                       





                                                                    
                                                    



                                                                               







                                                                                  
                                              



                                                 
                                                                                      












                                                                                      
                                                                                                                                                                                                               




                                          
                                                            







                                                                           


                                                                                                                                                   





                                                   


                                                                                                                    


                                                     




                                                                                                         
                               
                                                                                  




















                                                                                                                       
                                                                                                                 

                                                                                                        
                                                                                                         













                                                                                                                
                                                                    

                                                                           
                                                                                                 








                                                                                                        



                                                                                      
                        





                                                                              
                                                                         











                                                                                         


                                             
 

                                                       
                                       
                                                                                                   



                                                                                             
                                                                         









                                                                       
                                                                                                                


                                                     
                                                                                            







                                                                                  
                                                                         

                                                                      
                                                                                                     













                                                                    
                                       
                                                                                                   



                                                                                             
                                                                                                                               












                                                                                                   
                                                                                                                     


                                                    
                                                                                                         














                                                            
                                                                                                                     
                               
                                                                                                










                                                     
                                                                                                   





                                            
// Copyright 2016 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.

/*
A simple http server interface to Swarm
*/
package http

import (
    "bytes"
    "fmt"
    "io"
    "net/http"
    "regexp"
    "strings"
    "sync"
    "time"

    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/log"
    "github.com/ethereum/go-ethereum/swarm/api"
    "github.com/ethereum/go-ethereum/swarm/storage"
    "github.com/rs/cors"
)

const (
    rawType = "application/octet-stream"
)

var (
    // accepted protocols: bzz (traditional), bzzi (immutable) and bzzr (raw)
    bzzPrefix       = regexp.MustCompile("^/+bzz[ir]?:/+")
    trailingSlashes = regexp.MustCompile("/+$")
    rootDocumentUri = regexp.MustCompile("^/+bzz[i]?:/+[^/]+$")
    // forever         = func() time.Time { return time.Unix(0, 0) }
    forever = time.Now
)

type sequentialReader struct {
    reader io.Reader
    pos    int64
    ahead  map[int64](chan bool)
    lock   sync.Mutex
}

// Server is the basic configuration needs for the HTTP server and also
// includes CORS settings.
type Server struct {
    Addr       string
    CorsString string
}

// browser API for registering bzz url scheme handlers:
// https://developer.mozilla.org/en/docs/Web-based_protocol_handlers
// electron (chromium) api for registering bzz url scheme handlers:
// https://github.com/atom/electron/blob/master/docs/api/protocol.md

// starts up http server
func StartHttpServer(api *api.Api, server *Server) {
    serveMux := http.NewServeMux()
    serveMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        handler(w, r, api)
    })
    var allowedOrigins []string
    for _, domain := range strings.Split(server.CorsString, ",") {
        allowedOrigins = append(allowedOrigins, strings.TrimSpace(domain))
    }
    c := cors.New(cors.Options{
        AllowedOrigins: allowedOrigins,
        AllowedMethods: []string{"POST", "GET", "DELETE", "PATCH", "PUT"},
        MaxAge:         600,
        AllowedHeaders: []string{"*"},
    })
    hdlr := c.Handler(serveMux)

    go http.ListenAndServe(server.Addr, hdlr)
    log.Info(fmt.Sprintf("Swarm HTTP proxy started on localhost:%s", server.Addr))
}

func handler(w http.ResponseWriter, r *http.Request, a *api.Api) {
    requestURL := r.URL
    // This is wrong
    //  if requestURL.Host == "" {
    //      var err error
    //      requestURL, err = url.Parse(r.Referer() + requestURL.String())
    //      if err != nil {
    //          http.Error(w, err.Error(), http.StatusBadRequest)
    //          return
    //      }
    //  }
    log.Debug(fmt.Sprintf("HTTP %s request URL: '%s', Host: '%s', Path: '%s', Referer: '%s', Accept: '%s'", r.Method, r.RequestURI, requestURL.Host, requestURL.Path, r.Referer(), r.Header.Get("Accept")))
    uri := requestURL.Path
    var raw, nameresolver bool
    var proto string

    // HTTP-based URL protocol handler
    log.Debug(fmt.Sprintf("BZZ request URI: '%s'", uri))

    path := bzzPrefix.ReplaceAllStringFunc(uri, func(p string) string {
        proto = p
        return ""
    })

    // protocol identification (ugly)
    if proto == "" {
        log.Error(fmt.Sprintf("[BZZ] Swarm: Protocol error in request `%s`.", uri))
        http.Error(w, "Invalid request URL: need access protocol (bzz:/, bzzr:/, bzzi:/) as first element in path.", http.StatusBadRequest)
        return
    }
    if len(proto) > 4 {
        raw = proto[1:5] == "bzzr"
        nameresolver = proto[1:5] != "bzzi"
    }

    log.Debug("", "msg", log.Lazy{Fn: func() string {
        return fmt.Sprintf("[BZZ] Swarm: %s request over protocol %s '%s' received.", r.Method, proto, path)
    }})

    switch {
    case r.Method == "POST" || r.Method == "PUT":
        if r.Header.Get("content-length") == "" {
            http.Error(w, "Missing Content-Length header in request.", http.StatusBadRequest)
            return
        }
        key, err := a.Store(io.LimitReader(r.Body, r.ContentLength), r.ContentLength, nil)
        if err == nil {
            log.Debug(fmt.Sprintf("Content for %v stored", key.Log()))
        } else {
            http.Error(w, err.Error(), http.StatusBadRequest)
            return
        }
        if r.Method == "POST" {
            if raw {
                w.Header().Set("Content-Type", "text/plain")
                http.ServeContent(w, r, "", time.Now(), bytes.NewReader([]byte(common.Bytes2Hex(key))))
            } else {
                http.Error(w, "No POST to "+uri+" allowed.", http.StatusBadRequest)
                return
            }
        } else {
            // PUT
            if raw {
                http.Error(w, "No PUT to /raw allowed.", http.StatusBadRequest)
                return
            } else {
                path = api.RegularSlashes(path)
                mime := r.Header.Get("Content-Type")
                // TODO proper root hash separation
                log.Debug(fmt.Sprintf("Modify '%s' to store %v as '%s'.", path, key.Log(), mime))
                newKey, err := a.Modify(path, common.Bytes2Hex(key), mime, nameresolver)
                if err == nil {
                    log.Debug(fmt.Sprintf("Swarm replaced manifest by '%s'", newKey))
                    w.Header().Set("Content-Type", "text/plain")
                    http.ServeContent(w, r, "", time.Now(), bytes.NewReader([]byte(newKey)))
                } else {
                    http.Error(w, "PUT to "+path+"failed.", http.StatusBadRequest)
                    return
                }
            }
        }
    case r.Method == "DELETE":
        if raw {
            http.Error(w, "No DELETE to /raw allowed.", http.StatusBadRequest)
            return
        } else {
            path = api.RegularSlashes(path)
            log.Debug(fmt.Sprintf("Delete '%s'.", path))
            newKey, err := a.Modify(path, "", "", nameresolver)
            if err == nil {
                log.Debug(fmt.Sprintf("Swarm replaced manifest by '%s'", newKey))
                w.Header().Set("Content-Type", "text/plain")
                http.ServeContent(w, r, "", time.Now(), bytes.NewReader([]byte(newKey)))
            } else {
                http.Error(w, "DELETE to "+path+"failed.", http.StatusBadRequest)
                return
            }
        }
    case r.Method == "GET" || r.Method == "HEAD":
        path = trailingSlashes.ReplaceAllString(path, "")
        if path == "" {
            http.Error(w, "Empty path not allowed", http.StatusBadRequest)
            return
        }
        if raw {
            var reader storage.LazySectionReader
            parsedurl, _ := api.Parse(path)

            if parsedurl == path {
                key, err := a.Resolve(parsedurl, nameresolver)
                if err != nil {
                    log.Error(fmt.Sprintf("%v", err))
                    http.Error(w, err.Error(), http.StatusBadRequest)
                    return
                }
                reader = a.Retrieve(key)
            } else {
                var status int
                readertmp, _, status, err := a.Get(path, nameresolver)
                if err != nil {
                    http.Error(w, err.Error(), status)
                    return
                }
                reader = readertmp
            }

            // retrieving content

            quitC := make(chan bool)
            size, err := reader.Size(quitC)
            if err != nil {
                log.Debug(fmt.Sprintf("Could not determine size: %v", err.Error()))
                //An error on call to Size means we don't have the root chunk
                http.Error(w, err.Error(), http.StatusNotFound)
                return
            }
            log.Debug(fmt.Sprintf("Reading %d bytes.", size))

            // setting mime type
            qv := requestURL.Query()
            mimeType := qv.Get("content_type")
            if mimeType == "" {
                mimeType = rawType
            }

            w.Header().Set("Content-Type", mimeType)
            http.ServeContent(w, r, uri, forever(), reader)
            log.Debug(fmt.Sprintf("Serve raw content '%s' (%d bytes) as '%s'", uri, size, mimeType))

            // retrieve path via manifest
        } else {
            log.Debug(fmt.Sprintf("Structured GET request '%s' received.", uri))
            // add trailing slash, if missing
            if rootDocumentUri.MatchString(uri) {
                http.Redirect(w, r, path+"/", http.StatusFound)
                return
            }
            reader, mimeType, status, err := a.Get(path, nameresolver)
            if err != nil {
                if _, ok := err.(api.ErrResolve); ok {
                    log.Debug(fmt.Sprintf("%v", err))
                    status = http.StatusBadRequest
                } else {
                    log.Debug(fmt.Sprintf("error retrieving '%s': %v", uri, err))
                    status = http.StatusNotFound
                }
                http.Error(w, err.Error(), status)
                return
            }
            // set mime type and status headers
            w.Header().Set("Content-Type", mimeType)
            if status > 0 {
                w.WriteHeader(status)
            } else {
                status = 200
            }
            quitC := make(chan bool)
            size, err := reader.Size(quitC)
            if err != nil {
                log.Debug(fmt.Sprintf("Could not determine size: %v", err.Error()))
                //An error on call to Size means we don't have the root chunk
                http.Error(w, err.Error(), http.StatusNotFound)
                return
            }
            log.Debug(fmt.Sprintf("Served '%s' (%d bytes) as '%s' (status code: %v)", uri, size, mimeType, status))

            http.ServeContent(w, r, path, forever(), reader)

        }
    default:
        http.Error(w, "Method "+r.Method+" is not supported.", http.StatusMethodNotAllowed)
    }
}

func (self *sequentialReader) ReadAt(target []byte, off int64) (n int, err error) {
    self.lock.Lock()
    // assert self.pos <= off
    if self.pos > off {
        log.Error(fmt.Sprintf("non-sequential read attempted from sequentialReader; %d > %d", self.pos, off))
        panic("Non-sequential read attempt")
    }
    if self.pos != off {
        log.Debug(fmt.Sprintf("deferred read in POST at position %d, offset %d.", self.pos, off))
        wait := make(chan bool)
        self.ahead[off] = wait
        self.lock.Unlock()
        if <-wait {
            // failed read behind
            n = 0
            err = io.ErrUnexpectedEOF
            return
        }
        self.lock.Lock()
    }
    localPos := 0
    for localPos < len(target) {
        n, err = self.reader.Read(target[localPos:])
        localPos += n
        log.Debug(fmt.Sprintf("Read %d bytes into buffer size %d from POST, error %v.", n, len(target), err))
        if err != nil {
            log.Debug(fmt.Sprintf("POST stream's reading terminated with %v.", err))
            for i := range self.ahead {
                self.ahead[i] <- true
                delete(self.ahead, i)
            }
            self.lock.Unlock()
            return localPos, err
        }
        self.pos += int64(n)
    }
    wait := self.ahead[self.pos]
    if wait != nil {
        log.Debug(fmt.Sprintf("deferred read in POST at position %d triggered.", self.pos))
        delete(self.ahead, self.pos)
        close(wait)
    }
    self.lock.Unlock()
    return localPos, err
}