Exemplo n.º 1
0
// ServeHTTP implements the httpserver.Handler interface.
func (i Internal) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {

	// Internal location requested? -> Not found.
	for _, prefix := range i.Paths {
		if httpserver.Path(r.URL.Path).Matches(prefix) {
			return http.StatusNotFound, nil
		}
	}

	// Use internal response writer to ignore responses that will be
	// redirected to internal locations
	iw := internalResponseWriter{ResponseWriter: w}
	status, err := i.Next.ServeHTTP(iw, r)

	for c := 0; c < maxRedirectCount && isInternalRedirect(iw); c++ {
		// Redirect - adapt request URL path and send it again
		// "down the chain"
		r.URL.Path = iw.Header().Get(redirectHeader)
		iw.ClearHeader()

		status, err = i.Next.ServeHTTP(iw, r)
	}

	if isInternalRedirect(iw) {
		// Too many redirect cycles
		iw.ClearHeader()
		return http.StatusInternalServerError, nil
	}

	return status, err
}
Exemplo n.º 2
0
// ServeHTTP implements the httpserver.Handler interface and serves requests,
// setting headers on the response according to the configured rules.
func (h Headers) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
	replacer := httpserver.NewReplacer(r, nil, "")
	rww := &responseWriterWrapper{w: w}
	for _, rule := range h.Rules {
		if httpserver.Path(r.URL.Path).Matches(rule.Path) {
			for name := range rule.Headers {

				// One can either delete a header, add multiple values to a header, or simply
				// set a header.

				if strings.HasPrefix(name, "-") {
					rww.delHeader(strings.TrimLeft(name, "-"))
				} else if strings.HasPrefix(name, "+") {
					for _, value := range rule.Headers[name] {
						rww.Header().Add(strings.TrimLeft(name, "+"), replacer.Replace(value))
					}
				} else {
					for _, value := range rule.Headers[name] {
						rww.Header().Set(name, replacer.Replace(value))
					}
				}
			}
		}
	}
	return h.Next.ServeHTTP(rww, r)
}
Exemplo n.º 3
0
// ServerHTTP is the HTTP handler for this middleware
func (s *Search) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
	if httpserver.Path(r.URL.Path).Matches(s.Config.Endpoint) {
		if r.Header.Get("Accept") == "application/json" || s.Config.Template == nil {
			return s.SearchJSON(w, r)
		}
		return s.SearchHTML(w, r)
	}

	record := s.Indexer.Record(r.URL.String())

	status, err := s.Next.ServeHTTP(&searchResponseWriter{w, record}, r)

	modif := w.Header().Get("Last-Modified")
	if len(modif) > 0 {
		modTime, err := time.Parse(`Mon, 2 Jan 2006 15:04:05 MST`, modif)
		if err == nil {
			record.SetModified(modTime)
		}
	}

	if status != http.StatusOK {
		record.Ignore()
	}

	go s.Pipeline.Pipe(record)

	return status, err
}
Exemplo n.º 4
0
// ServeHTTP handles requests to expvar's configured entry point with
// expvar, or passes all other requests up the chain.
func (e ExpVar) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
	if httpserver.Path(r.URL.Path).Matches(string(e.Resource)) {
		expvarHandler(w, r)
		return 0, nil
	}
	return e.Next.ServeHTTP(w, r)
}
Exemplo n.º 5
0
func (jph JsonPHandlerType) ServeHTTP(www http.ResponseWriter, req *http.Request) (int, error) {

	if db1 {
		fmt.Printf("JsonPHandlerType.ServeHTTP called\n")
	}

	for _, prefix := range jph.Paths {
		if db1 {
			fmt.Printf("Path Matches\n")
		}
		if httpserver.Path(req.URL.Path).Matches(prefix) {
			if db1 {
				fmt.Printf("A\n")
			}
			ResponseBodyRecorder := bufferhtml.NewBufferHTML()
			if db1 {
				fmt.Printf("B\n")
			}
			status, err := jph.Next.ServeHTTP(ResponseBodyRecorder, req)
			if db1 {
				fmt.Printf("C status=%d err=%s\n", status, err)
			}
			if status == 200 || status == 0 {
				// if there is a "callback" argument then use that to format the JSONp response.
				if db1 {
					fmt.Printf("D Inside, status=%d\n", status)
				}
				Prefix := ""
				Postfix := ""
				u, err := url.ParseRequestURI(req.RequestURI)
				if err != nil {
					ResponseBodyRecorder.FlushAtEnd(www, "", "")
					return status, nil
				}
				m, err := url.ParseQuery(u.RawQuery)
				if err != nil {
					ResponseBodyRecorder.FlushAtEnd(www, "", "")
					return status, nil
				}
				callback := m.Get("callback")
				if callback != "" {
					if db1 {
						fmt.Printf("Callback = -[%s]-\n", callback)
					}
					www.Header().Set("Content-Type", "application/javascript")
					Prefix = callback + "("
					Postfix = ");"
				}
				ResponseBodyRecorder.FlushAtEnd(www, Prefix, Postfix)
			} else {
				if db1 {
					fmt.Printf("E - error occured, %s\n", err)
				}
				log.Printf("Error (%s) status %d - from JSONP directive\n", err, status)
			}
			return status, err
		}
	}
	return jph.Next.ServeHTTP(www, req)
}
Exemplo n.º 6
0
// ServeHTTP handles requests to BasePath with pprof, or passes
// all other requests up the chain.
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
	if httpserver.Path(r.URL.Path).Matches(BasePath) {
		h.Mux.ServeHTTP(w, r)
		return 0, nil
	}
	return h.Next.ServeHTTP(w, r)
}
Exemplo n.º 7
0
// Rewrite rewrites the internal location of the current request.
func (r *ComplexRule) Rewrite(fs http.FileSystem, req *http.Request) (re Result) {
	rPath := req.URL.Path
	replacer := newReplacer(req)

	// validate base
	if !httpserver.Path(rPath).Matches(r.Base) {
		return
	}

	// validate extensions
	if !r.matchExt(rPath) {
		return
	}

	// validate regexp if present
	if r.Regexp != nil {
		// include trailing slash in regexp if present
		start := len(r.Base)
		if strings.HasSuffix(r.Base, "/") {
			start--
		}

		matches := r.FindStringSubmatch(rPath[start:])
		switch len(matches) {
		case 0:
			// no match
			return
		default:
			// set regexp match variables {1}, {2} ...

			// url escaped values of ? and #.
			q, f := url.QueryEscape("?"), url.QueryEscape("#")

			for i := 1; i < len(matches); i++ {
				// Special case of unescaped # and ? by stdlib regexp.
				// Reverse the unescape.
				if strings.ContainsAny(matches[i], "?#") {
					matches[i] = strings.NewReplacer("?", q, "#", f).Replace(matches[i])
				}

				replacer.Set(fmt.Sprint(i), matches[i])
			}
		}
	}

	// validate rewrite conditions
	for _, i := range r.Ifs {
		if !i.True(req) {
			return
		}
	}

	// if status is present, stop rewrite and return it.
	if r.Status != 0 {
		return RewriteStatus
	}

	// attempt rewrite
	return To(fs, req, r.To, replacer)
}
Exemplo n.º 8
0
func (l Logger) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
	for _, rule := range l.Rules {
		if httpserver.Path(r.URL.Path).Matches(rule.PathScope) {
			// Record the response
			responseRecorder := httpserver.NewResponseRecorder(w)

			// Attach the Replacer we'll use so that other middlewares can
			// set their own placeholders if they want to.
			rep := httpserver.NewReplacer(r, responseRecorder, CommonLogEmptyValue)
			responseRecorder.Replacer = rep

			// Bon voyage, request!
			status, err := l.Next.ServeHTTP(responseRecorder, r)

			if status >= 400 {
				// There was an error up the chain, but no response has been written yet.
				// The error must be handled here so the log entry will record the response size.
				if l.ErrorFunc != nil {
					l.ErrorFunc(responseRecorder, r, status)
				} else {
					// Default failover error handler
					responseRecorder.WriteHeader(status)
					fmt.Fprintf(responseRecorder, "%d %s", status, http.StatusText(status))
				}
				status = 0
			}

			// Write log entry
			rule.Log.Println(rep.Replace(rule.Format))

			return status, err
		}
	}
	return l.Next.ServeHTTP(w, r)
}
Exemplo n.º 9
0
// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met.
// If so, control is handed over to ServeListing.
func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
	var bc *Config
	// See if there's a browse configuration to match the path
	for i := range b.Configs {
		if httpserver.Path(r.URL.Path).Matches(b.Configs[i].PathScope) {
			bc = &b.Configs[i]
			goto inScope
		}
	}
	return b.Next.ServeHTTP(w, r)
inScope:

	// Browse works on existing directories; delegate everything else
	requestedFilepath, err := bc.Root.Open(r.URL.Path)
	if err != nil {
		switch {
		case os.IsPermission(err):
			return http.StatusForbidden, err
		case os.IsExist(err):
			return http.StatusNotFound, err
		default:
			return b.Next.ServeHTTP(w, r)
		}
	}
	defer requestedFilepath.Close()

	info, err := requestedFilepath.Stat()
	if err != nil {
		switch {
		case os.IsPermission(err):
			return http.StatusForbidden, err
		case os.IsExist(err):
			return http.StatusGone, err
		default:
			return b.Next.ServeHTTP(w, r)
		}
	}
	if !info.IsDir() {
		return b.Next.ServeHTTP(w, r)
	}

	// Do not reply to anything else because it might be nonsensical
	switch r.Method {
	case http.MethodGet, http.MethodHead:
		// proceed, noop
	case "PROPFIND", http.MethodOptions:
		return http.StatusNotImplemented, nil
	default:
		return b.Next.ServeHTTP(w, r)
	}

	// Browsing navigation gets messed up if browsing a directory
	// that doesn't end in "/" (which it should, anyway)
	if !strings.HasSuffix(r.URL.Path, "/") {
		staticfiles.Redirect(w, r, r.URL.Path+"/", http.StatusTemporaryRedirect)
		return 0, nil
	}

	return b.ServeListing(w, r, requestedFilepath, bc)
}
Exemplo n.º 10
0
// ShouldAllow takes a path and a request and decides if it should be allowed
func (ipf IPFilter) ShouldAllow(path IPPath, r *http.Request) (bool, string, error) {
	allow := true
	scopeMatched := ""

	// check if we are in one of our scopes.
	for _, scope := range path.PathScopes {
		if httpserver.Path(r.URL.Path).Matches(scope) {
			// extract the client's IP and parse it.
			clientIP, err := getClientIP(r, path.Strict)
			if err != nil {
				return false, scope, err
			}

			// request status.
			var rs Status

			if len(path.CountryCodes) != 0 {
				// do the lookup.
				var result OnlyCountry
				if err = ipf.Config.DBHandler.Lookup(clientIP, &result); err != nil {
					return false, scope, err
				}

				// get only the ISOCode out of the lookup results.
				clientCountry := result.Country.ISOCode
				for _, c := range path.CountryCodes {
					if clientCountry == c {
						rs.countryMatch = true
						break
					}
				}
			}

			if len(path.Nets) != 0 {
				for _, rng := range path.Nets {
					if rng.Contains(clientIP) {
						rs.inRange = true
						break
					}
				}
			}

			scopeMatched = scope
			if rs.Any() {
				// Rule matched, if the rule has IsBlock = true then we have to deny access
				allow = !path.IsBlock
			} else {
				// Rule did not match, if the rule has IsBlock = true then we have to allow access
				allow = path.IsBlock
			}

			// We only have to test the first path that matches because it is the most specific
			break
		}
	}

	// no scope match, pass-through.
	return allow, scopeMatched, nil
}
Exemplo n.º 11
0
func (u *staticUpstream) AllowedPath(requestPath string) bool {
	for _, ignoredSubPath := range u.IgnoredSubPaths {
		if httpserver.Path(path.Clean(requestPath)).Matches(path.Join(u.From(), ignoredSubPath)) {
			return false
		}
	}
	return true
}
Exemplo n.º 12
0
// AllowedPath checks if requestPath is not an ignored path.
func (r Rule) AllowedPath(requestPath string) bool {
	for _, ignoredSubPath := range r.IgnoredSubPaths {
		if httpserver.Path(path.Clean(requestPath)).Matches(path.Join(r.Path, ignoredSubPath)) {
			return false
		}
	}
	return true
}
Exemplo n.º 13
0
// ServeHTTP converts the HTTP request to a WebSocket connection and serves it up.
func (ws WebSocket) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
	for _, sockconfig := range ws.Sockets {
		if httpserver.Path(r.URL.Path).Matches(sockconfig.Path) {
			return serveWS(w, r, &sockconfig)
		}
	}

	// Didn't match a websocket path, so pass-through
	return ws.Next.ServeHTTP(w, r)
}
Exemplo n.º 14
0
// match finds the best match for a proxy config based
// on r.
func (p Proxy) match(r *http.Request) Upstream {
	var u Upstream
	var longestMatch int
	for _, upstream := range p.Upstreams {
		basePath := upstream.From()
		if !httpserver.Path(r.URL.Path).Matches(basePath) || !upstream.AllowedPath(r.URL.Path) {
			continue
		}
		if len(basePath) > longestMatch {
			longestMatch = len(basePath)
			u = upstream
		}
	}
	return u
}
Exemplo n.º 15
0
// ServeHTTP implements the httpserver.Handler interface and serves requests,
// setting headers on the response according to the configured rules.
func (h Headers) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
	replacer := httpserver.NewReplacer(r, nil, "")
	for _, rule := range h.Rules {
		if httpserver.Path(r.URL.Path).Matches(rule.Path) {
			for _, header := range rule.Headers {
				if strings.HasPrefix(header.Name, "-") {
					w.Header().Del(strings.TrimLeft(header.Name, "-"))
				} else {
					w.Header().Set(header.Name, replacer.Replace(header.Value))
				}
			}
		}
	}
	return h.Next.ServeHTTP(w, r)
}
Exemplo n.º 16
0
func (n *NLgids) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
	if r.Method != "POST" {
		return n.Next.ServeHTTP(w, r)
	}
	path := httpserver.Path(r.URL.Path)
	if !path.Matches("/api") {
		return n.Next.ServeHTTP(w, r)
	}

	switch {
	case path.Matches("/api/auth/invoice"):
		return n.WebInvoice(w, r)
	case path.Matches("/api/open/contact"):
		return n.WebContact(w, r)
	case path.Matches("/api/open/booking"):
		return n.WebBooking(w, r)
	case path.Matches("/api/open/calendar"):
		return n.WebCalendar(w, r)
	}
	return http.StatusOK, nil
}
Exemplo n.º 17
0
// ServeHTTP implements the httpserver.Handler interface.
func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {

	var hasAuth bool
	var isAuthenticated bool

	for _, rule := range a.Rules {
		for _, res := range rule.Resources {
			if !httpserver.Path(r.URL.Path).Matches(res) {
				continue
			}

			// Path matches; parse auth header
			username, password, ok := r.BasicAuth()
			hasAuth = true

			// Check credentials
			if !ok ||
				username != rule.Username ||
				!rule.Password(password) {
				//subtle.ConstantTimeCompare([]byte(password), []byte(rule.Password)) != 1 {
				continue
			}

			// Flag set only on successful authentication
			isAuthenticated = true
		}
	}

	if hasAuth {
		if !isAuthenticated {
			w.Header().Set("WWW-Authenticate", "Basic")
			return http.StatusUnauthorized, nil
		}
		// "It's an older code, sir, but it checks out. I was about to clear them."
		return a.Next.ServeHTTP(w, r)
	}

	// Pass-thru when no paths match
	return a.Next.ServeHTTP(w, r)
}
Exemplo n.º 18
0
func setup(c *caddy.Controller) error {
	rules, err := parseRules(c)
	if err != nil {
		return err
	}
	siteConfig := httpserver.GetConfig(c)
	siteConfig.AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
		return httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
			for _, rule := range rules {
				if httpserver.Path(r.URL.Path).Matches(rule.Path) {
					rule.Conf.HandleRequest(w, r)
					if cors.IsPreflight(r) {
						return 200, nil
					}
					break
				}
			}
			return next.ServeHTTP(w, r)
		})
	})
	return nil
}
Exemplo n.º 19
0
// ServeHTTP implements the httpserver.Handler interface.
func (t Templates) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
	for _, rule := range t.Rules {
		if !httpserver.Path(r.URL.Path).Matches(rule.Path) {
			continue
		}

		// Check for index files
		fpath := r.URL.Path
		if idx, ok := httpserver.IndexFile(t.FileSys, fpath, rule.IndexFiles); ok {
			fpath = idx
		}

		// Check the extension
		reqExt := path.Ext(fpath)

		for _, ext := range rule.Extensions {
			if reqExt == ext {
				// Create execution context
				ctx := httpserver.Context{Root: t.FileSys, Req: r, URL: r.URL}

				// New template
				templateName := filepath.Base(fpath)
				tpl := template.New(templateName)

				// Set delims
				if rule.Delims != [2]string{} {
					tpl.Delims(rule.Delims[0], rule.Delims[1])
				}

				// Build the template
				templatePath := filepath.Join(t.Root, fpath)
				tpl, err := tpl.ParseFiles(templatePath)
				if err != nil {
					if os.IsNotExist(err) {
						return http.StatusNotFound, nil
					} else if os.IsPermission(err) {
						return http.StatusForbidden, nil
					}
					return http.StatusInternalServerError, err
				}

				// Execute it
				var buf bytes.Buffer
				err = tpl.Execute(&buf, ctx)
				if err != nil {
					return http.StatusInternalServerError, err
				}

				templateInfo, err := os.Stat(templatePath)
				if err == nil {
					// add the Last-Modified header if we were able to read the stamp
					httpserver.SetLastModifiedHeader(w, templateInfo.ModTime())
				}
				buf.WriteTo(w)

				return http.StatusOK, nil
			}
		}
	}

	return t.Next.ServeHTTP(w, r)
}
Exemplo n.º 20
0
// ServeHTTP satisfies the httpserver.Handler interface.
func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
	for _, rule := range h.Rules {

		// First requirement: Base path must match and the path must be allowed.
		if !httpserver.Path(r.URL.Path).Matches(rule.Path) || !rule.AllowedPath(r.URL.Path) {
			continue
		}

		// In addition to matching the path, a request must meet some
		// other criteria before being proxied as FastCGI. For example,
		// we probably want to exclude static assets (CSS, JS, images...)
		// but we also want to be flexible for the script we proxy to.

		fpath := r.URL.Path

		if idx, ok := httpserver.IndexFile(h.FileSys, fpath, rule.IndexFiles); ok {
			fpath = idx
			// Index file present.
			// If request path cannot be split, return error.
			if !rule.canSplit(fpath) {
				return http.StatusInternalServerError, ErrIndexMissingSplit
			}
		} else {
			// No index file present.
			// If request path cannot be split, ignore request.
			if !rule.canSplit(fpath) {
				continue
			}
		}

		// These criteria work well in this order for PHP sites
		if !h.exists(fpath) || fpath[len(fpath)-1] == '/' || strings.HasSuffix(fpath, rule.Ext) {

			// Create environment for CGI script
			env, err := h.buildEnv(r, rule, fpath)
			if err != nil {
				return http.StatusInternalServerError, err
			}

			// Connect to FastCGI gateway
			fcgiBackend, err := rule.dialer.Dial()
			if err != nil {
				return http.StatusBadGateway, err
			}

			var resp *http.Response
			contentLength, _ := strconv.Atoi(r.Header.Get("Content-Length"))
			switch r.Method {
			case "HEAD":
				resp, err = fcgiBackend.Head(env)
			case "GET":
				resp, err = fcgiBackend.Get(env)
			case "OPTIONS":
				resp, err = fcgiBackend.Options(env)
			default:
				resp, err = fcgiBackend.Post(env, r.Method, r.Header.Get("Content-Type"), r.Body, contentLength)
			}

			if err != nil && err != io.EOF {
				return http.StatusBadGateway, err
			}

			// Write response header
			writeHeader(w, resp)

			// Write the response body
			_, err = io.Copy(w, resp.Body)
			if err != nil {
				return http.StatusBadGateway, err
			}

			defer rule.dialer.Close(fcgiBackend)

			// Log any stderr output from upstream
			if stderr := fcgiBackend.StdErr(); stderr.Len() != 0 {
				// Remove trailing newline, error logger already does this.
				err = LogError(strings.TrimSuffix(stderr.String(), "\n"))
			}

			// Normally we would return the status code if it is an error status (>= 400),
			// however, upstream FastCGI apps don't know about our contract and have
			// probably already written an error page. So we just return 0, indicating
			// that the response body is already written. However, we do return any
			// error value so it can be logged.
			// Note that the proxy middleware works the same way, returning status=0.
			return 0, err
		}
	}

	return h.Next.ServeHTTP(w, r)
}
Exemplo n.º 21
0
// ShouldCompress checks if the request path matches any of the
// registered paths to ignore. It returns false if an ignored path
// is found and true otherwise.
func (p PathFilter) ShouldCompress(r *http.Request) bool {
	return !p.IgnoredPaths.ContainsFunc(func(value string) bool {
		return httpserver.Path(r.URL.Path).Matches(value)
	})
}
Exemplo n.º 22
0
// ServeHTTP implements the http.Handler interface.
func (md Markdown) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
	var cfg *Config
	for _, c := range md.Configs {
		if httpserver.Path(r.URL.Path).Matches(c.PathScope) { // not negated
			cfg = c
			break // or goto
		}
	}
	if cfg == nil {
		return md.Next.ServeHTTP(w, r) // exit early
	}

	// We only deal with HEAD/GET
	switch r.Method {
	case http.MethodGet, http.MethodHead:
	default:
		return http.StatusMethodNotAllowed, nil
	}

	var dirents []os.FileInfo
	var lastModTime time.Time
	fpath := r.URL.Path
	if idx, ok := httpserver.IndexFile(md.FileSys, fpath, md.IndexFiles); ok {
		// We're serving a directory index file, which may be a markdown
		// file with a template.  Let's grab a list of files this directory
		// URL points to, and pass that in to any possible template invocations,
		// so that templates can customize the look and feel of a directory.
		fdp, err := md.FileSys.Open(fpath)
		switch {
		case err == nil: // nop
		case os.IsPermission(err):
			return http.StatusForbidden, err
		case os.IsExist(err):
			return http.StatusNotFound, nil
		default: // did we run out of FD?
			return http.StatusInternalServerError, err
		}
		defer fdp.Close()

		// Grab a possible set of directory entries.  Note, we do not check
		// for errors here (unreadable directory, for example).  It may
		// still be useful to have a directory template file, without the
		// directory contents being present.  Note, the directory's last
		// modification is also present here (entry ".").
		dirents, _ = fdp.Readdir(-1)
		for _, d := range dirents {
			lastModTime = latest(lastModTime, d.ModTime())
		}

		// Set path to found index file
		fpath = idx
	}

	// If not supported extension, pass on it
	if _, ok := cfg.Extensions[path.Ext(fpath)]; !ok {
		return md.Next.ServeHTTP(w, r)
	}

	// At this point we have a supported extension/markdown
	f, err := md.FileSys.Open(fpath)
	switch {
	case err == nil: // nop
	case os.IsPermission(err):
		return http.StatusForbidden, err
	case os.IsExist(err):
		return http.StatusNotFound, nil
	default: // did we run out of FD?
		return http.StatusInternalServerError, err
	}
	defer f.Close()

	fs, err := f.Stat()
	if err != nil {
		return http.StatusGone, nil
	}
	lastModTime = latest(lastModTime, fs.ModTime())

	ctx := httpserver.Context{
		Root: md.FileSys,
		Req:  r,
		URL:  r.URL,
	}
	html, err := cfg.Markdown(title(fpath), f, dirents, ctx)
	if err != nil {
		return http.StatusInternalServerError, err
	}

	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	w.Header().Set("Content-Length", strconv.FormatInt(int64(len(html)), 10))
	httpserver.SetLastModifiedHeader(w, lastModTime)
	if r.Method == http.MethodGet {
		w.Write(html)
	}
	return http.StatusOK, nil
}