-
Notifications
You must be signed in to change notification settings - Fork 8
/
api.go
208 lines (186 loc) · 5.8 KB
/
api.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
package main
import (
"database/sql"
"github.com/coocood/jas"
"math/rand"
"net"
"net/http"
"path"
"time"
)
// Api is the JAS-required type which is passed to all API-related
// functions.
type Api struct{}
var (
ReadOnlyError = jas.NewRequestError("database in readonly mode")
)
// apiStatus is a simple summary of the current NodeAtlas instance
// status.
type apiStatus struct {
Name string `json:"name"`
Nodes int `json:"nodes"`
Available int `json:"available"`
}
// RegisterAPI invokes http.Handle() with a JAS router using the
// default net/http server. It will respond to any URL "<prefix>/api".
func RegisterAPI(prefix string) {
// Initialize a JAS router with appropriate attributes.
router := jas.NewRouter(new(Api))
router.BasePath = path.Join("/", prefix)
// Disable automatic internal error logging.
router.InternalErrorLogger = nil
l.Debug("API paths:\n", router.HandledPaths(true))
// Seed the random number generator with the current Unix
// time. This is not random, but it should be Good Enough.
rand.Seed(time.Now().Unix())
// Handle "<prefix>/api/". Note that it must begin and end with /.
http.Handle(path.Join("/", prefix, "api")+"/", router)
}
// GetStatus responds with a status summary of the map, including the
// map name, total number of nodes, number available (pingable), etc.
// (Not yet implemented.)
func (*Api) GetStatus(ctx *jas.Context) {
ctx.Data = apiStatus{
Name: Conf.Name,
Nodes: Db.LenNodes(false),
}
}
// GetNode retrieves a single node from the database, removes
// sensitive data (such as an email address) and sets ctx.Data to it.
func (*Api) GetNode(ctx *jas.Context) {
ip := IP(net.ParseIP(ctx.RequireString("address")))
if ip == nil {
// If this is encountered, the address was incorrectly
// formatted.
ctx.Error = jas.NewRequestError("addressInvalid")
return
}
node, err := Db.GetNode(ip)
if err != nil {
// If there has been a database error, log it and report the
// failure.
ctx.Error = jas.NewInternalError(err)
l.Err(err)
return
}
if node == nil {
// If there are simply no matching nodes, set the error and
// return.
ctx.Error = jas.NewRequestError("No matching node")
return
}
// Remove any sensitive data.
node.OwnerEmail = ""
// Finally, set the data and exit.
ctx.Data = node
}
// PostNode creates a *Node from the submitted form and queues it for
// addition with a positive 64 bit integer as an ID.
func (*Api) PostNode(ctx *jas.Context) {
if Db.ReadOnly {
// If the database is readonly, set that as the error and
// return.
ctx.Error = ReadOnlyError
return
}
// Initialize the node and retrieve fields.
node := new(Node)
ip := IP(net.ParseIP(ctx.RequireString("address")))
if ip == nil {
// If the address is invalid, return that error.
ctx.Error = jas.NewRequestError("addressInvalid")
return
}
node.Addr = ip
node.Latitude = ctx.RequireFloat("latitude")
node.Longitude = ctx.RequireFloat("longitude")
node.OwnerName = ctx.RequireString("name")
node.OwnerEmail = ctx.RequireString("email")
status, _ := ctx.FindInt("status")
node.Status = int(status)
// TODO(DuoNoxSol): Authenticate/limit node registration.
// If SMTP is not disabled, send an email to verify. In future
// versions, SMTP will be required to verify nodes, unless
// explicitly set.
if Conf.SMTP != nil {
id := rand.Int63() // Pseudo-random positive int64
if err := Db.QueueNode(id, Conf.VerificationExpiration,
node); err != nil {
// If there is a database failure, report it as an
// internal error.
ctx.Error = jas.NewInternalError(err)
l.Err(err)
return
}
if err := SendVerificationEmail(id, node); err != nil {
// If the sending of the email fails, set the internal
// error, log it, and remove the node from the database.
ctx.Error = jas.NewInternalError(err)
l.Err(err)
// The cached node will be removed when it expires, so it
// may not be worth it to do it here.
return
}
ctx.Data = "verification email sent"
l.Infof("Node %q entered, waiting for verification", ip)
} else {
err := Db.AddNode(node)
if err != nil {
// If there was an error, log it and report the failure.
ctx.Error = jas.NewInternalError(err)
l.Err(err)
return
}
ctx.Data = "node registered"
l.Infof("Node %q registered\n", ip)
}
}
// GetVerify moves a node from the verification queue to the normal
// database, as identified by its long random ID.
func (*Api) GetVerify(ctx *jas.Context) {
id := ctx.RequireInt("id")
ip, err := Db.VerifyQueuedNode(id)
if err == sql.ErrNoRows {
// If we encounter a ErrNoRows, then there was no node with
// that ID. Report it.
ctx.Error = jas.NewRequestError("invalid id")
l.Noticef("%q attempted to verify invalid ID\n", ctx.RemoteAddr)
return
} else if err != nil {
// If we encounter any other database error, it is an internal
// error and needs to be logged.
ctx.Error = jas.NewInternalError(err)
l.Err(err)
return
}
// If there was no error, inform the user that it was successful,
// and log it.
ctx.Data = "successful"
l.Infof("Node %q verified", ip)
}
// GetAll dumps the entire database of nodes, including cached
// ones. If the form value 'geojson' is present, then the "data" field
// contains the dump in GeoJSON compliant form.
func (*Api) GetAll(ctx *jas.Context) {
nodes, err := Db.DumpNodes()
if err != nil {
ctx.Error = jas.NewInternalError(err)
l.Err(err)
return
}
// We must invoke ParseForm() so that we can access ctx.Form.
ctx.ParseForm()
// If the form value 'geojson' is included, dump in GeoJSON
// form. Otherwise, just dump with normal marhshalling.
if _, ok := ctx.Form["geojson"]; ok {
ctx.Data = FeatureCollectionNodes(nodes)
} else {
mappedNodes, err := Db.CacheFormatNodes(nodes)
if err != nil {
ctx.Error = jas.NewInternalError(err)
l.Err(err)
return
}
ctx.Data = mappedNodes
}
}