gateway_handler.go 22.6 KB
Newer Older
1
package corehttp
2 3

import (
4
	"context"
5
	"fmt"
6
	"io"
7
	"mime"
8
	"net/http"
9
	"net/url"
10
	"os"
Jeromy's avatar
Jeromy committed
11
	gopath "path"
12
	"regexp"
13
	"runtime/debug"
14
	"strconv"
15
	"strings"
16
	"time"
17

18
	humanize "github.com/dustin/go-humanize"
Gowtham G's avatar
Gowtham G committed
19
	"github.com/gabriel-vasile/mimetype"
tavit ohanian's avatar
tavit ohanian committed
20 21 22 23 24 25 26 27 28 29
	"gitlab.dms3.io/dms3/go-cid"
	files "gitlab.dms3.io/dms3/go-dms3-files"
	assets "gitlab.dms3.io/dms3/go-dms3/assets"
	dag "gitlab.dms3.io/dms3/go-merkledag"
	mfs "gitlab.dms3.io/dms3/go-mfs"
	path "gitlab.dms3.io/dms3/go-path"
	"gitlab.dms3.io/dms3/go-path/resolver"
	coreiface "gitlab.dms3.io/dms3/interface-go-dms3-core"
	ipath "gitlab.dms3.io/dms3/interface-go-dms3-core/path"
	routing "gitlab.dms3.io/p2p/go-p2p-core/routing"
30 31
)

32
const (
tavit ohanian's avatar
tavit ohanian committed
33 34
	dms3PathPrefix   = "/dms3/"
	dms3nsPathPrefix = "/dms3ns/"
35 36
)

37 38
var onlyAscii = regexp.MustCompile("[[:^ascii:]]")

tavit ohanian's avatar
tavit ohanian committed
39 40
// gatewayHandler is a HTTP handler that serves DMS3 objects (accessible by default at /dms3/<path>)
// (it serves requests like GET /dms3/QmVRzPKPzNtSrEzBFm2UZfxmPAgnaLke4DMcerbsGGSaFe/link)
41
type gatewayHandler struct {
42
	config GatewayConfig
43
	api    coreiface.CoreAPI
44 45
}

46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
// StatusResponseWriter enables us to override HTTP Status Code passed to
// WriteHeader function inside of http.ServeContent.  Decision is based on
// presence of HTTP Headers such as Location.
type statusResponseWriter struct {
	http.ResponseWriter
}

func (sw *statusResponseWriter) WriteHeader(code int) {
	// Check if we need to adjust Status Code to account for scheduled redirect
	// This enables us to return payload along with HTTP 301
	// for subdomain redirect in web browsers while also returning body for cli
	// tools which do not follow redirects by default (curl, wget).
	redirect := sw.ResponseWriter.Header().Get("Location")
	if redirect != "" && code == http.StatusOK {
		code = http.StatusMovedPermanently
	}
	sw.ResponseWriter.WriteHeader(code)
}

65
func newGatewayHandler(c GatewayConfig, api coreiface.CoreAPI) *gatewayHandler {
66
	i := &gatewayHandler{
67 68
		config: c,
		api:    api,
69
	}
70
	return i
71 72
}

tavit ohanian's avatar
tavit ohanian committed
73
func parseDms3Path(p string) (cid.Cid, string, error) {
74 75 76 77 78 79 80
	rootPath, err := path.ParsePath(p)
	if err != nil {
		return cid.Cid{}, "", err
	}

	// Check the path.
	rsegs := rootPath.Segments()
tavit ohanian's avatar
tavit ohanian committed
81 82
	if rsegs[0] != "dms3" {
		return cid.Cid{}, "", fmt.Errorf("WritableGateway: only dms3 paths supported")
83 84 85 86 87 88 89 90
	}

	rootCid, err := cid.Decode(rsegs[1])
	if err != nil {
		return cid.Cid{}, "", err
	}

	return rootCid, path.Join(rsegs[2:]), nil
91 92
}

93
func (i *gatewayHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
94
	// the hour is a hard fallback, we don't expect it to happen, but just in case
Jeromy's avatar
Jeromy committed
95
	ctx, cancel := context.WithTimeout(r.Context(), time.Hour)
96
	defer cancel()
97
	r = r.WithContext(ctx)
98

99 100 101 102 103 104 105 106
	defer func() {
		if r := recover(); r != nil {
			log.Error("A panic occurred in the gateway handler!")
			log.Error(r)
			debug.PrintStack()
		}
	}()

107 108
	if i.config.Writable {
		switch r.Method {
Steven Allen's avatar
Steven Allen committed
109
		case http.MethodPost:
110
			i.postHandler(w, r)
111
			return
Steven Allen's avatar
Steven Allen committed
112
		case http.MethodPut:
113 114
			i.putHandler(w, r)
			return
Steven Allen's avatar
Steven Allen committed
115
		case http.MethodDelete:
116 117 118
			i.deleteHandler(w, r)
			return
		}
119 120
	}

Steven Allen's avatar
Steven Allen committed
121 122
	switch r.Method {
	case http.MethodGet, http.MethodHead:
123
		i.getOrHeadHandler(w, r)
124
		return
Steven Allen's avatar
Steven Allen committed
125
	case http.MethodOptions:
126 127 128 129
		i.optionsHandler(w, r)
		return
	}

130
	errmsg := "Method " + r.Method + " not allowed: "
131
	var status int
132
	if !i.config.Writable {
133
		status = http.StatusMethodNotAllowed
134
		errmsg = errmsg + "read only access"
135 136 137
		w.Header().Add("Allow", http.MethodGet)
		w.Header().Add("Allow", http.MethodHead)
		w.Header().Add("Allow", http.MethodOptions)
138
	} else {
139
		status = http.StatusBadRequest
140 141
		errmsg = errmsg + "bad request for " + r.URL.Path
	}
142
	http.Error(w, errmsg, status)
143 144
}

145 146 147 148 149 150 151 152 153
func (i *gatewayHandler) optionsHandler(w http.ResponseWriter, r *http.Request) {
	/*
		OPTIONS is a noop request that is used by the browsers to check
		if server accepts cross-site XMLHttpRequest (indicated by the presence of CORS headers)
		https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Preflighted_requests
	*/
	i.addUserHeaders(w) // return all custom headers (including CORS ones, if set)
}

154
func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request) {
whyrusleeping's avatar
whyrusleeping committed
155
	begin := time.Now()
156
	urlPath := r.URL.Path
157
	escapedURLPath := r.URL.EscapedPath()
158

159 160 161 162
	// If the gateway is behind a reverse proxy and mounted at a sub-path,
	// the prefix header can be set to signal this sub-path.
	// It will be prepended to links in directory listings and the index.html redirect.
	prefix := ""
tavit ohanian's avatar
tavit ohanian committed
163
	if prfx := r.Header.Get("X-Dms3-Gateway-Prefix"); len(prfx) > 0 {
164 165 166 167 168 169
		for _, p := range i.config.PathPrefixes {
			if prfx == p || strings.HasPrefix(prfx, p+"/") {
				prefix = prfx
				break
			}
		}
170 171
	}

tavit ohanian's avatar
tavit ohanian committed
172
	// HostnameOption might have constructed an DMS3NS/DMS3 path using the Host header.
173 174
	// In this case, we need the original path for constructing redirects
	// and links that match the requested URL.
tavit ohanian's avatar
tavit ohanian committed
175 176
	// For example, http://example.net would become /dms3ns/example.net, and
	// the redirects and links would end up as http://example.net/dms3ns/example.net
177 178 179
	requestURI, err := url.ParseRequestURI(r.RequestURI)
	if err != nil {
		webError(w, "failed to parse request path", err, http.StatusInternalServerError)
180
		return
181
	}
182
	originalUrlPath := prefix + requestURI.Path
183

184 185 186
	// ?uri query param support for requests produced by web browsers
	// via navigator.registerProtocolHandler Web API
	// https://developer.mozilla.org/en-US/docs/Web/API/Navigator/registerProtocolHandler
tavit ohanian's avatar
tavit ohanian committed
187
	// TLDR: redirect /dms3/?uri=dms3%3A%2F%2Fcid%3Fquery%3Dval to /dms3/cid?query=val
188
	if uriParam := r.URL.Query().Get("uri"); uriParam != "" {
189
		u, err := url.Parse(uriParam)
190 191 192 193
		if err != nil {
			webError(w, "failed to parse uri query parameter", err, http.StatusBadRequest)
			return
		}
tavit ohanian's avatar
tavit ohanian committed
194 195
		if u.Scheme != "dms3" && u.Scheme != "dms3ns" {
			webError(w, "uri query parameter scheme must be dms3 or dms3ns", err, http.StatusBadRequest)
196 197
			return
		}
198 199 200 201 202
		path := u.Path
		if u.RawQuery != "" { // preserve query if present
			path = path + "?" + u.RawQuery
		}
		http.Redirect(w, r, gopath.Join("/", prefix, u.Scheme, u.Host, path), http.StatusMovedPermanently)
203 204 205
		return
	}

206 207 208
	// Service Worker registration request
	if r.Header.Get("Service-Worker") == "script" {
		// Disallow Service Worker registration on namespace roots
tavit ohanian's avatar
tavit ohanian committed
209 210
		// https://gitlab.dms3.io/dms3/go-dms3/issues/4025
		matched, _ := regexp.MatchString(`^/ip[fn]s/[^/]+$`, r.URL.Path) // TODO: also check for dms3, dms3ns
211 212 213 214 215 216 217
		if matched {
			err := fmt.Errorf("registration is not allowed for this scope")
			webError(w, "navigator.serviceWorker", err, http.StatusBadRequest)
			return
		}
	}

218
	parsedPath := ipath.New(urlPath)
219
	if err := parsedPath.IsValid(); err != nil {
tavit ohanian's avatar
tavit ohanian committed
220
		webError(w, "invalid dms3 path", err, http.StatusBadRequest)
221 222 223
		return
	}

Remco Bloemen's avatar
Remco Bloemen committed
224
	// Resolve path to the final DAG node for the ETag
225
	resolvedPath, err := i.api.ResolvePath(r.Context(), parsedPath)
226 227 228
	switch err {
	case nil:
	case coreiface.ErrOffline:
tavit ohanian's avatar
tavit ohanian committed
229
		webError(w, "dms3 resolve -r "+escapedURLPath, err, http.StatusServiceUnavailable)
230
		return
231
	default:
232 233 234 235
		if i.servePretty404IfPresent(w, r, parsedPath) {
			return
		}

tavit ohanian's avatar
tavit ohanian committed
236
		webError(w, "dms3 resolve -r "+escapedURLPath, err, http.StatusNotFound)
237
		return
238 239
	}

240
	dr, err := i.api.Unixfs().Get(r.Context(), resolvedPath)
241
	if err != nil {
tavit ohanian's avatar
tavit ohanian committed
242
		webError(w, "dms3 cat "+escapedURLPath, err, http.StatusNotFound)
243 244
		return
	}
245 246

	unixfsGetMetric.WithLabelValues(parsedPath.Namespace()).Observe(time.Since(begin).Seconds())
247

248
	defer dr.Close()
249

250 251 252 253 254 255 256 257 258 259 260 261 262
	var responseEtag string

	// we need to figure out whether this is a directory before doing most of the heavy lifting below
	_, ok := dr.(files.Directory)

	if ok && assets.BindataVersionHash != "" {
		responseEtag = `"DirIndex-` + assets.BindataVersionHash + `_CID-` + resolvedPath.Cid().String() + `"`
	} else {
		responseEtag = `"` + resolvedPath.Cid().String() + `"`
	}

	// Check etag sent back to us
	if r.Header.Get("If-None-Match") == responseEtag || r.Header.Get("If-None-Match") == `W/`+responseEtag {
263 264 265 266
		w.WriteHeader(http.StatusNotModified)
		return
	}

267
	i.addUserHeaders(w) // ok, _now_ write user's headers.
tavit ohanian's avatar
tavit ohanian committed
268
	w.Header().Set("X-DMS3-Path", urlPath)
269
	w.Header().Set("Etag", responseEtag)
270

271
	// set these headers _after_ the error, for we may just not have it
Dimitris Apostolou's avatar
Dimitris Apostolou committed
272
	// and don't want the client to cache a 500 response...
tavit ohanian's avatar
tavit ohanian committed
273 274
	// and only if it's /dms3!
	// TODO: break this out when we split /dms3 /dms3ns routes.
275
	modtime := time.Now()
276

277
	if f, ok := dr.(files.File); ok {
tavit ohanian's avatar
tavit ohanian committed
278
		if strings.HasPrefix(urlPath, dms3PathPrefix) {
279
			w.Header().Set("Cache-Control", "public, max-age=29030400, immutable")
280

281 282 283
			// set modtime to a really long time ago, since files are immutable and should stay cached
			modtime = time.Unix(1, 0)
		}
284

285 286 287
		urlFilename := r.URL.Query().Get("filename")
		var name string
		if urlFilename != "" {
288 289 290 291
			disposition := "inline"
			if r.URL.Query().Get("download") == "true" {
				disposition = "attachment"
			}
292 293 294
			utf8Name := url.PathEscape(urlFilename)
			asciiName := url.PathEscape(onlyAscii.ReplaceAllLiteralString(urlFilename, "_"))
			w.Header().Set("Content-Disposition", fmt.Sprintf("%s; filename=\"%s\"; filename*=UTF-8''%s", disposition, asciiName, utf8Name))
295 296
			name = urlFilename
		} else {
297
			name = getFilename(urlPath)
298
		}
299
		i.serveFile(w, r, name, modtime, f)
300
		return
301
	}
302 303 304
	dir, ok := dr.(files.Directory)
	if !ok {
		internalWebError(w, fmt.Errorf("unsupported file type"))
305 306
		return
	}
307

308
	idx, err := i.api.Unixfs().Get(r.Context(), ipath.Join(resolvedPath, "index.html"))
Łukasz Magiera's avatar
Łukasz Magiera committed
309 310
	switch err.(type) {
	case nil:
311 312 313
		dirwithoutslash := urlPath[len(urlPath)-1] != '/'
		goget := r.URL.Query().Get("go-get") == "1"
		if dirwithoutslash && !goget {
314
			// See comment above where originalUrlPath is declared.
315 316 317 318 319 320
			suffix := "/"
			if r.URL.RawQuery != "" {
				// preserve query parameters
				suffix = suffix + "?" + r.URL.RawQuery
			}
			http.Redirect(w, r, originalUrlPath+suffix, 302)
321
			return
322 323
		}

324
		f, ok := idx.(files.File)
325 326 327 328 329
		if !ok {
			internalWebError(w, files.ErrNotReader)
			return
		}

330
		// write to request
331
		i.serveFile(w, r, "index.html", modtime, f)
332
		return
Łukasz Magiera's avatar
Łukasz Magiera committed
333 334
	case resolver.ErrNoLink:
		// no index.html; noop
335 336 337 338 339
	default:
		internalWebError(w, err)
		return
	}

340
	// See statusResponseWriter.WriteHeader
tavit ohanian's avatar
tavit ohanian committed
341
	// and https://gitlab.dms3.io/dms3/go-dms3/issues/7164
342 343 344 345 346 347 348
	// Note: this needs to occur before listingTemplate.Execute otherwise we get
	// superfluous response.WriteHeader call from prometheus/client_golang
	if w.Header().Get("Location") != "" {
		w.WriteHeader(http.StatusMovedPermanently)
		return
	}

349 350 351
	// A HTML directory index will be presented, be sure to set the correct
	// type instead of relying on autodetection (which may fail).
	w.Header().Set("Content-Type", "text/html")
Steven Allen's avatar
Steven Allen committed
352
	if r.Method == http.MethodHead {
353 354 355 356 357
		return
	}

	// storage for directory listing
	var dirListing []directoryItem
358 359
	dirit := dir.Entries()
	for dirit.Next() {
360 361 362 363
		size := "?"
		if s, err := dirit.Node().Size(); err == nil {
			// Size may not be defined/supported. Continue anyways.
			size = humanize.Bytes(uint64(s))
364 365
		}

366 367 368 369 370 371
		hash := ""
		if r, err := i.api.ResolvePath(r.Context(), ipath.Join(resolvedPath, dirit.Name())); err == nil {
			// Path may not be resolved. Continue anyways.
			hash = r.Cid().String()
		}

372
		// See comment above where originalUrlPath is declared.
373 374 375 376 377 378 379
		di := directoryItem{
			Size:      size,
			Name:      dirit.Name(),
			Path:      gopath.Join(originalUrlPath, dirit.Name()),
			Hash:      hash,
			ShortHash: shortHash(hash),
		}
380
		dirListing = append(dirListing, di)
381 382 383 384 385
	}
	if dirit.Err() != nil {
		internalWebError(w, dirit.Err())
		return
	}
386

387
	// construct the correct back link
tavit ohanian's avatar
tavit ohanian committed
388
	// https://gitlab.dms3.io/dms3/go-dms3/issues/1365
389
	var backLink string = originalUrlPath
Henry's avatar
Henry committed
390

tavit ohanian's avatar
tavit ohanian committed
391
	// don't go further up than /dms3/$hash/
392
	pathSplit := path.SplitList(urlPath)
393 394
	switch {
	// keep backlink
tavit ohanian's avatar
tavit ohanian committed
395
	case len(pathSplit) == 3: // url: /dms3/$hash
396

397
	// keep backlink
tavit ohanian's avatar
tavit ohanian committed
398
	case len(pathSplit) == 4 && pathSplit[3] == "": // url: /dms3/$hash/
399

Dimitris Apostolou's avatar
Dimitris Apostolou committed
400
	// add the correct link depending on whether the path ends with a slash
401 402 403 404 405
	default:
		if strings.HasSuffix(backLink, "/") {
			backLink += "./.."
		} else {
			backLink += "/.."
406
		}
407
	}
408

409 410 411 412 413 414
	size := "?"
	if s, err := dir.Size(); err == nil {
		// Size may not be defined/supported. Continue anyways.
		size = humanize.Bytes(uint64(s))
	}

Steven Allen's avatar
Steven Allen committed
415
	hash := resolvedPath.Cid().String()
416

417 418 419
	// Gateway root URL to be used when linking to other rootIDs.
	// This will be blank unless subdomain or DNSLink resolution is being used
	// for this request.
420 421 422 423 424 425 426 427 428
	var gwURL string

	// Get gateway hostname and build gateway URL.
	if h, ok := r.Context().Value("gw-hostname").(string); ok {
		gwURL = "//" + h
	} else {
		gwURL = ""
	}

429 430
	dnslink := hasDNSLinkOrigin(gwURL, urlPath)

431 432
	// See comment above where originalUrlPath is declared.
	tplData := listingTemplateData{
433
		GatewayURL:  gwURL,
434
		DNSLink:     dnslink,
435 436 437
		Listing:     dirListing,
		Size:        size,
		Path:        urlPath,
438
		Breadcrumbs: breadcrumbs(urlPath, dnslink),
439 440
		BackLink:    backLink,
		Hash:        hash,
441
	}
442

443 444 445 446 447
	err = listingTemplate.Execute(w, tplData)
	if err != nil {
		internalWebError(w, err)
		return
	}
448 449
}

450 451 452 453 454
func (i *gatewayHandler) serveFile(w http.ResponseWriter, req *http.Request, name string, modtime time.Time, file files.File) {
	size, err := file.Size()
	if err != nil {
		http.Error(w, "cannot serve files with unknown sizes", http.StatusBadGateway)
		return
455
	}
456

457 458 459
	content := &lazySeeker{
		size:   size,
		reader: file,
460 461
	}

462 463 464 465 466 467 468 469
	var ctype string
	if _, isSymlink := file.(*files.Symlink); isSymlink {
		// We should be smarter about resolving symlinks but this is the
		// "most correct" we can be without doing that.
		ctype = "inode/symlink"
	} else {
		ctype = mime.TypeByExtension(gopath.Ext(name))
		if ctype == "" {
470
			// uses https://github.com/gabriel-vasile/mimetype library to determine the content type.
tavit ohanian's avatar
tavit ohanian committed
471
			// Fixes https://gitlab.dms3.io/dms3/go-dms3/issues/7252
472 473
			mimeType, err := mimetype.DetectReader(content)
			if err != nil {
Gowtham G's avatar
Gowtham G committed
474
				http.Error(w, fmt.Sprintf("cannot detect content-type: %s", err.Error()), http.StatusInternalServerError)
475 476 477 478 479
				return
			}

			ctype = mimeType.String()
			_, err = content.Seek(0, io.SeekStart)
480 481 482 483 484 485 486 487
			if err != nil {
				http.Error(w, "seeker can't seek", http.StatusInternalServerError)
				return
			}
		}
		// Strip the encoding from the HTML Content-Type header and let the
		// browser figure it out.
		//
tavit ohanian's avatar
tavit ohanian committed
488
		// Fixes https://gitlab.dms3.io/dms3/go-dms3/issues/2203
489 490
		if strings.HasPrefix(ctype, "text/html;") {
			ctype = "text/html"
491
		}
492
	}
493
	w.Header().Set("Content-Type", ctype)
494

495
	w = &statusResponseWriter{w}
496 497 498
	http.ServeContent(w, req, name, modtime, content)
}

499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528
func (i *gatewayHandler) servePretty404IfPresent(w http.ResponseWriter, r *http.Request, parsedPath ipath.Path) bool {
	resolved404Path, ctype, err := i.searchUpTreeFor404(r, parsedPath)
	if err != nil {
		return false
	}

	dr, err := i.api.Unixfs().Get(r.Context(), resolved404Path)
	if err != nil {
		return false
	}
	defer dr.Close()

	f, ok := dr.(files.File)
	if !ok {
		return false
	}

	size, err := f.Size()
	if err != nil {
		return false
	}

	log.Debugf("using pretty 404 file for %s", parsedPath.String())
	w.Header().Set("Content-Type", ctype)
	w.Header().Set("Content-Length", strconv.FormatInt(size, 10))
	w.WriteHeader(http.StatusNotFound)
	_, err = io.CopyN(w, f, size)
	return err == nil
}

529 530
func (i *gatewayHandler) postHandler(w http.ResponseWriter, r *http.Request) {
	p, err := i.api.Unixfs().Add(r.Context(), files.NewReaderFile(r.Body))
531
	if err != nil {
532 533 534 535
		internalWebError(w, err)
		return
	}

536
	i.addUserHeaders(w) // ok, _now_ write user's headers.
tavit ohanian's avatar
tavit ohanian committed
537
	w.Header().Set("DMS3-Hash", p.Cid().String())
538
	http.Redirect(w, r, p.String(), http.StatusCreated)
539 540
}

541
func (i *gatewayHandler) putHandler(w http.ResponseWriter, r *http.Request) {
542 543 544 545
	ctx := r.Context()
	ds := i.api.Dag()

	// Parse the path
tavit ohanian's avatar
tavit ohanian committed
546
	rootCid, newPath, err := parseDms3Path(r.URL.Path)
547
	if err != nil {
548
		webError(w, "WritableGateway: failed to parse the path", err, http.StatusBadRequest)
549 550
		return
	}
551 552
	if newPath == "" || newPath == "/" {
		http.Error(w, "WritableGateway: empty path", http.StatusBadRequest)
553 554
		return
	}
555
	newDirectory, newFileName := gopath.Split(newPath)
556

557
	// Resolve the old root.
558

559 560 561 562
	rnode, err := ds.Get(ctx, rootCid)
	if err != nil {
		webError(w, "WritableGateway: Could not create DAG from request", err, http.StatusInternalServerError)
		return
563 564
	}

565 566 567 568 569
	pbnd, ok := rnode.(*dag.ProtoNode)
	if !ok {
		webError(w, "Cannot read non protobuf nodes through gateway", dag.ErrNotProtobuf, http.StatusBadRequest)
		return
	}
570

571 572 573 574 575 576
	// Create the new file.
	newFilePath, err := i.api.Unixfs().Add(ctx, files.NewReaderFile(r.Body))
	if err != nil {
		webError(w, "WritableGateway: could not create DAG from request", err, http.StatusInternalServerError)
		return
	}
577

578 579 580 581 582
	newFile, err := ds.Get(ctx, newFilePath.Cid())
	if err != nil {
		webError(w, "WritableGateway: failed to resolve new file", err, http.StatusInternalServerError)
		return
	}
583

584
	// Patch the new file into the old root.
585

586 587 588 589 590
	root, err := mfs.NewRoot(ctx, ds, pbnd, nil)
	if err != nil {
		webError(w, "WritableGateway: failed to create MFS root", err, http.StatusBadRequest)
		return
	}
591

592 593
	if newDirectory != "" {
		err := mfs.Mkdir(root, newDirectory, mfs.MkdirOpts{Mkparents: true, Flush: false})
594
		if err != nil {
595
			webError(w, "WritableGateway: failed to create MFS directory", err, http.StatusInternalServerError)
596 597
			return
		}
598 599 600 601 602 603 604 605 606 607 608 609 610 611
	}
	dirNode, err := mfs.Lookup(root, newDirectory)
	if err != nil {
		webError(w, "WritableGateway: failed to lookup directory", err, http.StatusInternalServerError)
		return
	}
	dir, ok := dirNode.(*mfs.Directory)
	if !ok {
		http.Error(w, "WritableGateway: target directory is not a directory", http.StatusBadRequest)
		return
	}
	err = dir.Unlink(newFileName)
	switch err {
	case os.ErrNotExist, nil:
612
	default:
613
		webError(w, "WritableGateway: failed to replace existing file", err, http.StatusBadRequest)
614 615
		return
	}
616 617 618 619 620 621 622 623 624 625 626
	err = dir.AddChild(newFileName, newFile)
	if err != nil {
		webError(w, "WritableGateway: failed to link file into directory", err, http.StatusInternalServerError)
		return
	}
	nnode, err := root.GetDirectory().GetNode()
	if err != nil {
		webError(w, "WritableGateway: failed to finalize", err, http.StatusInternalServerError)
		return
	}
	newcid := nnode.Cid()
627

628
	i.addUserHeaders(w) // ok, _now_ write user's headers.
tavit ohanian's avatar
tavit ohanian committed
629 630
	w.Header().Set("DMS3-Hash", newcid.String())
	http.Redirect(w, r, gopath.Join(dms3PathPrefix, newcid.String(), newPath), http.StatusCreated)
631 632 633
}

func (i *gatewayHandler) deleteHandler(w http.ResponseWriter, r *http.Request) {
634 635 636
	ctx := r.Context()

	// parse the path
637

tavit ohanian's avatar
tavit ohanian committed
638
	rootCid, newPath, err := parseDms3Path(r.URL.Path)
639
	if err != nil {
640
		webError(w, "WritableGateway: failed to parse the path", err, http.StatusBadRequest)
641 642
		return
	}
643 644
	if newPath == "" || newPath == "/" {
		http.Error(w, "WritableGateway: empty path", http.StatusBadRequest)
645 646
		return
	}
647 648 649
	directory, filename := gopath.Split(newPath)

	// lookup the root
650

tavit ohanian's avatar
tavit ohanian committed
651
	rootNodeLD, err := i.api.Dag().Get(ctx, rootCid)
652
	if err != nil {
653
		webError(w, "WritableGateway: failed to resolve root CID", err, http.StatusInternalServerError)
654 655
		return
	}
tavit ohanian's avatar
tavit ohanian committed
656
	rootNode, ok := rootNodeLD.(*dag.ProtoNode)
657
	if !ok {
658
		http.Error(w, "WritableGateway: empty path", http.StatusInternalServerError)
659 660 661
		return
	}

662 663 664
	// construct the mfs root

	root, err := mfs.NewRoot(ctx, i.api.Dag(), rootNode, nil)
665
	if err != nil {
666
		webError(w, "WritableGateway: failed to construct the MFS root", err, http.StatusBadRequest)
667 668 669
		return
	}

670
	// lookup the parent directory
671

672 673 674 675
	parentNode, err := mfs.Lookup(root, directory)
	if err != nil {
		webError(w, "WritableGateway: failed to look up parent", err, http.StatusInternalServerError)
		return
676 677
	}

678 679 680
	parent, ok := parentNode.(*mfs.Directory)
	if !ok {
		http.Error(w, "WritableGateway: parent is not a directory", http.StatusInternalServerError)
681 682 683
		return
	}

684
	// delete the file
685

686 687 688 689 690
	switch parent.Unlink(filename) {
	case nil, os.ErrNotExist:
	default:
		webError(w, "WritableGateway: failed to remove file", err, http.StatusInternalServerError)
		return
691 692
	}

693
	nnode, err := root.GetDirectory().GetNode()
694
	if err != nil {
695
		webError(w, "WritableGateway: failed to finalize", err, http.StatusInternalServerError)
696
	}
697
	ncid := nnode.Cid()
698

699
	i.addUserHeaders(w) // ok, _now_ write user's headers.
tavit ohanian's avatar
tavit ohanian committed
700
	w.Header().Set("DMS3-Hash", ncid.String())
701
	// note: StatusCreated is technically correct here as we created a new resource.
tavit ohanian's avatar
tavit ohanian committed
702
	http.Redirect(w, r, gopath.Join(dms3PathPrefix+ncid.String(), directory), http.StatusCreated)
703 704
}

705 706 707 708 709 710
func (i *gatewayHandler) addUserHeaders(w http.ResponseWriter) {
	for k, v := range i.config.Headers {
		w.Header()[k] = v
	}
}

711
func webError(w http.ResponseWriter, message string, err error, defaultCode int) {
712
	if _, ok := err.(resolver.ErrNoLink); ok {
713 714 715 716 717 718 719 720 721 722 723
		webErrorWithCode(w, message, err, http.StatusNotFound)
	} else if err == routing.ErrNotFound {
		webErrorWithCode(w, message, err, http.StatusNotFound)
	} else if err == context.DeadlineExceeded {
		webErrorWithCode(w, message, err, http.StatusRequestTimeout)
	} else {
		webErrorWithCode(w, message, err, defaultCode)
	}
}

func webErrorWithCode(w http.ResponseWriter, message string, err error, code int) {
724
	http.Error(w, fmt.Sprintf("%s: %s", message, err), code)
725
	if code >= 500 {
726
		log.Warnf("server error: %s: %s", err)
727
	}
728
}
729

Simon Kirkby's avatar
Simon Kirkby committed
730 731
// return a 500 error and log
func internalWebError(w http.ResponseWriter, err error) {
732
	webErrorWithCode(w, "internalWebError", err, http.StatusInternalServerError)
Simon Kirkby's avatar
Simon Kirkby committed
733
}
734 735

func getFilename(s string) string {
tavit ohanian's avatar
tavit ohanian committed
736 737
	if (strings.HasPrefix(s, dms3PathPrefix) || strings.HasPrefix(s, dms3nsPathPrefix)) && strings.Count(gopath.Clean(s), "/") <= 2 {
		// Don't want to treat dms3.io in /dms3ns/dms3.io as a filename.
738 739 740 741
		return ""
	}
	return gopath.Base(s)
}
742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776

func (i *gatewayHandler) searchUpTreeFor404(r *http.Request, parsedPath ipath.Path) (ipath.Resolved, string, error) {
	filename404, ctype, err := preferred404Filename(r.Header.Values("Accept"))
	if err != nil {
		return nil, "", err
	}

	pathComponents := strings.Split(parsedPath.String(), "/")

	for idx := len(pathComponents); idx >= 3; idx-- {
		pretty404 := gopath.Join(append(pathComponents[0:idx], filename404)...)
		parsed404Path := ipath.New("/" + pretty404)
		if parsed404Path.IsValid() != nil {
			break
		}
		resolvedPath, err := i.api.ResolvePath(r.Context(), parsed404Path)
		if err != nil {
			continue
		}
		return resolvedPath, ctype, nil
	}

	return nil, "", fmt.Errorf("no pretty 404 in any parent folder")
}

func preferred404Filename(acceptHeaders []string) (string, string, error) {
	// If we ever want to offer a 404 file for a different content type
	// then this function will need to parse q weightings, but for now
	// the presence of anything matching HTML is enough.
	for _, acceptHeader := range acceptHeaders {
		accepted := strings.Split(acceptHeader, ",")
		for _, spec := range accepted {
			contentType := strings.SplitN(spec, ";", 1)[0]
			switch contentType {
			case "*/*", "text/*", "text/html":
tavit ohanian's avatar
tavit ohanian committed
777
				return "dms3-404.html", "text/html", nil
778 779 780 781 782 783
			}
		}
	}

	return "", "", fmt.Errorf("there is no 404 file for the requested content types")
}