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

import (
4
	"errors"
5
	"fmt"
6 7
	"io"
	"net/http"
8
	"net/url"
9 10
	"os"
	"runtime"
11
	"strconv"
12
	"strings"
Artem Andreenko's avatar
Artem Andreenko committed
13
	"sync"
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"
rht's avatar
rht committed
17
	"github.com/ipfs/go-ipfs/repo/config"
18

19
	cmds "github.com/ipfs/go-ipfs/commands"
Jeromy's avatar
Jeromy committed
20
	logging "github.com/ipfs/go-ipfs/vendor/QmQg1J6vikuXF9oDvm4wpdeAUvvkVEKW1EYDw9HhTMnP2b/go-log"
21 22
)

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

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

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

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

44
const (
45
	StreamErrHeader        = "X-Stream-Error"
46 47
	streamHeader           = "X-Stream-Output"
	channelHeader          = "X-Chunked-Output"
48
	uaHeader               = "User-Agent"
49 50
	contentTypeHeader      = "Content-Type"
	contentLengthHeader    = "Content-Length"
51
	contentDispHeader      = "Content-Disposition"
52
	transferEncodingHeader = "Transfer-Encoding"
53
	applicationJson        = "application/json"
54 55
	applicationOctetStream = "application/octet-stream"
	plainText              = "text/plain"
56 57 58 59 60 61 62
	originHeader           = "origin"
)

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

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

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 96
func NewHandler(ctx cmds.Context, root *cmds.Command, cfg *ServerConfig) *Handler {
	if cfg == nil {
97
		panic("must provide a valid ServerConfig")
98
	}
99 100

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

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

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

115 116 117 118 119 120 121 122 123 124
	defer func() {
		if r := recover(); r != nil {
			log.Error(r)

			buf := make([]byte, 4096)
			n := runtime.Stack(buf, false)
			fmt.Fprintln(os.Stderr, string(buf[:n]))
		}
	}()

125 126 127 128 129 130 131
	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
	}

132
	req, err := Parse(r, i.root)
133
	if err != nil {
134 135 136 137 138 139
		if err == ErrNotFound {
			w.WriteHeader(http.StatusNotFound)
		} else {
			w.WriteHeader(http.StatusBadRequest)
		}
		w.Write([]byte(err.Error()))
140 141
		return
	}
142 143 144 145

	// get the node's context to pass into the commands.
	node, err := i.ctx.GetNode()
	if err != nil {
Jeromy's avatar
Jeromy committed
146 147
		s := fmt.Sprintf("cmds/http: couldn't GetNode(): %s", err)
		http.Error(w, s, http.StatusInternalServerError)
148 149 150
		return
	}

Jeromy's avatar
Jeromy committed
151 152
	//ps: take note of the name clash - commands.Context != context.Context
	req.SetInvocContext(i.ctx)
153 154 155

	ctx, cancel := context.WithCancel(node.Context())
	defer cancel()
156 157 158 159 160 161 162 163 164
	if cn, ok := w.(http.CloseNotifier); ok {
		go func() {
			select {
			case <-cn.CloseNotify():
			case <-ctx.Done():
			}
			cancel()
		}()
	}
165 166

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

172
	// call the command
173
	res := i.root.Call(req)
174

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

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

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

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

	return mimeTypes[cmds.JSON], nil
Jeromy's avatar
Jeromy committed
201 202
}

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

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

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

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

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

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

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

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

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

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

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

Jeromy's avatar
Jeromy committed
262 263 264 265 266
	// 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")

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

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

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

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

Artem Andreenko's avatar
Artem Andreenko committed
318 319 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
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
}

363 364 365 366 367 368 369 370 371 372 373
// 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
374 375
	origins := cfg.AllowedOrigins()
	for _, o := range origins {
376 377 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
		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
	}

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

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

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

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

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