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

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

Jakub Sztandera's avatar
Jakub Sztandera committed
15
	cmds "github.com/ipfs/go-ipfs/commands"
rht's avatar
rht committed
16
	"github.com/ipfs/go-ipfs/repo/config"
17

Jakub Sztandera's avatar
Jakub Sztandera committed
18
	cors "gx/ipfs/QmPG2kW5t27LuHgHnvhUwbHCNHAt2eUcb4gPHqofrESUdB/cors"
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
	StreamErrHeader          = "X-Stream-Error"
	streamHeader             = "X-Stream-Output"
	channelHeader            = "X-Chunked-Output"
	extraContentLengthHeader = "X-Content-Length"
	uaHeader                 = "User-Agent"
	contentTypeHeader        = "Content-Type"
50 51 52
	applicationJson          = "application/json"
	applicationOctetStream   = "application/octet-stream"
	plainText                = "text/plain"
53 54
)

55 56 57
var AllowedExposedHeadersArr = []string{streamHeader, channelHeader, extraContentLengthHeader}
var AllowedExposedHeaders = strings.Join(AllowedExposedHeadersArr, ", ")

58 59 60 61
const (
	ACAOrigin      = "Access-Control-Allow-Origin"
	ACAMethods     = "Access-Control-Allow-Methods"
	ACACredentials = "Access-Control-Allow-Credentials"
62
)
63

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

71
type ServerConfig struct {
72 73
	// Headers is an optional map of headers that is written out.
	Headers map[string][]string
74

Artem Andreenko's avatar
Artem Andreenko committed
75 76 77 78 79
	// cORSOpts is a set of options for CORS headers.
	cORSOpts *cors.Options

	// cORSOptsRWMutex is a RWMutex for read/write CORSOpts
	cORSOptsRWMutex sync.RWMutex
80 81
}

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

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

100 101 102
	// setup request logger
	ctx.ReqLog = new(cmds.ReqLog)

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

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

119
func (i internalHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
120
	log.Debug("incoming API request: ", r.URL)
121

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

127
			debug.PrintStack()
128 129 130
		}
	}()

131 132 133 134 135 136 137 138 139 140 141
	// 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 {
142
		clientGone := cn.CloseNotify()
143 144
		go func() {
			select {
145
			case <-clientGone:
146 147 148 149 150 151
			case <-ctx.Done():
			}
			cancel()
		}()
	}

152 153 154 155 156 157 158
	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
	}

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

170 171 172
	rlog := i.ctx.ReqLog.Add(req)
	defer rlog.Finish()

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

	err = req.SetRootContext(ctx)
177 178 179 180
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
181

182
	// call the command
183
	res := i.root.Call(req)
184

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

192
	// now handle responding to the client properly
193
	sendResponse(w, r, res, req)
194 195
}

Jeromy's avatar
Jeromy committed
196 197 198 199 200 201 202 203 204 205
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")
	}

206 207 208 209 210
	if m, ok := mimeTypes[enc]; ok {
		return m, nil
	}

	return mimeTypes[cmds.JSON], nil
Jeromy's avatar
Jeromy committed
211 212
}

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

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

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

235
	out, err := res.Reader()
236
	if err != nil {
237
		http.Error(w, err.Error(), http.StatusInternalServerError)
238
		return
239
	}
240

241 242 243
	// Set up our potential trailer
	h.Set("Trailer", StreamErrHeader)

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

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

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

Jeromy's avatar
Jeromy committed
262 263 264
	if isChan {
		h.Set(channelHeader, "1")
	}
Jeromy's avatar
Jeromy committed
265

266 267 268
	// catch-all, set to text as default
	if mime == "" {
		mime = "text/plain"
269
	}
270 271

	h.Set(contentTypeHeader, mime)
272

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

278 279 280 281
	if r.Method == "HEAD" { // after all the headers.
		return
	}

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

290 291 292 293 294 295 296 297 298 299 300
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
301 302 303 304 305
			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.
306 307 308 309 310 311 312 313 314 315 316 317 318 319
		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)
		}
320 321

		f.Flush()
322 323 324
	}
}

325 326 327 328 329 330
func sanitizedErrStr(err error) string {
	s := err.Error()
	s = strings.Split(s, "\n")[0]
	s = strings.Split(s, "\r")[0]
	return s
}
331

Artem Andreenko's avatar
Artem Andreenko committed
332 333 334 335 336 337 338 339 340 341 342 343 344 345 346
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
347 348 349
	o := make([]string, len(origins))
	copy(o, origins)
	cfg.cORSOpts.AllowedOrigins = o
Artem Andreenko's avatar
Artem Andreenko committed
350 351 352 353 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
}

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
}

379 380 381 382 383 384 385 386 387 388 389
// 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
390 391
	origins := cfg.AllowedOrigins()
	for _, o := range origins {
392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418
		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
	}

419 420 421 422 423 424 425 426 427 428
	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

429 430 431
	// 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
432 433
	origins := cfg.AllowedOrigins()
	for _, o := range origins {
434 435 436 437 438
		if o == "*" { // ok! you asked for it!
			return true
		}

		// referer is allowed explicitly
439
		if o == origin {
440 441 442 443 444 445
			return true
		}
	}

	return false
}
rht's avatar
rht committed
446 447 448 449 450 451 452 453 454 455 456 457

// 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
458
	daemonVersion := config.ApiVersion
rht's avatar
rht committed
459 460 461 462 463
	if daemonVersion != clientVersion {
		return fmt.Errorf("%s (%s != %s)", errApiVersionMismatch, daemonVersion, clientVersion)
	}
	return nil
}