handler.go 9.83 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"
17

18
	cmds "github.com/ipfs/go-ipfs/commands"
19
	logging "github.com/ipfs/go-ipfs/vendor/QmTBXYb6y2ZcJmoXVKk3pf9rzSEjbCg7tQaJW7RSuH14nv/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

38
var ErrNotFound = errors.New("404 page not found")
39

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

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

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

67
type ServerConfig struct {
68 69
	// Headers is an optional map of headers that is written out.
	Headers map[string][]string
70

Artem Andreenko's avatar
Artem Andreenko committed
71 72 73 74 75
	// cORSOpts is a set of options for CORS headers.
	cORSOpts *cors.Options

	// cORSOptsRWMutex is a RWMutex for read/write CORSOpts
	cORSOptsRWMutex sync.RWMutex
76 77
}

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

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

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

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

108
func (i internalHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
109 110
	log.Debug("Incoming API request: ", r.URL)

111 112 113 114 115 116 117 118 119 120
	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]))
		}
	}()

121 122 123 124 125 126 127
	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
	}

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

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

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

	ctx, cancel := context.WithCancel(node.Context())
	defer cancel()

	err = req.SetRootContext(ctx)
154 155 156 157
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
158

159
	// call the command
160
	res := i.root.Call(req)
161

162 163 164 165 166 167 168
	// set user's headers first.
	for k, v := range i.cfg.Headers {
		if !skipAPIHeader(k) {
			w.Header()[k] = v
		}
	}

169
	// now handle responding to the client properly
170
	sendResponse(w, r, res, req)
171 172
}

Jeromy's avatar
Jeromy committed
173 174 175 176 177 178 179 180 181 182
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")
	}

183 184 185 186 187
	if m, ok := mimeTypes[enc]; ok {
		return m, nil
	}

	return mimeTypes[cmds.JSON], nil
Jeromy's avatar
Jeromy committed
188 189
}

190
func sendResponse(w http.ResponseWriter, r *http.Request, res cmds.Response, req cmds.Request) {
Jeromy's avatar
Jeromy committed
191 192 193 194
	mime, err := guessMimeType(res)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
195 196
	}

197
	status := http.StatusOK
198 199 200
	// if response contains an error, write an HTTP error status code
	if e := res.Error(); e != nil {
		if e.Code == cmds.ErrClient {
201
			status = http.StatusBadRequest
202
		} else {
203
			status = http.StatusInternalServerError
204
		}
Jeromy's avatar
Jeromy committed
205
		// NOTE: The error will actually be written out by the reader below
206 207
	}

208
	out, err := res.Reader()
209
	if err != nil {
210
		http.Error(w, err.Error(), http.StatusInternalServerError)
211
		return
212
	}
213

Jeromy's avatar
Jeromy committed
214
	h := w.Header()
215 216 217 218

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

Jeromy's avatar
Jeromy committed
219 220 221 222
	if res.Length() > 0 {
		h.Set(contentLengthHeader, strconv.FormatUint(res.Length(), 10))
	}

223
	if _, ok := res.Output().(io.Reader); ok {
224 225 226
		// set streams output type to text to avoid issues with browsers rendering
		// html pages on priveleged api ports
		mime = "text/plain"
227 228 229
		h.Set(streamHeader, "1")
	}

230 231 232
	// if output is a channel and user requested streaming channels,
	// use chunk copier for the output
	_, isChan := res.Output().(chan interface{})
233 234 235 236
	if !isChan {
		_, isChan = res.Output().(<-chan interface{})
	}

Jeromy's avatar
Jeromy committed
237 238 239
	if isChan {
		h.Set(channelHeader, "1")
	}
Jeromy's avatar
Jeromy committed
240

241 242 243
	// catch-all, set to text as default
	if mime == "" {
		mime = "text/plain"
244
	}
245 246

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

249 250 251 252
	if r.Method == "HEAD" { // after all the headers.
		return
	}

253
	w.WriteHeader(status)
254
	err = flushCopy(w, out)
255
	if err != nil {
256 257
		log.Error("err: ", err)
		w.Header().Set(StreamErrHeader, sanitizedErrStr(err))
Jeromy's avatar
Jeromy committed
258 259 260
	}
}

261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292
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
}

293 294 295 296 297 298
func sanitizedErrStr(err error) string {
	s := err.Error()
	s = strings.Split(s, "\n")[0]
	s = strings.Split(s, "\r")[0]
	return s
}
299

Artem Andreenko's avatar
Artem Andreenko committed
300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 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
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
}

345 346 347 348 349 350 351 352 353 354 355
// 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
356 357
	origins := cfg.AllowedOrigins()
	for _, o := range origins {
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 383 384
		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
	}

385 386 387 388 389 390 391 392 393 394
	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

395 396 397
	// 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
398 399
	origins := cfg.AllowedOrigins()
	for _, o := range origins {
400 401 402 403 404
		if o == "*" { // ok! you asked for it!
			return true
		}

		// referer is allowed explicitly
405
		if o == origin {
406 407 408 409 410 411
			return true
		}
	}

	return false
}