-
Notifications
You must be signed in to change notification settings - Fork 42
/
handler.go
211 lines (179 loc) · 5.02 KB
/
handler.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
package http
import (
"context"
"errors"
"net/http"
"runtime/debug"
"strings"
"sync"
"time"
cmds "github.com/ipfs/go-ipfs-cmds"
logging "github.com/ipfs/go-log"
cors "github.com/rs/cors"
)
var log = logging.Logger("cmds/http")
var (
// ErrNotFound is returned when the endpoint does not exist.
ErrNotFound = errors.New("404 page not found")
)
const (
// StreamErrHeader is used as trailer when stream errors happen.
StreamErrHeader = "X-Stream-Error"
streamHeader = "X-Stream-Output"
channelHeader = "X-Chunked-Output"
extraContentLengthHeader = "X-Content-Length"
uaHeader = "User-Agent"
contentTypeHeader = "Content-Type"
contentDispHeader = "Content-Disposition"
transferEncodingHeader = "Transfer-Encoding"
originHeader = "origin"
applicationJSON = "application/json"
applicationOctetStream = "application/octet-stream"
plainText = "text/plain"
)
func skipAPIHeader(h string) bool {
switch h {
case "Access-Control-Allow-Origin":
return true
case "Access-Control-Allow-Methods":
return true
case "Access-Control-Allow-Credentials":
return true
default:
return false
}
}
// the internal handler for the API
type handler struct {
root *cmds.Command
cfg *ServerConfig
env cmds.Environment
}
// NewHandler creates the http.Handler for the given commands.
func NewHandler(env cmds.Environment, root *cmds.Command, cfg *ServerConfig) http.Handler {
if cfg == nil {
panic("must provide a valid ServerConfig")
}
c := cors.New(*cfg.corsOpts)
var h http.Handler
h = &handler{
env: env,
root: root,
cfg: cfg,
}
if cfg.APIPath != "" {
h = newPrefixHandler(cfg.APIPath, h) // wrap with path prefix checker and trimmer
}
h = c.Handler(h) // wrap with CORS handler
return h
}
type requestLogger interface {
LogRequest(*cmds.Request) func()
}
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log.Debug("incoming API request: ", r.URL)
var re ResponseEmitter
defer func() {
if r := recover(); r != nil {
log.Error("a panic has occurred in the commands handler!")
log.Error(r)
log.Errorf("stack trace:\n%s", debug.Stack())
if re != nil {
if err := re.CloseWithError(errors.New("an error occurred")); err != nil {
log.Errorf("error closing ResponseEmitter: %s", err)
return
}
}
}
}()
// First of all, check if we are allowed to handle the request method
// or we are configured not to.
//
// Always allow OPTIONS, POST
switch r.Method {
case http.MethodOptions:
// If we get here, this is a normal (non-preflight) request.
// The CORS library handles all other requests.
// Tell the user the allowed methods, and return.
setAllowHeader(w, h.cfg.AllowGet)
w.WriteHeader(http.StatusNoContent)
return
case http.MethodPost:
case http.MethodGet, http.MethodHead:
if h.cfg.AllowGet {
break
}
fallthrough
default:
setAllowHeader(w, h.cfg.AllowGet)
http.Error(w, "405 - Method Not Allowed", http.StatusMethodNotAllowed)
log.Warnf("The IPFS API does not support %s requests.", r.Method)
return
}
if !allowOrigin(r, h.cfg) || !allowReferer(r, h.cfg) || !allowUserAgent(r, h.cfg) {
http.Error(w, "403 - Forbidden", http.StatusForbidden)
log.Warnf("API blocked request to %s. (possible CSRF)", r.URL)
return
}
// If we have a request body, make sure the preamble
// knows that it should close the body if it wants to
// write before completing reading.
// FIXME: https://github.com/ipfs/go-ipfs/issues/5168
// FIXME: https://github.com/golang/go/issues/15527
var bodyEOFChan chan struct{}
if r.Body != http.NoBody {
bodyEOFChan = make(chan struct{})
var once sync.Once
bw := bodyWrapper{
ReadCloser: r.Body,
onEOF: func() {
once.Do(func() { close(bodyEOFChan) })
},
}
r.Body = bw
}
req, err := parseRequest(r, h.root)
if err != nil {
status := http.StatusBadRequest
if err == ErrNotFound {
status = http.StatusNotFound
}
http.Error(w, err.Error(), status)
return
}
// set user's headers first.
for k, v := range h.cfg.Headers {
if !skipAPIHeader(k) {
w.Header()[k] = v
}
}
// Handle the timeout up front.
var cancel func()
if timeoutStr, ok := req.Options[cmds.TimeoutOpt]; ok {
timeout, err := time.ParseDuration(timeoutStr.(string))
if err != nil {
return
}
req.Context, cancel = context.WithTimeout(req.Context, timeout)
} else {
req.Context, cancel = context.WithCancel(req.Context)
}
defer cancel()
re, err = NewResponseEmitter(w, r.Method, req, withRequestBodyEOFChan(bodyEOFChan))
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if reqLogger, ok := h.env.(requestLogger); ok {
done := reqLogger.LogRequest(req)
defer done()
}
h.root.Call(req, re, h.env)
}
func setAllowHeader(w http.ResponseWriter, allowGet bool) {
allowedMethods := []string{http.MethodOptions, http.MethodPost}
if allowGet {
allowedMethods = append(allowedMethods, http.MethodHead, http.MethodGet)
}
w.Header().Set("Allow", strings.Join(allowedMethods, ", "))
}