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

import (
4
	"errors"
5
	"fmt"
6 7
	"io"
	"net/http"
8
	"net/url"
9
	"runtime/debug"
10
	"strconv"
11
	"strings"
Artem Andreenko's avatar
Artem Andreenko committed
12
	"sync"
13

14
	context "context"
rht's avatar
rht committed
15
	"github.com/ipfs/go-ipfs/repo/config"
Jakub Sztandera's avatar
Jakub Sztandera committed
16
	cors "gx/ipfs/QmQzTLDsi3a37CJyMDBXnjiHKQpth3AGS1yqwU57FfLwfG/cors"
17

18
	cmds "github.com/ipfs/go-ipfs/commands"
Jeromy's avatar
Jeromy committed
19
	logging "gx/ipfs/QmSpJByNKFX1sCsHBEp3R73FL4NF6FnQTEGyNAXHm2GS52/go-log"
20 21
)

Jeromy's avatar
Jeromy committed
22
var log = logging.Logger("commands/http")
23

24 25 26 27
// the internal handler for the API
type internalHandler struct {
	ctx  cmds.Context
	root *cmds.Command
28
	cfg  *ServerConfig
29 30 31 32
}

// The Handler struct is funny because we want to wrap our internal handler
// with CORS while keeping our fields.
33
type Handler struct {
34 35
	internalHandler
	corsHandler http.Handler
36
}
37

rht's avatar
rht committed
38 39 40 41
var (
	ErrNotFound           = errors.New("404 page not found")
	errApiVersionMismatch = errors.New("api version mismatch")
)
42

43
const (
44 45 46 47 48 49 50 51 52 53 54 55
	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"
	applicationJson          = "application/json"
	applicationOctetStream   = "application/octet-stream"
	plainText                = "text/plain"
	originHeader             = "origin"
56 57
)

58 59 60
var AllowedExposedHeadersArr = []string{streamHeader, channelHeader, extraContentLengthHeader}
var AllowedExposedHeaders = strings.Join(AllowedExposedHeadersArr, ", ")

61 62 63 64
const (
	ACAOrigin      = "Access-Control-Allow-Origin"
	ACAMethods     = "Access-Control-Allow-Methods"
	ACACredentials = "Access-Control-Allow-Credentials"
65
)
66

67
var mimeTypes = map[string]string{
68 69 70 71
	cmds.Protobuf: "application/protobuf",
	cmds.JSON:     "application/json",
	cmds.XML:      "application/xml",
	cmds.Text:     "text/plain",
72 73
}

74
type ServerConfig struct {
75 76
	// Headers is an optional map of headers that is written out.
	Headers map[string][]string
77

Artem Andreenko's avatar
Artem Andreenko committed
78 79 80 81 82
	// cORSOpts is a set of options for CORS headers.
	cORSOpts *cors.Options

	// cORSOptsRWMutex is a RWMutex for read/write CORSOpts
	cORSOptsRWMutex sync.RWMutex
83 84
}

85 86 87
func skipAPIHeader(h string) bool {
	switch h {
	case "Access-Control-Allow-Origin":
88
		return true
89
	case "Access-Control-Allow-Methods":
90
		return true
91
	case "Access-Control-Allow-Credentials":
92 93 94
		return true
	default:
		return false
95 96 97
	}
}

98
func NewHandler(ctx cmds.Context, root *cmds.Command, cfg *ServerConfig) http.Handler {
99
	if cfg == nil {
100
		panic("must provide a valid ServerConfig")
101
	}
102

103 104 105
	// setup request logger
	ctx.ReqLog = new(cmds.ReqLog)

106
	// Wrap the internal handler with CORS handling-middleware.
107
	// Create a handler for the API.
108 109 110 111 112
	internal := internalHandler{
		ctx:  ctx,
		root: root,
		cfg:  cfg,
	}
Artem Andreenko's avatar
Artem Andreenko committed
113
	c := cors.New(*cfg.cORSOpts)
114
	return &Handler{internal, c.Handler(internal)}
115 116
}

Jeromy's avatar
Jeromy committed
117 118 119 120 121
func (i Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// Call the CORS handler which wraps the internal handler.
	i.corsHandler.ServeHTTP(w, r)
}

122
func (i internalHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
123
	log.Debug("incoming API request: ", r.URL)
124

125 126
	defer func() {
		if r := recover(); r != nil {
127
			log.Error("a panic has occurred in the commands handler!")
128 129
			log.Error(r)

130
			debug.PrintStack()
131 132 133
		}
	}()

134 135 136 137 138 139 140 141 142 143 144
	// get the node's context to pass into the commands.
	node, err := i.ctx.GetNode()
	if err != nil {
		s := fmt.Sprintf("cmds/http: couldn't GetNode(): %s", err)
		http.Error(w, s, http.StatusInternalServerError)
		return
	}

	ctx, cancel := context.WithCancel(node.Context())
	defer cancel()
	if cn, ok := w.(http.CloseNotifier); ok {
145
		clientGone := cn.CloseNotify()
146 147
		go func() {
			select {
148
			case <-clientGone:
149 150 151 152 153 154
			case <-ctx.Done():
			}
			cancel()
		}()
	}

155 156 157 158 159 160 161
	if !allowOrigin(r, i.cfg) || !allowReferer(r, i.cfg) {
		w.WriteHeader(http.StatusForbidden)
		w.Write([]byte("403 - Forbidden"))
		log.Warningf("API blocked request to %s. (possible CSRF)", r.URL)
		return
	}

162
	req, err := Parse(r, i.root)
163
	if err != nil {
164 165 166 167 168 169
		if err == ErrNotFound {
			w.WriteHeader(http.StatusNotFound)
		} else {
			w.WriteHeader(http.StatusBadRequest)
		}
		w.Write([]byte(err.Error()))
170 171
		return
	}
172

173 174 175
	rlog := i.ctx.ReqLog.Add(req)
	defer rlog.Finish()

Jeromy's avatar
Jeromy committed
176 177
	//ps: take note of the name clash - commands.Context != context.Context
	req.SetInvocContext(i.ctx)
178 179

	err = req.SetRootContext(ctx)
180 181 182 183
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
184

185
	// call the command
186
	res := i.root.Call(req)
187

188 189 190 191 192 193 194
	// set user's headers first.
	for k, v := range i.cfg.Headers {
		if !skipAPIHeader(k) {
			w.Header()[k] = v
		}
	}

195
	// now handle responding to the client properly
196
	sendResponse(w, r, res, req)
197 198
}

Jeromy's avatar
Jeromy committed
199 200 201 202 203 204 205 206 207 208
func guessMimeType(res cmds.Response) (string, error) {
	// Try to guess mimeType from the encoding option
	enc, found, err := res.Request().Option(cmds.EncShort).String()
	if err != nil {
		return "", err
	}
	if !found {
		return "", errors.New("no encoding option set")
	}

209 210 211 212 213
	if m, ok := mimeTypes[enc]; ok {
		return m, nil
	}

	return mimeTypes[cmds.JSON], nil
Jeromy's avatar
Jeromy committed
214 215
}

216
func sendResponse(w http.ResponseWriter, r *http.Request, res cmds.Response, req cmds.Request) {
Jakub Sztandera's avatar
Jakub Sztandera committed
217 218
	h := w.Header()
	// Expose our agent to allow identification
219 220
	h.Set("Server", "go-ipfs/"+config.CurrentVersionNumber)

Jeromy's avatar
Jeromy committed
221 222 223 224
	mime, err := guessMimeType(res)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
225 226
	}

227
	status := http.StatusOK
228 229 230
	// if response contains an error, write an HTTP error status code
	if e := res.Error(); e != nil {
		if e.Code == cmds.ErrClient {
231
			status = http.StatusBadRequest
232
		} else {
233
			status = http.StatusInternalServerError
234
		}
Jeromy's avatar
Jeromy committed
235
		// NOTE: The error will actually be written out by the reader below
236 237
	}

238
	out, err := res.Reader()
239
	if err != nil {
240
		http.Error(w, err.Error(), http.StatusInternalServerError)
241
		return
242
	}
243

244 245 246
	// Set up our potential trailer
	h.Set("Trailer", StreamErrHeader)

Jeromy's avatar
Jeromy committed
247
	if res.Length() > 0 {
248
		h.Set("X-Content-Length", strconv.FormatUint(res.Length(), 10))
Jeromy's avatar
Jeromy committed
249 250
	}

251
	if _, ok := res.Output().(io.Reader); ok {
252 253 254
		// set streams output type to text to avoid issues with browsers rendering
		// html pages on priveleged api ports
		mime = "text/plain"
255 256 257
		h.Set(streamHeader, "1")
	}

258 259 260
	// if output is a channel and user requested streaming channels,
	// use chunk copier for the output
	_, isChan := res.Output().(chan interface{})
261 262 263 264
	if !isChan {
		_, isChan = res.Output().(<-chan interface{})
	}

Jeromy's avatar
Jeromy committed
265 266 267
	if isChan {
		h.Set(channelHeader, "1")
	}
Jeromy's avatar
Jeromy committed
268

269 270 271
	// catch-all, set to text as default
	if mime == "" {
		mime = "text/plain"
272
	}
273 274

	h.Set(contentTypeHeader, mime)
275

Jeromy's avatar
Jeromy committed
276
	// set 'allowed' headers
277
	h.Set("Access-Control-Allow-Headers", AllowedExposedHeaders)
Jeromy's avatar
Jeromy committed
278
	// expose those headers
279
	h.Set("Access-Control-Expose-Headers", AllowedExposedHeaders)
Jeromy's avatar
Jeromy committed
280

281 282 283 284
	if r.Method == "HEAD" { // after all the headers.
		return
	}

285
	w.WriteHeader(status)
286
	err = flushCopy(w, out)
287
	if err != nil {
288 289
		log.Error("err: ", err)
		w.Header().Set(StreamErrHeader, sanitizedErrStr(err))
Jeromy's avatar
Jeromy committed
290 291 292
	}
}

293 294 295 296 297 298 299 300 301 302 303
func flushCopy(w io.Writer, r io.Reader) error {
	buf := make([]byte, 4096)
	f, ok := w.(http.Flusher)
	if !ok {
		_, err := io.Copy(w, r)
		return err
	}
	for {
		n, err := r.Read(buf)
		switch err {
		case io.EOF:
Jeromy's avatar
Jeromy committed
304 305 306 307 308
			if n <= 0 {
				return nil
			}
			// if data was returned alongside the EOF, pretend we didnt
			// get an EOF. The next read call should also EOF.
309 310 311 312 313 314 315 316 317 318 319 320 321 322
		case nil:
			// continue
		default:
			return err
		}

		nw, err := w.Write(buf[:n])
		if err != nil {
			return err
		}

		if nw != n {
			return fmt.Errorf("http write failed to write full amount: %d != %d", nw, n)
		}
323 324

		f.Flush()
325 326 327 328
	}
	return nil
}

329 330 331 332 333 334
func sanitizedErrStr(err error) string {
	s := err.Error()
	s = strings.Split(s, "\n")[0]
	s = strings.Split(s, "\r")[0]
	return s
}
335

Artem Andreenko's avatar
Artem Andreenko committed
336 337 338 339 340 341 342 343 344 345 346 347 348 349 350
func NewServerConfig() *ServerConfig {
	cfg := new(ServerConfig)
	cfg.cORSOpts = new(cors.Options)
	return cfg
}

func (cfg ServerConfig) AllowedOrigins() []string {
	cfg.cORSOptsRWMutex.RLock()
	defer cfg.cORSOptsRWMutex.RUnlock()
	return cfg.cORSOpts.AllowedOrigins
}

func (cfg *ServerConfig) SetAllowedOrigins(origins ...string) {
	cfg.cORSOptsRWMutex.Lock()
	defer cfg.cORSOptsRWMutex.Unlock()
rht's avatar
rht committed
351 352 353
	o := make([]string, len(origins))
	copy(o, origins)
	cfg.cORSOpts.AllowedOrigins = o
Artem Andreenko's avatar
Artem Andreenko committed
354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382
}

func (cfg *ServerConfig) AppendAllowedOrigins(origins ...string) {
	cfg.cORSOptsRWMutex.Lock()
	defer cfg.cORSOptsRWMutex.Unlock()
	cfg.cORSOpts.AllowedOrigins = append(cfg.cORSOpts.AllowedOrigins, origins...)
}

func (cfg ServerConfig) AllowedMethods() []string {
	cfg.cORSOptsRWMutex.RLock()
	defer cfg.cORSOptsRWMutex.RUnlock()
	return []string(cfg.cORSOpts.AllowedMethods)
}

func (cfg *ServerConfig) SetAllowedMethods(methods ...string) {
	cfg.cORSOptsRWMutex.Lock()
	defer cfg.cORSOptsRWMutex.Unlock()
	if cfg.cORSOpts == nil {
		cfg.cORSOpts = new(cors.Options)
	}
	cfg.cORSOpts.AllowedMethods = methods
}

func (cfg *ServerConfig) SetAllowCredentials(flag bool) {
	cfg.cORSOptsRWMutex.Lock()
	defer cfg.cORSOptsRWMutex.Unlock()
	cfg.cORSOpts.AllowCredentials = flag
}

383 384 385 386 387 388 389 390 391 392 393
// allowOrigin just stops the request if the origin is not allowed.
// the CORS middleware apparently does not do this for us...
func allowOrigin(r *http.Request, cfg *ServerConfig) bool {
	origin := r.Header.Get("Origin")

	// curl, or ipfs shell, typing it in manually, or clicking link
	// NOT in a browser. this opens up a hole. we should close it,
	// but right now it would break things. TODO
	if origin == "" {
		return true
	}
Artem Andreenko's avatar
Artem Andreenko committed
394 395
	origins := cfg.AllowedOrigins()
	for _, o := range origins {
396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422
		if o == "*" { // ok! you asked for it!
			return true
		}

		if o == origin { // allowed explicitly
			return true
		}
	}

	return false
}

// allowReferer this is here to prevent some CSRF attacks that
// the API would be vulnerable to. We check that the Referer
// is allowed by CORS Origin (origins and referrers here will
// work similarly in the normla uses of the API).
// See discussion at https://github.com/ipfs/go-ipfs/issues/1532
func allowReferer(r *http.Request, cfg *ServerConfig) bool {
	referer := r.Referer()

	// curl, or ipfs shell, typing it in manually, or clicking link
	// NOT in a browser. this opens up a hole. we should close it,
	// but right now it would break things. TODO
	if referer == "" {
		return true
	}

423 424 425 426 427 428 429 430 431 432
	u, err := url.Parse(referer)
	if err != nil {
		// bad referer. but there _is_ something, so bail.
		log.Debug("failed to parse referer: ", referer)
		// debug because referer comes straight from the client. dont want to
		// let people DOS by putting a huge referer that gets stored in log files.
		return false
	}
	origin := u.Scheme + "://" + u.Host

433 434 435
	// check CORS ACAOs and pretend Referer works like an origin.
	// this is valid for many (most?) sane uses of the API in
	// other applications, and will have the desired effect.
Artem Andreenko's avatar
Artem Andreenko committed
436 437
	origins := cfg.AllowedOrigins()
	for _, o := range origins {
438 439 440 441 442
		if o == "*" { // ok! you asked for it!
			return true
		}

		// referer is allowed explicitly
443
		if o == origin {
444 445 446 447 448 449
			return true
		}
	}

	return false
}
rht's avatar
rht committed
450 451 452 453 454 455 456 457 458 459 460 461

// apiVersionMatches checks whether the api client is running the
// same version of go-ipfs. for now, only the exact same version of
// client + server work. In the future, we should use semver for
// proper API versioning! \o/
func apiVersionMatches(r *http.Request) error {
	clientVersion := r.UserAgent()
	// skips check if client is not go-ipfs
	if clientVersion == "" || !strings.Contains(clientVersion, "/go-ipfs/") {
		return nil
	}

rht's avatar
rht committed
462
	daemonVersion := config.ApiVersion
rht's avatar
rht committed
463 464 465 466 467
	if daemonVersion != clientVersion {
		return fmt.Errorf("%s (%s != %s)", errApiVersionMismatch, daemonVersion, clientVersion)
	}
	return nil
}