handler.go 6.95 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
	context "github.com/ipfs/go-ipfs/Godeps/_workspace/src/golang.org/x/net/context"
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 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 110 111 112 113 114 115 116 117 118 119 120 121

	// 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
	}
	ctx, cancel := context.WithCancel(node.Context())
	defer cancel()
	/*
		TODO(cryptix): the next line looks very fishy to me..
		It looks like the the context for the command request beeing prepared here is shared across all incoming requests..

		I assume it really isn't because ServeHTTP() doesn't take a pointer receiver, but it's really subtule..

		Shouldn't the context be just put on the command request?

		ps: take note of the name clash - commands.Context != context.Context
	*/
	i.ctx.Context = ctx
122
	req.SetContext(i.ctx)
123 124

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

	// set the Content-Type based on res output
128
	if _, ok := res.Output().(io.Reader); ok {
129 130 131 132 133 134
		// 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
		w.Header().Set(streamHeader, "1")

135
	} else {
136 137
		enc, found, err := req.Option(cmds.EncShort).String()
		if err != nil || !found {
138 139 140
			w.WriteHeader(http.StatusInternalServerError)
			return
		}
141
		mime := mimeTypes[enc]
142
		w.Header().Set(contentTypeHeader, mime)
143 144
	}

145 146 147 148 149
	// set the Content-Length from the response length
	if res.Length() > 0 {
		w.Header().Set(contentLengthHeader, strconv.FormatUint(res.Length(), 10))
	}

150 151 152 153 154 155 156 157 158
	// if response contains an error, write an HTTP error status code
	if e := res.Error(); e != nil {
		if e.Code == cmds.ErrClient {
			w.WriteHeader(http.StatusBadRequest)
		} else {
			w.WriteHeader(http.StatusInternalServerError)
		}
	}

159
	out, err := res.Reader()
160
	if err != nil {
161
		w.Header().Set(contentTypeHeader, "text/plain")
162
		w.WriteHeader(http.StatusInternalServerError)
163
		w.Write([]byte(err.Error()))
164
		return
165
	}
166

167 168 169
	// if output is a channel and user requested streaming channels,
	// use chunk copier for the output
	_, isChan := res.Output().(chan interface{})
170 171 172 173
	if !isChan {
		_, isChan = res.Output().(<-chan interface{})
	}

174 175
	streamChans, _, _ := req.Option("stream-channels").Bool()
	if isChan && streamChans {
176 177 178
		// w.WriteString(transferEncodingHeader + ": chunked\r\n")
		// w.Header().Set(channelHeader, "1")
		// w.WriteHeader(200)
179
		err = copyChunks(applicationJson, w, out)
180
		if err != nil {
181
			log.Debug(err)
182 183
		}
		return
184
	}
185

186 187 188
	flushCopy(w, out)
}

189 190 191 192 193
func (i Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// Call the CORS handler which wraps the internal handler.
	i.corsHandler.ServeHTTP(w, r)
}

194 195 196 197 198 199 200 201 202
// flushCopy Copies from an io.Reader to a http.ResponseWriter.
// Flushes chunks over HTTP stream as they are read (if supported by transport).
func flushCopy(w http.ResponseWriter, out io.Reader) error {
	if _, ok := w.(http.Flusher); !ok {
		return copyChunks("", w, out)
	}

	io.Copy(&flushResponse{w}, out)
	return nil
203 204 205 206
}

// Copies from an io.Reader to a http.ResponseWriter.
// Flushes chunks over HTTP stream as they are read (if supported by transport).
207
func copyChunks(contentType string, w http.ResponseWriter, out io.Reader) error {
208 209 210 211 212 213 214 215 216 217 218
	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()

	writer.WriteString("HTTP/1.1 200 OK\r\n")
219 220 221
	if contentType != "" {
		writer.WriteString(contentTypeHeader + ": " + contentType + "\r\n")
	}
222 223 224
	writer.WriteString(transferEncodingHeader + ": chunked\r\n")
	writer.WriteString(channelHeader + ": 1\r\n\r\n")

225 226 227 228 229 230
	buf := make([]byte, 32*1024)

	for {
		n, err := out.Read(buf)

		if n > 0 {
231 232 233 234
			length := fmt.Sprintf("%x\r\n", n)
			writer.WriteString(length)

			_, err := writer.Write(buf[0:n])
235 236 237 238
			if err != nil {
				return err
			}

239 240
			writer.WriteString("\r\n")
			writer.Flush()
241 242
		}

243
		if err != nil && err != io.EOF {
244 245
			return err
		}
246 247 248
		if err == io.EOF {
			break
		}
249
	}
250 251 252 253

	writer.WriteString("0\r\n\r\n")
	writer.Flush()

254
	return nil
255
}
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271

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
}