github.com/keltia/go-ipfs@v0.3.8-0.20150909044612-210793031c63/commands/http/handler.go (about)

     1  package http
     2  
     3  import (
     4  	"bufio"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  	"net/url"
    10  	"os"
    11  	"runtime"
    12  	"strconv"
    13  	"strings"
    14  
    15  	cors "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/rs/cors"
    16  	context "github.com/ipfs/go-ipfs/Godeps/_workspace/src/golang.org/x/net/context"
    17  
    18  	cmds "github.com/ipfs/go-ipfs/commands"
    19  	u "github.com/ipfs/go-ipfs/util"
    20  )
    21  
    22  var log = u.Logger("commands/http")
    23  
    24  // the internal handler for the API
    25  type internalHandler struct {
    26  	ctx  cmds.Context
    27  	root *cmds.Command
    28  	cfg  *ServerConfig
    29  }
    30  
    31  // The Handler struct is funny because we want to wrap our internal handler
    32  // with CORS while keeping our fields.
    33  type Handler struct {
    34  	internalHandler
    35  	corsHandler http.Handler
    36  }
    37  
    38  var ErrNotFound = errors.New("404 page not found")
    39  
    40  const (
    41  	StreamErrHeader        = "X-Stream-Error"
    42  	streamHeader           = "X-Stream-Output"
    43  	channelHeader          = "X-Chunked-Output"
    44  	uaHeader               = "User-Agent"
    45  	contentTypeHeader      = "Content-Type"
    46  	contentLengthHeader    = "Content-Length"
    47  	contentDispHeader      = "Content-Disposition"
    48  	transferEncodingHeader = "Transfer-Encoding"
    49  	applicationJson        = "application/json"
    50  	applicationOctetStream = "application/octet-stream"
    51  	plainText              = "text/plain"
    52  	originHeader           = "origin"
    53  )
    54  
    55  const (
    56  	ACAOrigin      = "Access-Control-Allow-Origin"
    57  	ACAMethods     = "Access-Control-Allow-Methods"
    58  	ACACredentials = "Access-Control-Allow-Credentials"
    59  )
    60  
    61  var mimeTypes = map[string]string{
    62  	cmds.JSON: "application/json",
    63  	cmds.XML:  "application/xml",
    64  	cmds.Text: "text/plain",
    65  }
    66  
    67  type ServerConfig struct {
    68  	// Headers is an optional map of headers that is written out.
    69  	Headers map[string][]string
    70  
    71  	// CORSOpts is a set of options for CORS headers.
    72  	CORSOpts *cors.Options
    73  }
    74  
    75  func skipAPIHeader(h string) bool {
    76  	switch h {
    77  	case "Access-Control-Allow-Origin":
    78  		return true
    79  	case "Access-Control-Allow-Methods":
    80  		return true
    81  	case "Access-Control-Allow-Credentials":
    82  		return true
    83  	default:
    84  		return false
    85  	}
    86  }
    87  
    88  func NewHandler(ctx cmds.Context, root *cmds.Command, cfg *ServerConfig) *Handler {
    89  	if cfg == nil {
    90  		panic("must provide a valid ServerConfig")
    91  	}
    92  
    93  	// Wrap the internal handler with CORS handling-middleware.
    94  	// Create a handler for the API.
    95  	internal := internalHandler{ctx, root, cfg}
    96  	c := cors.New(*cfg.CORSOpts)
    97  	return &Handler{internal, c.Handler(internal)}
    98  }
    99  
   100  func (i Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   101  	// Call the CORS handler which wraps the internal handler.
   102  	i.corsHandler.ServeHTTP(w, r)
   103  }
   104  
   105  func (i internalHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   106  	log.Debug("Incoming API request: ", r.URL)
   107  
   108  	defer func() {
   109  		if r := recover(); r != nil {
   110  			log.Error(r)
   111  
   112  			buf := make([]byte, 4096)
   113  			n := runtime.Stack(buf, false)
   114  			fmt.Fprintln(os.Stderr, string(buf[:n]))
   115  		}
   116  	}()
   117  
   118  	if !allowOrigin(r, i.cfg) || !allowReferer(r, i.cfg) {
   119  		w.WriteHeader(http.StatusForbidden)
   120  		w.Write([]byte("403 - Forbidden"))
   121  		log.Warningf("API blocked request to %s. (possible CSRF)", r.URL)
   122  		return
   123  	}
   124  
   125  	req, err := Parse(r, i.root)
   126  	if err != nil {
   127  		if err == ErrNotFound {
   128  			w.WriteHeader(http.StatusNotFound)
   129  		} else {
   130  			w.WriteHeader(http.StatusBadRequest)
   131  		}
   132  		w.Write([]byte(err.Error()))
   133  		return
   134  	}
   135  
   136  	// get the node's context to pass into the commands.
   137  	node, err := i.ctx.GetNode()
   138  	if err != nil {
   139  		s := fmt.Sprintf("cmds/http: couldn't GetNode(): %s", err)
   140  		http.Error(w, s, http.StatusInternalServerError)
   141  		return
   142  	}
   143  
   144  	//ps: take note of the name clash - commands.Context != context.Context
   145  	req.SetInvocContext(i.ctx)
   146  
   147  	ctx, cancel := context.WithCancel(node.Context())
   148  	defer cancel()
   149  
   150  	err = req.SetRootContext(ctx)
   151  	if err != nil {
   152  		http.Error(w, err.Error(), http.StatusInternalServerError)
   153  		return
   154  	}
   155  
   156  	// call the command
   157  	res := i.root.Call(req)
   158  
   159  	// set user's headers first.
   160  	for k, v := range i.cfg.Headers {
   161  		if !skipAPIHeader(k) {
   162  			w.Header()[k] = v
   163  		}
   164  	}
   165  
   166  	// now handle responding to the client properly
   167  	sendResponse(w, r, res, req)
   168  }
   169  
   170  func guessMimeType(res cmds.Response) (string, error) {
   171  	// Try to guess mimeType from the encoding option
   172  	enc, found, err := res.Request().Option(cmds.EncShort).String()
   173  	if err != nil {
   174  		return "", err
   175  	}
   176  	if !found {
   177  		return "", errors.New("no encoding option set")
   178  	}
   179  
   180  	return mimeTypes[enc], nil
   181  }
   182  
   183  func sendResponse(w http.ResponseWriter, r *http.Request, res cmds.Response, req cmds.Request) {
   184  	mime, err := guessMimeType(res)
   185  	if err != nil {
   186  		http.Error(w, err.Error(), http.StatusInternalServerError)
   187  		return
   188  	}
   189  
   190  	status := http.StatusOK
   191  	// if response contains an error, write an HTTP error status code
   192  	if e := res.Error(); e != nil {
   193  		if e.Code == cmds.ErrClient {
   194  			status = http.StatusBadRequest
   195  		} else {
   196  			status = http.StatusInternalServerError
   197  		}
   198  		// NOTE: The error will actually be written out by the reader below
   199  	}
   200  
   201  	out, err := res.Reader()
   202  	if err != nil {
   203  		http.Error(w, err.Error(), http.StatusInternalServerError)
   204  		return
   205  	}
   206  
   207  	h := w.Header()
   208  	if res.Length() > 0 {
   209  		h.Set(contentLengthHeader, strconv.FormatUint(res.Length(), 10))
   210  	}
   211  
   212  	if _, ok := res.Output().(io.Reader); ok {
   213  		// we don't set the Content-Type for streams, so that browsers can MIME-sniff the type themselves
   214  		// we set this header so clients have a way to know this is an output stream
   215  		// (not marshalled command output)
   216  		mime = ""
   217  		h.Set(streamHeader, "1")
   218  	}
   219  
   220  	// if output is a channel and user requested streaming channels,
   221  	// use chunk copier for the output
   222  	_, isChan := res.Output().(chan interface{})
   223  	if !isChan {
   224  		_, isChan = res.Output().(<-chan interface{})
   225  	}
   226  
   227  	streamChans, _, _ := req.Option("stream-channels").Bool()
   228  	if isChan {
   229  		h.Set(channelHeader, "1")
   230  		if streamChans {
   231  			// streaming output from a channel will always be json objects
   232  			mime = applicationJson
   233  		}
   234  	}
   235  
   236  	if mime != "" {
   237  		h.Set(contentTypeHeader, mime)
   238  	}
   239  	h.Set(transferEncodingHeader, "chunked")
   240  
   241  	if r.Method == "HEAD" { // after all the headers.
   242  		return
   243  	}
   244  
   245  	if err := writeResponse(status, w, out); err != nil {
   246  		if strings.Contains(err.Error(), "broken pipe") {
   247  			log.Info("client disconnect while writing stream ", err)
   248  			return
   249  		}
   250  
   251  		log.Error("error while writing stream ", err)
   252  	}
   253  }
   254  
   255  // Copies from an io.Reader to a http.ResponseWriter.
   256  // Flushes chunks over HTTP stream as they are read (if supported by transport).
   257  func writeResponse(status int, w http.ResponseWriter, out io.Reader) error {
   258  	// hijack the connection so we can write our own chunked output and trailers
   259  	hijacker, ok := w.(http.Hijacker)
   260  	if !ok {
   261  		log.Error("Failed to create hijacker! cannot continue!")
   262  		return errors.New("Could not create hijacker")
   263  	}
   264  	conn, writer, err := hijacker.Hijack()
   265  	if err != nil {
   266  		return err
   267  	}
   268  	defer conn.Close()
   269  
   270  	// write status
   271  	writer.WriteString(fmt.Sprintf("HTTP/1.1 %d %s\r\n", status, http.StatusText(status)))
   272  
   273  	// Write out headers
   274  	w.Header().Write(writer)
   275  
   276  	// end of headers
   277  	writer.WriteString("\r\n")
   278  
   279  	// write body
   280  	streamErr := writeChunks(out, writer)
   281  
   282  	// close body
   283  	writer.WriteString("0\r\n")
   284  
   285  	// if there was a stream error, write out an error trailer. hopefully
   286  	// the client will pick it up!
   287  	if streamErr != nil {
   288  		writer.WriteString(StreamErrHeader + ": " + sanitizedErrStr(streamErr) + "\r\n")
   289  	}
   290  	writer.WriteString("\r\n") // close response
   291  	writer.Flush()
   292  	return streamErr
   293  }
   294  
   295  func writeChunks(r io.Reader, w *bufio.ReadWriter) error {
   296  	buf := make([]byte, 32*1024)
   297  	for {
   298  		n, err := r.Read(buf)
   299  
   300  		if n > 0 {
   301  			length := fmt.Sprintf("%x\r\n", n)
   302  			w.WriteString(length)
   303  
   304  			_, err := w.Write(buf[0:n])
   305  			if err != nil {
   306  				return err
   307  			}
   308  
   309  			w.WriteString("\r\n")
   310  			w.Flush()
   311  		}
   312  
   313  		if err != nil && err != io.EOF {
   314  			return err
   315  		}
   316  		if err == io.EOF {
   317  			break
   318  		}
   319  	}
   320  	return nil
   321  }
   322  
   323  func sanitizedErrStr(err error) string {
   324  	s := err.Error()
   325  	s = strings.Split(s, "\n")[0]
   326  	s = strings.Split(s, "\r")[0]
   327  	return s
   328  }
   329  
   330  // allowOrigin just stops the request if the origin is not allowed.
   331  // the CORS middleware apparently does not do this for us...
   332  func allowOrigin(r *http.Request, cfg *ServerConfig) bool {
   333  	origin := r.Header.Get("Origin")
   334  
   335  	// curl, or ipfs shell, typing it in manually, or clicking link
   336  	// NOT in a browser. this opens up a hole. we should close it,
   337  	// but right now it would break things. TODO
   338  	if origin == "" {
   339  		return true
   340  	}
   341  
   342  	for _, o := range cfg.CORSOpts.AllowedOrigins {
   343  		if o == "*" { // ok! you asked for it!
   344  			return true
   345  		}
   346  
   347  		if o == origin { // allowed explicitly
   348  			return true
   349  		}
   350  	}
   351  
   352  	return false
   353  }
   354  
   355  // allowReferer this is here to prevent some CSRF attacks that
   356  // the API would be vulnerable to. We check that the Referer
   357  // is allowed by CORS Origin (origins and referrers here will
   358  // work similarly in the normla uses of the API).
   359  // See discussion at https://github.com/ipfs/go-ipfs/issues/1532
   360  func allowReferer(r *http.Request, cfg *ServerConfig) bool {
   361  	referer := r.Referer()
   362  
   363  	// curl, or ipfs shell, typing it in manually, or clicking link
   364  	// NOT in a browser. this opens up a hole. we should close it,
   365  	// but right now it would break things. TODO
   366  	if referer == "" {
   367  		return true
   368  	}
   369  
   370  	u, err := url.Parse(referer)
   371  	if err != nil {
   372  		// bad referer. but there _is_ something, so bail.
   373  		log.Debug("failed to parse referer: ", referer)
   374  		// debug because referer comes straight from the client. dont want to
   375  		// let people DOS by putting a huge referer that gets stored in log files.
   376  		return false
   377  	}
   378  	origin := u.Scheme + "://" + u.Host
   379  
   380  	// check CORS ACAOs and pretend Referer works like an origin.
   381  	// this is valid for many (most?) sane uses of the API in
   382  	// other applications, and will have the desired effect.
   383  	for _, o := range cfg.CORSOpts.AllowedOrigins {
   384  		if o == "*" { // ok! you asked for it!
   385  			return true
   386  		}
   387  
   388  		// referer is allowed explicitly
   389  		if o == origin {
   390  			return true
   391  		}
   392  	}
   393  
   394  	return false
   395  }