handler.go 7.32 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

12
	cors "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
// the internal handler for the API
type internalHandler struct {
	ctx  cmds.Context
	root *cmds.Command
24
	cfg  *ServerConfig
25 26 27 28
}

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

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

36
const (
37
	StreamErrHeader        = "X-Stream-Error"
38 39
	streamHeader           = "X-Stream-Output"
	channelHeader          = "X-Chunked-Output"
40
	uaHeader               = "User-Agent"
41 42
	contentTypeHeader      = "Content-Type"
	contentLengthHeader    = "Content-Length"
43
	contentDispHeader      = "Content-Disposition"
44
	transferEncodingHeader = "Transfer-Encoding"
45
	applicationJson        = "application/json"
46 47
	applicationOctetStream = "application/octet-stream"
	plainText              = "text/plain"
48
)
49

50 51 52 53 54 55 56
var localhostOrigins = []string{
	"http://127.0.0.1",
	"https://127.0.0.1",
	"http://localhost",
	"https://localhost",
}

57 58 59 60 61 62
var mimeTypes = map[string]string{
	cmds.JSON: "application/json",
	cmds.XML:  "application/xml",
	cmds.Text: "text/plain",
}

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

	// CORSOpts is a set of options for CORS headers.
	CORSOpts *cors.Options
}

71 72 73
func skipAPIHeader(h string) bool {
	switch h {
	case "Access-Control-Allow-Origin":
74
		return true
75
	case "Access-Control-Allow-Methods":
76
		return true
77
	case "Access-Control-Allow-Credentials":
78 79 80
		return true
	default:
		return false
81 82 83
	}
}

84 85 86
func NewHandler(ctx cmds.Context, root *cmds.Command, cfg *ServerConfig) *Handler {
	if cfg == nil {
		cfg = &ServerConfig{}
87 88
	}

89 90 91
	if cfg.CORSOpts == nil {
		cfg.CORSOpts = new(cors.Options)
	}
92

93 94 95 96
	// by default, use GET, PUT, POST
	if cfg.CORSOpts.AllowedMethods == nil {
		cfg.CORSOpts.AllowedMethods = []string{"GET", "POST", "PUT"}
	}
97

98 99 100 101
	// by default, only let 127.0.0.1 through.
	if cfg.CORSOpts.AllowedOrigins == nil {
		cfg.CORSOpts.AllowedOrigins = localhostOrigins
	}
102 103

	// Wrap the internal handler with CORS handling-middleware.
104
	// Create a handler for the API.
105
	internal := internalHandler{ctx, root, cfg}
106
	c := cors.New(*cfg.CORSOpts)
107
	return &Handler{internal, c.Handler(internal)}
108 109
}

Jeromy's avatar
Jeromy committed
110 111 112 113 114
func (i Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// Call the CORS handler which wraps the internal handler.
	i.corsHandler.ServeHTTP(w, r)
}

115
func (i internalHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
116 117
	log.Debug("Incoming API request: ", r.URL)

118
	req, err := Parse(r, i.root)
119
	if err != nil {
120 121 122 123 124 125
		if err == ErrNotFound {
			w.WriteHeader(http.StatusNotFound)
		} else {
			w.WriteHeader(http.StatusBadRequest)
		}
		w.Write([]byte(err.Error()))
126 127
		return
	}
128 129 130 131

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

Jeromy's avatar
Jeromy committed
137 138 139
	//ps: take note of the name clash - commands.Context != context.Context
	req.SetInvocContext(i.ctx)
	err = req.SetRootContext(node.Context())
140 141 142 143
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
144

145
	// call the command
146
	res := i.root.Call(req)
147

148 149 150 151 152 153 154
	// set user's headers first.
	for k, v := range i.cfg.Headers {
		if !skipAPIHeader(k) {
			w.Header()[k] = v
		}
	}

155
	// now handle responding to the client properly
156
	sendResponse(w, r, res, req)
157 158
}

Jeromy's avatar
Jeromy committed
159 160 161 162 163 164 165 166 167 168 169 170 171
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")
	}

	return mimeTypes[enc], nil
}

172
func sendResponse(w http.ResponseWriter, r *http.Request, res cmds.Response, req cmds.Request) {
Jeromy's avatar
Jeromy committed
173 174 175 176
	mime, err := guessMimeType(res)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
177 178
	}

179
	status := http.StatusOK
180 181 182
	// if response contains an error, write an HTTP error status code
	if e := res.Error(); e != nil {
		if e.Code == cmds.ErrClient {
183
			status = http.StatusBadRequest
184
		} else {
185
			status = http.StatusInternalServerError
186
		}
Jeromy's avatar
Jeromy committed
187
		// NOTE: The error will actually be written out by the reader below
188 189
	}

190
	out, err := res.Reader()
191
	if err != nil {
192
		http.Error(w, err.Error(), http.StatusInternalServerError)
193
		return
194
	}
195

Jeromy's avatar
Jeromy committed
196 197 198 199 200
	h := w.Header()
	if res.Length() > 0 {
		h.Set(contentLengthHeader, strconv.FormatUint(res.Length(), 10))
	}

201
	if _, ok := res.Output().(io.Reader); ok {
Jeromy Johnson's avatar
Jeromy Johnson committed
202 203 204
		// 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)
205 206 207 208
		mime = ""
		h.Set(streamHeader, "1")
	}

209 210 211
	// if output is a channel and user requested streaming channels,
	// use chunk copier for the output
	_, isChan := res.Output().(chan interface{})
212 213 214 215
	if !isChan {
		_, isChan = res.Output().(<-chan interface{})
	}

216
	streamChans, _, _ := req.Option("stream-channels").Bool()
Jeromy's avatar
Jeromy committed
217 218 219 220 221 222 223
	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
224

Jeromy's avatar
Jeromy committed
225 226
	if mime != "" {
		h.Set(contentTypeHeader, mime)
227
	}
Jeromy's avatar
Jeromy committed
228
	h.Set(transferEncodingHeader, "chunked")
229

230 231 232 233
	if r.Method == "HEAD" { // after all the headers.
		return
	}

Jeromy's avatar
Jeromy committed
234
	if err := writeResponse(status, w, out); err != nil {
235
		log.Error("error while writing stream", err)
236
	}
237 238
}

239 240
// 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
241
func writeResponse(status int, w http.ResponseWriter, out io.Reader) error {
Jeromy's avatar
Jeromy committed
242
	// hijack the connection so we can write our own chunked output and trailers
243 244
	hijacker, ok := w.(http.Hijacker)
	if !ok {
Jeromy's avatar
Jeromy committed
245
		log.Error("Failed to create hijacker! cannot continue!")
246 247 248 249 250 251 252 253
		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
254
	// write status
255
	writer.WriteString(fmt.Sprintf("HTTP/1.1 %d %s\r\n", status, http.StatusText(status)))
256

Jeromy's avatar
Jeromy committed
257 258
	// Write out headers
	w.Header().Write(writer)
259

Jeromy's avatar
Jeromy committed
260 261
	// end of headers
	writer.WriteString("\r\n")
262

Jeromy's avatar
Jeromy committed
263 264
	// write body
	streamErr := writeChunks(out, writer)
265

Jeromy's avatar
Jeromy committed
266 267
	// close body
	writer.WriteString("0\r\n")
268

269 270 271
	// if there was a stream error, write out an error trailer. hopefully
	// the client will pick it up!
	if streamErr != nil {
272
		writer.WriteString(StreamErrHeader + ": " + sanitizedErrStr(streamErr) + "\r\n")
273 274 275
	}
	writer.WriteString("\r\n") // close response
	writer.Flush()
276
	return streamErr
277
}
278

Jeromy's avatar
Jeromy committed
279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306
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
}

307 308 309 310 311 312
func sanitizedErrStr(err error) string {
	s := err.Error()
	s = strings.Split(s, "\n")[0]
	s = strings.Split(s, "\r")[0]
	return s
}