├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── api.go ├── cache.go ├── captcha.go ├── conf.json.example ├── config.go ├── database.go ├── depslist ├── doc.go ├── doc ├── API.md └── CONFIGURATION.md ├── edges.go ├── getdeps.sh ├── import.go ├── nodeatlas.go ├── nodes.go ├── packaging └── PKGBUILD ├── res ├── email │ ├── message.txt │ └── verification.txt └── web │ ├── about │ └── index.html.tmpl │ ├── assets │ └── .gitignore │ ├── css │ └── style.css │ ├── custom │ └── .gitignore │ ├── img │ ├── icon │ │ └── nodeatlas.png │ ├── inactive.png │ ├── newUser.png │ ├── node.png │ ├── shadow.png │ └── vps.png │ ├── index.html.tmpl │ ├── js │ ├── captcha.js │ ├── config.js.tmpl │ ├── distance.js │ ├── form.js │ ├── icon.js │ ├── layers.js │ ├── loadmap.js │ ├── node.js │ ├── peers.js │ ├── search.js │ ├── status.js │ └── verify.js │ └── robots.txt ├── smtp.go ├── static.go ├── verify.go └── web.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Particular binaries 2 | nodeatlas 3 | 4 | # Configuration files 5 | conf.json 6 | 7 | # Ignore build files 8 | build/ 9 | 10 | # Logs 11 | *.log 12 | 13 | # Sqlite3 databases 14 | *.db 15 | 16 | # Any user-installed icons 17 | res/icon/* 18 | 19 | # Editor temporary files 20 | *~ 21 | *#* 22 | *.swp 23 | 24 | # Extra files on OS X 25 | .DS_Store 26 | ._* 27 | 28 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 29 | *.o 30 | *.a 31 | *.so 32 | 33 | # Folders 34 | _obj 35 | _test 36 | 37 | # Architecture specific extensions/prefixes 38 | *.[568vq] 39 | [568vq].out 40 | 41 | *.cgo1.go 42 | *.cgo2.c 43 | _cgo_defun.c 44 | _cgo_gotypes.go 45 | _cgo_export.* 46 | 47 | _testmain.go 48 | 49 | *.exe 50 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.1 5 | - 1.2 6 | - tip 7 | 8 | before_install: 9 | - go get 10 | 11 | install: 12 | - make 13 | - sudo make install 14 | 15 | after_success: 16 | - go test -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROGRAM_NAME := $(shell basename $(shell pwd)) 2 | VERSION := $(shell git describe --dirty=+) 3 | 4 | ifndef GOCOMPILER 5 | GOCOMPILER = go build $(GOFLAGS) 6 | endif 7 | 8 | # If the root and prefix are not yet defined, define them here. 9 | ifndef DESTDIR 10 | DESTDIR = / 11 | endif 12 | 13 | ifndef prefix 14 | prefix = usr/local 15 | endif 16 | 17 | GOFLAGS += -ldflags "-X main.Version $(VERSION) \ 18 | -X main.defaultResLocation $(DESTDIR)/$(prefix)/share/nodeatlas/ \ 19 | -X main.defaultConfLocation $(DESTDIR)etc/nodeatlas.conf" 20 | 21 | .PHONY: all install clean deps 22 | 23 | 24 | # DEPS are non-hidden files found in the assets directory. Because we 25 | # are using the glob rather than `wildcard`, this rule is *not* 26 | # skipped when there are no visible files in the deps directory. 27 | DEPSFILE := depslist 28 | DEPS := res/web/assets/* 29 | 30 | all: $(DEPS) $(PROGRAM_NAME) 31 | 32 | $(PROGRAM_NAME): $(wildcard *.go) 33 | $(GOCOMPILER) 34 | 35 | # Download dependencies if the dependency list has changed more 36 | # recently than the directory (or the directory is empty). 37 | $(DEPS): $(DEPSFILE) 38 | @- $(RM) $(wildcard $(DEPS)) 39 | - ./getdeps.sh 40 | 41 | install: all 42 | test -d $(DESTDIR)/$(prefix)/bin || mkdir -p $(DESTDIR)/$(prefix)/bin 43 | test -d $(DESTDIR)/$(prefix)/share/nodeatlas || \ 44 | mkdir -p $(DESTDIR)/$(prefix)/share/nodeatlas 45 | 46 | install -m 0755 $(PROGRAM_NAME) $(DESTDIR)/$(prefix)/bin/nodeatlas 47 | rm -rf $(DESTDIR)/$(prefix)/share/nodeatlas 48 | cp --no-preserve=all -r res $(DESTDIR)/$(prefix)/share/nodeatlas 49 | 50 | clean: 51 | @- $(RM) $(PROGRAM_NAME) $(DEPS) 52 | 53 | pkg_arch: 54 | mkdir -p build 55 | sed "s/pkgver=.*/pkgver=$(shell git describe | sed \ 56 | 's/-/_/g')/" < packaging/PKGBUILD \ 57 | | sed 's/$${_gitver}/'$(shell git rev-parse HEAD)/g > build/PKGBUILD 58 | updpkgsums build/PKGBUILD 59 | 60 | # vim: set noexpandtab: 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NodeAtlas 2 | ## Federated node mapping for mesh networks 3 | 4 | [![Build Status](https://travis-ci.org/ProjectMeshnet/nodeatlas.png?branch=master)](https://travis-ci.org/ProjectMeshnet/nodeatlas) 5 | 6 | NodeAtlas is a high-performance and very portable tool for 7 | geographically mapping mesh networks. It is used and designed by 8 | [Project Meshnet][Atlas]. 9 | 10 | [Atlas]: http://atlas.projectmeshnet.org 11 | [ProjectMeshnet]: https://projectmeshnet.org 12 | 13 | It runs as a server which provides a web interface with two parts: a 14 | map, and an API. The mapping portion provides a comfortable and 15 | functional user interface using [Bootstrap][]. The map itself is 16 | provided by [Leafletjs][], which loads tiles from [OpenStreetMap][] 17 | (by default). Nodes are loaded by [JQuery][] from the API. 18 | 19 | [Bootstrap]: http://twitter.github.io/bootstrap/ 20 | [Leafletjs]: http://leafletjs.com 21 | [JQuery]: http://jquery.com 22 | [OpenStreetMap]: http://www.openstreetmap.org 23 | 24 | The NodeAtlas itself is written in [Go][], and its API is powered by 25 | [JAS][], a RESTful JSON API framework. 26 | 27 | [Go]: http://golang.org 28 | [JAS]: https://github.com/coocood/jas#jas 29 | 30 | In addition to the API, the Go backend provides a simple and powerful 31 | means of federation. Child maps are specified in the configuration, 32 | and NodeAtlas regularly queries their APIs, and pulls a list of node 33 | information, including nodes from sub-children, when are then 34 | displayed on the parent instance. This way, NodeAtlas is capable of 35 | acting as a regional map, incorporating nodes from multiple more 36 | localized instances. (More documentation on this behavior will be 37 | added in the future.) 38 | 39 | 40 | ## Install 41 | 42 | Currently the only option to install NodeAtlas is to compile from 43 | source. In the future it will be packaged for ease. 44 | 45 | Clone the repository: 46 | 47 | ``` 48 | git clone https://github.com/ProjectMeshnet/nodeatlas.git 49 | ``` 50 | Get go packages needed to build: 51 | ``` 52 | go get 53 | ``` 54 | Build the binary 55 | ``` 56 | make 57 | ``` 58 | Install on system: 59 | ``` 60 | sudo make install 61 | ``` 62 | 63 | ## Configuration 64 | 65 | NodeAtlas needs a configuration file. By default, NodeAtlas looks for 66 | `conf.json` in the current directory. There is a file called 67 | `conf.json.example` in the repository, which is a template for what 68 | the configuration file should look like. 69 | 70 | You can tell NodeAtlas to use a configuration file from anywhere else 71 | by using the `--conf` flag. For example: 72 | 73 | ``` 74 | nodeatlas --res res/ --conf /etc/nodeatlas.json 75 | ``` 76 | 77 | For documentation on what exactly every line in your configuration 78 | file does, see [CONFIGURATION.md][] in the `doc` folder. 79 | 80 | [CONFIGURATION.md]: ./doc/CONFIGURATION.md 81 | 82 | ## API 83 | 84 | NodeAtlas has a RESTful JSON API. For documentation on the NodeAtlas 85 | API, see [API.md][] in the `doc` folder. 86 | 87 | [API.md]: ./doc/API.md 88 | 89 | ## Contributing 90 | 91 | If you see something that needs fixing, or you can think of something 92 | that could make NodeAtlas better, please feel free to open an issue or 93 | submit a pull request. Check the open issues before doing so! If there 94 | is already an issue open for what you want to help with, don't open 95 | another; one will suffice. All issues and pull requests are welcome, 96 | and encouraged. 97 | 98 | ## Copyright & License 99 | 100 | © Alexander Bauer, Luke Evers, Dylan Whichard, and 101 | contributors. NodeAtlas is licensed under GPLv3. See [LICENSE][] for 102 | full detials. 103 | 104 | [LICENSE]: ./LICENSE 105 | -------------------------------------------------------------------------------- /api.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Copyright (C) 2013 Alexander Bauer, Luke Evers, Dylan Whichard, 4 | // and contributors; (GPLv3) see LICENSE or doc.go 5 | 6 | import ( 7 | "database/sql" 8 | "github.com/coocood/jas" 9 | "github.com/dchest/captcha" 10 | "html" 11 | "html/template" 12 | "math/rand" 13 | "net" 14 | "net/http" 15 | "path" 16 | "regexp" 17 | "time" 18 | ) 19 | 20 | const ( 21 | APIDocs = "https://github.com/ProjectMeshnet/nodeatlas/blob/master/doc/API.md" 22 | ) 23 | 24 | var ( 25 | PGPRegexp = regexp.MustCompilePOSIX("^[0-9A-Fa-f]{8}{0,2}$") 26 | EmailRegexp = regexp.MustCompilePOSIX("^[a-z0-9._%+-]+@([a-z0-9-]+\\.)+[a-z]+$") 27 | ) 28 | 29 | // Api is the JAS-required type which is passed to all API-related 30 | // functions. 31 | type Api struct{} 32 | 33 | var ( 34 | ActiveTokens = make(map[uint32]token) 35 | ) 36 | 37 | type token struct { 38 | IP string 39 | Issued time.Time 40 | } 41 | 42 | var ( 43 | ReadOnlyError = jas.NewRequestError("database in readonly mode") 44 | ) 45 | 46 | // RegisterAPI invokes http.Handle() with a JAS router using the 47 | // default net/http server. It will respond to any URL "/api". 48 | func RegisterAPI(prefix string) { 49 | // Initialize a JAS router with appropriate attributes. 50 | router := jas.NewRouter(new(Api)) 51 | router.BasePath = path.Join("/", prefix) 52 | // Disable automatic internal error logging. 53 | router.InternalErrorLogger = nil 54 | 55 | l.Debug("API paths:\n", router.HandledPaths(true)) 56 | 57 | // Seed the random number generator with the current Unix 58 | // time. This is not random, but it should be Good Enough. 59 | rand.Seed(time.Now().Unix()) 60 | 61 | // Handle "/api/". Note that it must begin and end with /. 62 | http.Handle(path.Join("/", prefix, "api")+"/", router) 63 | } 64 | 65 | // Get responds on the root API handler ("/api/") with 303 SeeOther 66 | // and a link to the API documentation on the project homepage. 67 | func (*Api) Get(ctx *jas.Context) { 68 | ctx.Status = http.StatusSeeOther 69 | ctx.ResponseHeader.Set("Location", APIDocs) 70 | ctx.Data = http.StatusText(http.StatusSeeOther) + ": " + APIDocs 71 | } 72 | 73 | // GetEcho responds with the remote address of the user 74 | func (*Api) GetEcho(ctx *jas.Context) { 75 | if Conf.Verify.Netmask != nil { 76 | i := net.ParseIP(ctx.RemoteAddr) 77 | if (*net.IPNet)(Conf.Verify.Netmask).Contains(i) { 78 | ctx.Data = ctx.RemoteAddr 79 | } else { 80 | ctx.Error = jas.NewRequestError("remote address not in subnet") 81 | } 82 | } else { 83 | ctx.Error = jas.NewRequestError("netmask not set") 84 | } 85 | } 86 | 87 | // GetStatus responds with a status summary of the map, including the 88 | // map name, total number of nodes, number available (pingable), etc. 89 | // (Not yet implemented.) 90 | func (*Api) GetStatus(ctx *jas.Context) { 91 | localNodes := Db.LenNodes(false) 92 | ctx.Data = map[string]interface{}{ 93 | "Name": Conf.Name, 94 | "LocalNodes": localNodes, 95 | "CachedNodes": Db.LenNodes(true) - localNodes, 96 | "CachedMaps": len(Conf.ChildMaps), 97 | } 98 | } 99 | 100 | // GetToken generates a short random token and stores it in an 101 | // in-memory map with its generation time. (See CheckToken.) 102 | func (*Api) GetToken(ctx *jas.Context) { 103 | tokenid := rand.Uint32() 104 | ActiveTokens[tokenid] = token{ctx.RemoteAddr, time.Now()} 105 | ctx.Data = tokenid 106 | } 107 | 108 | // GetKey generates a CAPTCHA ID and returns it. This can be combined 109 | // with the solution to the returned CAPTCHA to authenticate certain 110 | // API functions. The CAPTCHAs can be accessed at /captcha/.png or 111 | // /captcha/.wav. 112 | func (*Api) GetKey(ctx *jas.Context) { 113 | ctx.Data = captcha.New() 114 | } 115 | 116 | // GetNode retrieves a single node from the database, removes 117 | // sensitive data (such as an email address) and sets ctx.Data to 118 | // it. If `?geojson` is set, then it returns it in geojson.Feature 119 | // form. 120 | func (*Api) GetNode(ctx *jas.Context) { 121 | ip := IP(net.ParseIP(ctx.RequireStringLen(0, 40, "address"))) 122 | if ip == nil { 123 | // If this is encountered, the address was incorrectly 124 | // formatted. 125 | ctx.Error = jas.NewRequestError("addressInvalid") 126 | return 127 | } 128 | node, err := Db.GetNode(ip) 129 | if err != nil { 130 | // If there has been a database error, log it and report the 131 | // failure. 132 | ctx.Error = jas.NewInternalError(err) 133 | l.Err(err) 134 | return 135 | } 136 | if node == nil { 137 | // If there are simply no matching nodes, set the error and 138 | // return. 139 | ctx.Error = jas.NewRequestError("No matching node") 140 | return 141 | } 142 | 143 | // We must invoke ParseForm() so that we can access ctx.Form. 144 | ctx.ParseForm() 145 | 146 | // If the form value 'geojson' is included, dump in GeoJSON 147 | // form. Otherwise, just dump with normal marhshalling. 148 | if _, ok := ctx.Form["geojson"]; ok { 149 | ctx.Data = node.Feature() 150 | return 151 | } else { 152 | // Only after removing any sensitive data, though. 153 | node.OwnerEmail = "" 154 | 155 | // Finally, set the data and exit. 156 | ctx.Data = node 157 | } 158 | } 159 | 160 | // PostNode creates a *Node from the submitted form and queues it for 161 | // addition with a positive 64 bit integer as an ID. 162 | func (*Api) PostNode(ctx *jas.Context) { 163 | if Db.ReadOnly { 164 | // If the database is readonly, set that as the error and 165 | // return. 166 | ctx.Error = ReadOnlyError 167 | return 168 | } 169 | var err error 170 | 171 | // Require a token, because this is mildly sensitive. 172 | RequireToken(ctx) 173 | 174 | // Initialize the node and retrieve fields. 175 | node := new(Node) 176 | 177 | ip := IP(net.ParseIP(ctx.RequireStringLen(0, 40, "address"))) 178 | if ip == nil { 179 | // If the address is invalid, return that error. 180 | ctx.Error = jas.NewRequestError("addressInvalid") 181 | return 182 | } 183 | node.Addr = ip 184 | node.Latitude = ctx.RequireFloat("latitude") 185 | node.Longitude = ctx.RequireFloat("longitude") 186 | node.OwnerName = html.EscapeString(ctx.RequireString("name")) 187 | node.OwnerEmail = ctx.RequireStringMatch(EmailRegexp, "email") 188 | 189 | node.Contact, _ = ctx.FindString("contact") 190 | node.Contact = html.EscapeString(node.Contact) 191 | 192 | node.Details, _ = ctx.FindString("details") 193 | node.Details = html.EscapeString(node.Details) 194 | 195 | // If Contact, Details, or OwnerName are too long to fit in 196 | // the database, produce an error. 197 | if len(node.Contact) > 255 { 198 | ctx.Error = jas.NewRequestError("contactTooLong") 199 | return 200 | } 201 | if len(node.Details) > 255 { 202 | ctx.Error = jas.NewRequestError("detailsTooLong") 203 | return 204 | } 205 | if len(node.OwnerName) > 255 { 206 | ctx.Error = jas.NewRequestError("ownerNameTooLong") 207 | return 208 | } 209 | 210 | // Validate the PGP ID, if given. It can be an lowercase hex 211 | // string of length 0, 8, or 16. 212 | pgpstr, _ := ctx.FindStringMatch(PGPRegexp, "pgp") 213 | if node.PGP, err = DecodePGPID([]byte(pgpstr)); err != nil { 214 | ctx.Error = jas.NewRequestError("pgpInvalid") 215 | return 216 | } 217 | status, _ := ctx.FindPositiveInt("status") 218 | node.Status = uint32(status) 219 | 220 | // Ensure that the node is correct and usable. 221 | if err = Db.VerifyRegistrant(node); err != nil { 222 | ctx.Error = jas.NewRequestError(err.Error()) 223 | return 224 | } 225 | 226 | // TODO(DuoNoxSol): Authenticate/limit node registration. 227 | 228 | // If SMTP is missing from the config, we cannot continue. 229 | if Conf.SMTP == nil { 230 | ctx.Error = jas.NewInternalError(SMTPDisabledError) 231 | l.Err(SMTPDisabledError) 232 | return 233 | } 234 | 235 | // If SMTP verification is not explicitly disabled, and the 236 | // connecting address is not an admin, send an email. 237 | if !Conf.SMTP.VerifyDisabled && !IsAdmin(ctx.Request) { 238 | id := rand.Int63() // Pseudo-random positive int64 239 | 240 | emailsent := true 241 | if err := SendVerificationEmail(id, node.OwnerEmail); err != nil { 242 | // If the sending of the email fails, set the internal 243 | // error and log it, then set a bool so that email can be 244 | // resent. If email continues failing to send, it will 245 | // eventually expire and be removed from the database. 246 | ctx.Error = jas.NewInternalError(err) 247 | l.Err(err) 248 | emailsent = false 249 | // Note that we do *not* return here. 250 | } 251 | 252 | // Once we have attempted to send the email, queue the node 253 | // for verification. If the email has not been sent, it will 254 | // be recorded in the database. 255 | if err := Db.QueueNode(id, emailsent, 256 | Conf.VerificationExpiration, node); err != nil { 257 | // If there is a database failure, report it as an 258 | // internal error. 259 | ctx.Error = jas.NewInternalError(err) 260 | l.Err(err) 261 | return 262 | } 263 | 264 | // If the email could be sent successfully, report 265 | // it. Otherwise, report that it is in the queue, and the 266 | // email will be resent. 267 | if emailsent { 268 | ctx.Data = "verification email sent" 269 | l.Infof("Node %q entered, waiting for verification", ip) 270 | } else { 271 | ctx.Data = "verification email will be resent" 272 | l.Infof("Node %q entered, verification email will be resent", 273 | ip) 274 | } 275 | } else { 276 | err := Db.AddNode(node) 277 | if err != nil { 278 | // If there was an error, log it and report the failure. 279 | ctx.Error = jas.NewInternalError(err) 280 | l.Err(err) 281 | return 282 | } 283 | 284 | // Add the new node to the RSS feed. 285 | AddNodeToRSS(node, time.Now()) 286 | 287 | ctx.Data = "node registered" 288 | l.Infof("Node %q registered\n", ip) 289 | } 290 | } 291 | 292 | // PostUpdateNode removes a Node of a given IP from the database and 293 | // re-adds it with the supplied information. It is the equivalent of 294 | // removing a Node from the database, then invoking PostNode() with 295 | // its information, with the exception that it does not send a 296 | // verification email, and requires that the request be sent by the 297 | // Node that is being update. 298 | func (*Api) PostUpdateNode(ctx *jas.Context) { 299 | if Db.ReadOnly { 300 | // If the database is readonly, set that as the error and 301 | // return. 302 | ctx.Error = ReadOnlyError 303 | return 304 | } 305 | var err error 306 | 307 | // Require a token, because this is a very sensitive endpoint. 308 | RequireToken(ctx) 309 | 310 | // Retrieve the given IP address, check that it's sane, and check 311 | // that it exists in the *local* database. 312 | ip := IP(net.ParseIP(ctx.RequireStringLen(0, 40, "address"))) 313 | if ip == nil { 314 | // If the address is invalid, return that error. 315 | ctx.Error = jas.NewRequestError("addressInvalid") 316 | return 317 | } 318 | 319 | node, err := Db.GetNode(ip) 320 | if err != nil { 321 | ctx.Error = jas.NewInternalError(err.Error()) 322 | return 323 | } 324 | 325 | if node == nil || len(node.OwnerEmail) == 0 { 326 | ctx.Error = jas.NewRequestError("no matching local node") 327 | return 328 | } 329 | 330 | // Check to make sure that the Node is the one sending the 331 | // address, or an admin. If not, return an error. 332 | if !net.IP(ip).Equal(net.ParseIP(ctx.RemoteAddr)) && 333 | !IsAdmin(ctx.Request) { 334 | ctx.Error = jas.NewRequestError( 335 | RemoteAddressDoesNotMatchError.Error()) 336 | return 337 | } 338 | 339 | node.Addr = ip 340 | node.Latitude = ctx.RequireFloat("latitude") 341 | node.Longitude = ctx.RequireFloat("longitude") 342 | node.OwnerName = html.EscapeString(ctx.RequireString("name")) 343 | node.Contact, _ = ctx.FindString("contact") 344 | node.Contact = html.EscapeString(node.Contact) 345 | node.Details, _ = ctx.FindString("details") 346 | node.Details = html.EscapeString(node.Details) 347 | 348 | // If Contact, Details, or OwnerName are too long to fit in 349 | // the database, produce an error. 350 | if len(node.Contact) > 255 { 351 | ctx.Error = jas.NewRequestError("contactTooLong") 352 | return 353 | } 354 | if len(node.Details) > 255 { 355 | ctx.Error = jas.NewRequestError("detailsTooLong") 356 | return 357 | } 358 | if len(node.OwnerName) > 255 { 359 | ctx.Error = jas.NewRequestError("ownerNameTooLong") 360 | return 361 | } 362 | 363 | // Validate the PGP ID, if given. It can be an lowercase hex 364 | // string of length 0, 8, or 16. 365 | pgpstr, _ := ctx.FindStringMatch(PGPRegexp, "pgp") 366 | if node.PGP, err = DecodePGPID([]byte(pgpstr)); err != nil { 367 | ctx.Error = jas.NewRequestError("pgpInvalid") 368 | return 369 | } 370 | status, _ := ctx.FindPositiveInt("status") 371 | node.Status = uint32(status) 372 | 373 | // Note that we do not perform a verification step here, or send 374 | // an email. Because the Node was already verified once, we can 375 | // assume that it remains usable. 376 | 377 | // Update the Node in the database, replacing the one of matching 378 | // IP. 379 | err = Db.UpdateNode(node) 380 | if err != nil { 381 | ctx.Error = jas.NewInternalError(err) 382 | l.Errf("Error updating %q: %s", node.Addr, err) 383 | return 384 | } 385 | 386 | // If we reach this point, all was successful. 387 | ctx.Data = "successful" 388 | } 389 | 390 | // PostDeleteNode removes a node with the given address from the 391 | // database. This must be done from that node's address, or an admin 392 | // address. 393 | func (*Api) PostDeleteNode(ctx *jas.Context) { 394 | if Db.ReadOnly { 395 | // If the database is readonly, set that as the error and 396 | // return. 397 | ctx.Error = ReadOnlyError 398 | return 399 | } 400 | var err error 401 | 402 | // Require a token, because this is a very sensitive endpoint. 403 | RequireToken(ctx) 404 | 405 | // Retrieve the given IP address, check that it's sane, and check 406 | // that it exists in the *local* database. 407 | ip := IP(net.ParseIP(ctx.RequireStringLen(0, 40, "address"))) 408 | if ip == nil { 409 | // If the address is invalid, return that error. 410 | ctx.Error = jas.NewRequestError("addressInvalid") 411 | return 412 | } 413 | 414 | // Check to make sure that the Node is the one sending the 415 | // address, or an admin. If not, return an error. 416 | if !net.IP(ip).Equal(net.ParseIP(ctx.RemoteAddr)) && 417 | !IsAdmin(ctx.Request) { 418 | ctx.Error = jas.NewRequestError( 419 | RemoteAddressDoesNotMatchError.Error()) 420 | return 421 | } 422 | 423 | // If all is well, then delete it. 424 | err = Db.DeleteNode(ip) 425 | if err == sql.ErrNoRows { 426 | // If there are no rows with that IP, explain that in the 427 | // error. 428 | // 429 | // I'm not actually sure this can happen. (DuoNoxSol) 430 | ctx.Error = jas.NewRequestError("no matching node") 431 | } else if err != nil { 432 | ctx.Error = jas.NewInternalError(err) 433 | l.Errf("Error deleting node: %s\n") 434 | } else { 435 | l.Infof("Node %q deleted\n", ip) 436 | ctx.Data = "deleted" 437 | } 438 | } 439 | 440 | // GetVerify moves a node from the verification queue to the normal 441 | // database, as identified by its long random ID. 442 | func (*Api) GetVerify(ctx *jas.Context) { 443 | id := ctx.RequireInt("id") 444 | ip, verifyerr, err := Db.VerifyQueuedNode(id, ctx.Request) 445 | if verifyerr != nil { 446 | // If there was an error inverification, there was no internal 447 | // error, but the circumstances of the verification were 448 | // incorrect. It has not been removed from the database. 449 | ctx.Error = jas.NewRequestError(verifyerr.Error()) 450 | return 451 | } else if err == sql.ErrNoRows { 452 | // If we encounter a ErrNoRows, then there was no node with 453 | // that ID. Report it. 454 | ctx.Error = jas.NewRequestError("invalid id") 455 | l.Noticef("%q attempted to verify invalid ID\n", ctx.RemoteAddr) 456 | return 457 | } else if err != nil { 458 | // If we encounter any other database error, it is an internal 459 | // error and needs to be logged. 460 | ctx.Error = jas.NewInternalError(err) 461 | l.Err(err) 462 | return 463 | } 464 | // If there was no error, inform the user that it was successful, 465 | // and log it. 466 | ctx.Data = "successful" 467 | l.Infof("Node %q verified", ip) 468 | } 469 | 470 | // GetAll dumps the entire database of nodes, including cached 471 | // ones. If the form value `since` is supplied with a valid RFC3339 472 | // timestamp, only nodes updated or cached more recently than that 473 | // will be dumped. If 'geojson' is present, then the "data" field 474 | // contains the dump in GeoJSON compliant form. 475 | func (*Api) GetAll(ctx *jas.Context) { 476 | // We must invoke ParseForm() so that we can access ctx.Form. 477 | ctx.ParseForm() 478 | 479 | // In order to access this at the end, we need to declare nodes 480 | // here, so the results from the dump don't go out of scope. 481 | var nodes []*Node 482 | var err error 483 | 484 | // If the form value "since" was supplied, we will be doing a dump 485 | // based on update/retrieve time. 486 | if tstring := ctx.FormValue("since"); len(tstring) > 0 { 487 | var t time.Time 488 | t, err = time.Parse(time.RFC3339, tstring) 489 | if err != nil { 490 | ctx.Data = err.Error() 491 | ctx.Error = jas.NewRequestError("invalidTime") 492 | return 493 | } 494 | 495 | // Now, perform the time-based dump. Errors will be handled 496 | // outside the if block. 497 | nodes, err = Db.DumpChanges(t) 498 | } else { 499 | // If there was no "since," provide a simple full-database 500 | // dump. 501 | nodes, err = Db.DumpNodes() 502 | } 503 | 504 | // Handle any database errors here. 505 | if err != nil { 506 | ctx.Error = jas.NewInternalError(err) 507 | l.Err(err) 508 | return 509 | } 510 | 511 | // If the form value 'geojson' is included, dump in GeoJSON 512 | // form. Otherwise, just dump with normal marhshalling. 513 | if _, ok := ctx.Form["geojson"]; ok { 514 | ctx.Data = FeatureCollectionNodes(nodes) 515 | } else { 516 | mappedNodes, err := Db.CacheFormatNodes(nodes) 517 | if err != nil { 518 | ctx.Error = jas.NewInternalError(err) 519 | l.Err(err) 520 | return 521 | } 522 | ctx.Data = mappedNodes 523 | } 524 | } 525 | 526 | func (*Api) GetAllPeers(ctx *jas.Context) { 527 | ctx.Data = KnownPeers 528 | } 529 | 530 | // PostMessage emails the given message to the email address owned by 531 | // the node with the given IP. It requires a correct and non-expired 532 | // CAPTCHA pair be given. 533 | func (*Api) PostMessage(ctx *jas.Context) { 534 | // Because this is a somewhat sensitive endpoint, require a token. 535 | RequireToken(ctx) 536 | 537 | // Ensure that the given CAPTCHA pair is correct. If it is not, 538 | // then return the explanation. This is bypassed if the request 539 | // comes from an admin address. 540 | if !IsAdmin(ctx.Request) { 541 | err := VerifyCAPTCHA(ctx.Request) 542 | if err != nil { 543 | ctx.Error = jas.NewRequestError(err.Error()) 544 | return 545 | } 546 | } 547 | 548 | // Next, retrieve the IP of the node the user is attempting to 549 | // contact. 550 | ip := IP(net.ParseIP(ctx.RequireStringLen(0, 40, "address"))) 551 | if ip == nil { 552 | // If the address is invalid, return that error. 553 | ctx.Error = jas.NewRequestError("addressInvalid") 554 | return 555 | } 556 | 557 | // Find the appropriate variables. If any of these are not 558 | // found, JAS will return a request error. 559 | replyto := ctx.RequireStringMatch(EmailRegexp, "from") 560 | message := ctx.RequireStringLen(0, 1000, "message") 561 | 562 | // Retrieve the appropriate node from the database. 563 | node, err := Db.GetNode(ip) 564 | if err != nil { 565 | // If we encounter an error here, it was a database error. 566 | ctx.Error = jas.NewInternalError(err) 567 | l.Err("Error getting node %q: %s", ip, err) 568 | return 569 | } else if node == nil { 570 | // If the IP wasn't found, explain that there was no node with 571 | // that IP. 572 | ctx.Error = jas.NewRequestError("address unknown") 573 | return 574 | } else if len(node.OwnerEmail) == 0 { 575 | // If there was no email on the node, that probably means that 576 | // it was cached. 577 | ctx.Error = jas.NewRequestError("address belongs to cached node") 578 | return 579 | } 580 | 581 | // Create and send an email. Log any errors. 582 | e := &Email{ 583 | To: node.OwnerEmail, 584 | From: Conf.SMTP.EmailAddress, 585 | Subject: "Message via " + Conf.Name, 586 | } 587 | e.Data = map[string]interface{}{ 588 | "ReplyTo": replyto, 589 | "Message": template.HTML(message), 590 | "Name": Conf.Name, 591 | "Link": template.HTML(Conf.Web.Hostname + Conf.Web.Prefix + 592 | "/node/" + ip.String()), 593 | "AdminContact": Conf.AdminContact, 594 | 595 | // Generate a random number for use as a boundary marker in the 596 | // multipart/alternative email. 597 | "Boundary": rand.Int31(), 598 | } 599 | 600 | err = e.Send("message.txt") 601 | if err != nil { 602 | ctx.Error = jas.NewInternalError(err) 603 | l.Errf("Error messaging %q from %q: %s", 604 | node.OwnerEmail, replyto, err) 605 | return 606 | } 607 | 608 | // Even if there is no error, log the to and from info, in case it 609 | // is abusive or spam. 610 | l.Noticef("IP %q sent a message to %q from %q", 611 | ctx.Request.RemoteAddr, node.OwnerEmail, replyto) 612 | } 613 | 614 | func (*Api) GetChildMaps(ctx *jas.Context) { 615 | var err error 616 | ctx.Data, err = Db.DumpChildMaps() 617 | if err != nil { 618 | ctx.Error = jas.NewInternalError(err) 619 | l.Errf("Error dumping child maps: %s", err) 620 | } 621 | return 622 | } 623 | 624 | // RequireToken uses the finder to retrieve a value named "token", and 625 | // panics with "tokenInvalid" if there is either no such value, or it 626 | // is invalid or expired. 627 | func RequireToken(ctx *jas.Context) { 628 | tokeni, err := ctx.FindInt("token") 629 | if err != nil || !CheckToken(ctx.RemoteAddr, uint32(tokeni)) { 630 | panic(jas.NewRequestError("tokenInvalid")) 631 | } 632 | } 633 | 634 | // CheckToken ensures that a particular token is valid, meaning that 635 | // it is in the list, and has not expired. If so, it removes the token 636 | // and returns true. If the token is expired, it is removed, and the 637 | // function returns false. 638 | func CheckToken(IP string, token uint32) bool { 639 | t, ok := ActiveTokens[token] 640 | if ok { 641 | delete(ActiveTokens, token) 642 | } 643 | 644 | if !ok || time.Now().After(t.Issued.Add(time.Minute*5)) || 645 | t.IP != IP { 646 | return false 647 | } 648 | return true 649 | } 650 | 651 | // IsAdmin is a small wrapper function to check if the given address 652 | // belongs to an admin, as specified in Conf.AdminAddresses. 653 | func IsAdmin(req *http.Request) bool { 654 | remoteIP := net.ParseIP(req.RemoteAddr) 655 | for _, adminAddr := range Conf.AdminAddresses { 656 | if net.IP(adminAddr).Equal(remoteIP) { 657 | return true 658 | } 659 | } 660 | return false 661 | } 662 | -------------------------------------------------------------------------------- /cache.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Copyright (C) 2013 Alexander Bauer, Luke Evers, Dylan Whichard, 4 | // and contributors; (GPLv3) see LICENSE or doc.go 5 | 6 | import ( 7 | "database/sql" 8 | "encoding/json" 9 | "net/http" 10 | "strings" 11 | "sync" 12 | "time" 13 | ) 14 | 15 | // ChildMap represents a single child map, which is regularly cached. 16 | type ChildMap struct { 17 | ID int 18 | Name, Hostname string 19 | } 20 | 21 | // UpdateMapCache updates the node cache intelligently using 22 | // Conf.ChildMaps. Any unknown map addresses are added to the database 23 | // automatically, and errors are logged. 24 | func UpdateMapCache() { 25 | // If there are no addresses to retrieve from, do nothing. 26 | if len(Conf.ChildMaps) == 0 { 27 | return 28 | } 29 | 30 | // Because we are refreshing the entire cache, delete all cached 31 | // nodes. 32 | err := Db.ClearCache() 33 | if err != nil { 34 | l.Errf("Error clearing cache: %s", err) 35 | return 36 | } 37 | 38 | // Get a full database dump from all child maps and cache it. 39 | err = GetAllFromChildMaps(Conf.ChildMaps) 40 | if err != nil { 41 | l.Errf("Error updating map cache: %s", err) 42 | } 43 | } 44 | 45 | func (db DB) CacheNode(node *Node) (err error) { 46 | stmt, err := db.Prepare(`INSERT INTO nodes_cached 47 | (address, owner, details, lat, lon, status, expiration, updated) 48 | VALUES(?, ?, ?, ?, ?, ?, ?, ?)`) 49 | if err != nil { 50 | return 51 | } 52 | 53 | if node.RetrieveTime == 0 { 54 | node.RetrieveTime = time.Now().Unix() 55 | } 56 | 57 | _, err = stmt.Exec(node.Addr, node.OwnerName, node.Details, 58 | node.Latitude, node.Longitude, node.SourceID, node.Status, 59 | node.RetrieveTime) 60 | stmt.Close() 61 | return 62 | } 63 | 64 | func (db DB) CacheNodes(nodes []*Node) (err error) { 65 | stmt, err := db.Prepare(`INSERT INTO nodes_cached 66 | (address, owner, details, lat, lon, status, source, retrieved) 67 | VALUES (?, ?, ?, ?, ?, ?, ?, ?)`) 68 | if err != nil { 69 | return 70 | } 71 | 72 | for _, node := range nodes { 73 | if node.RetrieveTime == 0 { 74 | node.RetrieveTime = time.Now().Unix() 75 | } 76 | 77 | _, err = stmt.Exec([]byte(node.Addr), node.OwnerName, 78 | node.Details, 79 | node.Latitude, node.Longitude, 80 | node.Status, node.SourceID, node.RetrieveTime) 81 | if err != nil { 82 | return 83 | } 84 | } 85 | stmt.Close() 86 | return 87 | } 88 | 89 | func (db DB) ClearCache() (err error) { 90 | _, err = db.Exec(`DELETE FROM nodes_cached;`) 91 | return err 92 | } 93 | 94 | // AddNewMapSource inserts a new map address into the cached_maps 95 | // table. 96 | func (db DB) AddNewMapSource(address, name string) (err error) { 97 | _, err = db.Exec(`INSERT INTO cached_maps 98 | (hostname,name) VALUES(?, ?)`, address, name) 99 | return 100 | } 101 | 102 | // UpdateMapSourceData updates the name, and possibly other data, 103 | // of a cached map 104 | func (db DB) UpdateMapSourceData(address, name string) (err error) { 105 | _, err = db.Exec(`UPDATE cached_maps 106 | SET name=? WHERE hostname=?`, name, address) 107 | return 108 | } 109 | 110 | // DumpChildMaps returns a slice containing all known child maps. 111 | func (db DB) DumpChildMaps() (childMaps []*ChildMap, err error) { 112 | childMaps = make([]*ChildMap, 0) 113 | 114 | // Retrieve all child maps from the database. 115 | rows, err := db.Query(`SELECT name, hostname, id 116 | FROM cached_maps;`) 117 | if err == sql.ErrNoRows { 118 | return childMaps, nil 119 | } else if err != nil { 120 | return 121 | } 122 | 123 | // Scan in all of the values. 124 | for rows.Next() { 125 | childMap := &ChildMap{} 126 | if err = rows.Scan(&childMap.Name, &childMap.Hostname, 127 | &childMap.ID); err != nil { 128 | return 129 | } 130 | childMaps = append(childMaps, childMap) 131 | } 132 | 133 | return 134 | } 135 | 136 | // GetMapSourceToID returns a mapping of child map hostnames to their 137 | // local IDs. It also includes a mapping of "local" to id 0. 138 | func (db DB) GetMapSourceToID() (sourceToID map[string]int, err error) { 139 | // Initialize the map and insert the "local" id. 140 | sourceToID = map[string]int{ 141 | "local": 0, 142 | } 143 | 144 | // Retrieve every pair of hostnames and IDs. 145 | rows, err := db.Query(`SELECT hostname,id 146 | FROM cached_maps;`) 147 | if err == sql.ErrNoRows { 148 | return sourceToID, nil 149 | } else if err != nil { 150 | return 151 | } 152 | 153 | // Put in the rest of the mappings. 154 | for rows.Next() { 155 | var hostname string 156 | var id int 157 | if err = rows.Scan(&hostname, &id); err != nil { 158 | return 159 | } 160 | sourceToID[hostname] = id 161 | } 162 | 163 | return 164 | } 165 | 166 | // GetMapIDToSource returns a mapping of local IDs to public 167 | // hostnames. ID 0 is "local". 168 | func (db DB) GetMapIDToSource() (IDToSource map[int]string, err error) { 169 | // Initialize the slice with "local". 170 | IDToSource = map[int]string{ 171 | 0: "local", 172 | } 173 | 174 | // Retrieve every pair of IDs and hostnames. 175 | rows, err := db.Query(`SELECT id,hostname 176 | FROM cached_maps;`) 177 | if err == sql.ErrNoRows { 178 | return IDToSource, nil 179 | } else if err != nil { 180 | return 181 | } 182 | 183 | // Put in the rest of the IDs. 184 | for rows.Next() { 185 | var id int 186 | var hostname string 187 | if err = rows.Scan(&id, &hostname); err != nil { 188 | return 189 | } 190 | IDToSource[id] = hostname 191 | } 192 | return 193 | } 194 | 195 | func (db DB) FindSourceMap(id int) (source string, err error) { 196 | if id == 0 { 197 | return "local", nil 198 | } 199 | row := db.QueryRow(`SELECT hostname 200 | FROM cached_maps 201 | WHERE id=?`, id) 202 | 203 | err = row.Scan(&source) 204 | return 205 | } 206 | 207 | func (db DB) CacheFormatNodes(nodes []*Node) (sourceMaps map[string][]*Node, err error) { 208 | // First, get a mapping of IDs to sources for quick access. 209 | idSources, err := db.GetMapIDToSource() 210 | if err != nil { 211 | return 212 | } 213 | 214 | // Now, prepare the data to be returned. Nodes will be added one 215 | // at a time to the key arrays. 216 | sourceMaps = make(map[string][]*Node) 217 | for _, node := range nodes { 218 | hostname := idSources[node.SourceID] 219 | sourcemapNodes := sourceMaps[hostname] 220 | if sourcemapNodes == nil { 221 | sourcemapNodes = make([]*Node, 0, 5) 222 | } 223 | 224 | sourceMaps[hostname] = append(sourcemapNodes, node) 225 | } 226 | return 227 | } 228 | 229 | // nodeDumpWrapper is a structure which wraps a response from /api/all 230 | // in which the Data field is a map[string][]*Node. 231 | type nodeDumpWrapper struct { 232 | Data map[string][]*Node `json:"data"` 233 | Error interface{} `json:"error"` 234 | } 235 | 236 | type statusDumpWrapper struct { 237 | Data map[string]interface{} `json:"data"` 238 | Error interface{} `json:"error"` 239 | } 240 | 241 | // GetAllFromChildMaps accepts a list of child map addresses to 242 | // retrieve nodes from. It does this concurrently, and puts any nodes 243 | // and newly discovered addresses in the local ID table. 244 | func GetAllFromChildMaps(addresses []string) (err error) { 245 | // First off, initialize the slice into which we'll be appending 246 | // all the nodes, and the souceToID map and mutex. 247 | nodes := make([]*Node, 0) 248 | 249 | sourceToID, err := Db.GetMapSourceToID() 250 | if err != nil { 251 | return 252 | } 253 | sourceMutex := new(sync.RWMutex) 254 | 255 | // Next, we'll need a WaitGroup so we can block until all requests 256 | // complete and a mutex to control appending to nodes. 257 | waiter := new(sync.WaitGroup) 258 | nodesMutex := new(sync.Mutex) 259 | 260 | // We'll need to wait for len(addresses) goroutines to finish, so 261 | // put that number in the WaitGroup. 262 | waiter.Add(len(addresses)) 263 | 264 | // Now, start a separate goroutine for every address to 265 | // concurrently retrieve nodes and append them (thread-safely) to 266 | // nodes. Whenever appendNodesFromChildMap() finishes, it calls 267 | // waiter.Done(). 268 | for _, address := range addresses { 269 | go appendNodesFromChildMap(&nodes, address, 270 | &sourceToID, sourceMutex, nodesMutex, waiter) 271 | } 272 | 273 | // Block until all goroutines are finished. This is simple to do 274 | // with the WaitGroup, which keeps track of the number we're 275 | // waiting for. 276 | waiter.Wait() 277 | 278 | return Db.CacheNodes(nodes) 279 | } 280 | 281 | // appendNodesFromChildMap is a helper function used by 282 | // GetAllFromChildMaps() which calls GetAllFromChildMap() and 283 | // thread-safely appends the result to the given slice. At the end of 284 | // the function, it calls wg.Done(). 285 | func appendNodesFromChildMap(dst *[]*Node, address string, 286 | sourceToID *map[string]int, sourceMutex *sync.RWMutex, 287 | dstMutex *sync.Mutex, wg *sync.WaitGroup) { 288 | 289 | // First, retrieve the nodes if possible. If there was an error, 290 | // it will be logged, and if there were no nodes, we can stop 291 | // here. 292 | nodes := GetAllFromChildMap(address, sourceToID, sourceMutex) 293 | if nodes == nil { 294 | wg.Done() 295 | return 296 | } 297 | 298 | // Now that we have the nodes, we need to lock the destination 299 | // slice while we append to it. 300 | dstMutex.Lock() 301 | *dst = append(*dst, nodes...) 302 | dstMutex.Unlock() 303 | wg.Done() 304 | } 305 | 306 | func GetMapStatus(address string) (data map[string]interface{}) { 307 | resp, err := http.Get(strings.TrimRight(address, "/") + "/api/status") 308 | if err != nil { 309 | l.Errf("Querying status of %q produced: %s", address, err) 310 | return nil 311 | } 312 | 313 | var jresp statusDumpWrapper 314 | err = json.NewDecoder(resp.Body).Decode(&jresp) 315 | if err != nil { 316 | l.Errf("Querying status of %q produced: %s", address, err) 317 | return nil 318 | } else if jresp.Error != nil { 319 | l.Errf("Querying status of %q produced remote error: %s", 320 | address, jresp.Error) 321 | return nil 322 | } 323 | data = jresp.Data 324 | return 325 | } 326 | 327 | // GetAllFromChildMap retrieves a list of nodes from a single remote 328 | // address, and localizes them. If it encounters a remote address that 329 | // is not already known, it safely adds it to the sourceToID map. It 330 | // is safe for concurrent use. If it encounters an error, it will log 331 | // it and return nil. 332 | func GetAllFromChildMap(address string, sourceToID *map[string]int, 333 | sourceMutex *sync.RWMutex) (nodes []*Node) { 334 | // Query the node's status 335 | mapStatus := GetMapStatus(address) 336 | 337 | // Try to get all nodes via the API. 338 | resp, err := http.Get(strings.TrimRight(address, "/") + "/api/all") 339 | if err != nil { 340 | l.Errf("Caching %q produced: %s", address, err) 341 | return nil 342 | } 343 | 344 | // Read the data into a the nodeDumpWrapper type, so that it 345 | // decodes properly. 346 | var jresp nodeDumpWrapper 347 | err = json.NewDecoder(resp.Body).Decode(&jresp) 348 | if err != nil { 349 | l.Errf("Caching %q produced: %s", address, err) 350 | return nil 351 | } else if jresp.Error != nil { 352 | l.Errf("Caching %q produced remote error: %s", 353 | address, jresp.Error) 354 | return nil 355 | } 356 | 357 | // Prepare an initial slice so that it can be appended to, then 358 | // loop through and convert sources to IDs. 359 | // 360 | // Additionally, use a boolean to keep track of whether we've 361 | // replaced "local" with the actual address already, to save some 362 | // needless compares. 363 | nodes = make([]*Node, 0) 364 | var replacedLocal bool 365 | for source, remoteNodes := range jresp.Data { 366 | // If we come across "local", then replace it with the address 367 | // we're retrieving from. 368 | if !replacedLocal && source == "local" { 369 | source = address 370 | } 371 | 372 | // Get the name of the map from the status info 373 | name, ok := mapStatus["name"].(string) 374 | if !ok { 375 | name = "" 376 | } 377 | 378 | // First, check if the source is known. If not, then we need 379 | // to add it and refresh our map. Make sure all reads and 380 | // writes to sourceToID are threadsafe. 381 | sourceMutex.RLock() 382 | id, ok := (*sourceToID)[source] 383 | sourceMutex.RUnlock() 384 | if !ok { 385 | // Add the new source to the database, and put it in the 386 | // map under the ID len(sourceToID), because that should 387 | // be unique. 388 | sourceMutex.Lock() 389 | err := Db.AddNewMapSource(source, name) 390 | if err != nil { 391 | // Uh oh. 392 | sourceMutex.Unlock() 393 | l.Errf("Error while caching %q: %s", address, err) 394 | return 395 | } 396 | 397 | id = len(*sourceToID) 398 | (*sourceToID)[source] = id 399 | sourceMutex.Unlock() 400 | 401 | l.Debugf("Discovered new source map %q, ID %d\n", 402 | source, id) 403 | } else { 404 | err := Db.UpdateMapSourceData(address, name) 405 | if err != nil { 406 | l.Errf("Error while updating %q: %s", address, err) 407 | } 408 | } 409 | 410 | // Once the ID is set, proceed on to add it in all the 411 | // remoteNodes. 412 | for _, n := range remoteNodes { 413 | n.SourceID = id 414 | } 415 | 416 | // Finally, append remoteNodes to the slice we're returning. 417 | nodes = append(nodes, remoteNodes...) 418 | } 419 | return 420 | } 421 | -------------------------------------------------------------------------------- /captcha.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Copyright (C) 2013 Alexander Bauer, Luke Evers, Dylan Whichard, 4 | // and contributors; (GPLv3) see LICENSE or doc.go 5 | 6 | import ( 7 | "github.com/dchest/captcha" 8 | "database/sql" 9 | "net/http" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | const ( 15 | CAPTCHAGracePeriod = time.Minute * 10 16 | ) 17 | 18 | type CAPTCHAStore struct{} 19 | 20 | // Set inserts a new CAPTCHA ID and solution into the captcha table in 21 | // the database. It logs errors. 22 | func (CAPTCHAStore) Set(id string, digits []byte) { 23 | _, err := Db.Exec(`INSERT INTO captcha 24 | (id, solution, expiration) 25 | VALUES(?, ?, ?);`, 26 | []byte(id), digits, time.Now().Add(CAPTCHAGracePeriod)) 27 | if err != nil { 28 | l.Err("Error registering CAPTCHA:", err) 29 | } 30 | } 31 | 32 | // Get retrieves a CAPTCHA solution from the database and clears the 33 | // row if appropriate. It logs errors. 34 | func (CAPTCHAStore) Get(id string, clear bool) (digits []byte) { 35 | bid := []byte(id) 36 | row := Db.QueryRow(`SELECT solution 37 | FROM captcha 38 | WHERE id = ? AND expiration > ?;`, bid, time.Now()) 39 | err := row.Scan(&digits) 40 | if err == sql.ErrNoRows { 41 | // If there are no rows, then the ID was not found. 42 | return nil 43 | } else if err != nil { 44 | l.Err("Error retrieving CAPTCHA:", err) 45 | return nil 46 | } 47 | 48 | // If we're supposed to remove the CAPTCHA from the database, then 49 | // do so. 50 | if clear { 51 | _, err = Db.Exec(`DELETE FROM captcha 52 | WHERE id = ?;`, bid) 53 | if err != nil { 54 | l.Err("Error deleting CAPTCHA:", err) 55 | } 56 | } 57 | return 58 | } 59 | 60 | // ClearExpiredCAPTCHA removes any expired CAPTCHA solutions from the 61 | // database. It logs errors. 62 | func ClearExpiredCAPTCHA() { 63 | _, err := Db.Exec(`DELETE FROM captcha 64 | WHERE expiration <= ?;`, time.Now()) 65 | if err != nil { 66 | l.Err("Error deleting expired CAPTCHAs:", err) 67 | } 68 | } 69 | 70 | 71 | // VerifyCAPTCHA accepts a *http.Request and verifies that the given 72 | // 'captcha' form is valid. This is a string of the form 73 | // "id:solution". It will return IncorrectCAPTCHAError if the solution 74 | // or ID is invalid. 75 | func VerifyCAPTCHA(req *http.Request) error { 76 | // Get the "captcha" form value. 77 | solution := req.FormValue("captcha") 78 | 79 | // Find the point to split the form value at. If it's not found in 80 | // the string, return the InvalidCAPTCHAFormat error. 81 | index := strings.Index(solution, ":") 82 | if index < 0 { 83 | return InvalidCAPTCHAFormat 84 | } 85 | 86 | // If that was successful, try to verify it. If it returns false, 87 | // the ID or solution was invalid. 88 | if !captcha.VerifyString(solution[:index], solution[index+1:]) { 89 | return IncorrectCAPTCHA 90 | } 91 | 92 | // If we get to this point, then it was successfully validated and 93 | // we can return nil. 94 | return nil 95 | } 96 | -------------------------------------------------------------------------------- /conf.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "Name": "Meshnet", 3 | "AdminContact":{ 4 | "Name": "John Doe", 5 | "Email": "johndoe@example.com", 6 | "PGP": "0123ABCD" 7 | }, 8 | "AdminAddresses": [ "127.0.0.1" ], 9 | "Web": { 10 | "Hostname": "http://localhost", 11 | "Prefix": "", 12 | "Addr": "tcp://0.0.0.0:8077", 13 | "DeproxyHeaderFields": [ 14 | "X-Forwarded-For", 15 | "X-Real-Ip" 16 | ], 17 | "HeaderSnippet": "", 18 | "AboutSnippet": "Contact the administrator of this map for help!", 19 | "RSS": { 20 | "MaxAge": "2h" 21 | } 22 | }, 23 | "ChildMaps": [], 24 | "Database": { 25 | "DriverName": "sqlite3", 26 | "Resource": "example.db", 27 | "ReadOnly": true 28 | }, 29 | "HeartbeatRate": "10m", 30 | "CacheExpiration": "168h", 31 | "VerificationExpiration": "48h", 32 | "ExtraVerificationFlags": "-6", 33 | "SMTP": { 34 | "VerifyDisabled": false, 35 | "EmailAddress": "nodeatlas@example.com", 36 | "Username": "nodeatlas", 37 | "Password": "password123", 38 | "NoAuthenticate": false, 39 | "ServerAddress": "mail.example.com:587" 40 | }, 41 | "Map": { 42 | "Favicon": "nodeatlas.png", 43 | "Tileserver": "http://{s}.tile.osm.org/{z}/{x}/{y}.png", 44 | "Center": { 45 | "Latitude": 40, 46 | "Longitude": -100 47 | }, 48 | "Zoom": 4, 49 | "MaxZoom": 16, 50 | "ClusterRadius": 50, 51 | "Attribution": "© OpenStreetMap contributors", 52 | "AddressType": "Network-specific IP" 53 | }, 54 | "Verify": { 55 | "Netmask": "fc00::/8", 56 | "FromNode": true 57 | }, 58 | "NetworkAdmin": { 59 | "Type": "cjdns", 60 | "Credentials": { 61 | "addr": "127.0.0.1", 62 | "port": 11234, 63 | "password": "adminpassword", 64 | "config": "/etc/cjdroute.conf" 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Copyright (C) 2013 Alexander Bauer, Luke Evers, Dylan Whichard, 4 | // and contributors; (GPLv3) see LICENSE or doc.go 5 | 6 | import ( 7 | "encoding/json" 8 | "errors" 9 | "html/template" 10 | "net" 11 | "os" 12 | "time" 13 | ) 14 | 15 | type Config struct { 16 | // Name is the string by which this instance of NodeAtlas will be 17 | // referred to. It usually describes the entire project name or 18 | // the region about which it focuses. 19 | Name string 20 | 21 | // AdminContact is the structure which contains information 22 | // relating to where you can contact the administrator. 23 | AdminContact struct { 24 | // Name of the administrator 25 | Name string 26 | 27 | // Email of the administrator 28 | Email string 29 | 30 | // PGP key of the administrator 31 | PGP string 32 | } 33 | 34 | // AdminAddresses is a slice of addresses which are considered 35 | // fully authenticated. Connections originating from those 36 | // addresses will not be required to verify or perform any sort of 37 | // authentication, meaning that they can edit or register any 38 | // node. If it is not specified, no addresses are granted this 39 | // ability. 40 | AdminAddresses []IP 41 | 42 | // Web is the structure which contains information relating to the 43 | // backend of the HTTP webserver. 44 | Web struct { 45 | // Hostname is the address which NodeAtlas should identify 46 | // itself as. For example, in a verification email, NodeAtlas 47 | // would give the verification link as 48 | // http:///verify/ 49 | Hostname string 50 | 51 | // Prefix is the URL prefix which is required to access the 52 | // front end. For example, with a prefix of "/nodeatlas", 53 | // NodeAtlas would be able to respond to 54 | // http://example.com/nodeatlas. 55 | Prefix string 56 | 57 | // Addr is the network protocol, interface, and port to which 58 | // NodeAtlas should bind. For example, "tcp://0.0.0.0:8077" 59 | // will bind globally to the 8077 TCP port, and 60 | // "unix://nodeatlas.sock" will create a UNIX socket at 61 | // nodeatlas.sock. 62 | Addr string 63 | 64 | // DeproxyHeaderFields is a list of HTTP header fields that 65 | // should be used instead of the connecting IP when verifying 66 | // nodes and logging major errors. They must be in 67 | // canonicalized form, such as "X-Forwarded-For" or 68 | // "X-Real-IP". 69 | DeproxyHeaderFields []string 70 | 71 | // HeaderSnippet is a snippet of code which is inserted into 72 | // the of each page. For example, one could include a 73 | // script tieing into Pikwik. 74 | HeaderSnippet template.HTML 75 | 76 | // AboutSnippet is an excerpt that will get put into 77 | // the /about page for all to read upon going to the 78 | // /about page. 79 | AboutSnippet string 80 | 81 | // RSS is the structure which contains settings for the 82 | // built-in RSS feed generator. 83 | RSS struct { 84 | // MaxAge is the duration after which new nodes are 85 | // considered old, and should no longer populate the feed. 86 | MaxAge Duration 87 | } 88 | } 89 | 90 | // ChildMaps is a list of addresses from which to pull lists of 91 | // nodes every heartbeat. Please note that these maps are trusted 92 | // fully, and they could easily introduce false nodes to the 93 | // database temporarily (until cleared by the CacheExpiration. 94 | ChildMaps []string 95 | 96 | // Database is the structure which contains the database driver 97 | // name, such as "sqlite3" or "mysql", and the database resource, 98 | // such as a path to .db file, or username, password, and name. 99 | Database struct { 100 | DriverName string 101 | Resource string 102 | ReadOnly bool 103 | } 104 | 105 | // HeartbeatRate is the amount of time to wait between performing 106 | // regular tasks, such as clearing expired nodes from the queue 107 | // and cache. 108 | HeartbeatRate Duration 109 | 110 | // CacheExpiration is the amount of time for which to store cached 111 | // nodes before considering them outdated, and removing them. 112 | CacheExpiration Duration 113 | 114 | // VerificationExpiration is the amount of time to allow users to 115 | // verify nodes by email after initially placing them. See the 116 | // documentation for time.ParseDuration for format information. 117 | VerificationExpiration Duration 118 | 119 | // ExtraVerificationFlags can be specified to add additional flags 120 | // (such as "-6") to the curl and wget instructions in the 121 | // verification email. 122 | ExtraVerificationFlags string 123 | 124 | // SMTP contains the information necessary to connect to a mail 125 | // relay, so as to send verification email to registered nodes. 126 | SMTP *struct { 127 | // VerifyDisabled controls whether email verification is used 128 | // for newly registered nodes. If it is false or omitted, an 129 | // email will be sent using the SMTP settings defined in this 130 | // struct. 131 | VerifyDisabled bool 132 | 133 | // EmailAddress will be given as the "From" address when 134 | // sending email. 135 | EmailAddress string 136 | 137 | // NoAuthenticate determines whether NodeAtlas should attempt to 138 | // authenticate with the SMTP relay or not. Unless the relay 139 | // is local, leave this false. 140 | NoAuthenticate bool 141 | 142 | // Username and Password are the credentials required by the 143 | // server to log in. 144 | Username, Password string 145 | 146 | // ServerAddress is the address of the SMTP relay, including 147 | // the port. 148 | ServerAddress string 149 | } 150 | 151 | // Map contains the information used by NodeAtlas to power the 152 | // Leaflet.js map. 153 | Map struct { 154 | // Favicon is the icon to be displayed in the browser when 155 | // viewing the map. It is a filename to be loaded from 156 | // `<*fRes>/icon/`. 157 | Favicon string 158 | 159 | // Tileserver is the URL used for loading tiles. It is of the 160 | // form "http://{s}.tile.osm.org/{z}/{x}/{y}.png", so that 161 | // Leaflet.js can use it. 162 | Tileserver string 163 | 164 | // Center contains the coordinates on which to center the map. 165 | Center struct { 166 | Latitude, Longitude float64 167 | } 168 | 169 | // Zoom is the Leaflet.js zoom level to start the map at. 170 | Zoom int 171 | 172 | // MaxZoom is the maximum zoom level to allow Leaflet.js. 173 | MaxZoom int 174 | 175 | // ClusterRadius is the range (in pixels) at which markers on 176 | // the map will cluster together. 177 | ClusterRadius int 178 | 179 | // Attribution is the "map data" copyright notice placed at 180 | // the bottom right of the map, meant to credit the 181 | // maintainers of the tileserver. 182 | Attribution template.HTML 183 | 184 | // AddressType is the text that is displayed on the 185 | // map next to "Address" when adding a new node or 186 | // editing a previous node. The default is 187 | // "Network-specific IP", but due to how general that 188 | // is, it should be changed to whatever is the most 189 | // helpful for people to understand. 190 | AddressType string 191 | } 192 | 193 | // Verify contains the list of steps used to ensure that new nodes 194 | // are valid when registered. They can be enabled or disabled 195 | // according to one's needs. 196 | Verify struct { 197 | // Netmask, if not nil, is a CIDR-form network mask which 198 | // requires that nodes registered have an Addr which matches 199 | // it. For example, "fc00::/8" would only allow IPv6 addresses 200 | // in which the first two digits are "fc", and 201 | // "192.168.0.0/16" would only allow IPv4 addresses in which 202 | // the first two bytes are "192.168". 203 | Netmask *IPNet 204 | 205 | // FromNode requires the verification request (GET 206 | // /api/verify?id=) to originate from the 207 | // address of the node that is being verified. 208 | FromNode bool 209 | } 210 | 211 | // NetworkAdmin is the set of configuration options which allows 212 | // NodeAtlas to connect to the administration interface of the 213 | // network device, if applicable. If it is not given, the feature 214 | // is disabled. 215 | NetworkAdmin *struct { 216 | // Type is the string which identifies the network 217 | // administration type to connect to. Currently, only "cjdns" 218 | // is supported. See . 219 | Type string 220 | 221 | Credentials map[string]interface{} 222 | } 223 | } 224 | 225 | // ReadConfig uses os and encoding/json to read a configuration from 226 | // the filesystem. It returns any errors it encounters. 227 | func ReadConfig(path string) (conf *Config, err error) { 228 | f, err := os.Open(path) 229 | if err != nil { 230 | return 231 | } 232 | defer f.Close() 233 | 234 | conf = &Config{} 235 | err = json.NewDecoder(f).Decode(conf) 236 | return 237 | } 238 | 239 | // WriteConfig uses os and encoding/json to write a configuration to 240 | // the filesystem. It creates the file if it doesn't exist and returns 241 | // any errors it encounters. 242 | func WriteConfig(conf *Config, path string) (err error) { 243 | f, err := os.Create(path) 244 | if err != nil { 245 | return 246 | } 247 | defer f.Close() 248 | 249 | err = json.NewEncoder(f).Encode(conf) 250 | return 251 | } 252 | 253 | type Duration time.Duration 254 | 255 | func (d Duration) MarshalJSON() ([]byte, error) { 256 | return json.Marshal(time.Duration(d).String()) 257 | } 258 | 259 | func (d *Duration) UnmarshalJSON(b []byte) error { 260 | if b[0] != '"' { 261 | // If the duration is not a string, then consider it to be the 262 | // zero duration, so we do not have to set it. 263 | return nil 264 | } 265 | dur, err := time.ParseDuration(string(b[1 : len(b)-1])) 266 | if err != nil { 267 | return err 268 | } 269 | *d = *(*Duration)(&dur) 270 | return nil 271 | } 272 | 273 | // IPNet is a wrapper for net.IPNet which implements json.Unmarshaler. 274 | 275 | type IPNet net.IPNet 276 | 277 | var InvalidIPNetError = errors.New("network mask is invalid") 278 | 279 | func (n *IPNet) UnmarshalJSON(b []byte) error { 280 | if b[0] != '"' { 281 | // If the IPNet is not given as a string, then it is invalid 282 | // and should return an error. 283 | return InvalidIPNetError 284 | } 285 | _, ipnet, err := net.ParseCIDR(string(b[1 : len(b)-1])) 286 | if err != nil { 287 | return err 288 | } 289 | *n = *(*IPNet)(ipnet) 290 | return nil 291 | } 292 | -------------------------------------------------------------------------------- /database.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Copyright (C) 2013 Alexander Bauer, Luke Evers, Dylan Whichard, 4 | // and contributors; (GPLv3) see LICENSE or doc.go 5 | 6 | import ( 7 | _ "code.google.com/p/go-sqlite/go1/sqlite3" 8 | "database/sql" 9 | "errors" 10 | _ "github.com/go-sql-driver/mysql" 11 | "time" 12 | ) 13 | 14 | var ( 15 | Db DB 16 | ) 17 | 18 | type DB struct { 19 | *sql.DB 20 | DriverName string 21 | ReadOnly bool 22 | } 23 | 24 | // InitializeTables issues the commands to create all tables and 25 | // columns. Will not create the tables if they exist already. If 26 | // it encounters an error, it is returned. 27 | func (db DB) InitializeTables() (err error) { 28 | // First, create the 'nodes' table. 29 | _, err = db.Query(`CREATE TABLE IF NOT EXISTS nodes ( 30 | address BINARY(16) PRIMARY KEY, 31 | owner VARCHAR(255) NOT NULL, 32 | email VARCHAR(255) NOT NULL, 33 | contact VARCHAR(255), 34 | details VARCHAR(255), 35 | pgp BINARY(8), 36 | lat FLOAT NOT NULL, 37 | lon FLOAT NOT NULL, 38 | status INT NOT NULL, 39 | updated INT NOT NULL);`) 40 | if err != nil { 41 | return 42 | } 43 | _, err = db.Query(`CREATE TABLE IF NOT EXISTS nodes_cached ( 44 | address BINARY(16) PRIMARY KEY, 45 | owner VARCHAR(255) NOT NULL, 46 | details VARCHAR(255), 47 | lat FLOAT NOT NULL, 48 | lon FLOAT NOT NULL, 49 | status INT NOT NULL, 50 | source INT NOT NULL, 51 | retrieved INT NOT NULL);`) 52 | if err != nil { 53 | return 54 | } 55 | _, err = db.Query(`CREATE TABLE IF NOT EXISTS nodes_verify_queue ( 56 | id INT PRIMARY KEY, 57 | address BINARY(16) NOT NULL, 58 | owner VARCHAR(255) NOT NULL, 59 | email VARCHAR(255) NOT NULL, 60 | contact VARCHAR(255), 61 | details VARCHAR(255), 62 | pgp BINARY(8), 63 | lat FLOAT NOT NULL, 64 | lon FLOAT NOT NULL, 65 | status INT NOT NULL, 66 | verifysent BOOL NOT NULL, 67 | expiration INT NOT NULL);`) 68 | if err != nil { 69 | return 70 | } 71 | // SQL? A standard? Hahahaha! 72 | if db.DriverName == "mysql" { 73 | _, err = db.Query(`CREATE TABLE IF NOT EXISTS cached_maps ( 74 | id INTEGER PRIMARY KEY AUTO_INCREMENT, 75 | hostname VARCHAR(255) NOT NULL, 76 | name VARCHAR(255) NOT NULL);`) 77 | } else { 78 | _, err = db.Query(`CREATE TABLE IF NOT EXISTS cached_maps ( 79 | id INTEGER PRIMARY KEY AUTOINCREMENT, 80 | hostname VARCHAR(255) NOT NULL, 81 | name VARCHAR(255) NOT NULL);`) 82 | } 83 | if err != nil { 84 | return 85 | } 86 | 87 | _, err = db.Query(`CREATE TABLE IF NOT EXISTS captcha ( 88 | id BINARY(32) NOT NULL, 89 | solution BINARY(6) NOT NULL, 90 | expiration INT NOT NULL);`) 91 | if err != nil { 92 | return 93 | } 94 | 95 | return 96 | } 97 | 98 | // LenNodes returns the number of nodes in the database. If there is 99 | // an error, it returns -1 and logs the incident. 100 | func (db DB) LenNodes(useCached bool) (n int) { 101 | // Count the number of rows in the 'nodes' table. 102 | var row *sql.Row 103 | if useCached { 104 | row = db.QueryRow("SELECT COUNT(*) FROM (SELECT address FROM nodes UNION SELECT address FROM nodes_cached) AS cachedNodes;") 105 | } else { 106 | row = db.QueryRow("SELECT COUNT(*) FROM nodes;") 107 | } 108 | // Write that number to n, and return if there is no 109 | // error. Otherwise, log it and return zero. 110 | if err := row.Scan(&n); err != nil { 111 | l.Errf("Error counting the number of nodes: %s", err) 112 | n = -1 113 | } 114 | return 115 | } 116 | 117 | // DumpNodes returns an array containing all nodes in the database, 118 | // including both local and cached nodes. 119 | func (db DB) DumpNodes() (nodes []*Node, err error) { 120 | // Begin by getting the required length of the array. If we get 121 | // -1, then there has been an error. 122 | if n := db.LenNodes(true); n != -1 { 123 | // If successful, initialize the array with the length. 124 | nodes = make([]*Node, n) 125 | } else { 126 | // Otherwise, error out. 127 | l.Errf("Could not count number of nodes in database\n") 128 | return nil, errors.New("Could not count number of nodes") 129 | } 130 | 131 | // Perform the query. 132 | rows, err := db.Query(` 133 | SELECT address,owner,contact,details,pgp,lat,lon,status,0 134 | FROM nodes 135 | UNION SELECT address,owner,"",details,"",lat,lon,status,source 136 | FROM nodes_cached;`) 137 | if err != nil { 138 | l.Errf("Error dumping database: %s", err) 139 | return 140 | } 141 | defer rows.Close() 142 | 143 | // Now, loop through, initialize the nodes, and fill them out 144 | // using only the selected columns. 145 | for i := 0; rows.Next(); i++ { 146 | // Initialize the node and put it in the table. 147 | node := new(Node) 148 | nodes[i] = node 149 | 150 | // Create temporary values to simplify scanning. 151 | contact := sql.NullString{} 152 | details := sql.NullString{} 153 | 154 | // Scan all of the values into it. 155 | err = rows.Scan(&node.Addr, &node.OwnerName, 156 | &contact, &details, &node.PGP, 157 | &node.Latitude, &node.Longitude, &node.Status, &node.SourceID) 158 | if err != nil { 159 | l.Errf("Error dumping database: %s", err) 160 | return 161 | } 162 | 163 | node.Contact = contact.String 164 | node.Details = details.String 165 | } 166 | return 167 | } 168 | 169 | // DumpLocal returns a slice containing all of the local nodes in the 170 | // database. 171 | func (db DB) DumpLocal() (nodes []*Node, err error) { 172 | // Begin by getting the required length of the array. If we get 173 | // -1, then there has been an error. 174 | if n := db.LenNodes(true); n != -1 { 175 | // If successful, initialize the array with the length. 176 | nodes = make([]*Node, n) 177 | } else { 178 | // Otherwise, error out. 179 | l.Errf("Could not count number of nodes in database\n") 180 | return nil, errors.New("Could not count number of nodes") 181 | } 182 | 183 | // Perform the query. 184 | rows, err := db.Query(` 185 | SELECT address,owner,contact,details,pgp,lat,lon,status 186 | FROM nodes;`) 187 | if err != nil { 188 | l.Errf("Error dumping database: %s", err) 189 | return 190 | } 191 | defer rows.Close() 192 | 193 | // Now, loop through, initialize the nodes, and fill them out 194 | // using only the selected columns. 195 | for i := 0; rows.Next(); i++ { 196 | // Initialize the node and put it in the table. 197 | node := new(Node) 198 | nodes[i] = node 199 | 200 | // Create temporary values to simplify scanning. 201 | contact := sql.NullString{} 202 | details := sql.NullString{} 203 | 204 | // Scan all of the values into it. 205 | err = rows.Scan(&node.Addr, &node.OwnerName, 206 | &contact, &details, &node.PGP, 207 | &node.Latitude, &node.Longitude, &node.Status) 208 | if err != nil { 209 | l.Errf("Error dumping database: %s", err) 210 | return 211 | } 212 | 213 | node.Contact = contact.String 214 | node.Details = details.String 215 | } 216 | return 217 | } 218 | 219 | // DumpChanges returns all nodes, both local and cached, which have 220 | // been updated or retrieved more recently than the given time. 221 | func (db DB) DumpChanges(time time.Time) (nodes []*Node, err error) { 222 | rows, err := db.Query(` 223 | SELECT address,owner,contact,details,pgp,lat,lon,status 224 | FROM nodes WHERE updated >= ? 225 | UNION 226 | SELECT address,owner,"",details,"",lat,lon,status 227 | FROM nodes_cached WHERE retrieved >= ?;`, time, time) 228 | if err != nil { 229 | return 230 | } 231 | 232 | // Append each node to the array in sequence. 233 | for rows.Next() { 234 | node := new(Node) 235 | 236 | contact := sql.NullString{} 237 | details := sql.NullString{} 238 | 239 | err = rows.Scan(&node.Addr, &node.OwnerName, 240 | &contact, &details, &node.PGP, 241 | &node.Latitude, &node.Longitude, &node.Status) 242 | if err != nil { 243 | return 244 | } 245 | 246 | node.Contact = contact.String 247 | node.Details = details.String 248 | 249 | nodes = append(nodes, node) 250 | } 251 | return 252 | } 253 | 254 | // AddNode inserts a node into the 'nodes' table with the current 255 | // timestamp. 256 | func (db DB) AddNode(node *Node) (err error) { 257 | // Inserts a new node into the database 258 | stmt, err := db.Prepare(`INSERT INTO nodes 259 | (address, owner, email, contact, details, pgp, lat, lon, status, updated) 260 | VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`) 261 | if err != nil { 262 | return 263 | } 264 | _, err = stmt.Exec([]byte(node.Addr), node.OwnerName, node.OwnerEmail, 265 | node.Contact, node.Details, []byte(node.PGP), 266 | node.Latitude, node.Longitude, node.Status, 267 | time.Now()) 268 | stmt.Close() 269 | return 270 | } 271 | 272 | func (db DB) AddNodes(nodes []*Node) (err error) { 273 | stmt, err := db.Prepare(`INSERT INTO nodes 274 | (address, owner, email, contact, details, pgp, lat, lon, status, updated) 275 | VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);`) 276 | if err != nil { 277 | return 278 | } 279 | 280 | for _, node := range nodes { 281 | _, err = stmt.Exec([]byte(node.Addr), 282 | node.OwnerName, node.OwnerEmail, 283 | node.Contact, node.Details, []byte(node.PGP), 284 | node.Latitude, node.Longitude, node.Status, 285 | time.Now()) 286 | if err != nil { 287 | return 288 | } 289 | } 290 | stmt.Close() 291 | return 292 | } 293 | 294 | // UpdateNode replaces the node in the database with the IP matching 295 | // the given node. 296 | func (db DB) UpdateNode(node *Node) (err error) { 297 | // Updates an existing node in the database 298 | stmt, err := db.Prepare(`UPDATE nodes SET 299 | owner = ?, contact = ?, details = ?, pgp = ?, lat = ?, lon = ?, status = ? 300 | WHERE address = ?`) 301 | if err != nil { 302 | return 303 | } 304 | _, err = stmt.Exec(node.OwnerName, node.Contact, 305 | node.Details, []byte(node.PGP), 306 | node.Latitude, node.Longitude, node.Status, []byte(node.Addr)) 307 | stmt.Close() 308 | return 309 | } 310 | 311 | // DeleteNode removes the node with the matching IP from the 'nodes' 312 | // table in the database. 313 | func (db DB) DeleteNode(addr IP) (err error) { 314 | // Deletes the given node from the database 315 | stmt, err := db.Prepare("DELETE FROM nodes WHERE address = ?") 316 | if err != nil { 317 | return 318 | } 319 | _, err = stmt.Exec([]byte(addr)) 320 | 321 | stmt.Close() 322 | return 323 | } 324 | 325 | // GetNode retrieves a single node from the database using the given 326 | // address. If there is a database error, it will be returned. If no 327 | // node matches, however, both return values will be nil. 328 | func (db DB) GetNode(addr IP) (node *Node, err error) { 329 | // Retrieves the node with the given address from the database 330 | stmt, err := db.Prepare(` 331 | SELECT owner, email, contact, details, pgp, lat, lon, status 332 | FROM nodes 333 | WHERE address = ? 334 | UNION 335 | SELECT owner, "", "", details, "", lat, lon, status 336 | FROM nodes_cached 337 | WHERE address = ? 338 | LIMIT 1`) 339 | if err != nil { 340 | return 341 | } 342 | 343 | // Initialize the node and temporary variable. 344 | node = &Node{Addr: addr} 345 | baddr := []byte(addr) 346 | contact := sql.NullString{} 347 | details := sql.NullString{} 348 | 349 | // Perform the actual query. 350 | row := stmt.QueryRow(baddr, baddr) 351 | err = row.Scan(&node.OwnerName, &node.OwnerEmail, 352 | &contact, &details, &node.PGP, 353 | &node.Latitude, &node.Longitude, &node.Status) 354 | stmt.Close() 355 | 356 | node.Contact = contact.String 357 | node.Details = details.String 358 | 359 | // If the error is of the particular type sql.ErrNoRows, it simply 360 | // means that the node does not exist. In that case, return (nil, 361 | // nil). 362 | if err == sql.ErrNoRows { 363 | return nil, nil 364 | } 365 | 366 | return 367 | } 368 | -------------------------------------------------------------------------------- /depslist: -------------------------------------------------------------------------------- 1 | leaflet.min.js http://cdn.leafletjs.com/leaflet-0.5/leaflet.js 2 | leaflet.css http://cdn.leafletjs.com/leaflet-0.5/leaflet.css 3 | leaflet.ie.css http://cdn.leafletjs.com/leaflet-0.5/leaflet.ie.css 4 | jquery.min.js http://code.jquery.com/jquery-2.0.3.min.js 5 | bootstrap.min.css https://raw.github.com/twbs/bootstrap/v3.1.1/dist/css/bootstrap.min.css 6 | bootstrap.min.js https://raw.github.com/twbs/bootstrap/v3.1.1/dist/js/bootstrap.min.js 7 | leaflet.markercluster.min.js https://raw.github.com/Leaflet/Leaflet.markercluster/0.2/dist/leaflet.markercluster.js 8 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | NodeAtlas - Federated mapping for mesh networks 3 | 4 | Copyright (C) 2013 Alexander Bauer, Luke Evers, Dylan Whichard, 5 | and contributors 6 | 7 | This program is free software: you can redistribute it and/or 8 | modify it under the terms of the GNU General Public License as 9 | published by the Free Software Foundation, either version 3 of the 10 | License, or (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, but 13 | WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see http://www.gnu.org/licenses/ 19 | */ 20 | package main 21 | 22 | // Copyright (C) 2013 Alexander Bauer, Luke Evers, Dylan Whichard, 23 | // and contributors; (GPLv3) see LICENSE or doc.go 24 | // Copyright (C) 2013 Developers - see LICENSE or doc.go 25 | -------------------------------------------------------------------------------- /doc/API.md: -------------------------------------------------------------------------------- 1 | API 2 | === 3 | 4 | NodeAtlas was designed to be entirely usable via just the API, which 5 | is powered by the Go package [JAS][]. The web interface is just a 6 | prettier front end to the API - requests are simply made through 7 | [JQuery][]. This means that it's entirely possible to build 8 | alternative clients for NodeAtlas or access it via the command line. 9 | 10 | [JAS]: http://godoc.org/github.com/coocood/jas 11 | [JQuery]: http://jquery.com/ 12 | 13 | For example, when `Verify.FromNode` is `true` in the configuration 14 | file, verification emails are sent with the instruction to, if the 15 | user is trying to add a remote node, execute the command `curl 16 | http://nodeatlas.example.com/api/verify?id=0123456789` from the node 17 | they're adding. This will allow them to verify their remote node from 18 | its own address, which is required on certain instances. 19 | 20 | This document describes API behavior as of version 21 | `v0.5.9-17-g0ee47aa`, and possibly later. Major changes will likely 22 | not be left undocumented, but there may be minor discrepancies. 23 | 24 | 25 | ## Accessing the API ## 26 | 27 | The API for every NodeAtlas instance (even severely outdated versions) 28 | is accessible at `/api/`. It returns a [JSON][]-encoded response of 29 | the form `{ "data": {}, "error": null }`. 30 | 31 | [JSON]: http://json.org 32 | 33 | It can be accessed by any HTTP client, though a JSON-decoder is needed 34 | to make programmatic use of the response. This is one advantage of 35 | using JQuery to access it. JSON is human-readable, though, so it is 36 | easy enough to use [`curl`][cURL] or [`wget`][wget] to access it by 37 | hand. The API expects only HTTP GET and POST requests. 38 | 39 | [cURL]: http://curl.haxx.se/ 40 | [wget]: https://www.gnu.org/software/wget/ 41 | 42 | ## Endpoints ## 43 | 44 | API endpoints are paths such as `/api/status` which return data of the 45 | aforementioned form. All API outputs given below are piped through 46 | `python -mjson.tool` for readability. 47 | 48 | ### / ### 49 | 50 | `GET /api/` redirects (`303 See Other`) to this document as hosted on 51 | the GitHub home page. It attaches the HTTP header `Location` in order 52 | to do so, but also gives the URL in the `data` field. 53 | 54 | ```json 55 | // curl -s "http://localhost:8077/api/" 56 | { 57 | "data": "See Other: https://github.com/ProjectMeshnet/nodeatlas/blob/master/doc/API.md", 58 | "error": null 59 | } 60 | ``` 61 | 62 | ### all ### 63 | 64 | `GET /api/all` returns a complete list of nodes, both local and 65 | cached, in native form. The data is given as a map of arrays, with the 66 | key being the link to the parent node, or "local." Private email 67 | addresses are never included. 68 | 69 | The only error it will return is `InternalError`, which is usually 70 | related to a database problem. 71 | 72 | ```json 73 | // curl -s "http://localhost:8077/api/all" 74 | { 75 | "data": { 76 | "http://map.maryland.projectmeshnet.org": [ 77 | { 78 | "Addr": "fcdf:db8b:fbf5:d3d7:64a:5aa3:f326:149c", 79 | "Latitude": 39.522979, 80 | "Longitude": -76.993403, 81 | "OwnerName": "Alexander Bauer", 82 | "Status": 385 83 | } 84 | ], 85 | "local": [ 86 | { 87 | "Addr": "fcdf:db8b:fbf5:d3d7:64a:5aa3:f326:149b", 88 | "Contact": "XMPP: duonoxsol@rows.io", 89 | "Details": "Bay node", 90 | "Latitude": 39.134321, 91 | "Longitude": -76.360474, 92 | "OwnerName": "Alexander Bauer", 93 | "PGP": "76aad89b", 94 | "Status": 257 95 | } 96 | ] 97 | }, 98 | "error": null 99 | } 100 | ``` 101 | 102 | Additionally, if the `?geojson` argument is supplied, data will be 103 | dumped in [GeoJSON][] format. This is extremely useful for displaying 104 | nodes. 105 | 106 | [GeoJSON]: http://geojson.org/ 107 | 108 | ```json 109 | // curl -s "http://localhost:8077/api/all?geojson" 110 | { 111 | "data": { 112 | "features": [ 113 | { 114 | "geometry": { 115 | "coordinates": [ 116 | -76.360474, 117 | 39.134321 118 | ], 119 | "type": "Point" 120 | }, 121 | "id": "fcdf:db8b:fbf5:d3d7:64a:5aa3:f326:149b", 122 | "properties": { 123 | "Contact": "XMPP: duonoxsol@rows.io", 124 | "Details": "Bay node", 125 | "OwnerName": "Alexander Bauer", 126 | "PGP": "76aad89b", 127 | "Status": 257 128 | }, 129 | "type": "Feature" 130 | }, 131 | { 132 | "geometry": { 133 | "coordinates": [ 134 | -76.993403, 135 | 39.522979 136 | ], 137 | "type": "Point" 138 | }, 139 | "id": "fcdf:db8b:fbf5:d3d7:64a:5aa3:f326:149c", 140 | "properties": { 141 | "OwnerName": "Alexander Bauer", 142 | "SourceID": 1, 143 | "Status": 385 144 | }, 145 | "type": "Feature" 146 | } 147 | ], 148 | "type": "FeatureCollection" 149 | }, 150 | "error": null 151 | } 152 | ``` 153 | 154 | ### child_maps ### 155 | 156 | `GET /api/child_maps` returns an array of objects containing the 157 | hostname/link of the child map, its ID local to this instance, and the 158 | name reported by querying `/api/status`. 159 | 160 | The only error it will return is `InternalError`, which is usually 161 | related to a database problem. 162 | 163 | ```json 164 | // curl -s "http://localhost:8077/api/child_maps" 165 | { 166 | "data": [ 167 | { 168 | "Hostname": "http://map.maryland.projectmeshnet.org", 169 | "ID": 1, 170 | "Name": "Maryland Mesh" 171 | } 172 | ], 173 | "error": null 174 | } 175 | ``` 176 | 177 | ### key ### 178 | 179 | `GET /api/key` generates a new CAPTCHA ID and solution pair in the 180 | database, which will be stored for ten minutes, and returns the ID. 181 | 182 | It will never return an error. 183 | 184 | ```json 185 | // curl -s "http://localhost:8077/api/key" 186 | { 187 | "data": "XpqCrgvtbyAJnKfYeaXN", 188 | "error": null 189 | } 190 | ``` 191 | 192 | ### node ### 193 | 194 | #### GET #### 195 | 196 | `GET /api/node` retrieves data for precisely one node as addressed by 197 | its IP, which can be either local or cached. 198 | 199 | If the IP is misformatted or not present, it will return 200 | `addressInvalid` or `No matching node` in the error field, 201 | respectively. 202 | 203 | ```json 204 | // curl -s "http://localhost:8077/api/node?address=fcdf:db8b:fbf5:d3d7:64a:5aa3:f326:149b" 205 | { 206 | "data": { 207 | "Addr": "fcdf:db8b:fbf5:d3d7:64a:5aa3:f326:149b", 208 | "Contact": "XMPP: duonoxsol@rows.io", 209 | "Details": "Bay node", 210 | "Latitude": 39.134321, 211 | "Longitude": -76.360474, 212 | "OwnerName": "Alexander Bauer", 213 | "PGP": "76aad89b", 214 | "Status": 257 215 | }, 216 | "error": null 217 | } 218 | ``` 219 | 220 | It can also be formatted with `?geojson`, but that is currently 221 | outdated and discouraged. 222 | 223 | #### POST #### 224 | 225 | `POST /api/node` is the means by which nodes are added to the map. If 226 | `SMTP.VerifyDisabled` is `false` in the configuration file, this will 227 | attempt to send a verification email on completion. It requires the 228 | following fields. 229 | 230 | ```json 231 | { 232 | "address": "fcdf:db8b:fbf5:d3d7:64a:5aa3:f326:149d", 233 | "latitude": 40.12345, 234 | "longitude": -80.54321, 235 | "name": "Alexander Bauer", 236 | "email": "duonoxsol@example.com", 237 | } 238 | ``` 239 | 240 | The following fields can also be given, but are not required. The 241 | `contact` and `details` fields must be shorter than 256 characters, 242 | but are otherwise arbitrary plaintext. `pgp` can be 16, 8, or 0 hex 243 | digits, and must be all lowercase, and `status` is a decimal `int32` 244 | composed of single-bit flags, as specified [here][status]. 245 | 246 | [status]: https://github.com/ProjectMeshnet/nodeatlas/issues/111 247 | 248 | ```json 249 | { 250 | "contact": "XMPP: duonoxsol@rows.io", 251 | "details": "arbitrary data", 252 | "pgp": "76AAD89B", 253 | "status": 385 254 | } 255 | ``` 256 | 257 | In addition, it requires a token. 258 | 259 | If there is an error, it will will either be of the form 260 | `Invalid`, such as `addressInvalid` or `emailInvalid`. If 261 | there is a database error, then it will return an `InternalError`. 262 | 263 | ```json 264 | // curl -s -d "address=fcdf:db8b:fbf5:d3d7:64a:5aa3:f326:149d" -d "latitude=40.12345" -d "longitude=-80.54321" -d "name=Alexander Bauer" -d "email=duonoxsol@example.com" -d "contact=XMPP: duonoxsol@rows.io" -d "pgp=76AAD89B" -d "status=385" "http://localhost:8077/api/node" 265 | { 266 | "data": "verification email sent", 267 | "error": null 268 | } 269 | ``` 270 | 271 | Or, if `SMTP.VerifyDisabled` is `true` in the configuration file, no 272 | email will be sent, and the response will be: 273 | 274 | ```json 275 | { 276 | "data": "successful", 277 | "error": null 278 | } 279 | ``` 280 | 281 | ### status ### 282 | 283 | `GET /api/status` returns simple parameters about the instance. 284 | 285 | It will never return an error. 286 | 287 | ```json 288 | // curl -s "http://localhost:8077/api/status" 289 | { 290 | "data": { 291 | "CachedMaps": 1, 292 | "CachedNodes": 7, 293 | "LocalNodes": 49, 294 | "Name": "Project Meshnet" 295 | }, 296 | "error": null 297 | } 298 | ``` 299 | 300 | ### verify ### 301 | 302 | `GET /api/verify` is used to verify a particular node ID via email. If 303 | `Verify.FromNode` in the configuration is `true`, then it requires 304 | that the request come from the address which is being verified. 305 | 306 | If it returns an error, it will be either `verify: remote address does not match Node address` or a database-related `InternalError`. 307 | 308 | ```json 309 | // curl -s "http://localhost:8077/api/verify?id=5085217136501410721" 310 | { 311 | "data": "successful", 312 | "error": null 313 | } 314 | ``` 315 | 316 | ### delete_node ### 317 | 318 | `POST /api/delete_node` removes a local node from the database. It 319 | requires that the connecting address match the address to be deleted, 320 | or to be registered as an admin. 321 | 322 | In addition, it requires a token. 323 | 324 | If it returns an error, it will either be verify: `remote address does 325 | not match Node address` or a database-related InternalError. 326 | 327 | ```json 328 | // curl -s -d "address=fcdf:db8b:fbf5:d3d7:64a:5aa3:f326:149d" http://localhost:8077/api/delete_node 329 | { 330 | "data": "deleted", 331 | "error": null 332 | } 333 | ``` 334 | 335 | ### message ### 336 | 337 | `POST /api/message` creates and sends an email to the address of the 338 | owner of the given node. The address to which it is sent remains 339 | private, and the IP of the sender is logged. It requires a non-expired 340 | CAPTCHA id and solution pair to be provided, and the message must be 341 | 1000 characters or under. 342 | 343 | Required fields are as follows. 344 | 345 | ```json 346 | { 347 | "captcha": "id:solution", 348 | "address": "fcdf:db8b:fbf5:d3d7:64a:5aa3:f326:149d", 349 | "from": "me@example.com", 350 | "message": "Hello, I'd like to peer with your node on the Project Meshnet\n 351 | NodeAtlas instance. Would you please provide peering details?" 352 | } 353 | ``` 354 | 355 | In addition, it requires a token. 356 | 357 | If there is an error, it will be a `CAPTCHA ID or solution is 358 | incorrect`, `CAPTCHA format invalid`, `Invalid` error, or a 359 | database-related `InternalError`. 360 | 361 | ```json 362 | // curl -s -d "captcha=n2teCkgMKdceXkEs5HiC:595801" -d "address=fcdf:db8b:fbf5:d3d7:64a:5aa3:f326:149d" -d "from=me@example.com" -d "message=Hello, I'd like to peer with your node on the Project Meshnet\nNodeAtlas instance. Would you please provide peering details?" "http://localhost:8077/api/message" 363 | { 364 | "data":null, 365 | "error":null 366 | } 367 | ``` 368 | 369 | ### update_node ### 370 | 371 | `POST /api/update_node` is very similar to [`POST /api/node`](#post), 372 | except that it does not take the `email` form, and it can only be used 373 | to update existing nodes. It requires that the request be sent from 374 | the address which is being updated, or from an admin address. 375 | 376 | In addition, it requires a token. 377 | 378 | If there is an error, it will be of the form `Invalid` or 379 | `InternalError`. 380 | -------------------------------------------------------------------------------- /doc/CONFIGURATION.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | NodeAtlas needs a configuration file. By default, NodeAtlas looks for 4 | `conf.json` in the current directory. There is a file called 5 | `conf.json.example` in the repository, which is a template for what 6 | the configuration file should look like. 7 | 8 | You can tell NodeAtlas to use a configuration file from anywhere else 9 | by using the `--conf` flag. For example: 10 | 11 | ``` 12 | nodeatlas --res res/ --conf /etc/nodeatlas.json 13 | ``` 14 | 15 | Below is a list of every config variable and what it is for. 16 | 17 | ### Name 18 | 19 | Name is the string by which this instance of NodeAtlas will be 20 | referred to. It usually describes the entire project name or the 21 | region about which it focuses. 22 | 23 | ### AdminContact 24 | 25 | AdminContact is the structure which contains information relating to 26 | where you can contact the administrator. 27 | 28 | #### Name 29 | 30 | Name of the administrator. 31 | 32 | #### Email 33 | 34 | Email of the administrator. 35 | 36 | #### PGP 37 | 38 | PGP key of the administrator. 39 | 40 | ### AdminAddresses 41 | 42 | AdminAddresses is a slice of addresses which are considered fully 43 | authenticated. Connections originating from those addresses will not 44 | be required to verify or perform any sort of authentication, meaning 45 | that they can edit or register any node. If it is not specified, no 46 | addresses are granted this ability. 47 | 48 | ### Web 49 | 50 | Web is the structure which contains information relating to the 51 | backend of the HTTP webserver. 52 | 53 | #### Hostname 54 | 55 | Hostname is the address which NodeAtlas should identify itself as. For 56 | example, in a verification email, NodeAtlas would give the 57 | verification link as `http:///verify/` 58 | 59 | #### Prefix 60 | 61 | Prefix is the URL prefix which is required to access the front 62 | end. For example, with a prefix of `/nodeatlas`, NodeAtlas would be 63 | able to respond to `http://example.com/nodeatlas`. 64 | 65 | #### Addr 66 | 67 | Addr is the network protocol, interface, and port to which NodeAtlas 68 | should bind. For example, `tcp://0.0.0.0:8077` will bind globally to 69 | the 8077 TCP port, and `unix://nodeatlas.sock` will create a UNIX 70 | socket at nodeatlas.sock. 71 | 72 | #### DeproxyHeaderFields 73 | 74 | DeproxyHeaderFields is a list of HTTP header fields that should be 75 | used instead of the connecting IP when verifying nodes and logging 76 | major errors. They must be in canonicalized form, such as 77 | "X-Forwarded-For" or "X-Real-IP". 78 | 79 | #### HeaderSnippet 80 | 81 | HeaderSnippet is a snippet of code which is inserted into the 82 | of each page. For example, one could include a script tieing into 83 | Pikwik. 84 | 85 | #### AboutSnippet 86 | 87 | AboutSnippet is an excerpt that will get put into the /about page for 88 | all to read upon going to the /about page. 89 | 90 | #### RSS 91 | 92 | RSS is the structure which contains settings for the built-in RSS feed 93 | generator. 94 | 95 | ##### MaxAge 96 | 97 | MaxAge is the duration after which new nodes are considered old, and 98 | should no longer populate the feed. 99 | 100 | ### ChildMaps 101 | 102 | ChildMaps is a list of addresses from which to pull lists of nodes 103 | every heartbeat. Please note that these maps are trusted fully, and 104 | they could easily introduce false nodes to the database temporarily 105 | (until cleared by the CacheExpiration). 106 | 107 | ### Database 108 | 109 | Database is the structure 110 | 111 | #### DriverName 112 | 113 | DriverName contains the database driver name, such as "sqlite3" or 114 | "mysql." 115 | 116 | #### Resource 117 | 118 | Resource is the database resource, such as a path to .db file, or 119 | username, password, and name. 120 | 121 | #### ReadOnly 122 | 123 | ReadOnly is a true/false variable deciding if we can write to the 124 | database or not. 125 | 126 | ### HeartbeatRate 127 | 128 | HeartbeatRate is the amount of time to wait between performing regular 129 | tasks, such as clearing expired nodes from the queue and cache. 130 | 131 | ### CacheExpiration 132 | 133 | CacheExpiration is the amount of time for which to store cached nodes 134 | before considering them outdated, and removing them. 135 | 136 | ### VerificationExpiration 137 | 138 | VerificationExpiration is the amount of time to allow users to verify 139 | nodes by email after initially placing them. See the documentation for 140 | time.ParseDuration for format information. 141 | 142 | ### ExtraVerificationFlags 143 | 144 | ExtraVerificationFlags can be specified to add additional flags (such 145 | as "-6") to the curl and wget instructions in the verification email. 146 | 147 | ### SMTP 148 | 149 | SMTP contains the information necessary to connect to a mail relay, so 150 | as to send verification email to registered nodes. 151 | 152 | #### VerifyDisabled 153 | 154 | VerifyDisabled controls whether email verification is used for newly 155 | registered nodes. If it is false or omitted, an email will be sent 156 | using the SMTP settings defined in this struct. 157 | 158 | #### EmailAddress 159 | 160 | EmailAddress will be given as the "From" address when sending email. 161 | 162 | #### Username 163 | 164 | Username is the username required by the server to login. 165 | 166 | #### Password 167 | 168 | Password is the password required by the server to login. 169 | 170 | #### NoAuthenticate 171 | 172 | NoAuthenticate determines whether NodeAtlas should attempt to 173 | authenticate with the SMTP relay or not. Unless the relay is local, 174 | leave this false. 175 | 176 | #### ServerAddress 177 | 178 | ServerAddress is the address of the SMTP relay, including the port. 179 | 180 | ### Map 181 | 182 | Map contains the information used by NodeAtlas to power the Leaflet.js 183 | map. 184 | 185 | #### Favicon 186 | 187 | Favicon is the icon to be displayed in the browser when viewing the 188 | map. It is a filename to be loaded from `<*fRes>/icon/`. 189 | 190 | #### Tileserver 191 | 192 | Tileserver is the URL used for loading tiles. It is of the form 193 | `http://{s}.tile.osm.org/{z}/{x}/{y}.png`, so that Leaflet.js can use 194 | it. 195 | 196 | #### Center 197 | 198 | Center contains the coordinates on which to center the map. 199 | 200 | ##### Latitude 201 | 202 | The latitude of the coordinate on which to center the map. 203 | 204 | ##### Longitude 205 | 206 | The longitude of the coordinates on which to center the map 207 | 208 | #### Zoom 209 | 210 | Zoom is the Leaflet.js zoom level to start the map at. 211 | 212 | #### ClusterRadius 213 | 214 | ClusterRadius is the range (in pixels) at which markers on the map 215 | will cluster together. 216 | 217 | #### Attibution 218 | 219 | Attribution is the "map data" copyright notice placed at the bottom 220 | right of the map, meant to credit the maintainers of the tileserver. 221 | 222 | #### AddressType 223 | 224 | AddressType is the text that is displayed on the map next to "Address" 225 | when adding a new node or editing a previous node. The default is 226 | "Network-specific IP", but due to how general that is, it should be 227 | changed to whatever is the mosthelpful for people to understand. 228 | 229 | ### Verify 230 | 231 | Verify contains the list of steps used to ensure that new nodes are 232 | valid when registered. They can be enabled or disabled according to 233 | one's needs. 234 | 235 | #### Netmask 236 | 237 | Netmask, if not nil, is a CIDR-form network mask which requires that 238 | nodes registered have an Addr which matches it. For example, 239 | `fc00::/8` would only allow IPv6 addresses in which the first two 240 | digits are "fc", and `192.168.0.0/16` would only allow IPv4 addresses 241 | in which the first two bytes are `192.168`. 242 | 243 | #### FromNode 244 | 245 | FromNode requires the verification request (`GET 246 | /api/verify?id=`) to originate from the address of the 247 | node that is being verified. 248 | -------------------------------------------------------------------------------- /edges.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "github.com/inhies/go-cjdns/admin" 6 | "net" 7 | "strings" 8 | ) 9 | 10 | var NetworkAdminNotConnectedError = errors.New("Network admin interface not connected") 11 | var NetworkAdminCredentialsMissingError = errors.New("Network admin credentials missing") 12 | var NetworkAdminCredentialsInvalidError = errors.New("Network admin credentials invalid") 13 | 14 | var KnownPeers []Pair 15 | 16 | type Pair struct { 17 | A IP 18 | B IP 19 | } 20 | 21 | type Peers struct { 22 | Source IP 23 | Destinations []IP 24 | } 25 | 26 | type Network interface { 27 | // Connect initializes the object and connects to whatever 28 | // administration interfaces necessary. 29 | Connect(*Config) error 30 | 31 | // Close closes any open connections and removes any stored 32 | // passwords. 33 | Close() error 34 | 35 | // PeersOf retrieves all IP addresses known to be connected to the 36 | // given IP. It can return nil. 37 | PeersOf(IP) (*Peers, error) 38 | 39 | // PeersOfAll functions similarly to PeersOf, but gives connected 40 | // IPs for all given IPs, in the order they are given. Slices can 41 | // be nil. 42 | PeersOfAll([]IP) ([]*Peers, error) 43 | } 44 | 45 | // PopulateRoutes finds the peers of every known node in the 46 | // database. It is blocking, and may wait on network IO. 47 | func PopulatePeers(db DB) { 48 | if Conf.NetworkAdmin == nil { 49 | l.Infoln("Network admin interface not specified; skipping") 50 | return 51 | } 52 | 53 | // Choose which kind of network to connect to. 54 | var network Network 55 | switch strings.ToLower(Conf.NetworkAdmin.Type) { 56 | case "cjdns": 57 | network = &CJDNSNetwork{} 58 | } 59 | 60 | // Dump all the nodes in the database. 61 | nodes, err := db.DumpNodes() 62 | if err != nil { 63 | l.Errf("Error listing peers: %s", err) 64 | return 65 | } 66 | 67 | // Reduce the list of nodes into just a list of IPs. 68 | ips := make([]IP, len(nodes)) 69 | for i, node := range nodes { 70 | ips[i] = node.Addr 71 | } 72 | 73 | // Connect to the network and find the peers for the whole list of 74 | // IPs. 75 | err = network.Connect(Conf) 76 | if err != nil { 77 | l.Errf("Error listing peers: %s", err) 78 | return 79 | } 80 | peers, err := network.PeersOfAll(ips) 81 | if err != nil { 82 | l.Errf("Error listing peers: %s", err) 83 | return 84 | } 85 | 86 | // Allocate a set of pairs with enough capacity to hold the entire 87 | // list of peers, assuming each one has two connections. 88 | pairs := make([]Pair, 0, len(peers)) 89 | 90 | // Flatten the peer network. Remove duplicates by only adding a 91 | // connection between nodes if they are already sorted such that 92 | // the node with the lesser IP is first. 93 | // TODO(DuoNoxSol): Only discard them in this way if both nodes 94 | // are in the map 95 | for _, peer := range peers { 96 | for _, destinationIP := range peer.Destinations { 97 | if peer.Source.LessThan(destinationIP) { 98 | pairs = append(pairs, Pair{ 99 | A: peer.Source, 100 | B: destinationIP, 101 | }) 102 | } 103 | } 104 | } 105 | 106 | l.Infof("Peering data refreshed") 107 | KnownPeers = pairs 108 | } 109 | 110 | type CJDNSNetwork struct { 111 | // connected reports whether the Network is currently connected to 112 | // the admin interface. 113 | connected bool 114 | 115 | // conn is the connection opened to the CJDNS admin interface. Its 116 | // methods can be used to access the interface. 117 | conn *admin.Conn 118 | 119 | // Routes is the slice of all known routes in the currently 120 | // connected network. It is used in calculating the peers of any 121 | // given node. 122 | Routes admin.Routes 123 | } 124 | 125 | func (n *CJDNSNetwork) Connect(conf *Config) (err error) { 126 | // Check to make sure that the credentials can be retrieved. If 127 | // not, error and exit. 128 | if conf.NetworkAdmin == nil || 129 | conf.NetworkAdmin.Credentials == nil { 130 | return NetworkAdminCredentialsMissingError 131 | } 132 | 133 | // Try to cast the credentials to the appropriate type. If this 134 | // fails, report them invalid. 135 | credentials, err := makeCJDNSAdminConfig( 136 | conf.NetworkAdmin.Credentials) 137 | if err != nil { 138 | return 139 | } 140 | 141 | n.conn, err = admin.Connect(credentials) 142 | if err == nil { 143 | n.connected = true 144 | } 145 | return 146 | } 147 | 148 | func (n *CJDNSNetwork) Close() error { 149 | n.connected = false 150 | return n.conn.Conn.Close() 151 | } 152 | 153 | func (n *CJDNSNetwork) PeersOf(ip IP) (peers *Peers, err error) { 154 | // First, ensure that the Network is connected. If not, return the 155 | // appropriate error. 156 | if !n.connected { 157 | return nil, NetworkAdminNotConnectedError 158 | } 159 | 160 | if len(n.Routes) == 0 { 161 | n.Routes, err = n.conn.NodeStore_dumpTable() 162 | if err != nil { 163 | return 164 | } 165 | } 166 | 167 | // Find all of the routes from the given IP to its peers. Strip 168 | // these of extra data, and return just the slice of IPs. 169 | peerRoutes := n.Routes.Peers(net.IP(ip)) 170 | if err != nil { 171 | return 172 | } 173 | 174 | peers = &Peers{ 175 | Source: ip, 176 | Destinations: make([]IP, len(peerRoutes)), 177 | } 178 | 179 | for i, route := range peerRoutes { 180 | peers.Destinations[i] = IP(*route.IP) 181 | } 182 | return 183 | } 184 | 185 | func (n *CJDNSNetwork) PeersOfAll(ips []IP) (peers []*Peers, err error) { 186 | peers = make([]*Peers, len(ips)) 187 | for i, ip := range ips { 188 | peers[i], err = n.PeersOf(ip) 189 | if err != nil { 190 | return nil, err 191 | } 192 | } 193 | return 194 | } 195 | 196 | func makeCJDNSAdminConfig(raw map[string]interface{}) (netconf *admin.CjdnsAdminConfig, err error) { 197 | // Set a default error, to save some keystrokes. 198 | err = NetworkAdminCredentialsInvalidError 199 | var ok bool 200 | 201 | netconf = &admin.CjdnsAdminConfig{} 202 | netconf.Addr, ok = raw["addr"].(string) 203 | if !ok { 204 | return 205 | } 206 | portf, ok := raw["port"].(float64) 207 | if !ok { 208 | return 209 | } 210 | netconf.Port = int(portf) 211 | netconf.Password, ok = raw["password"].(string) 212 | if !ok { 213 | return 214 | } 215 | 216 | // This last one is optional, so don't return an error if it 217 | // fails. 218 | netconf.Config, ok = raw["config"].(string) 219 | 220 | return netconf, nil 221 | } 222 | -------------------------------------------------------------------------------- /getdeps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | ## getdeps.sh 4 | # 5 | # This script is included as part of NodeAtlas for retrieving static 6 | # dependencies from content distribution networks (CDNs) or whatever 7 | # permanent home they might have. The purpose of this is to keep them 8 | # out of the Git repository, as per GitHub's recommendations on 9 | # [repository size]. 10 | # 11 | # When this script is run, a download tool will be chosen, which will 12 | # default to either `curl` or `wget`, (in that order), depending on 13 | # what is installed. If neither is installed, it will fail, unless a 14 | # preferred command is specified. 15 | # 16 | # Dependencies should be listed in a file called `deps.txt` in the 17 | # following format, one per line. 18 | # 19 | # file.ext http://example.com/downloads/file.min.ext 20 | # 21 | # If the file is downloaded successfully, it will be placed in the 22 | # assets directory, which is `res/web/assets`. 23 | # 24 | # If a dependency cannot be retrieved, its intended filename will be 25 | # given on `stdout` with a warning that it could not be downloaded, 26 | # and the script will continue. When it exits, however, and at least 27 | # one dependency could not be retrieved, it will exit with status 3. 28 | # 29 | # If there is any other sort of error, it will exit with status 1. 30 | # 31 | # [repository size]: http://git.io/2uKFOw 32 | # 33 | 34 | ############# 35 | # Variables # 36 | ############# 37 | 38 | DOWNLOADER= 39 | 40 | ############# 41 | # Constants # 42 | ############# 43 | 44 | DEFAULT_ASSETSDIR='res/web/assets' 45 | DEFAULT_DEPFILE='depslist' 46 | DEFAULT_DOWNLOADERS='curl -s -o,wget -qO' 47 | 48 | COLOR_RED="\033[1;31m" 49 | COLOR_DEFAULT="\033[0m" 50 | 51 | ######################### 52 | # On to the actual code # 53 | ######################### 54 | 55 | # Test if the dependency list is available and readable. 56 | if [ ! -f "$DEFAULT_DEPFILE" ]; then 57 | echo "$DEFAULT_DEPFILE is not a regular file - exiting" 58 | exit 1 59 | fi 60 | 61 | # Select the first appropriate downloader, but only if DOWNLOADER is 62 | # not specified. 63 | if [ -z "$DOWNLOADER" ]; then 64 | # Set the IFS so that we only split the list on commas, but make 65 | # sure we can restore the old IFS. Note that DEFAULT_DOWNLOADERS 66 | # is not quoted in the for loop declaration. 67 | OLDIFS="$IFS" 68 | IFS="," 69 | for command in $DEFAULT_DOWNLOADERS; do 70 | # Check if the first word of the given download command is on 71 | # the system path. If so, choose it and break from the loop. 72 | which "$(echo $command | cut -d\ -f1)" > /dev/null 73 | if [ "$?" -eq 0 ]; then 74 | echo "Using $command as preferred downloader" 75 | DOWNLOADER="$command" 76 | break 77 | fi 78 | done 79 | 80 | # Restore the old IFS so that we don't make hard to find problems 81 | # later. 82 | IFS="$OLDIFS" 83 | 84 | # If we reach this point, and the DOWNLOADER is still not set, we 85 | # couldn't find an appropriate downloader, and we should quit. 86 | if [ -z "$DOWNLOADER" ]; then 87 | echo "Could not find an appropriate downloader" 88 | echo "Please specify one in $0" 89 | exit 1 90 | fi 91 | fi 92 | 93 | # RETRIEVE_ALL will be used to record the success status. 94 | RETRIEVE_ALL=0 95 | 96 | # Once the downloader has been selected, loop through the file line by 97 | # line. 98 | i=0 99 | successes=0 100 | while read outfile url; do 101 | i=$(($i + 1)) 102 | 103 | # If either field is missing, explain that it is misformatted and 104 | # denote the failure, but continue. 105 | if [ -z "$outfile" -o -z "$url" ]; then 106 | echo " $COLOR_RED MISFORMATTED$COLOR_DEFAULT (line $i)" 107 | RETRIEVE_ALL=1 108 | continue 109 | fi 110 | 111 | # Report the status as we go. 112 | echo -n " $outfile..." 113 | 114 | # The downloader should function such that '$DOWNLOADER 115 | # $DEFAULT_ASSETSDIR/$outfile $url' will retrieve a file at $url 116 | # and place it in the $DEFAULT_ASSETSDIR at $outfile. 117 | $DOWNLOADER "$DEFAULT_ASSETSDIR/$outfile" "$url" 1>&2 118 | 119 | # Check the exit status from the $DOWNLOADER, and make sure it's 120 | # zero. If it's not, denote the failure and continue. 121 | if [ "$?" -ne 0 ]; then 122 | echo " $COLOR_RED FAILED$COLOR_DEFAULT (line $i)" 123 | RETRIEVE_ALL=1 124 | continue 125 | fi 126 | 127 | # If all is well, state the success. 128 | echo " downloaded" 129 | successes=$(($successes + 1)) 130 | done < "$DEFAULT_DEPFILE" 131 | 132 | # Report the status at the end, and exit with an appropriate code. 133 | if [ "$RETRIEVE_ALL" -eq 0 ]; then 134 | echo "All dependencies downloaded: $i" 135 | exit 0 136 | else 137 | echo "Dependencies downloaded: $successes (of $i)" 138 | echo "Errors were encountered during retrieval" 139 | exit 3 140 | fi 141 | -------------------------------------------------------------------------------- /import.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Copyright (C) 2013 Alexander Bauer, Luke Evers, Dylan Whichard, 4 | // and contributors; (GPLv3) see LICENSE or doc.go 5 | 6 | import ( 7 | "encoding/json" 8 | "io" 9 | "os" 10 | ) 11 | 12 | // Import reads a slice of JSON-encoded Nodes from the given io.Reader 13 | // and adds them to the database. Cache-related fields such as 14 | // RetrieveTime are discarded. 15 | func Import(r io.Reader) (err error) { 16 | // Unmarshal the Nodes from the given io.Reader. 17 | nodes := make([]*Node, 0) 18 | err = json.NewDecoder(r).Decode(&nodes) 19 | if err != nil { 20 | return 21 | } 22 | 23 | // Insert them into the database as new. Timestamps will be the 24 | // current time. 25 | err = Db.AddNodes(nodes) 26 | return 27 | } 28 | 29 | // ImportFile opens the given file and imports JSON-encoded Nodes from 30 | // it. 31 | func ImportFile(path string) (err error) { 32 | // Open the file in readonly mode. 33 | f, err := os.Open(path) 34 | if err != nil { 35 | return 36 | } 37 | defer f.Close() 38 | 39 | // Pass it along to Import. 40 | return Import(f) 41 | } 42 | -------------------------------------------------------------------------------- /nodeatlas.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Copyright (C) 2013 Alexander Bauer, Luke Evers, Dylan Whichard, 4 | // and contributors; (GPLv3) see LICENSE or doc.go 5 | 6 | import ( 7 | "database/sql" 8 | "flag" 9 | "fmt" 10 | "github.com/inhies/go-log" 11 | "html/template" 12 | "os" 13 | "os/signal" 14 | "sync" 15 | "syscall" 16 | "time" 17 | ) 18 | 19 | var Version = "0.6.0" 20 | 21 | var ( 22 | LogLevel = log.LogLevel(log.INFO) 23 | LogFlags = log.Ldate | log.Ltime // 2006/01/02 15:04:05 24 | LogFile = os.Stdout // *os.File type 25 | ) 26 | 27 | var ( 28 | Conf *Config 29 | StaticDir string // Directory for compiled files. 30 | Pulse *time.Ticker 31 | 32 | t *template.Template 33 | l *log.Logger 34 | 35 | shutdown = sync.NewCond(&sync.Mutex{}) 36 | ) 37 | 38 | var ( 39 | ignoreServerCrash bool 40 | 41 | // defaultResLocation and defaultConfLocation are used as flag 42 | // defaults. 43 | defaultResLocation = "res/" 44 | defaultConfLocation = "conf.json" 45 | ) 46 | 47 | var ( 48 | fConf = flag.String("conf", defaultConfLocation, 49 | "path to configuration file") 50 | fRes = flag.String("res", defaultResLocation, 51 | "path to resource directory") 52 | 53 | fLog = flag.String("file", "", "Logfile (defaults to stdout)") 54 | fDebug = flag.Bool("debug", false, "maximize verbosity") 55 | fQuiet = flag.Bool("q", false, "only output errors") 56 | 57 | fReadOnly = flag.Bool("readonly", false, "disallow database changes") 58 | 59 | fImport = flag.String("import", "", "import a JSON array of nodes") 60 | ) 61 | 62 | func main() { 63 | flag.Parse() 64 | 65 | // Load the configuration. 66 | var err error 67 | Conf, err = ReadConfig(*fConf) 68 | if err != nil { 69 | fmt.Printf("Could not read conf: %s", err) 70 | os.Exit(1) 71 | } 72 | 73 | // Set logging parameters based on flags. 74 | if *fDebug { 75 | LogLevel = log.DEBUG 76 | LogFlags |= log.Lshortfile // Include the filename and line 77 | } else if *fQuiet { 78 | LogLevel = log.ERR 79 | } 80 | 81 | if len(*fLog) > 0 { 82 | // If a file is specified, open it with the appropriate flags, 83 | // which will cause it to be created if not existent, and only 84 | // append data when writing to it. It will inherit all 85 | // permissions from its parent directory. 86 | LogFile, err = os.OpenFile(*fLog, 87 | os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) 88 | if err != nil { 89 | fmt.Printf("Could not open logfile: %s", err) 90 | os.Exit(1) 91 | } 92 | defer LogFile.Close() 93 | } // Otherwise, default to os.Stdout. 94 | 95 | l, err = log.NewLevel(LogLevel, true, LogFile, "", LogFlags) 96 | if err != nil { 97 | fmt.Printf("Could start logger: %s", err) 98 | os.Exit(1) 99 | } 100 | 101 | l.Infof("Starting NodeAtlas %s\n", Version) 102 | 103 | // Compile and template the static directory. 104 | StaticDir, err = CompileStatic(*fRes, Conf) 105 | if err != nil { 106 | if len(StaticDir) > 0 { 107 | // Try to remove the directory if it was created. 108 | err := os.RemoveAll(StaticDir) 109 | if err != nil { 110 | l.Emergf("Could not remove static directory %q: %s", 111 | StaticDir, err) 112 | } 113 | } 114 | l.Fatalf("Could not compile static files: %s", err) 115 | } 116 | l.Debugf("Compiled static files to %q\n", StaticDir) 117 | 118 | // Connect to the database with configured parameters. 119 | db, err := sql.Open(Conf.Database.DriverName, 120 | Conf.Database.Resource) 121 | if err != nil { 122 | l.Fatalf("Could not connect to database: %s", err) 123 | } 124 | // Wrap the *sql.DB type. 125 | Db = DB{ 126 | DB: db, 127 | DriverName: Conf.Database.DriverName, 128 | ReadOnly: (*fReadOnly || Conf.Database.ReadOnly), 129 | } 130 | l.Debug("Connected to database\n") 131 | if Db.ReadOnly { 132 | l.Warning("Database is read only\n") 133 | } 134 | 135 | // Initialize the database with all of its tables. 136 | err = Db.InitializeTables() 137 | if err != nil { 138 | l.Fatalf("Could not initialize database: %s", err) 139 | } 140 | l.Debug("Initialized database\n") 141 | l.Infof("Nodes: %d (%d local)\n", Db.LenNodes(true), Db.LenNodes(false)) 142 | 143 | // Check action flags and abandon normal startup if any are set. 144 | if len(*fImport) != 0 { 145 | err := ImportFile(*fImport) 146 | if err != nil { 147 | l.Fatalf("Import failed: %s", err) 148 | } else { 149 | l.Printf("Import successful!") 150 | } 151 | return 152 | } 153 | 154 | // Refresh peering data. This is also done on heartbeat, but it 155 | // should be done at startup until information is stored in the 156 | // database. 157 | go PopulatePeers(Db) 158 | 159 | // Listen for OS signals. 160 | go ListenSignal() 161 | 162 | // Set up the initial RSS feed so that it can be served once 163 | // online. If there is an error, it will be logged, but won't 164 | // prevent startup. 165 | CleanNodeRSS() 166 | 167 | // Start the Heartbeat. 168 | Heartbeat() 169 | l.Debug("Heartbeat started\n") 170 | 171 | go func() { 172 | // Start the HTTP server. This will block until the server 173 | // encounters an error. 174 | err = StartServer() 175 | if err != nil && !ignoreServerCrash { 176 | l.Fatalf("Server crashed: %s", err) 177 | } 178 | }() 179 | 180 | // Finally, block until told to exit. 181 | shutdown.L.Lock() 182 | shutdown.Wait() 183 | } 184 | 185 | // Heartbeat starts a time.Ticker to perform tasks on a regular 186 | // schedule, as set by Conf.HeartbeatRate, which are documented 187 | // below. The global variable Pulse is its ticker. To restart the 188 | // timer, invoke Heartbeat() again. 189 | // 190 | // Tasks: 191 | // - Db.DeleteExpiredFromQueue() 192 | // - UpdateMapCache() 193 | func Heartbeat() { 194 | // If the timer was not nil, then the timer must restart. 195 | if Pulse != nil { 196 | // If we are resetting, stop the existing timer before 197 | // replacing it. 198 | Pulse.Stop() 199 | } 200 | 201 | // Otherwise, create the Ticker and spawn a goroutine to check it. 202 | Pulse = time.NewTicker(time.Duration(Conf.HeartbeatRate)) 203 | go func() { 204 | for { 205 | if _, ok := <-Pulse.C; !ok { 206 | // If the channel closes, warn that the heartbeat has 207 | // stopped. 208 | l.Warning("Heartbeat stopped\n") 209 | return 210 | } 211 | // Otherwise, perform scheduled tasks. 212 | doHeartbeatTasks() 213 | } 214 | }() 215 | } 216 | 217 | // doHeartbeatTasks is the underlying function which is executed by 218 | // Heartbeat() at regular intervals. It can be called directly to 219 | // perform the tasks that are usually performed regularly. 220 | func doHeartbeatTasks() { 221 | l.Debug("Heartbeat\n") 222 | Db.DeleteExpiredFromQueue() 223 | UpdateMapCache() 224 | PopulatePeers(Db) 225 | ClearExpiredCAPTCHA() 226 | ResendVerificationEmails() 227 | CleanNodeRSS() 228 | } 229 | 230 | // ListenSignal uses os/signal to wait for OS signals, such as SIGHUP 231 | // and SIGINT, and perform the appropriate actions as listed below. 232 | // SIGHUP: reload configuration file 233 | // SIGINT, SIGKILL, SIGTERM: gracefully shut down 234 | func ListenSignal() { 235 | // Create the channel and use signal.Notify to listen for any 236 | // specified signals. 237 | c := make(chan os.Signal, 1) 238 | signal.Notify(c, syscall.SIGUSR1, syscall.SIGUSR2, 239 | os.Interrupt, os.Kill, syscall.SIGTERM) 240 | for sig := range c { 241 | switch sig { 242 | case syscall.SIGUSR1: 243 | l.Info("Forced heartbeat\n") 244 | doHeartbeatTasks() 245 | case syscall.SIGUSR2: 246 | l.Info("Reloading config\n") 247 | 248 | // Reload the configuration, but keep the old one if 249 | // there's an error. 250 | conf, err := ReadConfig(*fConf) 251 | if err != nil { 252 | l.Errf("Could not read conf; using old one: %s", err) 253 | continue 254 | } 255 | Conf = conf 256 | 257 | // Recompile the static directory, but be able to restore 258 | // the previous one if there's an error. 259 | oldStaticDir := StaticDir 260 | 261 | StaticDir, err = CompileStatic(*fRes, Conf) 262 | if err != nil { 263 | l.Errf("Error recompiling static directory: %s", err) 264 | StaticDir = oldStaticDir 265 | } 266 | // Remove the old one, and report if there's an error, but 267 | // continue even if there's an error. 268 | err = os.RemoveAll(oldStaticDir) 269 | if err != nil { 270 | l.Errf("Error removing old static directory: %s", err) 271 | } 272 | 273 | // Reload the email templates. 274 | err = RegisterTemplates() 275 | if err != nil { 276 | l.Errf("Error reloading email templates: %s", err) 277 | } 278 | 279 | // Restart the heartbeat ticker. 280 | Heartbeat() 281 | case os.Interrupt, os.Kill, syscall.SIGTERM: 282 | l.Infof("Caught %s; NodeAtlas over and out\n", sig) 283 | var err error 284 | 285 | // Close the HTTP Listener. If a UNIX socket is in use, it 286 | // will automatically be removed. We need to tell the 287 | // server to ignore errors, because closing the listener 288 | // will cause http.Server.Serve() to return one. 289 | ignoreServerCrash = true 290 | listener.Close() 291 | 292 | // Close the database connection. 293 | err = Db.Close() 294 | if err != nil { 295 | // If closing the database gave an error, report it 296 | // and set the exit code. 297 | l.Errf("Database could not be closed: %s", err) 298 | } 299 | 300 | // Delete the directory of static files. 301 | err = os.RemoveAll(StaticDir) 302 | if err != nil { 303 | // If the static directory coldn't be removed, report 304 | // it, give the location of the directory, and set the 305 | // exit code. 306 | l.Errf("Static directory %q could not be removed: %s", 307 | StaticDir, err) 308 | } 309 | 310 | // Finally, tell the main routine to stop waiting and 311 | // exit. 312 | shutdown.Broadcast() 313 | } 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /nodes.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Copyright (C) 2013 Alexander Bauer, Luke Evers, Dylan Whichard, 4 | // and contributors; (GPLv3) see LICENSE or doc.go 5 | 6 | import ( 7 | "bytes" 8 | "encoding/hex" 9 | "encoding/json" 10 | "errors" 11 | "github.com/baliw/moverss" 12 | "github.com/kpawlik/geojson" 13 | "net" 14 | ) 15 | 16 | // Statuses are the intended states of nodes. For example, if a node 17 | // is intended to be always-online and therefore should usually be 18 | // available, its status would be "active." 19 | const ( 20 | StatusActive = uint32(1 << iota) // << 0 active/planned 21 | _ 22 | _ 23 | _ 24 | _ // << 4 25 | _ 26 | _ 27 | StatusPhysical // physical server/virtual server 28 | StatusInternet // << 8 internet access/no internet 29 | StatusWireless // wireless access/no wireless 30 | StatusWired // wired access/no wired 31 | _ 32 | _ // << 12 33 | _ 34 | _ 35 | _ 36 | _ // << 16 37 | _ 38 | _ 39 | _ 40 | _ // << 20 41 | _ 42 | _ 43 | _ 44 | StatusPingable // << 24 pingable/down 45 | _ 46 | _ 47 | _ 48 | _ // << 28 49 | _ 50 | _ 51 | _ 52 | ) 53 | 54 | // Node is the basic type for a computer, radio, transmitter, or any 55 | // other sort of node in a mesh network. 56 | type Node struct { 57 | /* Required Fields */ 58 | // SourceID is the local ID of the source map of this node. If the 59 | // SourceID is 0, the node is considered to be local. 60 | SourceID int `json:"-"` 61 | 62 | // Status is a list of bit flags representing the node's status, 63 | // such as whether it is active or planned, has wireless access, 64 | // is a physical server, etc. 65 | Status uint32 66 | 67 | // Latitude and Longitude represent the physical location of the 68 | // node on earth. 69 | Latitude, Longitude float64 70 | 71 | // Addr is the network address (for the meshnet protocol) of the 72 | // node. 73 | Addr IP 74 | 75 | // RetrieveTime is only used if the node is cached, and comes from 76 | // another map. It is the Unix time (in seconds) at which the node 77 | // was retrieved from its home instance. If it is zero, the node 78 | // is not cached. 79 | RetrieveTime int64 `json:",omitempty"` 80 | 81 | // OwnerName is the node's owner's real or screen name. 82 | OwnerName string 83 | 84 | // OwnerEmail is the node's owner's email address. 85 | OwnerEmail string `json:",omitempty"` 86 | 87 | /* Optional Fields */ 88 | 89 | // Contact is public contact information, such as a nickname, 90 | // email, or xmpp username. 91 | Contact string `json:",omitempty"` 92 | 93 | // Details is extra information about node 94 | Details string `json:",omitempty"` 95 | 96 | // PGP is the key ID of the owner's public key. 97 | PGP PGPID `json:",omitempty"` 98 | } 99 | 100 | // Feature returns the Node as a *geojson.Feature. 101 | func (n *Node) Feature() (f *geojson.Feature) { 102 | // Set the properties. 103 | properties := make(map[string]interface{}, 6) 104 | properties["OwnerName"] = n.OwnerName 105 | properties["Status"] = n.Status 106 | 107 | if len(n.Contact) != 0 { 108 | properties["Contact"] = n.Contact 109 | } 110 | if len(n.PGP) != 0 { 111 | properties["PGP"] = n.PGP.String() 112 | } 113 | if len(n.Details) != 0 { 114 | properties["Details"] = n.Details 115 | } 116 | if n.SourceID != 0 { 117 | properties["SourceID"] = n.SourceID 118 | } 119 | 120 | // Create and return the feature. 121 | return geojson.NewFeature( 122 | geojson.NewPoint(geojson.Coordinate{ 123 | geojson.CoordType(n.Longitude), 124 | geojson.CoordType(n.Latitude)}), 125 | properties, 126 | n.Addr) 127 | } 128 | 129 | // Item returns the Node as a *moverss.Item. It does not set the 130 | // timestamp. 131 | func (n *Node) Item() (i *moverss.Item) { 132 | return &moverss.Item{ 133 | Link: Conf.Web.Hostname + "/node/" + n.Addr.String(), 134 | Title: n.OwnerName, 135 | XMLName: NodeXMLName, 136 | } 137 | } 138 | 139 | // IP is a wrapper for net.IP which implements the json.Marshaler and 140 | // json.Unmarshaler. 141 | type IP net.IP 142 | 143 | var IncorrectlyFormattedIP = errors.New("incorrectly formatted ip address") 144 | 145 | func (ip IP) MarshalJSON() ([]byte, error) { 146 | return json.Marshal(net.IP(ip).String()) 147 | } 148 | 149 | func (ip *IP) UnmarshalJSON(b []byte) error { 150 | if b[0] != '"' { 151 | // If a quote is not the first character, the next bit will 152 | // segfault, so we should return an error. 153 | return IncorrectlyFormattedIP 154 | } 155 | tip := net.ParseIP(string(b[1 : len(b)-1])) 156 | if tip == nil { 157 | return IncorrectlyFormattedIP 158 | } 159 | // Don't think too hard about this part. 160 | *ip = *(*IP)(&tip) 161 | return nil 162 | } 163 | 164 | // LessThan returns whether the called IP is less than the given IP 165 | // lexiographically. 166 | func (ip IP) LessThan(other IP) bool { 167 | return bytes.Compare(ip, other) < 0 168 | } 169 | 170 | func (ip IP) String() string { 171 | return net.IP(ip).String() 172 | } 173 | 174 | // FeatureCollectionNodes returns a *geojson.FeatureCollection type 175 | // from the given nodes, in order. 176 | func FeatureCollectionNodes(nodes []*Node) *geojson.FeatureCollection { 177 | features := make([]*geojson.Feature, len(nodes)) 178 | for i, n := range nodes { 179 | features[i] = n.Feature() 180 | } 181 | return geojson.NewFeatureCollection(features) 182 | } 183 | 184 | type PGPID []byte 185 | 186 | var IncorrectlyFormattedPGPID = errors.New("incorrectly formatted PGP ID") 187 | 188 | func (pgpid PGPID) MarshalJSON() ([]byte, error) { 189 | b := make([]byte, len(pgpid)*2) 190 | hex.Encode(b, pgpid) 191 | return json.Marshal(string(b)) 192 | } 193 | 194 | func (pgpid *PGPID) UnmarshalJSON(b []byte) error { 195 | if b[0] != '"' { 196 | // If a quote is not the first character, then the next part 197 | // will segfault. 198 | return IncorrectlyFormattedPGPID 199 | } 200 | b = b[1 : len(b)-1] 201 | if len(b) != 0 && len(b) != 8 && len(b) != 16 { 202 | return IncorrectlyFormattedPGPID 203 | } else if len(b) == 0 { 204 | // If the length is zero, make the result nil. 205 | *pgpid = nil 206 | } 207 | *pgpid = make(PGPID, len(b)/2) 208 | _, err := hex.Decode(*pgpid, b) 209 | return err 210 | } 211 | 212 | func (pgpid PGPID) String() string { 213 | if len(pgpid) == 0 { 214 | return "" 215 | } 216 | 217 | b := make([]byte, len(pgpid)*2) 218 | _ = hex.Encode(b, pgpid) 219 | return string(b) 220 | } 221 | 222 | func DecodePGPID(b []byte) (pgpid PGPID, err error) { 223 | if len(b) != 0 && len(b) != 8 && len(b) != 16 { 224 | return nil, IncorrectlyFormattedPGPID 225 | } 226 | pgpid = make(PGPID, len(b)/2) 227 | _, err = hex.Decode(pgpid, b) 228 | return 229 | } 230 | -------------------------------------------------------------------------------- /packaging/PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: Alexander Bauer 2 | pkgname=nodeatlas 3 | pkgver=0.0.0 4 | pkgrel=1 5 | pkgdesc="Federated node mapping for mesh networks" 6 | url="https://github.com/ProjectMeshnet/nodeatlas" 7 | arch=('x86_64' 'i686') 8 | license=('GPL3') 9 | depends=('glibc') 10 | makedepends=('go>=1.2') 11 | conflicts=() 12 | replaces=() 13 | backup=() 14 | 15 | source=("${url}/archive/${_gitver}.tar.gz") 16 | 17 | # Checksums are generated by updpkgsums prior to build. 18 | sha256sums=() #autofill using updpkgsums 19 | 20 | build() { 21 | cd "${srcdir}/${pkgname}-${_gitver}" 22 | DESTDIR="${pkgdir}" prefix="/usr" make 23 | } 24 | 25 | package() { 26 | cd "${srcdir}/${pkgname}-${_gitver}" 27 | DESTDIR="${pkgdir}" prefix="/usr" make install 28 | } 29 | 30 | # vim:set ts=2 sw=2 et: 31 | -------------------------------------------------------------------------------- /res/email/message.txt: -------------------------------------------------------------------------------- 1 | From: {{.From}} 2 | Subject: {{.Subject}} 3 | Date: {{.Header.Date}} 4 | To: {{.To}} 5 | Reply-To: {{.Data.ReplyTo}} 6 | MIME-version: 1.0 7 | Content-Type: multipart/alternative; boundary="========{{.Data.Boundary}}==" 8 | 9 | --========{{.Data.Boundary}}== 10 | Content-Type: text/plain; charset=us-ascii 11 | 12 | {{.Data.Message}} 13 | 14 | -- 15 | This email was sent through NodeAtlas by another user because your 16 | node is listed on 17 | {{.Data.Link}} 18 | 19 | If you choose to reply, make sure that you reply to 20 | {{.Data.ReplyTo}} 21 | 22 | If this email was abusive or spam, please email 23 | {{.Data.AdminContact.Name}} <{{.Data.AdminContact.Email}}> {{.Data.AdminContact.PGP}} 24 | 25 | https://github.com/ProjectMeshnet/nodeatlas 26 | 27 | --========{{.Data.Boundary}}== 28 | Content-Type: text/html; charset=UTF-8 29 | 30 |

{{html .Data.Message | markdownify}}

31 | 32 | --
33 | This email was sent through NodeAtlas by another user because your 34 | node is listed on {{.Data.Name}}.
35 | 36 | If you choose to reply, make sure that you reply to {{.Data.ReplyTo}}.
38 | 39 | If this email was abusive or spam, please email 40 | {{.Data.AdminContact.Name}} 41 | {{.Data.AdminContact.Email}} 42 | {{.Data.AdminContact.PGP}}
43 | 44 | NodeAtlas GitHub
45 | 46 | --========{{.Data.Boundary}}==-- 47 | -------------------------------------------------------------------------------- /res/email/verification.txt: -------------------------------------------------------------------------------- 1 | From: {{.From}} 2 | Subject: {{.Subject}} 3 | Date: {{.Header.Date}} 4 | To: {{.To}} 5 | MIME-version: 1.0 6 | Content-Type: multipart/alternative; boundary="========{{.Data.Boundary}}==" 7 | 8 | --========{{.Data.Boundary}}== 9 | Content-Type: text/plain; charset=us-ascii 10 | 11 | To verify your node, visit the below link.{{if .Data.FromNode}} Bear 12 | in mind that you must visit it from the address you registered on the 13 | map.{{end}} 14 | 15 | {{.Data.Link}}/verify/{{.Data.VerificationID}} 16 | {{if .Data.FromNode}} 17 | If you can't access the link from the node you placed, such as if it's 18 | a virtual private server, then you can access it via the command line. 19 | 20 | curl {{.Data.Flags}} {{.Data.Link}}/api/verify?id={{.Data.VerificationID}} 21 | 22 | If you don't have cURL, try the following. 23 | 24 | wget {{.Data.Flags}} -qO- {{.Data.Link}}/api/verify?id={{.Data.VerificationID}} 25 | {{end}} 26 | If you didn't place this node and your email address was entered by 27 | mistake, then please ignore this email. We're sorry. 28 | 29 | -- 30 | Automated email by NodeAtlas 31 | https://github.com/ProjectMeshnet/nodeatlas 32 | 33 | --========{{.Data.Boundary}}== 34 | Content-Type: text/html; charset=UTF-8 35 | 36 |

To verify your node, visit the below link.{{if .Data.FromNode}} Bear 37 | in mind that you must visit it from the address you registered on the 38 | map.{{end}}

39 | 40 |

{{.Data.Link}}/verify/{{.Data.VerificationID}}

41 | 42 | {{if .Data.FromNode}} 43 |

If you can't access the link from the node you placed, such as if it's 44 | a virtual private server, then you can access it via the command line.

45 | 46 | curl {{.Data.Flags}} {{.Data.Link}}/api/verify?id={{.Data.VerificationID}} 47 | 48 |

If you don't have cURL, try the following.

49 | 50 | wget {{.Data.Flags}} -qO- {{.Data.Link}}/api/verify?id={{.Data.VerificationID}} 51 | {{end}} 52 |

If you didn't place this node and your email address was entered by 53 | mistake, then please ignore this email. We're sorry.

54 | 55 | --
56 | Automated email by NodeAtlas
57 | NodeAtlas GitHub
58 | 59 | --========{{.Data.Boundary}}==-- 60 | -------------------------------------------------------------------------------- /res/web/about/index.html.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{.Name}} 8 | 9 | 10 | 11 | 12 | 13 | 14 | {{.Web.HeaderSnippet}} 15 | 16 | 17 | 36 |
37 |
38 |
39 |

About NodeAtlas

40 |
41 |
42 |

How do you make sense of a meshnet? You map it.

43 |

This is an early release of the NodeAtlas meshnet map, proudly built with Go, Leaflet.js, and Bootstrap

44 |
45 |
46 |
47 |
48 |

Admin Contact

49 |
50 |
51 |
52 |

{{.AdminContact.Name}} <{{.AdminContact.Email}}> {{.AdminContact.PGP}}

53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | {{.Web.AboutSnippet}} 61 |
62 |
63 |
64 |
65 | {{if .ChildMaps}} 66 |

Child Maps loaded:

67 | {{end}} 68 | {{range $i := .ChildMaps}} 69 | {{$i}}
70 | {{end}} 71 |
72 |
73 |
74 | 75 | 76 | -------------------------------------------------------------------------------- /res/web/assets/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore -------------------------------------------------------------------------------- /res/web/css/style.css: -------------------------------------------------------------------------------- 1 | @charset "Utf-8"; 2 | 3 | body { 4 | font-family: Helvetica, sans-seirf; 5 | font-size: 14px; 6 | font-weight: 400; 7 | color: #333; 8 | } 9 | 10 | a.btn { 11 | color: #FFF; 12 | } 13 | 14 | .btn { 15 | font-size: 12px; 16 | font-weight: 400; 17 | } 18 | 19 | #map { 20 | width:100%; 21 | height:700px; 22 | } 23 | 24 | .dropdown-menu a:hover { 25 | color: #AAA !important; 26 | } 27 | 28 | .popover { 29 | width: 225px !important; 30 | } 31 | 32 | .popover hr { 33 | margin: 6px -14px; 34 | } 35 | 36 | a.close { 37 | opacity: .5; 38 | } 39 | 40 | li a.disabled, .disabled { 41 | color: #AAA; 42 | } 43 | 44 | .leaflet-control-geoloc { 45 | background-image: url(/img/location.png); 46 | -webkit-border-radius: 5px 5px 5px 5px; 47 | border-radius: 5px 5px 5px 5px; 48 | } 49 | 50 | .leaflet-control-node { 51 | background-image: url(/img/node.png); 52 | -webkit-border-radius: 5px 5px 5px 5px; 53 | border-radius: 5px 5px 5px 5px; 54 | } 55 | 56 | .leaflet-control-about { 57 | background-image: url(/img/info.png); 58 | -webkit-border-radius: 5px 5px 5px 5px; 59 | border-radius: 5px 5px 5px 5px; 60 | } 61 | 62 | .leaflet-control-distance { 63 | background-image: url(/img/ruler.png); 64 | -webkit-border-radius: 5px 5px 5px 5px; 65 | border-radius: 5px 5px 5px 5px; 66 | } 67 | 68 | .desc { 69 | color: #AAA; 70 | font-size: 10px; 71 | } 72 | 73 | #two, #three, #four { 74 | display: none; 75 | } 76 | 77 | textarea.contact { 78 | width: 200px !important; 79 | } 80 | 81 | .property { 82 | color: #333; 83 | font-size: 16px; 84 | font-weight: 900; 85 | } 86 | 87 | li.active a, li.open a { 88 | background-color: rgba(255, 255, 255, 1) !important; 89 | } 90 | 91 | .navbar { 92 | position: absolute; 93 | z-index: 5000; 94 | width: 100%; 95 | background-color: rgba(255, 255, 255, .8); 96 | } 97 | 98 | .leaflet-top { 99 | top: 45px; 100 | } 101 | 102 | .padding { 103 | padding-top: 25px; 104 | } 105 | 106 | #inputform { 107 | -webkit-border-radius: 5px; 108 | -moz-border-radius: 5px; 109 | border-radius: 5px; 110 | position: absolute; 111 | top: 75px; 112 | display: none; 113 | right: 25px; 114 | background: rgba(255, 255, 255, .8); 115 | padding: 25px; 116 | overflow: hidden; 117 | z-index: 4000; 118 | font-family: Helvetica; 119 | width: 250px; 120 | } 121 | 122 | #bringnavbarback { 123 | display: none; 124 | position: absolute; 125 | z-index: 4000; 126 | top: 0px; 127 | right: 50px; 128 | background-color: rgba(255, 255, 255, .8); 129 | padding: 10px; 130 | } 131 | 132 | #bringnavbarback:hover { 133 | cursor: pointer; 134 | background-color: rgba(255, 255, 255, 1); 135 | } 136 | 137 | #search { 138 | margin-top: 6px; 139 | } 140 | 141 | .node, #messageCreate { 142 | -webkit-border-radius: 5px; 143 | -moz-border-radius: 5px; 144 | border-radius: 5px; 145 | position: absolute; 146 | top: 75px; 147 | right: 25px; 148 | background: rgba(255, 255, 255, .8); 149 | padding: 5px 25px 25px 25px; 150 | overflow: hidden; 151 | z-index: 4000; 152 | font-family: Helvetica; 153 | width: 300px; 154 | font-size: 12px; 155 | } 156 | 157 | #alert { 158 | position: absolute; 159 | top: 75px; 160 | right: 25px; 161 | z-index: 4000; 162 | } 163 | 164 | #alert-left { 165 | position: absolute; 166 | top: 75px; 167 | left: 25px; 168 | z-index: 4000; 169 | } 170 | 171 | .btn-mini { 172 | padding: 2px 4px 0px; 173 | font-size: 10px; 174 | } 175 | 176 | /* ========== MARKERCLUSTER ========== */ 177 | 178 | .marker-cluster-small { 179 | background-color: rgb(181, 226, 140); 180 | background-color: rgba(181, 226, 140, 0.6); 181 | } 182 | 183 | .marker-cluster-small div { 184 | background-color: rgb(110, 204, 57); 185 | background-color: rgba(110, 204, 57, 0.6); 186 | } 187 | 188 | .marker-cluster-medium { 189 | background-color: rgb(241, 211, 87); 190 | background-color: rgba(241, 211, 87, 0.6); 191 | } 192 | 193 | .marker-cluster-medium div { 194 | background-color: rgb(240, 194, 12); 195 | background-color: rgba(240, 194, 12, 0.6); 196 | } 197 | 198 | .marker-cluster-large { 199 | background-color: rgb(253, 156, 115); 200 | background-color: rgba(253, 156, 115, 0.6); 201 | } 202 | 203 | .marker-cluster-large div { 204 | background-color: rgb(241, 128, 23); 205 | background-color: rgba(241, 128, 23, 0.6); 206 | } 207 | 208 | .marker-cluster { 209 | background-clip: padding-box; 210 | border-radius: 20px; 211 | } 212 | 213 | .marker-cluster div { 214 | width: 30px; 215 | height: 30px; 216 | margin-left: 5px; 217 | margin-top: 5px; 218 | text-align: center; 219 | border-radius: 15px; 220 | font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif; 221 | } 222 | 223 | .marker-cluster span { 224 | line-height: 30px; 225 | } 226 | 227 | .leaflet-cluster-anim .leaflet-marker-icon, 228 | .leaflet-cluster-anim .leaflet-marker-shadow { 229 | -webkit-transition: -webkit-transform 0.25s ease-out, opacity 0.25s ease-in; 230 | -moz-transition: -moz-transform 0.25s ease-out, opacity 0.25s esae-in; 231 | -o-transition: -o-transform 0.25s ease-out, opacity 0.25s ease-in; 232 | transition: transform 0.25s ease-out, opacity 0.25s ease-in; 233 | } 234 | -------------------------------------------------------------------------------- /res/web/custom/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore -------------------------------------------------------------------------------- /res/web/img/icon/nodeatlas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectMeshnet/nodeatlas/87bd4d80dafd762abc7cb35cf5fa09eb400f2ac2/res/web/img/icon/nodeatlas.png -------------------------------------------------------------------------------- /res/web/img/inactive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectMeshnet/nodeatlas/87bd4d80dafd762abc7cb35cf5fa09eb400f2ac2/res/web/img/inactive.png -------------------------------------------------------------------------------- /res/web/img/newUser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectMeshnet/nodeatlas/87bd4d80dafd762abc7cb35cf5fa09eb400f2ac2/res/web/img/newUser.png -------------------------------------------------------------------------------- /res/web/img/node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectMeshnet/nodeatlas/87bd4d80dafd762abc7cb35cf5fa09eb400f2ac2/res/web/img/node.png -------------------------------------------------------------------------------- /res/web/img/shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectMeshnet/nodeatlas/87bd4d80dafd762abc7cb35cf5fa09eb400f2ac2/res/web/img/shadow.png -------------------------------------------------------------------------------- /res/web/img/vps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectMeshnet/nodeatlas/87bd4d80dafd762abc7cb35cf5fa09eb400f2ac2/res/web/img/vps.png -------------------------------------------------------------------------------- /res/web/index.html.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{.Name}} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {{.Web.HeaderSnippet}} 21 | 22 | 23 |
24 | 97 |