handler.go 11 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
	cors "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/rs/cors"
15
	context "github.com/ipfs/go-ipfs/Godeps/_workspace/src/golang.org/x/net/context"
rht's avatar
rht committed
16
	"github.com/ipfs/go-ipfs/repo/config"
17

18
	cmds "github.com/ipfs/go-ipfs/commands"
Jeromy's avatar
Jeromy committed
19
	logging "github.com/ipfs/go-ipfs/vendor/QmQg1J6vikuXF9oDvm4wpdeAUvvkVEKW1EYDw9HhTMnP2b/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
	StreamErrHeader        = "X-Stream-Error"
45 46
	streamHeader           = "X-Stream-Output"
	channelHeader          = "X-Chunked-Output"
47
	uaHeader               = "User-Agent"
48 49
	contentTypeHeader      = "Content-Type"
	contentLengthHeader    = "Content-Length"
50
	contentDispHeader      = "Content-Disposition"
51
	transferEncodingHeader = "Transfer-Encoding"
52
	applicationJson        = "application/json"
53 54
	applicationOctetStream = "application/octet-stream"
	plainText              = "text/plain"
55 56 57 58 59 60 61
	originHeader           = "origin"
)

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

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

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

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

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

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

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

	// Wrap the internal handler with CORS handling-middleware.
100
	// Create a handler for the API.
101
	internal := internalHandler{ctx, root, cfg}
Artem Andreenko's avatar
Artem Andreenko committed
102
	c := cors.New(*cfg.cORSOpts)
103
	return &Handler{internal, c.Handler(internal)}
104 105
}

Jeromy's avatar
Jeromy committed
106 107 108 109 110
func (i Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// Call the CORS handler which wraps the internal handler.
	i.corsHandler.ServeHTTP(w, r)
}

111
func (i internalHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
112 113
	log.Debug("Incoming API request: ", r.URL)

114 115
	defer func() {
		if r := recover(); r != nil {
116
			log.Error("A panic has occurred in the commands handler!")
117 118
			log.Error(r)

119
			debug.PrintStack()
120 121 122
		}
	}()

123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
	// 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 {
		go func() {
			select {
			case <-cn.CloseNotify():
			case <-ctx.Done():
			}
			cancel()
		}()
	}

143 144 145 146 147 148 149
	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
	}

150
	req, err := Parse(r, i.root)
151
	if err != nil {
152 153 154 155 156 157
		if err == ErrNotFound {
			w.WriteHeader(http.StatusNotFound)
		} else {
			w.WriteHeader(http.StatusBadRequest)
		}
		w.Write([]byte(err.Error()))
158 159
		return
	}
160

Jeromy's avatar
Jeromy committed
161 162
	//ps: take note of the name clash - commands.Context != context.Context
	req.SetInvocContext(i.ctx)
163 164

	err = req.SetRootContext(ctx)
165 166 167 168
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
169

170
	// call the command
171
	res := i.root.Call(req)
172

173 174 175 176 177 178 179
	// set user's headers first.
	for k, v := range i.cfg.Headers {
		if !skipAPIHeader(k) {
			w.Header()[k] = v
		}
	}

180
	// now handle responding to the client properly
181
	sendResponse(w, r, res, req)
182 183
}

Jeromy's avatar
Jeromy committed
184 185 186 187 188 189 190 191 192 193
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")
	}

194 195 196 197 198
	if m, ok := mimeTypes[enc]; ok {
		return m, nil
	}

	return mimeTypes[cmds.JSON], nil
Jeromy's avatar
Jeromy committed
199 200
}

201
func sendResponse(w http.ResponseWriter, r *http.Request, res cmds.Response, req cmds.Request) {
Jeromy's avatar
Jeromy committed
202 203 204 205
	mime, err := guessMimeType(res)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
206 207
	}

208
	status := http.StatusOK
209 210 211
	// if response contains an error, write an HTTP error status code
	if e := res.Error(); e != nil {
		if e.Code == cmds.ErrClient {
212
			status = http.StatusBadRequest
213
		} else {
214
			status = http.StatusInternalServerError
215
		}
Jeromy's avatar
Jeromy committed
216
		// NOTE: The error will actually be written out by the reader below
217 218
	}

219
	out, err := res.Reader()
220
	if err != nil {
221
		http.Error(w, err.Error(), http.StatusInternalServerError)
222
		return
223
	}
224

Jeromy's avatar
Jeromy committed
225
	h := w.Header()
226 227 228 229

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

Jeromy's avatar
Jeromy committed
230 231 232 233
	if res.Length() > 0 {
		h.Set(contentLengthHeader, strconv.FormatUint(res.Length(), 10))
	}

234
	if _, ok := res.Output().(io.Reader); ok {
235 236 237
		// set streams output type to text to avoid issues with browsers rendering
		// html pages on priveleged api ports
		mime = "text/plain"
238 239 240
		h.Set(streamHeader, "1")
	}

241 242 243
	// if output is a channel and user requested streaming channels,
	// use chunk copier for the output
	_, isChan := res.Output().(chan interface{})
244 245 246 247
	if !isChan {
		_, isChan = res.Output().(<-chan interface{})
	}

Jeromy's avatar
Jeromy committed
248 249 250
	if isChan {
		h.Set(channelHeader, "1")
	}
Jeromy's avatar
Jeromy committed
251

252 253 254
	// catch-all, set to text as default
	if mime == "" {
		mime = "text/plain"
255
	}
256 257

	h.Set(contentTypeHeader, mime)
Jeromy's avatar
Jeromy committed
258
	h.Set(transferEncodingHeader, "chunked")
259

Jeromy's avatar
Jeromy committed
260 261 262 263 264
	// set 'allowed' headers
	h.Set("Access-Control-Allow-Headers", "X-Stream-Output, X-Chunked-Output")
	// expose those headers
	h.Set("Access-Control-Expose-Headers", "X-Stream-Output, X-Chunked-Output")

265 266 267 268
	if r.Method == "HEAD" { // after all the headers.
		return
	}

269
	w.WriteHeader(status)
270
	err = flushCopy(w, out)
271
	if err != nil {
272 273
		log.Error("err: ", err)
		w.Header().Set(StreamErrHeader, sanitizedErrStr(err))
Jeromy's avatar
Jeromy committed
274 275 276
	}
}

277 278 279 280 281 282 283 284 285 286 287
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
288 289 290 291 292
			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.
293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312
		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)
		}

		f.Flush()
	}
	return nil
}

313 314 315 316 317 318
func sanitizedErrStr(err error) string {
	s := err.Error()
	s = strings.Split(s, "\n")[0]
	s = strings.Split(s, "\r")[0]
	return s
}
319

Artem Andreenko's avatar
Artem Andreenko committed
320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364
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()
	cfg.cORSOpts.AllowedOrigins = origins
}

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
}

365 366 367 368 369 370 371 372 373 374 375
// 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
376 377
	origins := cfg.AllowedOrigins()
	for _, o := range origins {
378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404
		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
	}

405 406 407 408 409 410 411 412 413 414
	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

415 416 417
	// 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
418 419
	origins := cfg.AllowedOrigins()
	for _, o := range origins {
420 421 422 423 424
		if o == "*" { // ok! you asked for it!
			return true
		}

		// referer is allowed explicitly
425
		if o == origin {
426 427 428 429 430 431
			return true
		}
	}

	return false
}
rht's avatar
rht committed
432 433 434 435 436 437 438 439 440 441 442 443

// 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
444
	daemonVersion := config.ApiVersion
rht's avatar
rht committed
445 446 447 448 449
	if daemonVersion != clientVersion {
		return fmt.Errorf("%s (%s != %s)", errApiVersionMismatch, daemonVersion, clientVersion)
	}
	return nil
}