summaryrefslogtreecommitdiff
path: root/http/server.go
blob: b0d0b56abd0996038da170603e3ca414c9ae0dc7 (plain)
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
package http

import (
	"github.com/spiral/roadrunner"
	"net/http"
	"strings"
	"path"
	"github.com/sirupsen/logrus"
	"os"
	"path/filepath"
	"encoding/json"
)

var (
	excludeFiles = []string{".php", ".htaccess"}
)

// Configures http rr
type Config struct {
	// ServeStatic enables static file serving from desired root directory.
	ServeStatic bool

	// Root directory, required when ServeStatic set to true.
	Root string
}

// Server serves http connections to underlying PHP application using PSR-7 protocol. Context will include request headers,
// parsed files and query, payload will include parsed form data (if any) - todo: do we need to do that?.
type Server struct {
	cfg  Config
	root http.Dir
	rr   *roadrunner.Server
}

// NewServer returns new instance of Server PSR7 server.
func NewServer(cfg Config, server *roadrunner.Server) *Server {
	h := &Server{cfg: cfg, rr: server}
	if cfg.ServeStatic {
		h.root = http.Dir(h.cfg.Root)
	}

	return h
}

// ServeHTTP serve using PSR-7 requests passed to underlying application.
func (svr *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) () {
	if svr.cfg.ServeStatic && svr.serveStatic(w, r) {
		// server always attempt to serve static files first
		return
	}

	req, err := ParseRequest(r)
	if err != nil {
		w.WriteHeader(500)
		w.Write([]byte(err.Error())) //todo: better errors
		return
	}
	defer req.Close()

	rsp, err := svr.rr.Exec(req.Payload())
	if err != nil {
		w.WriteHeader(500)
		w.Write([]byte(err.Error())) //todo: better errors
		return
	}

	resp := &Response{}
	if err = json.Unmarshal(rsp.Context, resp); err != nil {
		w.WriteHeader(500)
		w.Write([]byte(err.Error())) //todo: better errors
		return
	}

	resp.Write(w)
	w.Write(rsp.Body)
}

// serveStatic attempts to serve static file and returns true in case of success, will return false in case if file not
// found, not allowed or on read error.
func (svr *Server) serveStatic(w http.ResponseWriter, r *http.Request) bool {
	fpath := r.URL.Path
	if !strings.HasPrefix(fpath, "/") {
		fpath = "/" + fpath
	}
	fpath = path.Clean(fpath)

	if svr.excluded(fpath) {
		logrus.Warningf("attempt to access forbidden file %s", fpath)
		return false
	}

	f, err := svr.root.Open(fpath)
	if err != nil {
		if !os.IsNotExist(err) {
			// rr or access error
			logrus.Error(err)
		}

		return false
	}
	defer f.Close()

	d, err := f.Stat()
	if err != nil {
		// rr error
		logrus.Error(err)
		return false
	}

	if d.IsDir() {
		// we are not serving directories
		return false
	}

	http.ServeContent(w, r, d.Name(), d.ModTime(), f)
	return true
}

// excluded returns true if file has forbidden extension.
func (svr *Server) excluded(path string) bool {
	ext := strings.ToLower(filepath.Ext(path))
	for _, exl := range excludeFiles {
		if ext == exl {
			return true
		}
	}

	return false
}