This repository was archived by the owner on Nov 14, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathfourohfourfound.go
224 lines (193 loc) · 6.56 KB
/
fourohfourfound.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
// fourohfourfound is a fallback HTTP server that may redirect requests.
// It is primarily for creating redirections for web servers like nginx
// where you would otherwise have to edit the configuration and restart to
// modify redirections. Eventually, it will provide statistics for tracking
// if you are, for example, placing these redirected urls on physical ads.
package main
import (
"bytes"
"encoding/json"
"flag"
"io"
"io/ioutil"
"log"
"net/http"
"strconv"
"strings"
"sync"
)
// The host to listen on.
var host *string = flag.String("host", "localhost", "listen host")
// The port to listen on.
var port *int = flag.Int("port", 4404, "listen port")
// The location of a JSON configuration file specifying the redirections.
var configFile *string = flag.String("config", "config.json", "configuration file")
// Configuration file format:
//
// {
// "redirections": {
// "source":"destination",
// "another source":"another destination",
// ...
// }
// }
// The redirection code to send to clients.
var redirectionCode *int = flag.Int("code", 302, "redirection code")
// The configuration for the handlers includes the redirection code (e.g., 301) and
// a mapping of /source to /destination redirections.
type Redirector struct {
code int
mu sync.RWMutex
Redirections map[string]string `json:"redirections"`
}
// Create a new Redirector with a default code of StatusFound (302) and an empty redirections map.
func NewRedirector() *Redirector {
return &Redirector{code: http.StatusFound, Redirections: make(map[string]string)}
}
// The remote address is either the client's address or X-Real-Ip, if set.
// X-Real-Ip must be sent by the forwarding server to us.
func realAddr(req *http.Request) (addr string) {
if headerAddr := req.Header.Get("X-Real-Ip"); headerAddr != "" {
return headerAddr
}
return req.RemoteAddr
}
// A handler wrapped with onlyLocal will return http.StatusUnauthorized if the client
// is not localhost. The upstream server must send X-Real-Ip to work properly.
func onlyLocal(w http.ResponseWriter, req *http.Request, fn func()) {
addr := strings.SplitN(realAddr(req), ":", 2)[0]
switch addr {
case "localhost", "127.0.0.1":
fn()
default:
http.Error(w, "Unauthorized", http.StatusUnauthorized)
}
}
// Get will redirect the client if the path is found in the redirections map.
// Otherwise, a 404 is returned.
func (redir *Redirector) Get(w http.ResponseWriter, req *http.Request) {
redir.mu.RLock()
defer redir.mu.RUnlock()
if destination, ok := redir.Redirections[req.URL.Path]; ok {
log.Println(realAddr(req), "redirected from", req.URL.Path, "to", destination)
http.Redirect(w, req, destination, redir.code)
} else {
log.Println(realAddr(req), "sent 404 for", req.URL.Path)
http.NotFound(w, req)
}
}
// Put will add a redirection from the PUT path to the path specified in the
// request's data.
func (redir *Redirector) Put(w http.ResponseWriter, req *http.Request) {
redir.mu.Lock()
defer redir.mu.Unlock()
// TODO: Require authorization to change redirections
buf := new(bytes.Buffer)
io.Copy(buf, req.Body)
destination := buf.String()
redir.Redirections[req.URL.Path] = destination
log.Println(realAddr(req), "added redirection from", req.URL.Path, "to", destination)
}
// Delete removes the redirection at the specified path.
func (redir *Redirector) Delete(w http.ResponseWriter, req *http.Request) {
redir.mu.Lock()
defer redir.mu.Unlock()
// TODO: Require authorization to delete redirections
delete(redir.Redirections, req.URL.Path)
log.Println(realAddr(req), "removed redirection for", req.URL.Path)
}
func (redir *Redirector) ServeHTTP(w http.ResponseWriter, req *http.Request) {
switch req.Method {
case "GET":
redir.Get(w, req)
case "PUT":
onlyLocal(w, req, func() { redir.Put(w, req) })
case "DELETE":
onlyLocal(w, req, func() { redir.Delete(w, req) })
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
// Use the specified JSON configuration to configure the Redirector.
func (redir *Redirector) LoadConfig(config []byte) (err error) {
redir.mu.Lock()
defer redir.mu.Unlock()
err = json.Unmarshal(config, redir)
log.Printf("%d redirections loaded\n", len(redir.Redirections))
return
}
// Read the JSON configuration from a file to configure the Redirector.
func (redir *Redirector) LoadConfigFile(config string) (err error) {
bytes, err := ioutil.ReadFile(config)
if err != nil {
return
}
err = redir.LoadConfig(bytes)
return
}
// GETting the config supplies the client with a JSON formatted configuration
// suitable for storing as the configuration file.
func (redir *Redirector) GetConfig(w http.ResponseWriter, req *http.Request) {
redir.mu.RLock()
defer redir.mu.RUnlock()
jsonConfig, err := json.MarshalIndent(redir, "", " ")
if err != nil {
http.Error(w, "Error encoding JSON config", http.StatusInternalServerError)
return
}
io.WriteString(w, string(jsonConfig))
}
// Set the Redirector configuration from the JSON supplied in the PUT
// request's data.
func (redir *Redirector) SetConfig(w http.ResponseWriter, req *http.Request) {
buf := new(bytes.Buffer)
io.Copy(buf, req.Body)
err := redir.LoadConfig(buf.Bytes())
if err != nil {
http.Error(w, "Error decoding JSON config", http.StatusInternalServerError)
return
}
io.WriteString(w, "Configuration successfully loaded.\n")
}
// When deleted, the Redirector configuration is emptied.
func (redir *Redirector) DeleteConfig(w http.ResponseWriter, req *http.Request) {
redir.mu.Lock()
defer redir.mu.Unlock()
redir.Redirections = make(map[string]string)
}
// The ConfigHandler handles retrieving the Redirector configuration (GET) and
// setting it (PUT) through the configuration path.
func (redir *Redirector) ConfigHandler() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, req *http.Request) {
log.Println(realAddr(req), req.Method, req.URL.Path)
onlyLocal(w, req,
func() {
switch req.Method {
case "GET":
redir.GetConfig(w, req)
case "PUT":
redir.SetConfig(w, req)
case "DELETE":
redir.DeleteConfig(w, req)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
})
}
}
func main() {
flag.Parse()
addr := *host + ":" + strconv.Itoa(*port)
redirector := NewRedirector()
redirector.code = *redirectionCode
err := redirector.LoadConfigFile(*configFile)
if err != nil {
log.Fatal("LoadConfigFile: ", err)
}
http.Handle("/", redirector)
http.HandleFunc("/_config", redirector.ConfigHandler())
err = http.ListenAndServe(addr, nil)
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}