handler.go 7.43 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
	streamHeader           = "X-Stream-Output"
	channelHeader          = "X-Chunked-Output"
39
	uaHeader               = "User-Agent"
40 41
	contentTypeHeader      = "Content-Type"
	contentLengthHeader    = "Content-Length"
42
	contentDispHeader      = "Content-Disposition"
43
	transferEncodingHeader = "Transfer-Encoding"
44
	applicationJson        = "application/json"
45 46
	applicationOctetStream = "application/octet-stream"
	plainText              = "text/plain"
47
)
48

49 50 51 52 53 54
var mimeTypes = map[string]string{
	cmds.JSON: "application/json",
	cmds.XML:  "application/xml",
	cmds.Text: "text/plain",
}

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

61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
	// 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)}
77 78
}

Jeromy's avatar
Jeromy committed
79 80 81 82 83
func (i Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// Call the CORS handler which wraps the internal handler.
	i.corsHandler.ServeHTTP(w, r)
}

84
func (i internalHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
85 86
	log.Debug("Incoming API request: ", r.URL)

87 88 89 90 91 92 93 94 95 96 97 98 99 100
	// 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
	}

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

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

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

128
	// call the command
129
	res := i.root.Call(req)
130

131 132 133 134
	// now handle responding to the client properly
	sendResponse(w, req, res)
}

Jeromy's avatar
Jeromy committed
135
func guessMimeType(res cmds.Response) (string, error) {
136
	if _, ok := res.Output().(io.Reader); ok {
137 138 139 140
		// 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
141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
		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
161 162
	}

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

174
	out, err := res.Reader()
175
	if err != nil {
176
		http.Error(w, err.Error(), http.StatusInternalServerError)
177
		return
178
	}
179

Jeromy's avatar
Jeromy committed
180 181 182 183 184
	h := w.Header()
	if res.Length() > 0 {
		h.Set(contentLengthHeader, strconv.FormatUint(res.Length(), 10))
	}

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

192
	streamChans, _, _ := req.Option("stream-channels").Bool()
Jeromy's avatar
Jeromy committed
193 194 195 196 197 198 199
	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
200

Jeromy's avatar
Jeromy committed
201 202
	if mime != "" {
		h.Set(contentTypeHeader, mime)
203
	}
Jeromy's avatar
Jeromy committed
204 205
	h.Set(streamHeader, "1")
	h.Set(transferEncodingHeader, "chunked")
206

Jeromy's avatar
Jeromy committed
207
	if err := copyChunks(status, w, out); err != nil {
208
		log.Error("error while writing stream", err)
209
	}
210 211
}

212 213
// 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
214
func copyChunks(status int, w http.ResponseWriter, out io.Reader) error {
Jeromy's avatar
Jeromy committed
215
	// hijack the connection so we can write our own chunked output and trailers
216 217
	hijacker, ok := w.(http.Hijacker)
	if !ok {
Jeromy's avatar
Jeromy committed
218
		log.Error("Failed to create hijacker! cannot continue!")
219 220 221 222 223 224 225 226
		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
227
	// write status
228
	writer.WriteString(fmt.Sprintf("HTTP/1.1 %d %s\r\n", status, http.StatusText(status)))
229

Jeromy's avatar
Jeromy committed
230 231
	// Write out headers
	w.Header().Write(writer)
232

Jeromy's avatar
Jeromy committed
233 234
	// end of headers
	writer.WriteString("\r\n")
235

Jeromy's avatar
Jeromy committed
236 237
	// write body
	streamErr := writeChunks(out, writer)
238

Jeromy's avatar
Jeromy committed
239 240
	// close body
	writer.WriteString("0\r\n")
241

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

Jeromy's avatar
Jeromy committed
252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279
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
}

280 281 282 283 284 285 286
func sanitizedErrStr(err error) string {
	s := err.Error()
	s = strings.Split(s, "\n")[0]
	s = strings.Split(s, "\r")[0]
	return s
}

287 288 289 290 291 292 293 294 295 296 297 298 299 300 301
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
}