handler.go 4.3 KB
Newer Older
1 2 3
package http

import (
4
	"context"
5
	"errors"
6
	"net/http"
keks's avatar
keks committed
7
	"runtime/debug"
8
	"strings"
9
	"sync"
10
	"time"
11

12
	cmds "github.com/ipfs/go-ipfs-cmds"
keks's avatar
keks committed
13
	logging "github.com/ipfs/go-log"
14
	cors "github.com/rs/cors"
15 16
)

17
var log = logging.Logger("cmds/http")
18

rht's avatar
rht committed
19 20 21 22
var (
	ErrNotFound           = errors.New("404 page not found")
	errApiVersionMismatch = errors.New("api version mismatch")
)
23

24
const (
25 26 27 28 29 30 31 32 33
	StreamErrHeader          = "X-Stream-Error"
	streamHeader             = "X-Stream-Output"
	channelHeader            = "X-Chunked-Output"
	extraContentLengthHeader = "X-Content-Length"
	uaHeader                 = "User-Agent"
	contentTypeHeader        = "Content-Type"
	contentDispHeader        = "Content-Disposition"
	transferEncodingHeader   = "Transfer-Encoding"
	originHeader             = "origin"
34

35 36 37
	applicationJson        = "application/json"
	applicationOctetStream = "application/octet-stream"
	plainText              = "text/plain"
38
)
39

40 41 42
func skipAPIHeader(h string) bool {
	switch h {
	case "Access-Control-Allow-Origin":
43
		return true
44
	case "Access-Control-Allow-Methods":
45
		return true
46
	case "Access-Control-Allow-Credentials":
47 48 49
		return true
	default:
		return false
50 51 52
	}
}

53 54 55 56
// the internal handler for the API
type handler struct {
	root *cmds.Command
	cfg  *ServerConfig
57
	env  cmds.Environment
58 59
}

60
func NewHandler(env cmds.Environment, root *cmds.Command, cfg *ServerConfig) http.Handler {
61
	if cfg == nil {
62
		panic("must provide a valid ServerConfig")
63
	}
64

65 66 67
	c := cors.New(*cfg.corsOpts)

	var h http.Handler
68

69 70
	h = &handler{
		env:  env,
71 72 73
		root: root,
		cfg:  cfg,
	}
74 75 76 77 78 79 80

	if cfg.APIPath != "" {
		h = newPrefixHandler(cfg.APIPath, h) // wrap with path prefix checker and trimmer
	}
	h = c.Handler(h) // wrap with CORS handler

	return h
81 82
}

83 84
type requestLogger interface {
	LogRequest(*cmds.Request) func()
Jeromy's avatar
Jeromy committed
85 86
}

87
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
88
	log.Debug("incoming API request: ", r.URL)
89

90 91
	defer func() {
		if r := recover(); r != nil {
92
			log.Error("a panic has occurred in the commands handler!")
93
			log.Error(r)
keks's avatar
keks committed
94
			log.Errorf("stack trace:\n%s", debug.Stack())
95 96 97
		}
	}()

98 99 100 101 102 103 104 105 106
	// First of all, check if we are allowed to handle the request method
	// or we are configured not to.
	if !handleRequestMethod(r, h.cfg) {
		setAllowedHeaders(w, h.cfg.HandledMethods)
		http.Error(w, "405 - Method Not Allowed", http.StatusMethodNotAllowed)
		log.Warningf("The IPFS API does not support %s requests. All requests must use %s", h.cfg.HandledMethods)
		return
	}

107
	if !allowOrigin(r, h.cfg) || !allowReferer(r, h.cfg) {
108
		http.Error(w, "403 - Forbidden", http.StatusForbidden)
109 110 111 112
		log.Warningf("API blocked request to %s. (possible CSRF)", r.URL)
		return
	}

113 114 115 116 117
	// If we have a request body, make sure the preamble
	// knows that it should close the body if it wants to
	// write before completing reading.
	// FIXME: https://github.com/ipfs/go-ipfs/issues/5168
	// FIXME: https://github.com/golang/go/issues/15527
118
	var bodyEOFChan chan struct{}
119
	if r.Body != http.NoBody {
120 121
		bodyEOFChan = make(chan struct{})
		var once sync.Once
122 123
		bw := bodyWrapper{
			ReadCloser: r.Body,
124 125
			onEOF: func() {
				once.Do(func() { close(bodyEOFChan) })
126 127 128 129 130
			},
		}
		r.Body = bw
	}

Steven Allen's avatar
Steven Allen committed
131
	req, err := parseRequest(r, h.root)
132
	if err != nil {
133
		status := http.StatusBadRequest
134
		if err == ErrNotFound {
135
			status = http.StatusNotFound
136
		}
137 138

		http.Error(w, err.Error(), status)
139 140
		return
	}
141

142 143 144 145 146 147 148 149 150 151 152 153 154
	// Handle the timeout up front.
	var cancel func()
	if timeoutStr, ok := req.Options[cmds.TimeoutOpt]; ok {
		timeout, err := time.ParseDuration(timeoutStr.(string))
		if err != nil {
			return
		}
		req.Context, cancel = context.WithTimeout(req.Context, timeout)
	} else {
		req.Context, cancel = context.WithCancel(req.Context)
	}
	defer cancel()

155
	re, err := NewResponseEmitter(w, r.Method, req, withRequestBodyEOFChan(bodyEOFChan))
156
	if err != nil {
157
		http.Error(w, err.Error(), http.StatusBadRequest)
158 159 160
		return
	}

161 162 163
	if reqLogger, ok := h.env.(requestLogger); ok {
		done := reqLogger.LogRequest(req)
		defer done()
164
	}
165

166
	// set user's headers first.
167
	for k, v := range h.cfg.Headers {
168 169 170 171 172
		if !skipAPIHeader(k) {
			w.Header()[k] = v
		}
	}

173
	h.root.Call(req, re, h.env)
174 175
}

176 177 178 179 180 181
func sanitizedErrStr(err error) string {
	s := err.Error()
	s = strings.Split(s, "\n")[0]
	s = strings.Split(s, "\r")[0]
	return s
}
182 183 184 185 186 187

func setAllowedHeaders(w http.ResponseWriter, methods []string) {
	for _, m := range methods {
		w.Header().Add("Allow", m)
	}
}