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

import (
4
	"errors"
5
	"fmt"
6 7
	"io"
	"net/http"
8
	"strconv"
9
	"strings"
10

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

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

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

19 20 21 22 23 24 25 26
// 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.
27
type Handler struct {
28 29
	internalHandler
	corsHandler http.Handler
30
}
31

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

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

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

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

56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
	// 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)}
72 73
}

74
func (i internalHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
75 76
	log.Debug("Incoming API request: ", r.URL)

77 78 79 80 81 82 83 84 85 86 87 88 89 90
	// 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
	}

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

	// get the node's context to pass into the commands.
	node, err := i.ctx.GetNode()
	if err != nil {
		err = fmt.Errorf("cmds/http: couldn't GetNode(): %s", err)
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

Jeromy's avatar
Jeromy committed
110 111 112
	//ps: take note of the name clash - commands.Context != context.Context
	req.SetInvocContext(i.ctx)
	err = req.SetRootContext(node.Context())
113 114 115 116
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
117

118
	// call the command
119
	res := i.root.Call(req)
120

121 122 123 124 125 126 127
	// now handle responding to the client properly
	sendResponse(w, req, res)
}

func sendResponse(w http.ResponseWriter, req cmds.Request, res cmds.Response) {

	var mime string
128
	if _, ok := res.Output().(io.Reader); ok {
129
		mime = ""
130 131 132 133
		// 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
134
	} else {
135 136
		// Try to guess mimeType from the encoding option
		enc, found, err := res.Request().Option(cmds.EncShort).String()
137
		if err != nil || !found {
138 139 140
			w.WriteHeader(http.StatusInternalServerError)
			return
		}
141
		mime = mimeTypes[enc]
142 143
	}

144
	status := 200
145 146 147
	// if response contains an error, write an HTTP error status code
	if e := res.Error(); e != nil {
		if e.Code == cmds.ErrClient {
148
			status = http.StatusBadRequest
149
		} else {
150
			status = http.StatusInternalServerError
151
		}
152
		// TODO: do we just ignore this error? or what?
153 154
	}

155
	out, err := res.Reader()
156
	if err != nil {
157
		http.Error(w, err.Error(), http.StatusInternalServerError)
158
		return
159
	}
160

161 162 163
	// if output is a channel and user requested streaming channels,
	// use chunk copier for the output
	_, isChan := res.Output().(chan interface{})
164 165 166 167
	if !isChan {
		_, isChan = res.Output().(<-chan interface{})
	}

168 169
	streamChans, _, _ := req.Option("stream-channels").Bool()
	if isChan && streamChans {
170 171
		// streaming output from a channel will always be json objects
		mime = applicationJson
172
	}
173

174
	if err := copyChunks(mime, status, isChan, res.Length(), w, out); err != nil {
175
		log.Error("error while writing stream", err)
176
	}
177 178
}

179 180 181 182 183
func (i Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// Call the CORS handler which wraps the internal handler.
	i.corsHandler.ServeHTTP(w, r)
}

184 185
// Copies from an io.Reader to a http.ResponseWriter.
// Flushes chunks over HTTP stream as they are read (if supported by transport).
186
func copyChunks(contentType string, status int, channel bool, length uint64, w http.ResponseWriter, out io.Reader) error {
187 188 189 190 191 192 193 194 195 196
	hijacker, ok := w.(http.Hijacker)
	if !ok {
		return errors.New("Could not create hijacker")
	}
	conn, writer, err := hijacker.Hijack()
	if err != nil {
		return err
	}
	defer conn.Close()

197 198
	writer.WriteString(fmt.Sprintf("HTTP/1.1 %d %s\r\n", status, http.StatusText(status)))
	writer.WriteString(streamHeader + ": 1\r\n")
199 200 201
	if contentType != "" {
		writer.WriteString(contentTypeHeader + ": " + contentType + "\r\n")
	}
202 203 204 205 206 207
	if channel {
		writer.WriteString(channelHeader + ": 1\r\n")
	}
	if length > 0 {
		w.Header().Set(contentLengthHeader, strconv.FormatUint(length, 10))
	}
208
	writer.WriteString(transferEncodingHeader + ": chunked\r\n")
209 210

	writer.WriteString("\r\n")
211

212 213 214 215
	writeChunks := func() error {
		buf := make([]byte, 32*1024)
		for {
			n, err := out.Read(buf)
216

217 218 219
			if n > 0 {
				length := fmt.Sprintf("%x\r\n", n)
				writer.WriteString(length)
220

221 222 223 224
				_, err := writer.Write(buf[0:n])
				if err != nil {
					return err
				}
225

226 227
				writer.WriteString("\r\n")
				writer.Flush()
228 229
			}

230 231 232 233 234 235
			if err != nil && err != io.EOF {
				return err
			}
			if err == io.EOF {
				break
			}
236
		}
237
		return nil
238
	}
239

240 241
	streamErr := writeChunks()
	writer.WriteString("0\r\n") // close body
242

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

253 254 255 256 257 258 259
func sanitizedErrStr(err error) string {
	s := err.Error()
	s = strings.Split(s, "\n")[0]
	s = strings.Split(s, "\r")[0]
	return s
}

260 261 262 263 264 265 266 267 268 269 270 271 272 273 274
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
}