mirror of
https://github.com/ipfs/kubo.git
synced 2026-02-22 19:07:48 +08:00
Let's save log.Error for things the user can take action on. Moved all our diagnostics to log.Debug. We can ideally reduce them even further.
218 lines
5.1 KiB
Go
218 lines
5.1 KiB
Go
package http
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
context "github.com/jbenet/go-ipfs/Godeps/_workspace/src/code.google.com/p/go.net/context"
|
|
|
|
cmds "github.com/jbenet/go-ipfs/commands"
|
|
u "github.com/jbenet/go-ipfs/util"
|
|
)
|
|
|
|
var log = u.Logger("commands/http")
|
|
|
|
type Handler struct {
|
|
ctx cmds.Context
|
|
root *cmds.Command
|
|
origin string
|
|
}
|
|
|
|
var ErrNotFound = errors.New("404 page not found")
|
|
|
|
const (
|
|
streamHeader = "X-Stream-Output"
|
|
channelHeader = "X-Chunked-Output"
|
|
contentTypeHeader = "Content-Type"
|
|
contentLengthHeader = "Content-Length"
|
|
transferEncodingHeader = "Transfer-Encoding"
|
|
applicationJson = "application/json"
|
|
)
|
|
|
|
var mimeTypes = map[string]string{
|
|
cmds.JSON: "application/json",
|
|
cmds.XML: "application/xml",
|
|
cmds.Text: "text/plain",
|
|
}
|
|
|
|
func NewHandler(ctx cmds.Context, root *cmds.Command, origin string) *Handler {
|
|
// allow whitelisted origins (so we can make API requests from the browser)
|
|
if len(origin) > 0 {
|
|
log.Info("Allowing API requests from origin: " + origin)
|
|
}
|
|
|
|
return &Handler{ctx, root, origin}
|
|
}
|
|
|
|
func (i Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
// create a context.Context to pass into the commands.
|
|
ctx, cancel := context.WithCancel(context.TODO())
|
|
defer cancel()
|
|
i.ctx.Context = ctx
|
|
|
|
log.Debug("Incoming API request: ", r.URL)
|
|
|
|
if len(i.origin) > 0 {
|
|
w.Header().Set("Access-Control-Allow-Origin", i.origin)
|
|
}
|
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
|
|
|
req, err := Parse(r, i.root)
|
|
if err != nil {
|
|
if err == ErrNotFound {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
} else {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
}
|
|
w.Write([]byte(err.Error()))
|
|
return
|
|
}
|
|
req.SetContext(i.ctx)
|
|
|
|
// call the command
|
|
res := i.root.Call(req)
|
|
|
|
// set the Content-Type based on res output
|
|
if _, ok := res.Output().(io.Reader); ok {
|
|
// 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")
|
|
|
|
} else {
|
|
enc, found, err := req.Option(cmds.EncShort).String()
|
|
if err != nil || !found {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
mime := mimeTypes[enc]
|
|
w.Header().Set(contentTypeHeader, mime)
|
|
}
|
|
|
|
// set the Content-Length from the response length
|
|
if res.Length() > 0 {
|
|
w.Header().Set(contentLengthHeader, strconv.FormatUint(res.Length(), 10))
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
|
|
out, err := res.Reader()
|
|
if err != nil {
|
|
w.Header().Set(contentTypeHeader, "text/plain")
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
w.Write([]byte(err.Error()))
|
|
return
|
|
}
|
|
|
|
// if output is a channel and user requested streaming channels,
|
|
// use chunk copier for the output
|
|
_, isChan := res.Output().(chan interface{})
|
|
if !isChan {
|
|
_, isChan = res.Output().(<-chan interface{})
|
|
}
|
|
|
|
streamChans, _, _ := req.Option("stream-channels").Bool()
|
|
if isChan && streamChans {
|
|
// w.WriteString(transferEncodingHeader + ": chunked\r\n")
|
|
// w.Header().Set(channelHeader, "1")
|
|
// w.WriteHeader(200)
|
|
err = copyChunks(applicationJson, w, out)
|
|
if err != nil {
|
|
log.Debug(err)
|
|
}
|
|
return
|
|
}
|
|
|
|
flushCopy(w, out)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// Copies from an io.Reader to a http.ResponseWriter.
|
|
// Flushes chunks over HTTP stream as they are read (if supported by transport).
|
|
func copyChunks(contentType string, w http.ResponseWriter, out io.Reader) error {
|
|
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")
|
|
if contentType != "" {
|
|
writer.WriteString(contentTypeHeader + ": " + contentType + "\r\n")
|
|
}
|
|
writer.WriteString(transferEncodingHeader + ": chunked\r\n")
|
|
writer.WriteString(channelHeader + ": 1\r\n\r\n")
|
|
|
|
buf := make([]byte, 32*1024)
|
|
|
|
for {
|
|
n, err := out.Read(buf)
|
|
|
|
if n > 0 {
|
|
length := fmt.Sprintf("%x\r\n", n)
|
|
writer.WriteString(length)
|
|
|
|
_, err := writer.Write(buf[0:n])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
writer.WriteString("\r\n")
|
|
writer.Flush()
|
|
}
|
|
|
|
if err != nil && err != io.EOF {
|
|
return err
|
|
}
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
}
|
|
|
|
writer.WriteString("0\r\n\r\n")
|
|
writer.Flush()
|
|
|
|
return nil
|
|
}
|
|
|
|
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
|
|
}
|