-
Notifications
You must be signed in to change notification settings - Fork 20
/
Copy pathmain.go
257 lines (228 loc) · 6.35 KB
/
main.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
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
/*
Copyright 2011 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
webfront is an HTTP server and reverse proxy.
It reads a JSON-formatted rule file like this:
[
{"Host": "example.com", "Serve": "/var/www"},
{"Host": "example.org", "Forward": "localhost:8080"}
]
For all requests to the host example.com (or any name ending in
".example.com") it serves files from the /var/www directory.
For requests to example.org, it forwards the request to the HTTP
server listening on localhost port 8080.
Usage of webfront:
-http address
HTTP listen address (default ":http")
-letsencrypt_cache directory
letsencrypt cache directory (default is to disable HTTPS)
-poll interval
rule file poll interval (default 10s)
-rules file
rule definition file
webfront was written by Andrew Gerrand <[email protected]>
*/
package main
import (
"context"
"crypto/tls"
"encoding/json"
"flag"
"fmt"
"log"
"net/http"
"net/http/httputil"
"os"
"strings"
"sync"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"golang.org/x/crypto/acme/autocert"
)
var (
httpAddr = flag.String("http", ":http", "HTTP listen `address`")
metricsAddr = flag.String("metrics", "", "metrics HTTP listen `address`")
letsCacheDir = flag.String("letsencrypt_cache", "", "letsencrypt cache `directory` (default is to disable HTTPS)")
ruleFile = flag.String("rules", "", "rule definition `file`")
pollInterval = flag.Duration("poll", time.Second*10, "rule file poll `interval`")
)
var hitCounter = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "webfront_hits",
Help: "Cumulative hits since startup.",
},
[]string{"host"},
)
func init() {
prometheus.MustRegister(hitCounter)
}
func main() {
flag.Parse()
s, err := NewServer(*ruleFile, *pollInterval)
if err != nil {
log.Fatal(err)
}
if *metricsAddr != "" {
go func() {
log.Fatal(http.ListenAndServe(*metricsAddr, promhttp.Handler()))
}()
}
if *letsCacheDir != "" {
m := &autocert.Manager{
Cache: autocert.DirCache(*letsCacheDir),
Prompt: autocert.AcceptTOS,
HostPolicy: s.hostPolicy,
}
c := tls.Config{GetCertificate: m.GetCertificate}
l, err := tls.Listen("tcp", ":https", &c)
if err != nil {
log.Fatal(err)
}
go func() {
log.Fatal(http.Serve(l, s))
}()
log.Fatal(http.ListenAndServe(*httpAddr, m.HTTPHandler(s)))
} else {
log.Fatal(http.ListenAndServe(*httpAddr, s))
}
}
// Server implements an http.Handler that acts as either a reverse proxy or
// a simple file server, as determined by a rule set.
type Server struct {
mu sync.RWMutex // guards the fields below
last time.Time
rules []*Rule
}
// Rule represents a rule in a configuration file.
type Rule struct {
Host string // to match against request Host header
Forward string // non-empty if reverse proxy
Serve string // non-empty if file server
handler http.Handler
}
// NewServer constructs a Server that reads rules from file with a period
// specified by poll.
func NewServer(file string, poll time.Duration) (*Server, error) {
s := new(Server)
if err := s.loadRules(file); err != nil {
return nil, err
}
go s.refreshRules(file, poll)
return s, nil
}
// ServeHTTP matches the Request with a Rule and, if found, serves the
// request with the Rule's handler.
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if h := s.handler(r); h != nil {
h.ServeHTTP(w, r)
return
}
http.Error(w, "Not found.", http.StatusNotFound)
}
// handler returns the appropriate Handler for the given Request,
// or nil if none found.
func (s *Server) handler(req *http.Request) http.Handler {
s.mu.RLock()
defer s.mu.RUnlock()
h := req.Host
// Some clients include a port in the request host; strip it.
if i := strings.Index(h, ":"); i >= 0 {
h = h[:i]
}
for _, r := range s.rules {
if h == r.Host || strings.HasSuffix(h, "."+r.Host) {
hitCounter.With(prometheus.Labels{"host": r.Host}).Inc()
return r.handler
}
}
return nil
}
// refreshRules polls file periodically and refreshes the Server's rule
// set if the file has been modified.
func (s *Server) refreshRules(file string, poll time.Duration) {
for {
if err := s.loadRules(file); err != nil {
log.Println(err)
}
time.Sleep(poll)
}
}
// loadRules tests whether file has been modified since its last invocation
// and, if so, loads the rule set from file.
func (s *Server) loadRules(file string) error {
fi, err := os.Stat(file)
if err != nil {
return err
}
mtime := fi.ModTime()
if !mtime.After(s.last) && s.rules != nil {
return nil // no change
}
rules, err := parseRules(file)
if err != nil {
return err
}
s.mu.Lock()
s.last = mtime
s.rules = rules
s.mu.Unlock()
return nil
}
// hostPolicy implements autocert.HostPolicy by consulting
// the rules list for a matching host name.
func (s *Server) hostPolicy(ctx context.Context, host string) error {
s.mu.RLock()
defer s.mu.RUnlock()
for _, rule := range s.rules {
if host == rule.Host || host == "www."+rule.Host {
return nil
}
}
return fmt.Errorf("unrecognized host %q", host)
}
// parseRules reads rule definitions from file, constructs the Rule handlers,
// and returns the resultant Rules.
func parseRules(file string) ([]*Rule, error) {
f, err := os.Open(file)
if err != nil {
return nil, err
}
defer f.Close()
var rules []*Rule
if err := json.NewDecoder(f).Decode(&rules); err != nil {
return nil, err
}
for _, r := range rules {
r.handler = makeHandler(r)
if r.handler == nil {
log.Printf("bad rule: %#v", r)
}
}
return rules, nil
}
// makeHandler constructs the appropriate Handler for the given Rule.
func makeHandler(r *Rule) http.Handler {
if h := r.Forward; h != "" {
return &httputil.ReverseProxy{
Director: func(req *http.Request) {
req.URL.Scheme = "http"
req.URL.Host = h
},
}
}
if d := r.Serve; d != "" {
return http.FileServer(http.Dir(d))
}
return nil
}