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

import (
Jeromy's avatar
Jeromy committed
4
	"bufio"
5
	"errors"
6
	"fmt"
7 8
	"io"
	"net/http"
9
	"strconv"
10
	"strings"
11

David Braun's avatar
David Braun committed
12
	"github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/rs/cors"
13

14 15
	cmds "github.com/ipfs/go-ipfs/commands"
	u "github.com/ipfs/go-ipfs/util"
16 17
)

18 19
var log = u.Logger("commands/http")

20 21 22 23 24 25 26 27
// the internal handler for the API
type internalHandler struct {
	ctx  cmds.Context
	root *cmds.Command
}

// The Handler struct is funny because we want to wrap our internal handler
// with CORS while keeping our fields.
28
type Handler struct {
29 30
	internalHandler
	corsHandler http.Handler
31
}
32

33
var ErrNotFound = errors.New("404 page not found")
34

35
const (
36
	StreamErrHeader        = "X-Stream-Error"
37 38 39 40 41
	streamHeader           = "X-Stream-Output"
	channelHeader          = "X-Chunked-Output"
	contentTypeHeader      = "Content-Type"
	contentLengthHeader    = "Content-Length"
	transferEncodingHeader = "Transfer-Encoding"
42
	applicationJson        = "application/json"
43
)
44

45 46 47 48 49 50
var mimeTypes = map[string]string{
	cmds.JSON: "application/json",
	cmds.XML:  "application/xml",
	cmds.Text: "text/plain",
}

51
func NewHandler(ctx cmds.Context, root *cmds.Command, allowedOrigin string) *Handler {
52
	// allow whitelisted origins (so we can make API requests from the browser)
53 54
	if len(allowedOrigin) > 0 {
		log.Info("Allowing API requests from origin: " + allowedOrigin)
55 56
	}

57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
	// Create a handler for the API.
	internal := internalHandler{ctx, root}

	// Create a CORS object for wrapping the internal handler.
	c := cors.New(cors.Options{
		AllowedMethods: []string{"GET", "POST", "PUT"},

		// use AllowOriginFunc instead of AllowedOrigins because we want to be
		// restrictive by default.
		AllowOriginFunc: func(origin string) bool {
			return (allowedOrigin == "*") || (origin == allowedOrigin)
		},
	})

	// Wrap the internal handler with CORS handling-middleware.
	return &Handler{internal, c.Handler(internal)}
73 74
}

Jeromy's avatar
Jeromy committed
75 76 77 78 79
func (i Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// Call the CORS handler which wraps the internal handler.
	i.corsHandler.ServeHTTP(w, r)
}

80
func (i internalHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
81 82
	log.Debug("Incoming API request: ", r.URL)

83 84 85 86 87 88 89 90 91 92 93 94 95 96
	// error on external referers (to prevent CSRF attacks)
	referer := r.Referer()
	scheme := r.URL.Scheme
	if len(scheme) == 0 {
		scheme = "http"
	}
	host := fmt.Sprintf("%s://%s/", scheme, r.Host)
	// empty string means the user isn't following a link (they are directly typing in the url)
	if referer != "" && !strings.HasPrefix(referer, host) {
		w.WriteHeader(http.StatusForbidden)
		w.Write([]byte("403 - Forbidden"))
		return
	}

97
	req, err := Parse(r, i.root)
98
	if err != nil {
99 100 101 102 103 104
		if err == ErrNotFound {
			w.WriteHeader(http.StatusNotFound)
		} else {
			w.WriteHeader(http.StatusBadRequest)
		}
		w.Write([]byte(err.Error()))
105 106
		return
	}
107 108 109 110

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

Jeromy's avatar
Jeromy committed
116 117 118
	//ps: take note of the name clash - commands.Context != context.Context
	req.SetInvocContext(i.ctx)
	err = req.SetRootContext(node.Context())
119 120 121 122
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
123

124
	// call the command
125
	res := i.root.Call(req)
126

127 128 129 130
	// now handle responding to the client properly
	sendResponse(w, req, res)
}

Jeromy's avatar
Jeromy committed
131
func guessMimeType(res cmds.Response) (string, error) {
132
	if _, ok := res.Output().(io.Reader); ok {
133 134 135 136
		// we don't set the Content-Type for streams, so that browsers can MIME-sniff the type themselves
		// we set this header so clients have a way to know this is an output stream
		// (not marshalled command output)
		// TODO: set a specific Content-Type if the command response needs it to be a certain type
Jeromy's avatar
Jeromy committed
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156
		return "", nil
	}

	// 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")
	}

	return mimeTypes[enc], nil
}

func sendResponse(w http.ResponseWriter, req cmds.Request, res cmds.Response) {
	mime, err := guessMimeType(res)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
157 158
	}

159
	status := 200
160 161 162
	// if response contains an error, write an HTTP error status code
	if e := res.Error(); e != nil {
		if e.Code == cmds.ErrClient {
163
			status = http.StatusBadRequest
164
		} else {
165
			status = http.StatusInternalServerError
166
		}
Jeromy's avatar
Jeromy committed
167
		// NOTE: The error will actually be written out by the reader below
168 169
	}

170
	out, err := res.Reader()
171
	if err != nil {
172
		http.Error(w, err.Error(), http.StatusInternalServerError)
173
		return
174
	}
175

Jeromy's avatar
Jeromy committed
176 177 178 179 180
	h := w.Header()
	if res.Length() > 0 {
		h.Set(contentLengthHeader, strconv.FormatUint(res.Length(), 10))
	}

181 182 183
	// if output is a channel and user requested streaming channels,
	// use chunk copier for the output
	_, isChan := res.Output().(chan interface{})
184 185 186 187
	if !isChan {
		_, isChan = res.Output().(<-chan interface{})
	}

188
	streamChans, _, _ := req.Option("stream-channels").Bool()
Jeromy's avatar
Jeromy committed
189 190 191 192 193 194 195
	if isChan {
		h.Set(channelHeader, "1")
		if streamChans {
			// streaming output from a channel will always be json objects
			mime = applicationJson
		}
	}
Jeromy's avatar
Jeromy committed
196

Jeromy's avatar
Jeromy committed
197 198
	if mime != "" {
		h.Set(contentTypeHeader, mime)
199
	}
Jeromy's avatar
Jeromy committed
200 201
	h.Set(streamHeader, "1")
	h.Set(transferEncodingHeader, "chunked")
202

Jeromy's avatar
Jeromy committed
203
	if err := copyChunks(status, w, out); err != nil {
204
		log.Error("error while writing stream", err)
205
	}
206 207
}

208 209
// Copies from an io.Reader to a http.ResponseWriter.
// Flushes chunks over HTTP stream as they are read (if supported by transport).
Jeromy's avatar
Jeromy committed
210
func copyChunks(status int, w http.ResponseWriter, out io.Reader) error {
Jeromy's avatar
Jeromy committed
211
	// hijack the connection so we can write our own chunked output and trailers
212 213
	hijacker, ok := w.(http.Hijacker)
	if !ok {
Jeromy's avatar
Jeromy committed
214
		log.Error("Failed to create hijacker! cannot continue!")
215 216 217 218 219 220 221 222
		return errors.New("Could not create hijacker")
	}
	conn, writer, err := hijacker.Hijack()
	if err != nil {
		return err
	}
	defer conn.Close()

Jeromy's avatar
Jeromy committed
223
	// write status
224
	writer.WriteString(fmt.Sprintf("HTTP/1.1 %d %s\r\n", status, http.StatusText(status)))
225

Jeromy's avatar
Jeromy committed
226 227
	// Write out headers
	w.Header().Write(writer)
228

Jeromy's avatar
Jeromy committed
229 230
	// end of headers
	writer.WriteString("\r\n")
231

Jeromy's avatar
Jeromy committed
232 233
	// write body
	streamErr := writeChunks(out, writer)
234

Jeromy's avatar
Jeromy committed
235 236
	// close body
	writer.WriteString("0\r\n")
237

238 239 240
	// if there was a stream error, write out an error trailer. hopefully
	// the client will pick it up!
	if streamErr != nil {
241
		writer.WriteString(StreamErrHeader + ": " + sanitizedErrStr(streamErr) + "\r\n")
242 243 244
	}
	writer.WriteString("\r\n") // close response
	writer.Flush()
245
	return streamErr
246
}
247

Jeromy's avatar
Jeromy committed
248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275
func writeChunks(r io.Reader, w *bufio.ReadWriter) error {
	buf := make([]byte, 32*1024)
	for {
		n, err := r.Read(buf)

		if n > 0 {
			length := fmt.Sprintf("%x\r\n", n)
			w.WriteString(length)

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

			w.WriteString("\r\n")
			w.Flush()
		}

		if err != nil && err != io.EOF {
			return err
		}
		if err == io.EOF {
			break
		}
	}
	return nil
}

276 277 278 279 280 281 282
func sanitizedErrStr(err error) string {
	s := err.Error()
	s = strings.Split(s, "\n")[0]
	s = strings.Split(s, "\r")[0]
	return s
}

283 284 285 286 287 288 289 290 291 292 293 294 295 296 297
type flushResponse struct {
	W http.ResponseWriter
}

func (fr *flushResponse) Write(buf []byte) (int, error) {
	n, err := fr.W.Write(buf)
	if err != nil {
		return n, err
	}

	if flusher, ok := fr.W.(http.Flusher); ok {
		flusher.Flush()
	}
	return n, err
}