Compare commits
59
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1a4f81e50 | ||
|
|
7b7e35d339 | ||
|
|
3176629410 | ||
|
|
e3ccc45d19 | ||
|
|
beec9e834e | ||
|
|
f6f0486ff9 | ||
|
|
518f446d31 | ||
|
|
fbbd510088 | ||
|
|
e440d30028 | ||
|
|
44d30c83bf | ||
|
|
7ff7b55732 | ||
|
|
44346b3a5a | ||
|
|
23a538d61a | ||
|
|
dcb5026f33 | ||
|
|
778ff9daa9 | ||
|
|
ce9dc809bc | ||
|
|
59370588dd | ||
|
|
7d434aa9c4 | ||
|
|
59ce7c0424 | ||
|
|
9a0e5a7c18 | ||
|
|
8d0019595f | ||
|
|
6ff74cfcab | ||
|
|
aa50ef4069 | ||
|
|
e9063c639a | ||
|
|
8d6dedc15b | ||
|
|
1bc4c1a8ac | ||
|
|
1a35c440e8 | ||
|
|
2c6c84ac61 | ||
|
|
bd666daf82 | ||
|
|
ca3831c4f5 | ||
|
|
bbe0d34f43 | ||
|
|
dd364c962f | ||
|
|
50068b0b0f | ||
|
|
175769b53e | ||
|
|
07722dc33d | ||
|
|
f39f816a98 | ||
|
|
845f31b98f | ||
|
|
89b6c32cee | ||
|
|
6ee36fe361 | ||
|
|
b61d7c2428 | ||
|
|
bcc5d7c00f | ||
|
|
925f60d9c3 | ||
|
|
8b3f5fda07 | ||
|
|
ac17b2c584 | ||
|
|
c67c861dc6 | ||
|
|
09ba9e6259 | ||
|
|
0e167f5c24 | ||
|
|
c885903ff2 | ||
|
|
a91a836224 | ||
|
|
8450ab8dab | ||
|
|
168889d999 | ||
|
|
e1339628d9 | ||
|
|
425f61cf34 | ||
|
|
7d9df5abc6 | ||
|
|
118cba4d9b | ||
|
|
3cacb48f3c | ||
|
|
6965812d79 | ||
|
|
78fb7fe9f9 | ||
|
|
d7c8075862 |
+2
-2
@@ -1,7 +1,7 @@
|
||||
/syncthing
|
||||
/discosrv
|
||||
/stdiscosrv
|
||||
syncthing.exe
|
||||
discosrv.exe
|
||||
stdiscosrv.exe
|
||||
*.tar.gz
|
||||
*.zip
|
||||
*.asc
|
||||
|
||||
@@ -27,6 +27,11 @@ There are a few examples for keeping Syncthing running in the background
|
||||
on your system in [the etc directory][3]. There are also several [GUI
|
||||
implementations][11] for Windows, Mac and Linux.
|
||||
|
||||
## Vote on features/bugs
|
||||
|
||||
We'd like to encourage you to [vote][12] on issues that matter to you.
|
||||
This helps the team understand what are the biggest pain points for our users, and could potentially influence what is being worked on next.
|
||||
|
||||
## Getting in Touch
|
||||
|
||||
The first and best point of contact is the [Forum][8]. There is also an IRC
|
||||
@@ -66,3 +71,4 @@ All code is licensed under the [MPLv2 License][7].
|
||||
[9]: https://kiwiirc.com/client/irc.freenode.net/#syncthing
|
||||
[10]: https://github.com/syncthing/syncthing/issues
|
||||
[11]: http://docs.syncthing.net/users/contrib.html#gui-wrappers
|
||||
[12]: https://www.bountysource.com/teams/syncthing/issues
|
||||
|
||||
@@ -96,38 +96,55 @@ var targets = map[string]target{
|
||||
{src: "etc/linux-systemd/user/syncthing.service", dst: "deb/usr/lib/systemd/user/syncthing.service", perm: 0644},
|
||||
},
|
||||
},
|
||||
"discosrv": {
|
||||
name: "discosrv",
|
||||
buildPkg: "./cmd/discosrv",
|
||||
binaryName: "discosrv", // .exe will be added automatically for Windows builds
|
||||
"stdiscosrv": {
|
||||
name: "stdiscosrv",
|
||||
buildPkg: "./cmd/stdiscosrv",
|
||||
binaryName: "stdiscosrv", // .exe will be added automatically for Windows builds
|
||||
archiveFiles: []archiveFile{
|
||||
{src: "{{binary}}", dst: "{{binary}}", perm: 0755},
|
||||
{src: "cmd/discosrv/README.md", dst: "README.txt", perm: 0644},
|
||||
{src: "cmd/discosrv/LICENSE", dst: "LICENSE.txt", perm: 0644},
|
||||
{src: "cmd/stdiscosrv/README.md", dst: "README.txt", perm: 0644},
|
||||
{src: "cmd/stdiscosrv/LICENSE", dst: "LICENSE.txt", perm: 0644},
|
||||
{src: "AUTHORS", dst: "AUTHORS.txt", perm: 0644},
|
||||
},
|
||||
debianFiles: []archiveFile{
|
||||
{src: "{{binary}}", dst: "deb/usr/bin/{{binary}}", perm: 0755},
|
||||
{src: "cmd/discosrv/README.md", dst: "deb/usr/share/doc/discosrv/README.txt", perm: 0644},
|
||||
{src: "cmd/discosrv/LICENSE", dst: "deb/usr/share/doc/discosrv/LICENSE.txt", perm: 0644},
|
||||
{src: "AUTHORS", dst: "deb/usr/share/doc/discosrv/AUTHORS.txt", perm: 0644},
|
||||
{src: "cmd/stdiscosrv/README.md", dst: "deb/usr/share/doc/stdiscosrv/README.txt", perm: 0644},
|
||||
{src: "cmd/stdiscosrv/LICENSE", dst: "deb/usr/share/doc/stdiscosrv/LICENSE.txt", perm: 0644},
|
||||
{src: "AUTHORS", dst: "deb/usr/share/doc/stdiscosrv/AUTHORS.txt", perm: 0644},
|
||||
},
|
||||
tags: []string{"purego"},
|
||||
},
|
||||
"relaysrv": {
|
||||
name: "relaysrv",
|
||||
buildPkg: "./cmd/relaysrv",
|
||||
binaryName: "relaysrv", // .exe will be added automatically for Windows builds
|
||||
"strelaysrv": {
|
||||
name: "strelaysrv",
|
||||
buildPkg: "./cmd/strelaysrv",
|
||||
binaryName: "strelaysrv", // .exe will be added automatically for Windows builds
|
||||
archiveFiles: []archiveFile{
|
||||
{src: "{{binary}}", dst: "{{binary}}", perm: 0755},
|
||||
{src: "cmd/relaysrv/README.md", dst: "README.txt", perm: 0644},
|
||||
{src: "cmd/relaysrv/LICENSE", dst: "LICENSE.txt", perm: 0644},
|
||||
{src: "cmd/strelaysrv/README.md", dst: "README.txt", perm: 0644},
|
||||
{src: "cmd/strelaysrv/LICENSE", dst: "LICENSE.txt", perm: 0644},
|
||||
{src: "AUTHORS", dst: "AUTHORS.txt", perm: 0644},
|
||||
},
|
||||
debianFiles: []archiveFile{
|
||||
{src: "{{binary}}", dst: "deb/usr/bin/{{binary}}", perm: 0755},
|
||||
{src: "cmd/relaysrv/README.md", dst: "deb/usr/share/doc/relaysrv/README.txt", perm: 0644},
|
||||
{src: "cmd/relaysrv/LICENSE", dst: "deb/usr/share/doc/relaysrv/LICENSE.txt", perm: 0644},
|
||||
{src: "cmd/strelaysrv/README.md", dst: "deb/usr/share/doc/strelaysrv/README.txt", perm: 0644},
|
||||
{src: "cmd/strelaysrv/LICENSE", dst: "deb/usr/share/doc/strelaysrv/LICENSE.txt", perm: 0644},
|
||||
{src: "AUTHORS", dst: "deb/usr/share/doc/strelaysrv/AUTHORS.txt", perm: 0644},
|
||||
},
|
||||
},
|
||||
"strelaypoolsrv": {
|
||||
name: "strelaypoolsrv",
|
||||
buildPkg: "./cmd/strelaypoolsrv",
|
||||
binaryName: "strelaypoolsrv", // .exe will be added automatically for Windows builds
|
||||
archiveFiles: []archiveFile{
|
||||
{src: "{{binary}}", dst: "{{binary}}", perm: 0755},
|
||||
{src: "cmd/strelaypoolsrv/README.md", dst: "README.txt", perm: 0644},
|
||||
{src: "cmd/strelaypoolsrv/LICENSE", dst: "LICENSE.txt", perm: 0644},
|
||||
{src: "AUTHORS", dst: "AUTHORS.txt", perm: 0644},
|
||||
},
|
||||
debianFiles: []archiveFile{
|
||||
{src: "{{binary}}", dst: "deb/usr/bin/{{binary}}", perm: 0755},
|
||||
{src: "cmd/strelaypoolsrv/README.md", dst: "deb/usr/share/doc/relaysrv/README.txt", perm: 0644},
|
||||
{src: "cmd/strelaypoolsrv/LICENSE", dst: "deb/usr/share/doc/relaysrv/LICENSE.txt", perm: 0644},
|
||||
{src: "AUTHORS", dst: "deb/usr/share/doc/relaysrv/AUTHORS.txt", perm: 0644},
|
||||
},
|
||||
},
|
||||
@@ -546,16 +563,17 @@ func listFiles(dir string) []string {
|
||||
|
||||
func rebuildAssets() {
|
||||
runPipe("lib/auto/gui.files.go", "go", "run", "script/genassets.go", "gui")
|
||||
runPipe("cmd/strelaypoolsrv/auto/gui.go", "go", "run", "script/genassets.go", "cmd/strelaypoolsrv/gui")
|
||||
}
|
||||
|
||||
func lazyRebuildAssets() {
|
||||
if shouldRebuildAssets() {
|
||||
if shouldRebuildAssets("lib/auto/gui.files.go", "gui") || shouldRebuildAssets("cmd/strelaypoolsrv/auto/gui.go", "cmd/strelaypoolsrv/auto/gui") {
|
||||
rebuildAssets()
|
||||
}
|
||||
}
|
||||
|
||||
func shouldRebuildAssets() bool {
|
||||
info, err := os.Stat("lib/auto/gui.files.go")
|
||||
func shouldRebuildAssets(target, srcdir string) bool {
|
||||
info, err := os.Stat(target)
|
||||
if err != nil {
|
||||
// If the file doesn't exist, we must rebuild it
|
||||
return true
|
||||
@@ -565,7 +583,7 @@ func shouldRebuildAssets() bool {
|
||||
// so we should rebuild it.
|
||||
currentBuild := info.ModTime()
|
||||
assetsAreNewer := false
|
||||
filepath.Walk("gui", func(path string, info os.FileInfo, err error) error {
|
||||
filepath.Walk(srcdir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
discosrv
|
||||
========
|
||||
stdiscosrv
|
||||
==========
|
||||
|
||||
[](http://build.syncthing.net/job/discosrv/lastBuild/)
|
||||
[](http://build.syncthing.net/job/stdiscosrv/lastBuild/)
|
||||
|
||||
This is the global discovery server for the `syncthing` project.
|
||||
|
||||
To get it, run `go get github.com/syncthing/discosrv` or download the
|
||||
[latest build](http://build.syncthing.net/job/discosrv/lastSuccessfulBuild/artifact/)
|
||||
To get it, run `go get github.com/syncthing/stdiscosrv` or download the
|
||||
[latest build](http://build.syncthing.net/job/stdiscosrv/lastSuccessfulBuild/artifact/)
|
||||
from the build server.
|
||||
|
||||
Usage
|
||||
@@ -19,15 +19,15 @@ By default it will use in-memory `ql` backend. If you wish to persist the
|
||||
information on disk between restarts in `ql`, specify a file DSN:
|
||||
|
||||
```bash
|
||||
$ discosrv -db-dsn="file:///var/run/discosrv.db"
|
||||
$ stdiscosrv -db-dsn="file:///var/run/stdiscosrv.db"
|
||||
```
|
||||
|
||||
For `postgres`, you will need to create a database and a user with permissions
|
||||
to create tables in it, then start the discosrv as follows:
|
||||
to create tables in it, then start the stdiscosrv as follows:
|
||||
|
||||
```bash
|
||||
$ export DISCOSRV_DB_DSN="postgres://user:password@localhost/databasename"
|
||||
$ discosrv -db-backend="postgres"
|
||||
$ export STDISCOSRV_DB_DSN="postgres://user:password@localhost/databasename"
|
||||
$ stdiscosrv -db-backend="postgres"
|
||||
```
|
||||
|
||||
You can pass the DSN as command line option, but the value what you pass in will
|
||||
@@ -37,4 +37,4 @@ to other users.
|
||||
In all cases, the appropriate tables and indexes will be created at first
|
||||
startup. If it doesn't exit with an error, you're fine.
|
||||
|
||||
See `discosrv -help` for other options.
|
||||
See `stdiscosrv -help` for other options.
|
||||
@@ -38,7 +38,7 @@ func init() {
|
||||
BuildDate = time.Unix(int64(stamp), 0)
|
||||
|
||||
date := BuildDate.UTC().Format("2006-01-02 15:04:05 MST")
|
||||
LongVersion = fmt.Sprintf(`discosrv %s (%s %s-%s) %s@%s %s`, Version, runtime.Version(), runtime.GOOS, runtime.GOARCH, BuildUser, BuildHost, date)
|
||||
LongVersion = fmt.Sprintf(`stdiscosrv %s (%s %s-%s) %s@%s %s`, Version, runtime.Version(), runtime.GOOS, runtime.GOARCH, BuildUser, BuildHost, date)
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -48,7 +48,7 @@ var (
|
||||
globalStats stats
|
||||
statsFile string
|
||||
backend = "ql"
|
||||
dsn = getEnvDefault("DISCOSRV_DB_DSN", "memory://discosrv")
|
||||
dsn = getEnvDefault("STDISCOSRV_DB_DSN", "memory://stdiscosrv")
|
||||
certFile = "cert.pem"
|
||||
keyFile = "key.pem"
|
||||
debug = false
|
||||
@@ -28,7 +28,7 @@ func postgresSetup(db *sql.DB) error {
|
||||
}
|
||||
|
||||
row := db.QueryRow(`SELECT 'DevicesDeviceIDIndex'::regclass`)
|
||||
if err := row.Scan(nil); err != nil {
|
||||
if err = row.Scan(nil); err != nil {
|
||||
_, err = db.Exec(`CREATE INDEX DevicesDeviceIDIndex ON Devices (DeviceID)`)
|
||||
}
|
||||
if err != nil {
|
||||
@@ -36,7 +36,7 @@ func postgresSetup(db *sql.DB) error {
|
||||
}
|
||||
|
||||
row = db.QueryRow(`SELECT 'DevicesSeenIndex'::regclass`)
|
||||
if err := row.Scan(nil); err != nil {
|
||||
if err = row.Scan(nil); err != nil {
|
||||
_, err = db.Exec(`CREATE INDEX DevicesSeenIndex ON Devices (Seen)`)
|
||||
}
|
||||
if err != nil {
|
||||
@@ -53,7 +53,7 @@ func postgresSetup(db *sql.DB) error {
|
||||
}
|
||||
|
||||
row = db.QueryRow(`SELECT 'AddressesDeviceIDSeenIndex'::regclass`)
|
||||
if err := row.Scan(nil); err != nil {
|
||||
if err = row.Scan(nil); err != nil {
|
||||
_, err = db.Exec(`CREATE INDEX AddressesDeviceIDSeenIndex ON Addresses (DeviceID, Seen)`)
|
||||
}
|
||||
if err != nil {
|
||||
@@ -61,7 +61,7 @@ func postgresSetup(db *sql.DB) error {
|
||||
}
|
||||
|
||||
row = db.QueryRow(`SELECT 'AddressesDeviceIDAddressIndex'::regclass`)
|
||||
if err := row.Scan(nil); err != nil {
|
||||
if err = row.Scan(nil); err != nil {
|
||||
_, err = db.Exec(`CREATE INDEX AddressesDeviceIDAddressIndex ON Addresses (DeviceID, Address)`)
|
||||
}
|
||||
if err != nil {
|
||||
@@ -0,0 +1,15 @@
|
||||
# relaypoolsrv
|
||||
|
||||
[](http://build.syncthing.net/job/relaypoolsrv/lastBuild/)
|
||||
|
||||
This is the relay pool server for the `syncthing` project, which allows community hosted [relaysrv](https://github.com/syncthing/relaysrv)'s to join the public pool.
|
||||
|
||||
Servers that join the pool are then advertised to users of `syncthing` as potential connection points for those who are unable to connect directly due to NAT or firewall issues.
|
||||
|
||||
There is very little reason why you'd want to run this yourself, as `relaypoolsrv` is just used for announcement and lookup of public relay servers. If you are looking to setup a private or a public relay, please check the documentation for [relaysrv](https://github.com/syncthing/relaysrv), which also explains how to join the default public pool.
|
||||
|
||||
If you still want to run it, you can run `go get github.com/syncthing/relaypoolsrv` download it or download the
|
||||
[latest build](http://build.syncthing.net/job/relaypoolsrv/lastSuccessfulBuild/artifact/)
|
||||
from the build server.
|
||||
|
||||
See `relaypoolsrv -help` for configuration options.
|
||||
@@ -0,0 +1 @@
|
||||
gui.go
|
||||
@@ -0,0 +1,395 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en" ng-app="syncthing" ng-controller="relayDataController">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="">
|
||||
|
||||
<title>Relay stats</title>
|
||||
<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.6.1/css/font-awesome.min.css">
|
||||
|
||||
<style>
|
||||
#map {
|
||||
height: 600px;
|
||||
}
|
||||
.ng-cloak {
|
||||
display: none;
|
||||
}
|
||||
table {
|
||||
font-size: 11px !important;
|
||||
width: 100%;
|
||||
border: 1px;
|
||||
|
||||
}
|
||||
td {
|
||||
padding: 0px !important;
|
||||
}
|
||||
tfoot td {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="ng-cloak">
|
||||
<div class="container">
|
||||
<h1>Relay Pool Data</h2>
|
||||
<div ng-if="relays === undefined" class="text-center">
|
||||
<img src="//cdnjs.cloudflare.com/ajax/libs/galleriffic/2.0.1/css/loader.gif"/>
|
||||
<p>Please wait while we gather data</p>
|
||||
</div>
|
||||
<div>
|
||||
<div ng-show="relays !== undefined" class="ng-hide">
|
||||
<p>
|
||||
Currently {{ relays.length }} relays online ({{ totals.goMaxProcs }} cores in total).
|
||||
</p>
|
||||
</div>
|
||||
<div id="map"></div> <!-- Can't hide the map, otherwise it freaks out -->
|
||||
<p>The circle size represents how much bytes the relay transfered relative to other relays</p>
|
||||
</div>
|
||||
<div>
|
||||
<table class="table table-striped table-condensed table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th rowspan="2">Address</td>
|
||||
<th rowspan="2">
|
||||
<a ng-click="sortType = 'status.numActiveSessions || -1'; sortReverse = !sortReverse">
|
||||
Sessions
|
||||
<span ng-show="sortType == 'status.numActiveSessions || -1' && !sortReverse" class="fa fa-caret-down"></span>
|
||||
<span ng-show="sortType == 'status.numActiveSessions || -1' && sortReverse" class="fa fa-caret-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th rowspan="2">
|
||||
<a ng-click="sortType = 'status.numConnections || -1'; sortReverse = !sortReverse">
|
||||
Connections
|
||||
<span ng-show="sortType == 'status.numConnections || -1' && !sortReverse" class="fa fa-caret-down"></span>
|
||||
<span ng-show="sortType == 'status.numConnections || -1' && sortReverse" class="fa fa-caret-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th rowspan="2">
|
||||
<a ng-click="sortType = 'status.bytesProxied || -1'; sortReverse = !sortReverse">
|
||||
Data relayed
|
||||
<span ng-show="sortType == 'status.bytesProxied || -1' && !sortReverse" class="fa fa-caret-down"></span>
|
||||
<span ng-show="sortType == 'status.bytesProxied || -1' && sortReverse" class="fa fa-caret-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th colspan="6" class="text-center">Transfer rate in the last period</th>
|
||||
<th rowspan="2">
|
||||
<a ng-click="sortType = 'status.uptimeSeconds || -1'; sortReverse = !sortReverse">
|
||||
Uptime hours
|
||||
<span ng-show="sortType == 'status.uptimeSeconds || -1' && !sortReverse" class="fa fa-caret-down"></span>
|
||||
<span ng-show="sortType == 'status.uptimeSeconds || -1' && sortReverse" class="fa fa-caret-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th rowspan="2">
|
||||
<a ng-click="sortType = 'status.options[\'provided-by\'] || \'\''; sortReverse = !sortReverse">
|
||||
Provided by
|
||||
<span ng-show="sortType == 'status.options[\'provided-by\'] || \'\'' && !sortReverse" class="fa fa-caret-down"></span>
|
||||
<span ng-show="sortType == 'status.options[\'provided-by\'] || \'\'' && sortReverse" class="fa fa-caret-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
<a ng-click="sortType = 'status.kbps10s1m5m15m30m60m[0] || -1'; sortReverse = !sortReverse">
|
||||
10s
|
||||
<span ng-show="sortType == 'status.kbps10s1m5m15m30m60m[0] || -1' && !sortReverse" class="fa fa-caret-down"></span>
|
||||
<span ng-show="sortType == 'status.kbps10s1m5m15m30m60m[0] || -1' && sortReverse" class="fa fa-caret-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="sortType = 'status.kbps10s1m5m15m30m60m[1] || -1'; sortReverse = !sortReverse">
|
||||
1m
|
||||
<span ng-show="sortType == 'status.kbps10s1m5m15m30m60m[1] || -1' && !sortReverse" class="fa fa-caret-down"></span>
|
||||
<span ng-show="sortType == 'status.kbps10s1m5m15m30m60m[1] || -1' && sortReverse" class="fa fa-caret-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="sortType = 'status.kbps10s1m5m15m30m60m[2] || -1'; sortReverse = !sortReverse">
|
||||
5m
|
||||
<span ng-show="sortType == 'status.kbps10s1m5m15m30m60m[2] || -1' && !sortReverse" class="fa fa-caret-down"></span>
|
||||
<span ng-show="sortType == 'status.kbps10s1m5m15m30m60m[2] || -1' && sortReverse" class="fa fa-caret-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="sortType = 'status.kbps10s1m5m15m30m60m[3] || -1'; sortReverse = !sortReverse">
|
||||
15m
|
||||
<span ng-show="sortType == 'status.kbps10s1m5m15m30m60m[3] || -1' && !sortReverse" class="fa fa-caret-down"></span>
|
||||
<span ng-show="sortType == 'status.kbps10s1m5m15m30m60m[3] || -1' && sortReverse" class="fa fa-caret-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="sortType = 'status.kbps10s1m5m15m30m60m[4] || -1'; sortReverse = !sortReverse">
|
||||
30m
|
||||
<span ng-show="sortType == 'status.kbps10s1m5m15m30m60m[4] || -1' && !sortReverse" class="fa fa-caret-down"></span>
|
||||
<span ng-show="sortType == 'status.kbps10s1m5m15m30m60m[4] || -1' && sortReverse" class="fa fa-caret-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="sortType = 'status.kbps10s1m5m15m30m60m[5] || -1'; sortReverse = !sortReverse">
|
||||
60m
|
||||
<span ng-show="sortType == 'status.kbps10s1m5m15m30m60m[5] || -1' && !sortReverse" class="fa fa-caret-down"></span>
|
||||
<span ng-show="sortType == 'status.kbps10s1m5m15m30m60m[5] || -1' && sortReverse" class="fa fa-caret-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="relay in relays | orderBy:sortType:sortReverse ">
|
||||
<td>{{ relay.address }}</td>
|
||||
<td ng-if="relay.status === undefined" colspan="11" class="text-center">Looking up...</td>
|
||||
<td ng-if-start="relay.status !== undefined">{{ relay.status.numActiveSessions }}</td>
|
||||
<td>{{ relay.status.numConnections }}</td>
|
||||
<td>{{ relay.status.bytesProxied | bytes }}</td>
|
||||
<td>{{ relay.status.kbps10s1m5m15m30m60m[0] * 128 | bytes }}/s</td>
|
||||
<td>{{ relay.status.kbps10s1m5m15m30m60m[1] * 128 | bytes }}/s</td>
|
||||
<td>{{ relay.status.kbps10s1m5m15m30m60m[2] * 128 | bytes }}/s</td>
|
||||
<td>{{ relay.status.kbps10s1m5m15m30m60m[3] * 128 | bytes }}/s</td>
|
||||
<td>{{ relay.status.kbps10s1m5m15m30m60m[4] * 128 | bytes }}/s</td>
|
||||
<td>{{ relay.status.kbps10s1m5m15m30m60m[5] * 128 | bytes }}/s</td>
|
||||
<td ng-if="relay.status.uptimeSeconds != undefined">{{ relay.status.uptimeSeconds/60/60 | number:0 }}</td>
|
||||
<td ng-if="relay.status.uptimeSeconds == undefined"></td>
|
||||
<td title="{{ relay.status.options['provided-by'] || '' }}" ng-if-end>
|
||||
{{ relay.status.options['provided-by'] || '' | limitTo:50 }}
|
||||
<span ng-if="(relay.status.options['provided-by'] || '').length > 50">…
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td>Totals</td>
|
||||
<td>{{ totals.numActiveSessions }}</td>
|
||||
<td>{{ totals.numConnections }}</td>
|
||||
<td>{{ totals.bytesProxied | bytes }}</td>
|
||||
<td>{{ totals.kbps10s1m5m15m30m60m[0] * 128 | bytes }}/s</td>
|
||||
<td>{{ totals.kbps10s1m5m15m30m60m[1] * 128 | bytes }}/s</td>
|
||||
<td>{{ totals.kbps10s1m5m15m30m60m[2] * 128 | bytes }}/s</td>
|
||||
<td>{{ totals.kbps10s1m5m15m30m60m[3] * 128 | bytes }}/s</td>
|
||||
<td>{{ totals.kbps10s1m5m15m30m60m[4] * 128 | bytes }}/s</td>
|
||||
<td>{{ totals.kbps10s1m5m15m30m60m[5] * 128 | bytes }}/s</td>
|
||||
<td>{{ totals.uptimeSeconds/60/60 | number:0 }} hours</td>
|
||||
<td>{{ relays.length }} relays</td>
|
||||
</tr>
|
||||
</tfoor>
|
||||
</table>
|
||||
</div>
|
||||
<hr>
|
||||
<p>
|
||||
This product includes GeoLite2 data created by MaxMind, available from
|
||||
<a href="http://www.maxmind.com">http://www.maxmind.com</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<script src="//code.jquery.com/jquery-2.1.4.min.js"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.7/angular.min.js"></script>
|
||||
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
|
||||
<script src="//maps.googleapis.com/maps/api/js"></script>
|
||||
</body>
|
||||
|
||||
<script>
|
||||
angular.module('syncthing', [
|
||||
])
|
||||
.config(function($httpProvider) {
|
||||
$httpProvider.defaults.timeout = 5000;
|
||||
})
|
||||
.filter('bytes', function() {
|
||||
return function(bytes, precision) {
|
||||
if (isNaN(parseFloat(bytes)) || !isFinite(bytes)) return '-';
|
||||
if (typeof precision === 'undefined') precision = 1;
|
||||
|
||||
var units = ['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'],
|
||||
number = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
|
||||
var value = (bytes / Math.pow(1000, Math.floor(number)));
|
||||
if (!isFinite(value)) {
|
||||
value = 0;
|
||||
precision = 0;
|
||||
}
|
||||
if (!isFinite(number)) {
|
||||
units = 'bytes';
|
||||
} else {
|
||||
units = units[number];
|
||||
}
|
||||
return value.toFixed(precision) + ' ' + units;
|
||||
}
|
||||
})
|
||||
.controller('relayDataController', ['$scope', '$rootScope', '$http', '$q', '$compile', '$timeout', function($scope, $rootScope, $http, $q, $compile, $timeout) {
|
||||
$scope.totals = {
|
||||
bytesProxied: 0,
|
||||
goMaxProcs: 0,
|
||||
kbps10s1m5m15m30m60m: [0, 0, 0, 0, 0, 0],
|
||||
numActiveSessions: 0,
|
||||
numConnections: 0,
|
||||
numPendingSessionKeys: 0,
|
||||
numProxies: 0,
|
||||
uptimeSeconds: 0,
|
||||
};
|
||||
$scope.map = new google.maps.Map(document.getElementById('map'), {
|
||||
zoom: 1,
|
||||
mapTypeId: google.maps.MapTypeId.ROADMAP
|
||||
});
|
||||
$scope.mapBounds = new google.maps.LatLngBounds();
|
||||
$scope.tooltipTemplate = $('#infoTemplate').html();
|
||||
$scope.usedLocations = {};
|
||||
$scope.sortType = 'status.numActiveSessions || -1';
|
||||
$scope.sortReverse = true;
|
||||
|
||||
$http.get("/endpoint").then(function(response) {
|
||||
$scope.relays = response.data.relays;
|
||||
var promises = [];
|
||||
angular.forEach($scope.relays, function(relay) {
|
||||
|
||||
relay.uri = constructURI(relay.url);
|
||||
relay.address = relay.url.split('/')[2];
|
||||
|
||||
addMarkerToMap(relay);
|
||||
|
||||
promises.push(getRelayStatus(relay));
|
||||
});
|
||||
|
||||
// Can only add circles once we know the totals for transfers, which means
|
||||
// we need to resolve all statuses.
|
||||
$q.all(promises).then(function() {
|
||||
angular.forEach($scope.relays, function(relay) {
|
||||
if (relay.status) {
|
||||
addCircleToMap(relay);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$scope.map.fitBounds($scope.mapBounds);
|
||||
if ($scope.relays.length == 1) {
|
||||
$scope.map.setZoom(13);
|
||||
}
|
||||
});
|
||||
|
||||
function addMarkerToMap(relay) {
|
||||
var loc = relay.location.latitude + "," + relay.location.longitude;
|
||||
|
||||
// Deal with overlapping markers
|
||||
while (loc in $scope.usedLocations) {
|
||||
var locParts = loc.split(',');
|
||||
locParts = [parseFloat(locParts[0]), parseFloat(locParts[1])];
|
||||
locParts[Math.round(Math.random())] += 0.5 * (Math.random() >= 0.5 ? 1 : -1);
|
||||
loc = locParts.join(',');
|
||||
}
|
||||
|
||||
$scope.usedLocations[loc] = true;
|
||||
|
||||
var locParts = loc.split(',');
|
||||
|
||||
relay.marker = new google.maps.Marker({
|
||||
map: $scope.map,
|
||||
position: new google.maps.LatLng(locParts[0], locParts[1]),
|
||||
title: relay.url,
|
||||
});
|
||||
|
||||
var scope = $rootScope.$new(true);
|
||||
scope.relay = relay;
|
||||
|
||||
relay.marker.info = new google.maps.InfoWindow({
|
||||
content: $compile($scope.tooltipTemplate)(scope)[0],
|
||||
});
|
||||
|
||||
relay.marker.addListener('mouseover', function() {
|
||||
relay.marker.info.open($scope.map, relay.marker);
|
||||
});
|
||||
|
||||
relay.marker.addListener('mouseout', function() {
|
||||
relay.marker.info.close();
|
||||
});
|
||||
|
||||
$scope.mapBounds.extend(relay.marker.position);
|
||||
}
|
||||
|
||||
function addCircleToMap(relay) {
|
||||
relay.marker.circle = new google.maps.Circle({
|
||||
strokeColor: '#FF0000',
|
||||
strokeOpacity: 0.8,
|
||||
strokeWeight: 2,
|
||||
fillColor: '#FF0000',
|
||||
fillOpacity: 0.35,
|
||||
map: $scope.map,
|
||||
center: relay.marker.position,
|
||||
radius: ((relay.status.bytesProxied * 100) / $scope.totals.bytesProxied) * 10000
|
||||
});
|
||||
}
|
||||
|
||||
function getRelayStatus(relay) {
|
||||
// Normal timeout doesn't deal with relays which accept the TCP connection
|
||||
// but don't respond (some firewalls do that), so deal with it this way.
|
||||
var timeoutRequest = $q.defer();
|
||||
var resolveStatus = $q.defer();
|
||||
|
||||
$http.get("http://" + relay.uri.hostname + (relay.uri.args.statusAddr || ":22070") + "/status", { timeout: timeoutRequest.promise }).then(function (response) {
|
||||
relay.status = response.data;
|
||||
resolveStatus.resolve();
|
||||
angular.forEach($scope.totals, function(value, key) {
|
||||
if (typeof $scope.totals[key] == 'number') {
|
||||
$scope.totals[key] += response.data[key];
|
||||
} else if (typeof $scope.totals[key] == 'object' && $scope.totals[key] instanceof Array) {
|
||||
angular.forEach($scope.totals[key], function(value, index) {
|
||||
$scope.totals[key][index] += response.data[key][index];
|
||||
});
|
||||
}
|
||||
});
|
||||
}, function() {
|
||||
relay.status = null;
|
||||
resolveStatus.resolve();
|
||||
});
|
||||
|
||||
$timeout(function() {
|
||||
timeoutRequest.resolve();
|
||||
}, 5000);
|
||||
|
||||
return resolveStatus.promise;
|
||||
}
|
||||
|
||||
function constructURI(url) {
|
||||
var uri = document.createElement('a');
|
||||
|
||||
// HAX, otherwise doesn't work
|
||||
uri.href = url.replace('relay://', 'http://');
|
||||
|
||||
// Convert query string to object
|
||||
uri.args = {};
|
||||
angular.forEach(uri.search.replace(/^\?/, '').split('&'), function(query) {
|
||||
var split = query.split('=');
|
||||
uri.args[split[0]] = split[1];
|
||||
});
|
||||
|
||||
return uri;
|
||||
}
|
||||
}]);
|
||||
</script>
|
||||
|
||||
<script type="text/template" id="infoTemplate">
|
||||
<div>
|
||||
<p><b>{{ relay.uri.hostname }}</b> <span ng-if="relay.status.options['provided-by']">provided by <u>{{ relay.status.options['provided-by'] }}</u></span></p>
|
||||
<div ng-if="relay.status">
|
||||
<span ng-if="relay.status.startTime">Start time: {{ relay.status.startTime | date:"medium" }}</br></span>
|
||||
<span ng-if="relay.status.bytesProxied != undefined">Proxied: {{ relay.status.bytesProxied | bytes }}</br></span>
|
||||
<span ng-if="relay.status.numActiveSessions != undefined">Sessions: {{ relay.status.numActiveSessions }}</br></span>
|
||||
<span ng-if="relay.status.numConnections != undefined">Clients: {{ relay.status.numConnections }}</br></span>
|
||||
<span ng-if="relay.status.options.pools">Pools: {{ relay.status.options.pools.join(', ') }}</br></span>
|
||||
<span ng-if="relay.status.options['global-rate'] != undefined">
|
||||
<span ng-if="relay.status.options['global-rate'] > 0">Global rate limit: {{ relay.status.options['global-rate'] | bytes }}/s</span>
|
||||
<span ng-if="relay.status.options['global-rate'] == 0">Global rate limit: unlimited</span>
|
||||
</br>
|
||||
</span>
|
||||
<span ng-if="relay.status.options['per-session-rate'] != undefined">
|
||||
<span ng-if="relay.status.options['per-session-rate'] > 0">Session rate limit: {{ relay.status.options['per-session-rate'] | bytes }}/s</span>
|
||||
<span ng-if="relay.status.options['per-session-rate'] == 0">Session rate limit: unlimited</span>
|
||||
</br>
|
||||
</span>
|
||||
</div>
|
||||
<div ng-if="!relay.status">
|
||||
Data unavailable.
|
||||
<div>
|
||||
</div>
|
||||
</script>
|
||||
</html>
|
||||
@@ -0,0 +1,536 @@
|
||||
// Copyright (C) 2015 Audrius Butkevicius and Contributors (see the CONTRIBUTORS file).
|
||||
|
||||
//go:generate go run genassets.go gui auto/gui.go
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/rand"
|
||||
"mime"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang/groupcache/lru"
|
||||
"github.com/juju/ratelimit"
|
||||
|
||||
"github.com/oschwald/geoip2-golang"
|
||||
|
||||
"github.com/syncthing/syncthing/cmd/strelaypoolsrv/auto"
|
||||
"github.com/syncthing/syncthing/lib/relay/client"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
"github.com/syncthing/syncthing/lib/tlsutil"
|
||||
)
|
||||
|
||||
type location struct {
|
||||
Latitude float64 `json:"latitude"`
|
||||
Longitude float64 `json:"longitude"`
|
||||
}
|
||||
|
||||
type relay struct {
|
||||
URL string `json:"url"`
|
||||
Location location `json:"location"`
|
||||
uri *url.URL
|
||||
}
|
||||
|
||||
func (r relay) String() string {
|
||||
return r.URL
|
||||
}
|
||||
|
||||
type request struct {
|
||||
relay relay
|
||||
uri *url.URL
|
||||
result chan result
|
||||
}
|
||||
|
||||
type result struct {
|
||||
err error
|
||||
eviction time.Duration
|
||||
}
|
||||
|
||||
var (
|
||||
testCert tls.Certificate
|
||||
listen = ":80"
|
||||
dir string
|
||||
evictionTime = time.Hour
|
||||
debug bool
|
||||
getLRUSize = 10 << 10
|
||||
getLimitBurst int64 = 10
|
||||
getLimitAvg = 1
|
||||
postLRUSize = 1 << 10
|
||||
postLimitBurst int64 = 2
|
||||
postLimitAvg = 1
|
||||
getLimit time.Duration
|
||||
postLimit time.Duration
|
||||
permRelaysFile string
|
||||
ipHeader string
|
||||
geoipPath string
|
||||
|
||||
getMut = sync.NewRWMutex()
|
||||
getLRUCache *lru.Cache
|
||||
|
||||
postMut = sync.NewRWMutex()
|
||||
postLRUCache *lru.Cache
|
||||
|
||||
requests = make(chan request, 10)
|
||||
|
||||
mut = sync.NewRWMutex()
|
||||
knownRelays = make([]relay, 0)
|
||||
permanentRelays = make([]relay, 0)
|
||||
evictionTimers = make(map[string]*time.Timer)
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.StringVar(&listen, "listen", listen, "Listen address")
|
||||
flag.StringVar(&dir, "keys", dir, "Directory where http-cert.pem and http-key.pem is stored for TLS listening")
|
||||
flag.BoolVar(&debug, "debug", debug, "Enable debug output")
|
||||
flag.DurationVar(&evictionTime, "eviction", evictionTime, "After how long the relay is evicted")
|
||||
flag.IntVar(&getLRUSize, "get-limit-cache", getLRUSize, "Get request limiter cache size")
|
||||
flag.IntVar(&getLimitAvg, "get-limit-avg", 2, "Allowed average get request rate, per 10 s")
|
||||
flag.Int64Var(&getLimitBurst, "get-limit-burst", getLimitBurst, "Allowed burst get requests")
|
||||
flag.IntVar(&postLRUSize, "post-limit-cache", postLRUSize, "Post request limiter cache size")
|
||||
flag.IntVar(&postLimitAvg, "post-limit-avg", 2, "Allowed average post request rate, per minute")
|
||||
flag.Int64Var(&postLimitBurst, "post-limit-burst", postLimitBurst, "Allowed burst post requests")
|
||||
flag.StringVar(&permRelaysFile, "perm-relays", "", "Path to list of permanent relays")
|
||||
flag.StringVar(&ipHeader, "ip-header", "", "Name of header which holds clients ip:port. Only meaningful when running behind a reverse proxy.")
|
||||
flag.StringVar(&geoipPath, "geoip", "GeoLite2-City.mmdb", "Path to GeoLite2-City database")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
getLimit = 10 * time.Second / time.Duration(getLimitAvg)
|
||||
postLimit = time.Minute / time.Duration(postLimitAvg)
|
||||
|
||||
getLRUCache = lru.New(getLRUSize)
|
||||
postLRUCache = lru.New(postLRUSize)
|
||||
|
||||
var listener net.Listener
|
||||
var err error
|
||||
|
||||
if permRelaysFile != "" {
|
||||
loadPermanentRelays(permRelaysFile)
|
||||
}
|
||||
|
||||
testCert = createTestCertificate()
|
||||
|
||||
go requestProcessor()
|
||||
|
||||
if dir != "" {
|
||||
if debug {
|
||||
log.Println("Starting TLS listener on", listen)
|
||||
}
|
||||
certFile, keyFile := filepath.Join(dir, "http-cert.pem"), filepath.Join(dir, "http-key.pem")
|
||||
var cert tls.Certificate
|
||||
cert, err = tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to load HTTP X509 key pair:", err)
|
||||
}
|
||||
|
||||
tlsCfg := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
MinVersion: tls.VersionTLS10, // No SSLv3
|
||||
CipherSuites: []uint16{
|
||||
// No RC4
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
|
||||
},
|
||||
}
|
||||
|
||||
listener, err = tls.Listen("tcp", listen, tlsCfg)
|
||||
} else {
|
||||
if debug {
|
||||
log.Println("Starting plain listener on", listen)
|
||||
}
|
||||
listener, err = net.Listen("tcp", listen)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Fatalln("listen:", err)
|
||||
}
|
||||
|
||||
handler := http.NewServeMux()
|
||||
handler.HandleFunc("/", handleAssets)
|
||||
handler.HandleFunc("/endpoint", handleRequest)
|
||||
|
||||
srv := http.Server{
|
||||
Handler: handler,
|
||||
ReadTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
err = srv.Serve(listener)
|
||||
if err != nil {
|
||||
log.Fatalln("serve:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func handleAssets(w http.ResponseWriter, r *http.Request) {
|
||||
assets := auto.Assets()
|
||||
path := r.URL.Path[1:]
|
||||
if path == "" {
|
||||
path = "index.html"
|
||||
}
|
||||
|
||||
bs, ok := assets[path]
|
||||
if !ok {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
mtype := mimeTypeForFile(path)
|
||||
if len(mtype) != 0 {
|
||||
w.Header().Set("Content-Type", mtype)
|
||||
}
|
||||
|
||||
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
} else {
|
||||
// ungzip if browser not send gzip accepted header
|
||||
var gr *gzip.Reader
|
||||
gr, _ = gzip.NewReader(bytes.NewReader(bs))
|
||||
bs, _ = ioutil.ReadAll(gr)
|
||||
gr.Close()
|
||||
}
|
||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(bs)))
|
||||
|
||||
w.Write(bs)
|
||||
}
|
||||
|
||||
func mimeTypeForFile(file string) string {
|
||||
// We use a built in table of the common types since the system
|
||||
// TypeByExtension might be unreliable. But if we don't know, we delegate
|
||||
// to the system.
|
||||
ext := filepath.Ext(file)
|
||||
switch ext {
|
||||
case ".htm", ".html":
|
||||
return "text/html"
|
||||
case ".css":
|
||||
return "text/css"
|
||||
case ".js":
|
||||
return "application/javascript"
|
||||
case ".json":
|
||||
return "application/json"
|
||||
case ".png":
|
||||
return "image/png"
|
||||
case ".ttf":
|
||||
return "application/x-font-ttf"
|
||||
case ".woff":
|
||||
return "application/x-font-woff"
|
||||
case ".svg":
|
||||
return "image/svg+xml"
|
||||
default:
|
||||
return mime.TypeByExtension(ext)
|
||||
}
|
||||
}
|
||||
|
||||
func handleRequest(w http.ResponseWriter, r *http.Request) {
|
||||
if ipHeader != "" {
|
||||
r.RemoteAddr = r.Header.Get(ipHeader)
|
||||
}
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
if limit(r.RemoteAddr, getLRUCache, getMut, getLimit, int64(getLimitBurst)) {
|
||||
w.WriteHeader(429)
|
||||
return
|
||||
}
|
||||
handleGetRequest(w, r)
|
||||
case "POST":
|
||||
if limit(r.RemoteAddr, postLRUCache, postMut, postLimit, int64(postLimitBurst)) {
|
||||
w.WriteHeader(429)
|
||||
return
|
||||
}
|
||||
handlePostRequest(w, r)
|
||||
default:
|
||||
if debug {
|
||||
log.Println("Unhandled HTTP method", r.Method)
|
||||
}
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetRequest(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
mut.RLock()
|
||||
relays := append(permanentRelays, knownRelays...)
|
||||
mut.RUnlock()
|
||||
|
||||
// Shuffle
|
||||
for i := range relays {
|
||||
j := rand.Intn(i + 1)
|
||||
relays[i], relays[j] = relays[j], relays[i]
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string][]relay{
|
||||
"relays": relays,
|
||||
})
|
||||
}
|
||||
|
||||
func handlePostRequest(w http.ResponseWriter, r *http.Request) {
|
||||
var newRelay relay
|
||||
err := json.NewDecoder(r.Body).Decode(&newRelay)
|
||||
r.Body.Close()
|
||||
|
||||
if err != nil {
|
||||
if debug {
|
||||
log.Println("Failed to parse payload")
|
||||
}
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
uri, err := url.Parse(newRelay.URL)
|
||||
if err != nil {
|
||||
if debug {
|
||||
log.Println("Failed to parse URI", newRelay.URL)
|
||||
}
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
host, port, err := net.SplitHostPort(uri.Host)
|
||||
if err != nil {
|
||||
if debug {
|
||||
log.Println("Failed to split URI", newRelay.URL)
|
||||
}
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the IP address of the client
|
||||
rhost, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
if debug {
|
||||
log.Println("Failed to split remote address", r.RemoteAddr)
|
||||
}
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
// The client did not provide an IP address, use the IP address of the client.
|
||||
if host == "" {
|
||||
uri.Host = net.JoinHostPort(rhost, port)
|
||||
newRelay.URL = uri.String()
|
||||
} else if host != rhost {
|
||||
if debug {
|
||||
log.Println("IP address advertised does not match client IP address", r.RemoteAddr, uri)
|
||||
}
|
||||
http.Error(w, "IP address does not match client IP", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
newRelay.uri = uri
|
||||
newRelay.Location = getLocation(uri.Host)
|
||||
|
||||
for _, current := range permanentRelays {
|
||||
if current.uri.Host == newRelay.uri.Host {
|
||||
if debug {
|
||||
log.Println("Asked to add a relay", newRelay, "which exists in permanent list")
|
||||
}
|
||||
http.Error(w, "Invalid request", 500)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
reschan := make(chan result)
|
||||
|
||||
select {
|
||||
case requests <- request{newRelay, uri, reschan}:
|
||||
result := <-reschan
|
||||
if result.err != nil {
|
||||
http.Error(w, result.err.Error(), 500)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(map[string]time.Duration{
|
||||
"evictionIn": result.eviction,
|
||||
})
|
||||
|
||||
default:
|
||||
if debug {
|
||||
log.Println("Dropping request")
|
||||
}
|
||||
w.WriteHeader(429)
|
||||
}
|
||||
}
|
||||
|
||||
func requestProcessor() {
|
||||
for request := range requests {
|
||||
if debug {
|
||||
log.Println("Request for", request.relay)
|
||||
}
|
||||
if !client.TestRelay(request.uri, []tls.Certificate{testCert}, time.Second, 2*time.Second, 3) {
|
||||
if debug {
|
||||
log.Println("Test for relay", request.relay, "failed")
|
||||
}
|
||||
request.result <- result{fmt.Errorf("test failed"), 0}
|
||||
continue
|
||||
}
|
||||
|
||||
mut.Lock()
|
||||
timer, ok := evictionTimers[request.relay.uri.Host]
|
||||
if ok {
|
||||
if debug {
|
||||
log.Println("Stopping existing timer for", request.relay)
|
||||
}
|
||||
timer.Stop()
|
||||
}
|
||||
|
||||
for i, current := range knownRelays {
|
||||
if current.uri.Host == request.relay.uri.Host {
|
||||
if debug {
|
||||
log.Println("Relay", request.relay, "already exists")
|
||||
}
|
||||
|
||||
// Evict the old entry anyway, as configuration might have changed.
|
||||
last := len(knownRelays) - 1
|
||||
knownRelays[i] = knownRelays[last]
|
||||
knownRelays = knownRelays[:last]
|
||||
|
||||
goto found
|
||||
}
|
||||
}
|
||||
|
||||
if debug {
|
||||
log.Println("Adding new relay", request.relay)
|
||||
}
|
||||
|
||||
found:
|
||||
|
||||
knownRelays = append(knownRelays, request.relay)
|
||||
|
||||
evictionTimers[request.relay.uri.Host] = time.AfterFunc(evictionTime, evict(request.relay))
|
||||
mut.Unlock()
|
||||
request.result <- result{nil, evictionTime}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func evict(relay relay) func() {
|
||||
return func() {
|
||||
mut.Lock()
|
||||
defer mut.Unlock()
|
||||
if debug {
|
||||
log.Println("Evicting", relay)
|
||||
}
|
||||
for i, current := range knownRelays {
|
||||
if current.uri.Host == relay.uri.Host {
|
||||
if debug {
|
||||
log.Println("Evicted", relay)
|
||||
}
|
||||
last := len(knownRelays) - 1
|
||||
knownRelays[i] = knownRelays[last]
|
||||
knownRelays = knownRelays[:last]
|
||||
}
|
||||
}
|
||||
delete(evictionTimers, relay.uri.Host)
|
||||
}
|
||||
}
|
||||
|
||||
func limit(addr string, cache *lru.Cache, lock sync.RWMutex, rate time.Duration, burst int64) bool {
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
lock.RLock()
|
||||
bkt, ok := cache.Get(host)
|
||||
lock.RUnlock()
|
||||
if ok {
|
||||
bkt := bkt.(*ratelimit.Bucket)
|
||||
if bkt.TakeAvailable(1) != 1 {
|
||||
// Rate limit
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
lock.Lock()
|
||||
cache.Add(host, ratelimit.NewBucket(rate, burst))
|
||||
lock.Unlock()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func loadPermanentRelays(file string) {
|
||||
content, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for _, line := range strings.Split(string(content), "\n") {
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
uri, err := url.Parse(line)
|
||||
if err != nil {
|
||||
if debug {
|
||||
log.Println("Skipping permanent relay", line, "due to parse error", err)
|
||||
}
|
||||
continue
|
||||
|
||||
}
|
||||
|
||||
permanentRelays = append(permanentRelays, relay{
|
||||
URL: line,
|
||||
Location: getLocation(uri.Host),
|
||||
uri: uri,
|
||||
})
|
||||
if debug {
|
||||
log.Println("Adding permanent relay", line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func createTestCertificate() tls.Certificate {
|
||||
tmpDir, err := ioutil.TempDir("", "relaypoolsrv")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
certFile, keyFile := filepath.Join(tmpDir, "cert.pem"), filepath.Join(tmpDir, "key.pem")
|
||||
cert, err := tlsutil.NewCertificate(certFile, keyFile, "relaypoolsrv", 3072)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to create test X509 key pair:", err)
|
||||
}
|
||||
|
||||
return cert
|
||||
}
|
||||
|
||||
func getLocation(host string) location {
|
||||
db, err := geoip2.Open(geoipPath)
|
||||
if err != nil {
|
||||
return location{}
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
addr, err := net.ResolveTCPAddr("tcp", host)
|
||||
if err != nil {
|
||||
return location{}
|
||||
}
|
||||
|
||||
city, err := db.City(addr.IP)
|
||||
if err != nil {
|
||||
return location{}
|
||||
}
|
||||
|
||||
return location{
|
||||
Latitude: city.Location.Latitude,
|
||||
Longitude: city.Location.Longitude,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 The Syncthing Project
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
relaysrv
|
||||
========
|
||||
strelaysrv
|
||||
==========
|
||||
|
||||
[](http://build.syncthing.net/job/relaysrv/lastBuild/)
|
||||
[](http://build.syncthing.net/job/strelaysrv/lastBuild/)
|
||||
|
||||
This is the relay server for the `syncthing` project.
|
||||
|
||||
To get it, run `go get github.com/syncthing/relaysrv` or download the
|
||||
[latest build](http://build.syncthing.net/job/relaysrv/lastSuccessfulBuild/artifact/)
|
||||
To get it, run `go get github.com/syncthing/strelaysrv` or download the
|
||||
[latest build](http://build.syncthing.net/job/strelaysrv/lastSuccessfulBuild/artifact/)
|
||||
from the build server.
|
||||
|
||||
:exclamation:Warnings:exclamation: - Read or regret
|
||||
@@ -16,13 +16,13 @@ By default, all relay servers will join the default public relay pool, which mea
|
||||
|
||||
If you wish to disable this behaviour, please specify `-pools=""` argument.
|
||||
|
||||
Please note that `relaysrv` is only usable by `syncthing` **version v0.12 and onwards**.
|
||||
Please note that `strelaysrv` is only usable by `syncthing` **version v0.12 and onwards**.
|
||||
|
||||
To run `relaysrv` you need to have port 22067 available to the internet, which means you might need to allow it through your firewall if you **have a public IP, or setup a port-forwarding** (22067 to 22067) if you are behind a router.
|
||||
To run `strelaysrv` you need to have port 22067 available to the internet, which means you might need to allow it through your firewall if you **have a public IP, or setup a port-forwarding** (22067 to 22067) if you are behind a router.
|
||||
|
||||
Furthermore, **by default relaysrv will also expose a /status HTTP endpoint on port 22070**, which is used by the pool servers to peek at metrics of the relaysrv, such as what are the current transfer rates, how many clients are connected, etc, etc. If you wish this information to be available, similarlly you might want to allow it through your firewall, or port-forward it (22070 to 22070) on your NAT device.
|
||||
Furthermore, **by default strelaysrv will also expose a /status HTTP endpoint on port 22070**, which is used by the pool servers to peek at metrics of the strelaysrv, such as what are the current transfer rates, how many clients are connected, etc, etc. If you wish this information to be available, similarlly you might want to allow it through your firewall, or port-forward it (22070 to 22070) on your NAT device.
|
||||
|
||||
This is **not mandatory** for the relaysrv to function, and is used only to gather metrics and present them in the overview page of the pool server, displaying stats about the specific relay.
|
||||
This is **not mandatory** for the strelaysrv to function, and is used only to gather metrics and present them in the overview page of the pool server, displaying stats about the specific relay.
|
||||
|
||||
At the point of writing the endpoint output looks as follows:
|
||||
|
||||
@@ -62,31 +62,31 @@ At the point of writing the endpoint output looks as follows:
|
||||
}
|
||||
```
|
||||
|
||||
If you wish to disable the /status endpoint, provide `-status-srv=""` as one of the arguments when starting the relaysrv.
|
||||
If you wish to disable the /status endpoint, provide `-status-srv=""` as one of the arguments when starting the strelaysrv.
|
||||
|
||||
Running for public use
|
||||
----
|
||||
Make sure you have a public IP with port 22067 open, or make sure you have port-forwarding (22067 to 22067) if you are behind a router.
|
||||
|
||||
Run the `relaysrv` with no arguments (or `-debug` if you want more output), and that should be enough for the server to join the public relay pool.
|
||||
Run the `strelaysrv` with no arguments (or `-debug` if you want more output), and that should be enough for the server to join the public relay pool.
|
||||
You should see a message saying:
|
||||
```
|
||||
2015/09/21 22:45:46 pool.go:60: Joined https://relays.syncthing.net/endpoint rejoining in 48m0s
|
||||
```
|
||||
|
||||
See `relaysrv -help` for other options, such as rate limits, timeout intervals, etc.
|
||||
See `strelaysrv -help` for other options, such as rate limits, timeout intervals, etc.
|
||||
|
||||
Running for private use
|
||||
-----
|
||||
|
||||
Once you've started the `relaysrv`, it will generate a key pair and print an URI:
|
||||
Once you've started the `strelaysrv`, it will generate a key pair and print an URI:
|
||||
```bash
|
||||
relay://:22067/?id=EZQOIDM-6DDD4ZI-DJ65NSM-4OQWRAT-EIKSMJO-OZ552BO-WQZEGYY-STS5RQM&pingInterval=1m0s&networkTimeout=2m0s&sessionLimitBps=0&globalLimitBps=0&statusAddr=:22070
|
||||
```
|
||||
|
||||
This URI contains partial address of the relay server, as well as it's options which in the future may be taken into account when choosing the best suitable relay out of multiple available.
|
||||
|
||||
Because `-listen` option was not used, the `relaysrv` does not know it's external IP, therefore you should replace the host part of the URI with your public IP address on which the `relaysrv` will be available:
|
||||
Because `-listen` option was not used, the `strelaysrv` does not know it's external IP, therefore you should replace the host part of the URI with your public IP address on which the `strelaysrv` will be available:
|
||||
|
||||
```bash
|
||||
relay://123.123.123.123:22067/?id=EZQOIDM-6DDD4ZI-DJ65NSM-4OQWRAT-EIKSMJO-OZ552BO-WQZEGYY-STS5RQM&pingInterval=1m0s&networkTimeout=2m0s&sessionLimitBps=0&globalLimitBps=0&statusAddr=:22070
|
||||
@@ -100,7 +100,7 @@ relay://123.123.123.123:22067
|
||||
|
||||
This URI can then be used in `syncthing` as one of the relay servers.
|
||||
|
||||
See `relaysrv -help` for other options, such as rate limits, timeout intervals, etc.
|
||||
See `strelaysrv -help` for other options, such as rate limits, timeout intervals, etc.
|
||||
|
||||
Other items available in this repo
|
||||
----
|
||||
+4
-4
@@ -3,10 +3,10 @@ Description=Syncthing relay server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=syncthing-relaysrv
|
||||
Group=syncthing-relaysrv
|
||||
ExecStart=/usr/bin/relaysrv
|
||||
WorkingDirectory=/var/lib/syncthing-relaysrv
|
||||
User=strelaysrv
|
||||
Group=strelaysrv
|
||||
ExecStart=/usr/bin/strelaysrv
|
||||
WorkingDirectory=/var/lib/strelaysrv
|
||||
|
||||
PrivateTmp=true
|
||||
ProtectSystem=full
|
||||
@@ -42,7 +42,7 @@ func init() {
|
||||
BuildDate = time.Unix(int64(stamp), 0)
|
||||
|
||||
date := BuildDate.UTC().Format("2006-01-02 15:04:05 MST")
|
||||
LongVersion = fmt.Sprintf(`relaysrv %s (%s %s-%s) %s@%s %s`, Version, runtime.Version(), runtime.GOOS, runtime.GOARCH, BuildUser, BuildHost, date)
|
||||
LongVersion = fmt.Sprintf(`strelaysrv %s (%s %s-%s) %s@%s %s`, Version, runtime.Version(), runtime.GOOS, runtime.GOARCH, BuildUser, BuildHost, date)
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -121,7 +121,7 @@ func main() {
|
||||
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
log.Println("Failed to load keypair. Generating one, this might take a while...")
|
||||
cert, err = tlsutil.NewCertificate(certFile, keyFile, "relaysrv", 3072)
|
||||
cert, err = tlsutil.NewCertificate(certFile, keyFile, "strelaysrv", 3072)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to generate X509 key pair:", err)
|
||||
}
|
||||
+13
-7
@@ -41,8 +41,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
configInSync = true
|
||||
startTime = time.Now()
|
||||
startTime = time.Now()
|
||||
)
|
||||
|
||||
type apiService struct {
|
||||
@@ -100,12 +99,13 @@ type configIntf interface {
|
||||
GUI() config.GUIConfiguration
|
||||
Raw() config.Configuration
|
||||
Options() config.OptionsConfiguration
|
||||
Replace(cfg config.Configuration) config.CommitResponse
|
||||
Replace(cfg config.Configuration) error
|
||||
Subscribe(c config.Committer)
|
||||
Folders() map[string]config.FolderConfiguration
|
||||
Devices() map[protocol.DeviceID]config.DeviceConfiguration
|
||||
Save() error
|
||||
ListenAddresses() []string
|
||||
RequiresRestart() bool
|
||||
}
|
||||
|
||||
type connectionsIntf interface {
|
||||
@@ -722,13 +722,19 @@ func (s *apiService) postSystemConfig(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Activate and save
|
||||
|
||||
resp := s.cfg.Replace(to)
|
||||
configInSync = !resp.RequiresRestart
|
||||
s.cfg.Save()
|
||||
if err := s.cfg.Replace(to); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.cfg.Save(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (s *apiService) getSystemConfigInsync(w http.ResponseWriter, r *http.Request) {
|
||||
sendJSON(w, map[string]bool{"configInSync": configInSync})
|
||||
sendJSON(w, map[string]bool{"configInSync": !s.cfg.RequiresRestart()})
|
||||
}
|
||||
|
||||
func (s *apiService) postSystemRestart(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -51,7 +51,7 @@ import (
|
||||
|
||||
var (
|
||||
Version = "unknown-dev"
|
||||
Codename = "Copper Cockroach"
|
||||
Codename = "Dysprosium Dragonfly"
|
||||
BuildStamp = "0"
|
||||
BuildDate time.Time
|
||||
BuildHost = "unknown"
|
||||
@@ -539,8 +539,9 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
|
||||
errors := logger.NewRecorder(l, logger.LevelWarn, maxSystemErrors, 0)
|
||||
systemLog := logger.NewRecorder(l, logger.LevelDebug, maxSystemLog, initialSystemLog)
|
||||
|
||||
// Event subscription for the API; must start early to catch the early events. The LocalChangeDetected
|
||||
// event might overwhelm the event reciever in some situations so we will not subscribe to it here.
|
||||
// Event subscription for the API; must start early to catch the early
|
||||
// events. The LocalChangeDetected event might overwhelm the event
|
||||
// receiver in some situations so we will not subscribe to it here.
|
||||
apiSub := events.NewBufferedSubscription(events.Default.Subscribe(events.AllEvents&^events.LocalChangeDetected), 1000)
|
||||
|
||||
if len(os.Getenv("GOMAXPROCS")) == 0 {
|
||||
|
||||
@@ -31,8 +31,8 @@ func (c *mockedConfig) Options() config.OptionsConfiguration {
|
||||
return config.OptionsConfiguration{}
|
||||
}
|
||||
|
||||
func (c *mockedConfig) Replace(cfg config.Configuration) config.CommitResponse {
|
||||
return config.CommitResponse{}
|
||||
func (c *mockedConfig) Replace(cfg config.Configuration) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *mockedConfig) Subscribe(cm config.Committer) {}
|
||||
@@ -48,3 +48,7 @@ func (c *mockedConfig) Devices() map[protocol.DeviceID]config.DeviceConfiguratio
|
||||
func (c *mockedConfig) Save() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *mockedConfig) RequiresRestart() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"A negative number of days doesn't make sense.": "음수로는 지정할 수 없습니다.",
|
||||
"A new major version may not be compatible with previous versions.": "새로운 메이저 버전은 이전 버전과 호환되지 않을 수 있습니다.",
|
||||
"API Key": "API 키",
|
||||
"About": " 정보",
|
||||
"About": "정보",
|
||||
"Actions": "동작",
|
||||
"Add": "추가",
|
||||
"Add Device": "기기 추가",
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
"Enable Relaying": "Habilitar retransmissão",
|
||||
"Enable UPnP": "Habilitar UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Insira endereços (\"tcp://ip:porta\", \"tcp://host:porta\") separados por vírgula ou \"dynamic\" para executar a descoberta automática do endereço.",
|
||||
"Enter ignore patterns, one per line.": "Insira os padrões de exclusão, um por linha.",
|
||||
"Enter ignore patterns, one per line.": "Insira os filtros, um por linha.",
|
||||
"Error": "Erro",
|
||||
"External File Versioning": "Versionamento externo de arquivo",
|
||||
"Failed Items": "Itens com falha",
|
||||
@@ -89,7 +89,7 @@
|
||||
"Help": "Ajuda",
|
||||
"Home page": "Página inicial",
|
||||
"Ignore": "Ignorar",
|
||||
"Ignore Patterns": "Padrões de exclusão",
|
||||
"Ignore Patterns": "Filtros",
|
||||
"Ignore Permissions": "Ignorar permissões",
|
||||
"Incoming Rate Limit (KiB/s)": "Limite de velocidade de recepção (KiB/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "A configuração incorreta poderá causar danos aos seus dados e tornar o Syncthing inoperante.",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"A device with that ID is already added.": "En enhet med det ID är redan tillagt.",
|
||||
"A negative number of days doesn't make sense.": "Ett negativt antal dagar är inte troligt.",
|
||||
"A device with that ID is already added.": "En enhet med det ID:t är redan tillagt.",
|
||||
"A negative number of days doesn't make sense.": "Ett negativt antal dagar är inte rimligt.",
|
||||
"A new major version may not be compatible with previous versions.": "En ny huvudversion kan eventuellt vara inkompatibel med tidigare versioner.",
|
||||
"API Key": "API-nyckel",
|
||||
"About": "Om",
|
||||
@@ -18,96 +18,96 @@
|
||||
"All Data": "All data",
|
||||
"Allow Anonymous Usage Reporting?": "Tillåt anonym användarstatistiksrapportering?",
|
||||
"Alphabetic": "Alfabetisk",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "Ett externt kommando sköter versionshanteringen. Det måste ta bort filen från den synkroniserade mappen.",
|
||||
"Anonymous Usage Reporting": "Anonym användarstatistik",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "Ett externt kommando sköter versionshanteringen. Den behöver ta bort filen från den synkroniserade mappen.",
|
||||
"Anonymous Usage Reporting": "Anonym användarstatistik rapportering",
|
||||
"Any devices configured on an introducer device will be added to this device as well.": "Enheter konfigurerade på en introduktörsenhet kommer också att läggas till den här enheten.",
|
||||
"Automatic upgrades": "Automatiska uppgraderingar",
|
||||
"Be careful!": "Var aktsam!",
|
||||
"Bugs": "Buggar",
|
||||
"CPU Utilization": "CPU-användning",
|
||||
"CPU Utilization": "CPU användning",
|
||||
"Changelog": "Ändringslogg",
|
||||
"Clean out after": "Rensa efteråt",
|
||||
"Close": "Stäng",
|
||||
"Command": "Kommando",
|
||||
"Comment, when used at the start of a line": "Kommentar, vid början av en rad.",
|
||||
"Comment, when used at the start of a line": "Kommentar, vid användning i början av en rad.",
|
||||
"Compression": "Komprimering",
|
||||
"Connection Error": "Anslutningsproblem",
|
||||
"Connection Type": "Anslutningstyp",
|
||||
"Copied from elsewhere": "Kopierat utifrån",
|
||||
"Copied from original": "Oförändrat",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 följande bidragande:",
|
||||
"Copied from original": "Kopierat från original",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 följande bidragare:",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 följande medverkande:",
|
||||
"Danger!": "Fara!",
|
||||
"Delete": "Ta bort",
|
||||
"Deleted": "Borttaget",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Enhet \"{{name}}\" ({{device}} på {{address}}) vill ansluta. Lägg till ny enhet?",
|
||||
"Device ID": "Enhets-ID",
|
||||
"Device ID": "Enhets ID",
|
||||
"Device Identification": "Enhetsidentifikation",
|
||||
"Device Name": "Enhetsnamn",
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Enheten {{device}} ({{address}}) vill ansluta. Lägg till ny enhet?",
|
||||
"Devices": "Enheter",
|
||||
"Disconnected": "Ej ansluten",
|
||||
"Discovery": "Uppslagning",
|
||||
"Disconnected": "Frånkopplad",
|
||||
"Discovery": "Upptäckt",
|
||||
"Documentation": "Dokumentation",
|
||||
"Download Rate": "Nedladdningshastighet",
|
||||
"Downloaded": "Nerladdat",
|
||||
"Downloading": "Laddar ner",
|
||||
"Download Rate": "Hämtningshastighet",
|
||||
"Downloaded": "Hämtat",
|
||||
"Downloading": "Hämtar",
|
||||
"Edit": "Redigera",
|
||||
"Edit Device": "Redigera enhet",
|
||||
"Edit Folder": "Redigera katalog",
|
||||
"Editing": "Redigerar",
|
||||
"Enable NAT traversal": "Aktivera NAT traversering",
|
||||
"Enable Relaying": "Aktivera reläa",
|
||||
"Enable UPnP": "Använd UPnP",
|
||||
"Enable UPnP": "Aktivera UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Ange kommaseparerade (\"tcp://ip:port\", \"tcp://host:port\")-adresser eller ordet \"dynamic\" för att använda automatisk uppslagning.",
|
||||
"Enter ignore patterns, one per line.": "Ange filmönster, ett per rad.",
|
||||
"Enter ignore patterns, one per line.": "Ange ignorera mönster, en per rad.",
|
||||
"Error": "Fel",
|
||||
"External File Versioning": "Extern versionshantering",
|
||||
"Failed Items": "Misslyckade filer",
|
||||
"File Pull Order": "Hämtningsprioritering av filer",
|
||||
"File Versioning": "Versionshantering",
|
||||
"External File Versioning": "Extern filversionshantering",
|
||||
"Failed Items": "Misslyckade objekt",
|
||||
"File Pull Order": "Filhämtningsprioritering",
|
||||
"File Versioning": "Filversionshantering",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Filrättigheter ignoreras vid sökning efter förändringar. Används på FAT-filsystem.",
|
||||
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Filer flyttas till katalogen .stversions om de ersätts eller raderas av Syncthing.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Filer flyttas till datummärkta versioner i en .stversions-mapp när de ersatts eller raderats av Syncthing.",
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Filer skyddas från ändringar gjorda på andra enheter, men ändringar som görs på den här noden skickas till de andra klustermedlemmarna.",
|
||||
"Folder": "Katalog",
|
||||
"Folder ID": "Katalog-ID",
|
||||
"Folder Label": "Katalog etikett",
|
||||
"Folder Master": "Huvudlagring",
|
||||
"Folder Path": "Sökväg",
|
||||
"Folder Label": "Katalog-etikett",
|
||||
"Folder Master": "Huvudkatalog",
|
||||
"Folder Path": "Katalog-sökväg",
|
||||
"Folder Type": "Katalogtyp",
|
||||
"Folders": "Kataloger",
|
||||
"GUI": "GUI",
|
||||
"GUI Authentication Password": "GUI-lösenord",
|
||||
"GUI Authentication User": "GUI-användare",
|
||||
"GUI Listen Addresses": "GUI-adress",
|
||||
"Generate": "Skapa",
|
||||
"Global Discovery": "Global uppslagning",
|
||||
"Global Discovery Server": "Global uppslagningsserver",
|
||||
"Global Discovery Servers": "Globala uppslagningsservrar",
|
||||
"GUI": "Grafiskt gränssnitt",
|
||||
"GUI Authentication Password": "Grafiska gränssnittets lösenord",
|
||||
"GUI Authentication User": "Grafiska gränssnittets användare",
|
||||
"GUI Listen Addresses": "Grafiska gränssnittets avlyssningsadresser",
|
||||
"Generate": "Generera",
|
||||
"Global Discovery": "Global upptäckt",
|
||||
"Global Discovery Server": "Global upptäcktsserver",
|
||||
"Global Discovery Servers": "Globala upptäcktsservrar",
|
||||
"Global State": "Global status",
|
||||
"Help": "Hjälp",
|
||||
"Home page": "Hemsida",
|
||||
"Ignore": "Ignorera",
|
||||
"Ignore Patterns": "Ignorerade filmönster",
|
||||
"Ignore Permissions": "Ignorera filrättigheter",
|
||||
"Incoming Rate Limit (KiB/s)": "Max nedladdningshastighet (KiB/s)",
|
||||
"Ignore Patterns": "Ignorera mönster",
|
||||
"Ignore Permissions": "Ignorera rättigheter",
|
||||
"Incoming Rate Limit (KiB/s)": "Nedladdningshastighetsgräns (KiB/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Inkorrekt konfiguration kan skada innehållet i katalogen and få Syncthing att sluta fungera.",
|
||||
"Introducer": "introduktör",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Vänder på villkoret, d.v.s. exkluderar inte.",
|
||||
"Keep Versions": "Behåll versioner",
|
||||
"Largest First": "Störst först",
|
||||
"Last File Received": "Senast Mottagna Fil",
|
||||
"Last Scan": "Senast skanning",
|
||||
"Last seen": "Senast online",
|
||||
"Largest First": "Största först",
|
||||
"Last File Received": "Senaste fil mottagen",
|
||||
"Last Scan": "Senaste skanning",
|
||||
"Last seen": "Senast sedd",
|
||||
"Later": "Senare",
|
||||
"Listeners": "Lyssnare",
|
||||
"Local Discovery": "Lokal uppslagning",
|
||||
"Local State": "Lokal status",
|
||||
"Local State (Total)": "Lokal status (Total)",
|
||||
"Major Upgrade": "Stor uppgradering",
|
||||
"Local State (Total)": "Lokal status (totalt)",
|
||||
"Major Upgrade": "Större uppgradering",
|
||||
"Master": "Huvud",
|
||||
"Maximum Age": "Högsta ålder",
|
||||
"Maximum Age": "Maximum ålder",
|
||||
"Metadata Only": "Endast metadata",
|
||||
"Minimum Free Disk Space": "Minimum ledigt diskutrymme",
|
||||
"Move to top of queue": "Flytta till överst i kön",
|
||||
@@ -117,7 +117,7 @@
|
||||
"New Folder": "Ny katalog",
|
||||
"Newest First": "Nyast först",
|
||||
"No": "Nej",
|
||||
"No File Versioning": "Ingen versionshantering",
|
||||
"No File Versioning": "Ingen filversionshantering",
|
||||
"Normal": "Normal",
|
||||
"Notice": "Observera",
|
||||
"OK": "OK",
|
||||
@@ -126,39 +126,39 @@
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Valfri beskrivande etikett för katalogen. Kan vara olika på varje enhet.",
|
||||
"Options": "Alternativ",
|
||||
"Out of Sync": "Osynkroniserad",
|
||||
"Out of Sync Items": "Osynkroniserade poster",
|
||||
"Out of Sync Items": "Osynkroniserade objekt",
|
||||
"Outgoing Rate Limit (KiB/s)": "Max uppladdningshastighet (KiB/s)",
|
||||
"Override Changes": "Skriv över ändringar",
|
||||
"Override Changes": "Åsidosätt förändringar",
|
||||
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Sökväg till katalogen på din dator. Kommer att skapas om det inte finns. Tecknet tilde (~) kan användas som en genväg för",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Sökväg där versioner sparas (lämna tomt för att använda .stversions i den ordinarie katalogen).",
|
||||
"Pause": "Paus",
|
||||
"Paused": "Pausad",
|
||||
"Please consult the release notes before performing a major upgrade.": "Läs igenom versionsnyheterna innan den stora uppgraderingen.",
|
||||
"Please set a GUI Authentication User and Password in the Settings dialog.": "Ställ in ett grafiskt användarautentisering och lösenord i dialogrutan Inställningar.",
|
||||
"Please set a GUI Authentication User and Password in the Settings dialog.": "Ställ in ett grafiska gränssnittets användarautentisering och lösenord i inställningsdialogrutan.",
|
||||
"Please wait": "Var god vänta",
|
||||
"Preview": "Förhandsgranska",
|
||||
"Preview Usage Report": "Förhandsgranska statistik",
|
||||
"Quick guide to supported patterns": "Snabb guide till filmönster som stöds",
|
||||
"RAM Utilization": "Minnesanvändning",
|
||||
"Quick guide to supported patterns": "Snabb handledning till mönster som stöds",
|
||||
"RAM Utilization": "RAM användning",
|
||||
"Random": "Slumpmässig",
|
||||
"Relay Servers": "Reläservrar",
|
||||
"Relayed via": "Vidarbefordras via",
|
||||
"Relays": "Vidarbefordringar",
|
||||
"Relays": "Reläservrar",
|
||||
"Release Notes": "Versionsanteckningar",
|
||||
"Remote Devices": "Fjärrenheter",
|
||||
"Remove": "Ta bort",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Krävs identifierare för katalogen. Måste vara densamma på alla kluster enheter.",
|
||||
"Rescan": "Uppdatera",
|
||||
"Rescan All": "Uppdatera alla",
|
||||
"Rescan Interval": "Uppdateringsintervall",
|
||||
"Rescan": "Skanna om",
|
||||
"Rescan All": "Skanna om alla",
|
||||
"Rescan Interval": "Omskanningsintervall",
|
||||
"Restart": "Starta om",
|
||||
"Restart Needed": "Omstart behövs",
|
||||
"Restarting": "Startar om",
|
||||
"Resume": "Återuppta",
|
||||
"Reused": "Återanvänt",
|
||||
"Reused": "Återanvänd",
|
||||
"Save": "Spara",
|
||||
"Scan Time Remaining": "Granska återstående tid",
|
||||
"Scanning": "Uppdaterar",
|
||||
"Scan Time Remaining": "Återstående skanningstid",
|
||||
"Scanning": "Skannar",
|
||||
"Select the devices to share this folder with.": "Ange enheterna att dela den här katalogen med.",
|
||||
"Select the folders to share with this device.": "Välj kataloger att dela med den här enheten.",
|
||||
"Settings": "Inställningar",
|
||||
@@ -166,21 +166,21 @@
|
||||
"Share Folder": "Dela katalog",
|
||||
"Share Folders With Device": "Dela kataloger med enhet",
|
||||
"Share With Devices": "Dela med enheter",
|
||||
"Share this folder?": "Dela den här katalogen?",
|
||||
"Share this folder?": "Dela denna katalog?",
|
||||
"Shared With": "Delad med",
|
||||
"Short identifier for the folder. Must be the same on all cluster devices.": "Kort identifieringssträng för katalogen. Måste vara samma på alla enheter i klustret.",
|
||||
"Show ID": "Visa ID",
|
||||
"Show QR": "Visa QR",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Visas i stället för enhets-ID. Skickas till andra enheter som namn på denna enhet.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Visas i stället för enhets-ID. Sätts till namnet på den andra enheten vid första anslutning om det lämnas tomt.",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Visas i stället för enhetens ID. Skickas till andra enheter som namn på denna enhet.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Visas i stället för enhetens ID. Sätts till namnet på den andra enheten vid första anslutning om det lämnas tomt.",
|
||||
"Shutdown": "Stäng av",
|
||||
"Shutdown Complete": "Avstängning klar",
|
||||
"Simple File Versioning": "Enkel versionshantering",
|
||||
"Simple File Versioning": "Enkel filversionshantering",
|
||||
"Single level wildcard (matches within a directory only)": "Jokertecken som representerar noll eller fler godtyckliga tecken i ett filnamn.",
|
||||
"Smallest First": "Minst först",
|
||||
"Source Code": "Källkod",
|
||||
"Staggered File Versioning": "Versionshantering i intervall",
|
||||
"Start Browser": "Starta browser",
|
||||
"Staggered File Versioning": "Filversionshantering i intervall",
|
||||
"Start Browser": "Starta webbläsare",
|
||||
"Statistics": "Statistik",
|
||||
"Stopped": "Stoppad",
|
||||
"Support": "Support",
|
||||
@@ -191,60 +191,60 @@
|
||||
"Syncthing is restarting.": "Syncthing startar om.",
|
||||
"Syncthing is upgrading.": "Syncthing uppgraderas.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing verkar avstängd, eller finns det problem med din Internetanslutning. Försöker igen...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing verkar ha drabbats av ett problem. Uppdatera sidan eller starta om Syncthing om problemet kvarstår.",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing verkar ha drabbats av ett problem med behandlingen av din begäran. Uppdatera sidan eller starta om Syncthing om problemet kvarstår.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "Syncthing administratör gränssnittet är konfigurerat för att tillåta fjärrtillträde utan ett lösenord.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "Den aggregerade statistiken är offentligt tillgängliga på webbadressen nedan.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Sammanställd statistik finns publikt tillgänglig på {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Konfigurationen har sparats men inte aktiverats. Syncthing måste startas om för att aktivera den nya konfigurationen.",
|
||||
"The device ID cannot be blank.": "Enhets-ID kan inte vara tomt.",
|
||||
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Enhets-ID som behövs här kan du hitta i \"Redigera > Visa ID\"-dialogen på den andra enheten. Mellanrum och bindestreck är valfria (ignoreras).",
|
||||
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Enhets-ID som behövs här kan du hitta i \"Redigera > Visa ID\"-dialogen på den andra enheten. Mellanrum och bindestreck är valfria (ignoreras).",
|
||||
"The device ID cannot be blank.": "Enhetens ID kan inte vara tomt.",
|
||||
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Enhetens ID som behövs här kan du hitta i \"Redigera > Visa ID\"-dialogen på den andra enheten. Mellanrum och bindestreck är valfria (ignoreras).",
|
||||
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Enhetens ID som behövs här kan du hitta i \"Redigera > Visa ID\"-dialogen på den andra enheten. Mellanrum och bindestreck är valfria (ignoreras).",
|
||||
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Den krypterade användarstatistiken skickas dagligen. Den används för att spåra vanliga plattformar, katalogstorlekar och versioner. Om datan som rapporteras ändras så kommer du att bli tillfrågad igen.",
|
||||
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Det inmatade enhets-ID:t verkar inte korrekt. Det ska vara en 52 eller 56 teckens sträng bestående av siffror och bokstäver, eventuellt med mellanrum och bindestreck.",
|
||||
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Det inmatade enhetens ID verkar inte korrekt. Det ska vara en 52 eller 56 teckens sträng bestående av siffror och bokstäver, eventuellt med mellanrum och bindestreck.",
|
||||
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "Den första kommandoparametern är sökvägen till mappen och den andra parametern är den relativa sökvägen i mappen.",
|
||||
"The folder ID cannot be blank.": "Ange ett enhets-ID.",
|
||||
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "Katalog-ID:t måste vara en kort sträng (64 tecken eller mindre), bestående av endast bokstäver, siffror, punkt (.), bindestreck (-) och understreck (_).",
|
||||
"The folder ID must be unique.": "Katalog-ID:t måste vara unikt.",
|
||||
"The folder path cannot be blank.": "Ange en sökväg.",
|
||||
"The folder ID cannot be blank.": "Katalogens ID får inte vara tomt.",
|
||||
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "Katalogens ID måste vara en kort sträng (64 tecken eller mindre), bestående av endast bokstäver, siffror, punkt (.), bindestreck (-) och understreck (_).",
|
||||
"The folder ID must be unique.": "Katalogens ID måste vara unikt.",
|
||||
"The folder path cannot be blank.": "Katalogsökvägen får inte vara tom.",
|
||||
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "De följande intervallen används: varje 30 sekunder under den första timmen; varje timme under den första dagen; varje dag för de första 30 dagarna; varje vecka tills den maximala åldersgränsen uppnås.",
|
||||
"The following items could not be synchronized.": "Följande filer kunde inte synkroniseras.",
|
||||
"The following items could not be synchronized.": "Följande objekt kunde inte synkroniseras.",
|
||||
"The maximum age must be a number and cannot be blank.": "Åldersgränsen måste vara ett tal och kan inte lämnas tomt.",
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Den längsta tiden att behålla en version (i dagar, sätt till 0 för att behålla versioner för evigt).",
|
||||
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "Minimum ledigt diskutrymme i procent måste vara en icke negativ siffra mellan 0 och 100 (inklusive).",
|
||||
"The number of days must be a number and cannot be blank.": "Antalet dagar måste vara en siffra och får inte vara tomt.",
|
||||
"The number of days to keep files in the trash can. Zero means forever.": "Antal dagar som filer ligger kvar i papperskorgen. Noll betyder för alltid.",
|
||||
"The number of days to keep files in the trash can. Zero means forever.": "Antalet dagar som filer ligger kvar i papperskorgen. Noll betyder för alltid.",
|
||||
"The number of old versions to keep, per file.": "Antalet gamla versioner som ska behållas, per fil.",
|
||||
"The number of versions must be a number and cannot be blank.": "Antalet versioner måste vara ett nummer och kan inte lämnas tomt.",
|
||||
"The path cannot be blank.": "Ange en sökväg",
|
||||
"The path cannot be blank.": "Sökvägen kan inte vara tom.",
|
||||
"The rate limit must be a non-negative number (0: no limit)": "Frekvensgränsen måste vara ett icke-negativt tal (0: ingen gräns)",
|
||||
"The rescan interval must be a non-negative number of seconds.": "Förnyelseintervallet måste vara ett positivt antal sekunder",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "De omprövas automatiskt och kommer att synkroniseras när felet är löst.",
|
||||
"This Device": "Denna enhet",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "Detta kan lätt ge hackare tillgång till att läsa och ändra några filer på datorn.",
|
||||
"This is a major version upgrade.": "Det här är en stor uppgradering.",
|
||||
"Trash Can File Versioning": "Versionshantering på filer i papperskorgen",
|
||||
"Unknown": "Okänt",
|
||||
"Trash Can File Versioning": "Papperskorgs filversionshantering",
|
||||
"Unknown": "Okänd",
|
||||
"Unshared": "Inte delad",
|
||||
"Unused": "Oanvänd",
|
||||
"Up to Date": "Helt uppdaterad",
|
||||
"Up to Date": "Uppdaterad",
|
||||
"Updated": "Uppdaterad",
|
||||
"Upgrade": "Uppgradering",
|
||||
"Upgrade To {%version%}": "Uppgradera till {{version}}",
|
||||
"Upgrading": "Uppgraderar",
|
||||
"Upload Rate": "Uppladdningshastighet",
|
||||
"Uptime": "Tid sedan start",
|
||||
"Use HTTPS for GUI": "Använd HTTPS för GUI",
|
||||
"Use HTTPS for GUI": "Använd HTTPS för grafiska gränssnittet",
|
||||
"Version": "Version",
|
||||
"Versions Path": "Katalog för versioner",
|
||||
"Versions Path": "Sökväg för versioner",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versioner tas bort automatiskt när de är äldre än den maximala åldersgränsen eller överstiger frekvensen i sitt interval.",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Varning, denna sökväg är en underkatalog till en befintlig katalog \"{{otherFolder}}\".",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "När du lägger till en ny enhet, kom ihåg att den här enheten måste läggas till på den andra enheten också.",
|
||||
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "När du lägger till ny katalog, tänk på att katalog-ID:t knyter ihop katalogen mellan olika noder. De måste vara exakt desamma mellan noder och stora eller små bokstäver har betydelse.",
|
||||
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "När du lägger till ny katalog, tänk på att katalogens ID knyter ihop katalogen mellan olika noder. De måste vara exakt desamma mellan noder och stora eller små bokstäver har betydelse.",
|
||||
"Yes": "Ja",
|
||||
"You must keep at least one version.": "Du måste behålla åtminstone en version.",
|
||||
"days": "dagar",
|
||||
"full documentation": "fullständig dokumentation",
|
||||
"items": "poster",
|
||||
"items": "objekt",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vill dela katalogen \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} vill dela katalogen \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} vill dela katalogen \"{{folderlabel}}\" ({{folder}})."
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
<p translate>
|
||||
The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.
|
||||
</p>
|
||||
<p translate translate-value-url="<a href="https://data.syncthing.net" target="_blank">https://data.syncthing.net</a>">
|
||||
The aggregated statistics are publicly available at {%url%}.
|
||||
</p>
|
||||
<p translate>The aggregated statistics are publicly available at the URL below.</p>
|
||||
<p><a href="https://data.syncthing.net/" target="_blank">https://data.syncthing.net/</a></p>
|
||||
<form>
|
||||
<textarea class="form-control" rows="20">{{reportData | json}}</textarea>
|
||||
</form>
|
||||
|
||||
+25
-16
@@ -11,12 +11,18 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
type requiresRestart struct{}
|
||||
type requiresRestart struct {
|
||||
committed chan struct{}
|
||||
}
|
||||
|
||||
func (requiresRestart) VerifyConfiguration(_, _ Configuration) error {
|
||||
return nil
|
||||
}
|
||||
func (requiresRestart) CommitConfiguration(_, _ Configuration) bool {
|
||||
func (c requiresRestart) CommitConfiguration(_, _ Configuration) bool {
|
||||
select {
|
||||
case c.committed <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
return false
|
||||
}
|
||||
func (requiresRestart) String() string {
|
||||
@@ -28,7 +34,7 @@ type validationError struct{}
|
||||
func (validationError) VerifyConfiguration(_, _ Configuration) error {
|
||||
return errors.New("some error")
|
||||
}
|
||||
func (validationError) CommitConfiguration(_, _ Configuration) bool {
|
||||
func (c validationError) CommitConfiguration(_, _ Configuration) bool {
|
||||
return true
|
||||
}
|
||||
func (validationError) String() string {
|
||||
@@ -44,11 +50,11 @@ func TestReplaceCommit(t *testing.T) {
|
||||
// Replace config. We should get back a clean response and the config
|
||||
// should change.
|
||||
|
||||
resp := w.Replace(Configuration{Version: 1})
|
||||
if resp.ValidationError != nil {
|
||||
t.Fatal("Should not have a validation error")
|
||||
err := w.Replace(Configuration{Version: 1})
|
||||
if err != nil {
|
||||
t.Fatal("Should not have a validation error:", err)
|
||||
}
|
||||
if resp.RequiresRestart {
|
||||
if w.RequiresRestart() {
|
||||
t.Fatal("Should not require restart")
|
||||
}
|
||||
if w.Raw().Version != 1 {
|
||||
@@ -58,13 +64,16 @@ func TestReplaceCommit(t *testing.T) {
|
||||
// Now with a subscriber requiring restart. We should get a clean response
|
||||
// but with the restart flag set, and the config should change.
|
||||
|
||||
w.Subscribe(requiresRestart{})
|
||||
sub0 := requiresRestart{committed: make(chan struct{}, 1)}
|
||||
w.Subscribe(sub0)
|
||||
|
||||
resp = w.Replace(Configuration{Version: 2})
|
||||
if resp.ValidationError != nil {
|
||||
t.Fatal("Should not have a validation error")
|
||||
err = w.Replace(Configuration{Version: 2})
|
||||
if err != nil {
|
||||
t.Fatal("Should not have a validation error:", err)
|
||||
}
|
||||
if !resp.RequiresRestart {
|
||||
|
||||
<-sub0.committed
|
||||
if !w.RequiresRestart() {
|
||||
t.Fatal("Should require restart")
|
||||
}
|
||||
if w.Raw().Version != 2 {
|
||||
@@ -76,12 +85,12 @@ func TestReplaceCommit(t *testing.T) {
|
||||
|
||||
w.Subscribe(validationError{})
|
||||
|
||||
resp = w.Replace(Configuration{Version: 3})
|
||||
if resp.ValidationError == nil {
|
||||
err = w.Replace(Configuration{Version: 3})
|
||||
if err == nil {
|
||||
t.Fatal("Should have a validation error")
|
||||
}
|
||||
if resp.RequiresRestart {
|
||||
t.Fatal("Should not require restart")
|
||||
if !w.RequiresRestart() {
|
||||
t.Fatal("Should still require restart")
|
||||
}
|
||||
if w.Raw().Version != 2 {
|
||||
t.Fatal("Config should not have changed")
|
||||
|
||||
@@ -105,7 +105,9 @@ func ReadJSON(r io.Reader, myID protocol.DeviceID) (Configuration, error) {
|
||||
return Configuration{}, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(bs, &cfg)
|
||||
if err := json.Unmarshal(bs, &cfg); err != nil {
|
||||
return Configuration{}, err
|
||||
}
|
||||
cfg.OriginalVersion = cfg.Version
|
||||
|
||||
if err := cfg.prepare(myID); err != nil {
|
||||
|
||||
@@ -549,6 +549,9 @@ func TestPullOrder(t *testing.T) {
|
||||
t.Logf("%s", buf.Bytes())
|
||||
|
||||
cfg, err = ReadXML(buf, device1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wrapper = Wrap("testdata/pullorder.xml", cfg)
|
||||
folders = wrapper.Folders()
|
||||
|
||||
|
||||
+35
-26
@@ -8,6 +8,7 @@ package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
@@ -41,11 +42,6 @@ type Committer interface {
|
||||
String() string
|
||||
}
|
||||
|
||||
type CommitResponse struct {
|
||||
ValidationError error
|
||||
RequiresRestart bool
|
||||
}
|
||||
|
||||
// A wrapper around a Configuration that manages loads, saves and published
|
||||
// notifications of changes to registered Handlers
|
||||
|
||||
@@ -58,6 +54,8 @@ type Wrapper struct {
|
||||
replaces chan Configuration
|
||||
subs []Committer
|
||||
mut sync.Mutex
|
||||
|
||||
requiresRestart uint32 // an atomic bool
|
||||
}
|
||||
|
||||
// Wrap wraps an existing Configuration structure and ties it to a file on
|
||||
@@ -128,32 +126,21 @@ func (w *Wrapper) Raw() Configuration {
|
||||
}
|
||||
|
||||
// Replace swaps the current configuration object for the given one.
|
||||
func (w *Wrapper) Replace(cfg Configuration) CommitResponse {
|
||||
func (w *Wrapper) Replace(cfg Configuration) error {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
|
||||
return w.replaceLocked(cfg)
|
||||
}
|
||||
|
||||
func (w *Wrapper) replaceLocked(to Configuration) CommitResponse {
|
||||
func (w *Wrapper) replaceLocked(to Configuration) error {
|
||||
from := w.cfg
|
||||
|
||||
for _, sub := range w.subs {
|
||||
l.Debugln(sub, "verifying configuration")
|
||||
if err := sub.VerifyConfiguration(from, to); err != nil {
|
||||
l.Debugln(sub, "rejected config:", err)
|
||||
return CommitResponse{
|
||||
ValidationError: err,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
allOk := true
|
||||
for _, sub := range w.subs {
|
||||
l.Debugln(sub, "committing configuration")
|
||||
ok := sub.CommitConfiguration(from, to)
|
||||
if !ok {
|
||||
l.Debugln(sub, "requires restart")
|
||||
allOk = false
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,8 +148,22 @@ func (w *Wrapper) replaceLocked(to Configuration) CommitResponse {
|
||||
w.deviceMap = nil
|
||||
w.folderMap = nil
|
||||
|
||||
return CommitResponse{
|
||||
RequiresRestart: !allOk,
|
||||
w.notifyListeners(from, to)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *Wrapper) notifyListeners(from, to Configuration) {
|
||||
for _, sub := range w.subs {
|
||||
go w.notifyListener(sub, from, to)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Wrapper) notifyListener(sub Committer, from, to Configuration) {
|
||||
l.Debugln(sub, "committing configuration")
|
||||
if !sub.CommitConfiguration(from, to) {
|
||||
l.Debugln(sub, "requires restart")
|
||||
w.setRequiresRestart()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,7 +183,7 @@ func (w *Wrapper) Devices() map[protocol.DeviceID]DeviceConfiguration {
|
||||
|
||||
// SetDevice adds a new device to the configuration, or overwrites an existing
|
||||
// device with the same ID.
|
||||
func (w *Wrapper) SetDevice(dev DeviceConfiguration) CommitResponse {
|
||||
func (w *Wrapper) SetDevice(dev DeviceConfiguration) error {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
|
||||
@@ -218,7 +219,7 @@ func (w *Wrapper) Folders() map[string]FolderConfiguration {
|
||||
|
||||
// SetFolder adds a new folder to the configuration, or overwrites an existing
|
||||
// folder with the same ID.
|
||||
func (w *Wrapper) SetFolder(fld FolderConfiguration) CommitResponse {
|
||||
func (w *Wrapper) SetFolder(fld FolderConfiguration) error {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
|
||||
@@ -246,7 +247,7 @@ func (w *Wrapper) Options() OptionsConfiguration {
|
||||
}
|
||||
|
||||
// SetOptions replaces the current options configuration object.
|
||||
func (w *Wrapper) SetOptions(opts OptionsConfiguration) CommitResponse {
|
||||
func (w *Wrapper) SetOptions(opts OptionsConfiguration) error {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
newCfg := w.cfg.Copy()
|
||||
@@ -262,7 +263,7 @@ func (w *Wrapper) GUI() GUIConfiguration {
|
||||
}
|
||||
|
||||
// SetGUI replaces the current GUI configuration object.
|
||||
func (w *Wrapper) SetGUI(gui GUIConfiguration) CommitResponse {
|
||||
func (w *Wrapper) SetGUI(gui GUIConfiguration) error {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
newCfg := w.cfg.Copy()
|
||||
@@ -332,3 +333,11 @@ func (w *Wrapper) ListenAddresses() []string {
|
||||
}
|
||||
return util.UniqueStrings(addresses)
|
||||
}
|
||||
|
||||
func (w *Wrapper) RequiresRestart() bool {
|
||||
return atomic.LoadUint32(&w.requiresRestart) != 0
|
||||
}
|
||||
|
||||
func (w *Wrapper) setRequiresRestart() {
|
||||
atomic.StoreUint32(&w.requiresRestart, 1)
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ type CacheEntry struct {
|
||||
when time.Time // When did we get the result
|
||||
found bool // Is it a success (cacheTime applies) or a failure (negCacheTime applies)?
|
||||
validUntil time.Time // Validity time, overrides normal calculation
|
||||
instanceID int64 // for local discovery, the instance ID (random on each restart)
|
||||
}
|
||||
|
||||
// A FinderService is a Finder that has background activity and must be run as
|
||||
|
||||
+11
-6
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/syncthing/syncthing/lib/beacon"
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/rand"
|
||||
"github.com/thejerf/suture"
|
||||
)
|
||||
|
||||
@@ -114,8 +115,9 @@ func (c *localClient) Error() error {
|
||||
|
||||
func (c *localClient) announcementPkt() Announce {
|
||||
return Announce{
|
||||
ID: c.myID[:],
|
||||
Addresses: c.addrList.AllAddresses(),
|
||||
ID: c.myID[:],
|
||||
Addresses: c.addrList.AllAddresses(),
|
||||
InstanceID: rand.Int63(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,9 +196,11 @@ func (c *localClient) registerDevice(src net.Addr, device Announce) bool {
|
||||
copy(id[:], device.ID)
|
||||
|
||||
// Remember whether we already had a valid cache entry for this device.
|
||||
// If the instance ID has changed the remote device has restarted since
|
||||
// we last heard from it, so we should treat it as a new device.
|
||||
|
||||
ce, existsAlready := c.Get(id)
|
||||
isNewDevice := !existsAlready || time.Since(ce.when) > CacheLifeTime
|
||||
isNewDevice := !existsAlready || time.Since(ce.when) > CacheLifeTime || ce.instanceID != device.InstanceID
|
||||
|
||||
// Any empty or unspecified addresses should be set to the source address
|
||||
// of the announcement. We also skip any addresses we can't parse.
|
||||
@@ -245,9 +249,10 @@ func (c *localClient) registerDevice(src net.Addr, device Announce) bool {
|
||||
}
|
||||
|
||||
c.Set(id, CacheEntry{
|
||||
Addresses: validAddresses,
|
||||
when: time.Now(),
|
||||
found: true,
|
||||
Addresses: validAddresses,
|
||||
when: time.Now(),
|
||||
found: true,
|
||||
instanceID: device.InstanceID,
|
||||
})
|
||||
|
||||
if isNewDevice {
|
||||
|
||||
@@ -30,8 +30,9 @@ var _ = math.Inf
|
||||
const _ = proto.GoGoProtoPackageIsVersion1
|
||||
|
||||
type Announce struct {
|
||||
ID []byte `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
Addresses []string `protobuf:"bytes,2,rep,name=addresses" json:"addresses,omitempty"`
|
||||
ID []byte `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
Addresses []string `protobuf:"bytes,2,rep,name=addresses" json:"addresses,omitempty"`
|
||||
InstanceID int64 `protobuf:"varint,3,opt,name=instance_id,json=instanceId,proto3" json:"instance_id,omitempty"`
|
||||
}
|
||||
|
||||
func (m *Announce) Reset() { *m = Announce{} }
|
||||
@@ -78,6 +79,11 @@ func (m *Announce) MarshalTo(data []byte) (int, error) {
|
||||
i += copy(data[i:], s)
|
||||
}
|
||||
}
|
||||
if m.InstanceID != 0 {
|
||||
data[i] = 0x18
|
||||
i++
|
||||
i = encodeVarintLocal(data, i, uint64(m.InstanceID))
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
|
||||
@@ -121,6 +127,9 @@ func (m *Announce) ProtoSize() (n int) {
|
||||
n += 1 + l + sovLocal(uint64(l))
|
||||
}
|
||||
}
|
||||
if m.InstanceID != 0 {
|
||||
n += 1 + sovLocal(uint64(m.InstanceID))
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
@@ -226,6 +235,25 @@ func (m *Announce) Unmarshal(data []byte) error {
|
||||
}
|
||||
m.Addresses = append(m.Addresses, string(data[iNdEx:postIndex]))
|
||||
iNdEx = postIndex
|
||||
case 3:
|
||||
if wireType != 0 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field InstanceID", wireType)
|
||||
}
|
||||
m.InstanceID = 0
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowLocal
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := data[iNdEx]
|
||||
iNdEx++
|
||||
m.InstanceID |= (int64(b) & 0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
default:
|
||||
iNdEx = preIndex
|
||||
skippy, err := skipLocal(data[iNdEx:])
|
||||
@@ -353,16 +381,18 @@ var (
|
||||
)
|
||||
|
||||
var fileDescriptorLocal = []byte{
|
||||
// 161 bytes of a gzipped FileDescriptorProto
|
||||
// 194 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0xe2, 0xce, 0xc9, 0x4f, 0x4e,
|
||||
0xcc, 0xd1, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x48, 0xc9, 0x2c, 0x4e, 0xce, 0x2f, 0x4b,
|
||||
0x2d, 0x92, 0xd2, 0x4d, 0xcf, 0x2c, 0xc9, 0x28, 0x4d, 0xd2, 0x4b, 0xce, 0xcf, 0xd5, 0x4f, 0xcf,
|
||||
0x4f, 0xcf, 0xd7, 0x07, 0x2b, 0x48, 0x2a, 0x4d, 0x03, 0xf3, 0xc0, 0x1c, 0x30, 0x0b, 0xa2, 0x51,
|
||||
0xc9, 0x81, 0x8b, 0xc3, 0x31, 0x2f, 0x2f, 0xbf, 0x34, 0x2f, 0x39, 0x55, 0x48, 0x8c, 0x8b, 0x29,
|
||||
0xa9, 0x90, 0x8b, 0xc3, 0x31, 0x2f, 0x2f, 0xbf, 0x34, 0x2f, 0x39, 0x55, 0x48, 0x8c, 0x8b, 0x29,
|
||||
0x33, 0x45, 0x82, 0x51, 0x81, 0x51, 0x83, 0xc7, 0x89, 0xed, 0xd1, 0x3d, 0x79, 0x26, 0x4f, 0x97,
|
||||
0x20, 0xa0, 0x88, 0x90, 0x0c, 0x17, 0x67, 0x62, 0x4a, 0x4a, 0x51, 0x6a, 0x71, 0x71, 0x6a, 0xb1,
|
||||
0x04, 0x93, 0x02, 0xb3, 0x06, 0x67, 0x10, 0x42, 0xc0, 0x49, 0xe4, 0xc4, 0x43, 0x39, 0x86, 0x13,
|
||||
0x8f, 0xe4, 0x18, 0x2f, 0x00, 0xf1, 0x83, 0x47, 0x72, 0x0c, 0x0b, 0x1e, 0xcb, 0x31, 0x26, 0xb1,
|
||||
0x81, 0x8d, 0x37, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff, 0xc9, 0xec, 0xea, 0xbc, 0xa6, 0x00, 0x00,
|
||||
0x00,
|
||||
0x04, 0x93, 0x02, 0xb3, 0x06, 0x67, 0x10, 0x42, 0x40, 0x48, 0x9f, 0x8b, 0x3b, 0x33, 0xaf, 0xb8,
|
||||
0x24, 0x11, 0x68, 0x42, 0x3c, 0x50, 0x3b, 0x33, 0x50, 0x3b, 0xb3, 0x13, 0x1f, 0x50, 0x3b, 0x97,
|
||||
0x27, 0x54, 0x18, 0x68, 0x0c, 0x17, 0x4c, 0x89, 0x67, 0x8a, 0x93, 0xc8, 0x89, 0x87, 0x72, 0x0c,
|
||||
0x27, 0x1e, 0xc9, 0x31, 0x5e, 0x00, 0xe2, 0x07, 0x8f, 0xe4, 0x18, 0x16, 0x3c, 0x96, 0x63, 0x4c,
|
||||
0x62, 0x03, 0xbb, 0xc7, 0x18, 0x10, 0x00, 0x00, 0xff, 0xff, 0x91, 0x3f, 0x96, 0x25, 0xd7, 0x00,
|
||||
0x00, 0x00,
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ option (gogoproto.sizer_all) = false;
|
||||
option (gogoproto.protosizer_all) = true;
|
||||
|
||||
message Announce {
|
||||
bytes id = 1 [(gogoproto.customname) = "ID"];
|
||||
repeated string addresses = 2;
|
||||
bytes id = 1 [(gogoproto.customname) = "ID"];
|
||||
repeated string addresses = 2;
|
||||
int64 instance_id = 3 [(gogoproto.customname) = "InstanceID"];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright (C) 2016 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package discover
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
)
|
||||
|
||||
func TestRandomLocalInstanceID(t *testing.T) {
|
||||
c, err := NewLocal(protocol.LocalDeviceID, ":0", &fakeAddressLister{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
go c.Serve()
|
||||
defer c.Stop()
|
||||
|
||||
lc := c.(*localClient)
|
||||
|
||||
p0 := lc.announcementPkt()
|
||||
p1 := lc.announcementPkt()
|
||||
if p0.InstanceID == p1.InstanceID {
|
||||
t.Error("each generated packet should have a new instance id")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalInstanceIDShouldTriggerNew(t *testing.T) {
|
||||
c, err := NewLocal(protocol.LocalDeviceID, ":0", &fakeAddressLister{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
lc := c.(*localClient)
|
||||
src := &net.UDPAddr{IP: []byte{10, 20, 30, 40}, Port: 50}
|
||||
|
||||
new := lc.registerDevice(src, Announce{
|
||||
ID: []byte{10, 20, 30, 40, 50, 60, 70, 80, 90},
|
||||
Addresses: []string{"tcp://0.0.0.0:22000"},
|
||||
InstanceID: 1234567890,
|
||||
})
|
||||
|
||||
if !new {
|
||||
t.Fatal("first register should be new")
|
||||
}
|
||||
|
||||
new = lc.registerDevice(src, Announce{
|
||||
ID: []byte{10, 20, 30, 40, 50, 60, 70, 80, 90},
|
||||
Addresses: []string{"tcp://0.0.0.0:22000"},
|
||||
InstanceID: 1234567890,
|
||||
})
|
||||
|
||||
if new {
|
||||
t.Fatal("second register should not be new")
|
||||
}
|
||||
|
||||
new = lc.registerDevice(src, Announce{
|
||||
ID: []byte{42, 10, 20, 30, 40, 50, 60, 70, 80, 90},
|
||||
Addresses: []string{"tcp://0.0.0.0:22000"},
|
||||
InstanceID: 1234567890,
|
||||
})
|
||||
|
||||
if !new {
|
||||
t.Fatal("new device ID should be new")
|
||||
}
|
||||
|
||||
new = lc.registerDevice(src, Announce{
|
||||
ID: []byte{10, 20, 30, 40, 50, 60, 70, 80, 90},
|
||||
Addresses: []string{"tcp://0.0.0.0:22000"},
|
||||
InstanceID: 91234567890,
|
||||
})
|
||||
|
||||
if !new {
|
||||
t.Fatal("new instance ID should be new")
|
||||
}
|
||||
}
|
||||
+3
-6
@@ -1293,7 +1293,7 @@ func (m *Model) localChangeDetected(folder, path string, files []protocol.FileIn
|
||||
objType := "file"
|
||||
action := "modified"
|
||||
|
||||
// If our local vector is verison 1 AND it is the only version
|
||||
// If our local vector is version 1 AND it is the only version
|
||||
// vector so far seen for this file then it is a new file. Else if
|
||||
// it is > 1 it's not new, and if it is 1 but another shortId
|
||||
// version vector exists then it is new for us but created elsewhere
|
||||
@@ -1311,11 +1311,8 @@ func (m *Model) localChangeDetected(folder, path string, files []protocol.FileIn
|
||||
action = "deleted"
|
||||
}
|
||||
|
||||
// If the file is a level or more deep then the forward slash seperator is embedded
|
||||
// in the filename and makes the path look wierd on windows, so lets fix it
|
||||
filename := filepath.FromSlash(file.Name)
|
||||
// And append it to the filepath
|
||||
path := filepath.Join(path, filename)
|
||||
// The full file path, adjusted to the local path separator character.
|
||||
path := filepath.Join(path, filepath.FromSlash(file.Name))
|
||||
|
||||
events.Default.Log(events.LocalChangeDetected, map[string]string{
|
||||
"folder": folder,
|
||||
|
||||
@@ -563,7 +563,7 @@ func TestIgnores(t *testing.T) {
|
||||
|
||||
// Invalid path, marker should be missing, hence returns an error.
|
||||
m.AddFolder(config.FolderConfiguration{ID: "fresh", RawPath: "XXX"})
|
||||
ignores, _, err = m.GetIgnores("fresh")
|
||||
_, _, err = m.GetIgnores("fresh")
|
||||
if err == nil {
|
||||
t.Error("No error")
|
||||
}
|
||||
|
||||
@@ -29,6 +29,9 @@ func TestSourceFileOK(t *testing.T) {
|
||||
|
||||
bs := make([]byte, 6)
|
||||
n, err := fd.Read(bs)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if n != len(bs) {
|
||||
t.Fatalf("Wrong read length %d != %d", n, len(bs))
|
||||
|
||||
+17
-7
@@ -56,16 +56,19 @@ func IsVersionMismatch(err error) bool {
|
||||
}
|
||||
|
||||
func readHello(c io.Reader) (HelloResult, error) {
|
||||
header := make([]byte, 8)
|
||||
header := make([]byte, 4)
|
||||
if _, err := io.ReadFull(c, header); err != nil {
|
||||
return HelloResult{}, err
|
||||
}
|
||||
|
||||
switch binary.BigEndian.Uint32(header[:4]) {
|
||||
switch binary.BigEndian.Uint32(header) {
|
||||
case HelloMessageMagic:
|
||||
// This is a v0.14 Hello message in proto format
|
||||
msgSize := binary.BigEndian.Uint32(header[4:])
|
||||
if msgSize > 1024 {
|
||||
if _, err := io.ReadFull(c, header[:2]); err != nil {
|
||||
return HelloResult{}, err
|
||||
}
|
||||
msgSize := binary.BigEndian.Uint16(header[:2])
|
||||
if msgSize > 32767 {
|
||||
return HelloResult{}, fmt.Errorf("hello message too big")
|
||||
}
|
||||
buf := make([]byte, msgSize)
|
||||
@@ -86,7 +89,10 @@ func readHello(c io.Reader) (HelloResult, error) {
|
||||
|
||||
case Version13HelloMagic:
|
||||
// This is a v0.13 Hello message in XDR format
|
||||
msgSize := binary.BigEndian.Uint32(header[4:])
|
||||
if _, err := io.ReadFull(c, header[:4]); err != nil {
|
||||
return HelloResult{}, err
|
||||
}
|
||||
msgSize := binary.BigEndian.Uint32(header[:4])
|
||||
if msgSize > 1024 {
|
||||
return HelloResult{}, fmt.Errorf("hello message too big")
|
||||
}
|
||||
@@ -120,10 +126,14 @@ func writeHello(c io.Writer, h HelloIntf) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(msg) > 32767 {
|
||||
// The header length must be a positive signed int16
|
||||
panic("bug: attempting to serialize too large hello message")
|
||||
}
|
||||
|
||||
header := make([]byte, 8)
|
||||
header := make([]byte, 6)
|
||||
binary.BigEndian.PutUint32(header[:4], h.Magic())
|
||||
binary.BigEndian.PutUint32(header[4:], uint32(len(msg)))
|
||||
binary.BigEndian.PutUint16(header[4:], uint16(len(msg)))
|
||||
|
||||
_, err = c.Write(append(header, msg...))
|
||||
return err
|
||||
|
||||
@@ -26,9 +26,9 @@ func TestVersion14Hello(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
hdrBuf := make([]byte, 8)
|
||||
hdrBuf := make([]byte, 6)
|
||||
binary.BigEndian.PutUint32(hdrBuf, HelloMessageMagic)
|
||||
binary.BigEndian.PutUint32(hdrBuf[4:], uint32(len(msgBuf)))
|
||||
binary.BigEndian.PutUint16(hdrBuf[4:], uint16(len(msgBuf)))
|
||||
|
||||
outBuf := new(bytes.Buffer)
|
||||
outBuf.Write(hdrBuf)
|
||||
|
||||
@@ -41,7 +41,7 @@ func (m nativeModel) Request(deviceID DeviceID, folder string, name string, offs
|
||||
|
||||
func fixupFiles(folder string, files []FileInfo) {
|
||||
for i, f := range files {
|
||||
if strings.ContainsAny(f.Name, disallowedCharacters) {
|
||||
if strings.ContainsAny(f.Name, disallowedCharacters) || strings.HasSuffix(f.Name, " ") {
|
||||
if f.IsDeleted() {
|
||||
// Don't complain if the file is marked as deleted, since it
|
||||
// can't possibly exist here anyway.
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (C) 2016 The Protocol Authors.
|
||||
|
||||
// +build windows
|
||||
|
||||
package protocol
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestFixupFiles(t *testing.T) {
|
||||
fs := []FileInfo{
|
||||
{Name: "ok"}, // This file is OK
|
||||
{Name: "b<d"}, // The rest should be marked as invalid
|
||||
{Name: "b?d"},
|
||||
{Name: "bad "},
|
||||
}
|
||||
|
||||
fixupFiles("default", fs)
|
||||
|
||||
if fs[0].IsInvalid() {
|
||||
t.Error("fs[0] should not be invalid")
|
||||
}
|
||||
for i := 1; i < len(fs); i++ {
|
||||
if !fs[i].IsInvalid() {
|
||||
t.Errorf("fs[%d] should be invalid", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -288,6 +288,10 @@ func (c *rawConnection) readerLoop() (err error) {
|
||||
}
|
||||
|
||||
msg, err := c.readMessage()
|
||||
if err == errUnknownMessage {
|
||||
// Unknown message types are skipped, for future extensibility.
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -470,8 +470,6 @@ func (w *walker) normalizePath(absPath, relPath string) (normPath string, skip b
|
||||
l.Infof(`File "%s" has UTF8 encoding conflict with another file; ignoring.`, relPath)
|
||||
return "", true
|
||||
}
|
||||
|
||||
relPath = normPath
|
||||
}
|
||||
|
||||
return normPath, false
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ func (s *FolderStatisticsReference) GetLastFile() LastFile {
|
||||
if !ok {
|
||||
return LastFile{}
|
||||
}
|
||||
deleted, ok := s.ns.Bool("lastFileDeleted")
|
||||
deleted, _ := s.ns.Bool("lastFileDeleted")
|
||||
return LastFile{
|
||||
At: at,
|
||||
Filename: file,
|
||||
|
||||
+424
-1166
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-CONFIG" "5" "July 02, 2016" "v0.13" "Syncthing"
|
||||
.TH "SYNCTHING-CONFIG" "5" "July 17, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-config \- Syncthing Configuration
|
||||
.
|
||||
@@ -455,7 +455,7 @@ These are equivalent and will result in Syncthing listening on all
|
||||
interfaces via both IPv4 and IPv6.
|
||||
.UNINDENT
|
||||
.TP
|
||||
.B username
|
||||
.B user
|
||||
Set to require authentication.
|
||||
.TP
|
||||
.B password
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-DEVICE-IDS" "7" "July 02, 2016" "v0.13" "Syncthing"
|
||||
.TH "SYNCTHING-DEVICE-IDS" "7" "July 17, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-device-ids \- Understanding Device IDs
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-EVENT-API" "7" "July 02, 2016" "v0.13" "Syncthing"
|
||||
.TH "SYNCTHING-EVENT-API" "7" "July 17, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-event-api \- Event API
|
||||
.
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-FAQ" "7" "July 02, 2016" "v0.13" "Syncthing"
|
||||
.TH "SYNCTHING-FAQ" "7" "July 17, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-faq \- Frequently Asked Questions
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-GLOBALDISCO" "7" "July 02, 2016" "v0.13" "Syncthing"
|
||||
.TH "SYNCTHING-GLOBALDISCO" "7" "July 17, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-globaldisco \- Global Discovery Protocol v3
|
||||
.
|
||||
|
||||
+33
-71
@@ -1,8 +1,8 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-LOCALDISCO" "7" "July 02, 2016" "v0.13" "Syncthing"
|
||||
.TH "SYNCTHING-LOCALDISCO" "7" "July 17, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-localdisco \- Local Discovery Protocol v3
|
||||
syncthing-localdisco \- Local Discovery Protocol v4
|
||||
.
|
||||
.nr rst2man-indent-level 0
|
||||
.
|
||||
@@ -39,16 +39,19 @@ reply; the only message type is Announcement.
|
||||
On multihomed hosts the announcement packets should be sent on each interface
|
||||
on which Syncthing will accept connections.
|
||||
.sp
|
||||
The announcement packet is sent over UDP.
|
||||
.sp
|
||||
For IPv4, the Announcement packet is broadcast either to the link\-specific
|
||||
broadcast address, or to the generic link\-local broadcast address
|
||||
\fB255.255.255.255\fP, with destination port 21027.
|
||||
.sp
|
||||
For IPv6, the Announcement packet is multicast to the transient link\-local
|
||||
multicast address \fB[ff12::8384]\fP, with destination port 21027.
|
||||
multicast address \fBff12::8384\fP, with destination port 21027.
|
||||
.sp
|
||||
It is recommended that local discovery Announcement packets be sent on a 30 to
|
||||
60 second interval, possibly with immediate transmissions when a previously
|
||||
unknown device is discovered.
|
||||
It is recommended that local discovery Announcement packets be sent on a 30
|
||||
to 60 second interval, possibly with immediate transmissions when a
|
||||
previously unknown device is discovered or a device has restarted (see the
|
||||
\fBinstance_id\fP field).
|
||||
.SH DEVICE ID
|
||||
.sp
|
||||
The device ID is the SHA\-256 (32 bytes) of the device X.509 certificate. See
|
||||
@@ -61,51 +64,13 @@ The Announcement packet has the following structure:
|
||||
.sp
|
||||
.nf
|
||||
.ft C
|
||||
Announce Structure:
|
||||
|
||||
0 1 2 3
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
| Magic |
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
/ /
|
||||
\e Device Structure \e
|
||||
/ /
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
| Number of Extra Devices |
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
/ /
|
||||
\e Zero or more Device Structures \e
|
||||
/ /
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
|
||||
Device Structure:
|
||||
|
||||
0 1 2 3
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
| Length of ID |
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
/ /
|
||||
\e ID (variable length) \e
|
||||
/ /
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
| Number of Addresses |
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
/ /
|
||||
\e Zero or more Address Structures \e
|
||||
/ /
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
|
||||
Address Structure:
|
||||
|
||||
0 1 2 3
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
| Length of URL |
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
/ /
|
||||
\e URL (variable length) \e
|
||||
\e Announce Message \e
|
||||
/ /
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
.ft P
|
||||
@@ -113,46 +78,43 @@ Address Structure:
|
||||
.UNINDENT
|
||||
.UNINDENT
|
||||
.sp
|
||||
The corresponding XDR representation is as follows (see
|
||||
\fI\%RFC4506\fP <\fBhttp://tools.ietf.org/html/rfc4506\fP> for the XDR format):
|
||||
There is no explicit length field as the length is given by the length of
|
||||
the discovery announcement packet itself.
|
||||
.sp
|
||||
The Magic field is a 32 bit word representing 0x2EA7D90B in network (big
|
||||
endian) byte order. It identifies the packet as being a Syncthing discovery
|
||||
protocol packet.
|
||||
.sp
|
||||
The Announce Message contents are in protocol buffer format using the
|
||||
following schema:
|
||||
.INDENT 0.0
|
||||
.INDENT 3.5
|
||||
.sp
|
||||
.nf
|
||||
.ft C
|
||||
struct Announcement {
|
||||
unsigned int Magic;
|
||||
Device This;
|
||||
Device Extra<>;
|
||||
}
|
||||
|
||||
struct Device {
|
||||
opaque ID<32>;
|
||||
Address Addresses<16>;
|
||||
}
|
||||
|
||||
struct Address {
|
||||
string URL<2083>;
|
||||
message Announce {
|
||||
bytes id = 1;
|
||||
repeated string addresses = 2;
|
||||
int64 instance_id = 3;
|
||||
}
|
||||
.ft P
|
||||
.fi
|
||||
.UNINDENT
|
||||
.UNINDENT
|
||||
.sp
|
||||
In the \fBAnnounce\fP structure field \fBMagic\fP is used to ensure
|
||||
a correct datagram was received and MUST be equal to \fB0x7D79BC40\fP\&.
|
||||
The \fBid\fP field contains the Device ID of the sending device.
|
||||
.sp
|
||||
The first Device structure contains information about the sending
|
||||
device. The following zero or more Extra devices contain information
|
||||
about other devices known to the sending device.
|
||||
The \fBaddresses\fP field contains a list of addresses where the device can be
|
||||
contacted. Direct connections will typically have the \fBtcp://\fP scheme.
|
||||
Relay connections will typically use the \fBrelay://\fP scheme.
|
||||
.sp
|
||||
In the \fBDevice\fP structure, field \fBDeviceID\fP is the SHA\-256 (32
|
||||
bytes) of the device X.509 certificate, as explained in section \fIDevice
|
||||
ID\fP\&.
|
||||
When interpreting addresses with an unspecified address, e.g.,
|
||||
\fBtcp://0.0.0.0:22000\fP or \fBtcp://:42424\fP, the source address of the
|
||||
discovery announcement is to be used.
|
||||
.sp
|
||||
For each \fBAddress\fP the \fBURL\fP field contains the actual target address.
|
||||
Direct connections will typically have the \fBtcp://\fP scheme. Relay connections
|
||||
will typically use the \fBrelay://\fP scheme.
|
||||
The \fBinstance_id\fP field is set to a randomly generated ID at client
|
||||
startup. Other devices on the network can detect a change in instance ID
|
||||
between two announces and conclude that the announcing device has restarted.
|
||||
.SH AUTHOR
|
||||
The Syncthing Authors
|
||||
.SH COPYRIGHT
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-NETWORKING" "7" "July 02, 2016" "v0.13" "Syncthing"
|
||||
.TH "SYNCTHING-NETWORKING" "7" "July 17, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-networking \- Firewall Setup
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-RELAY" "7" "July 02, 2016" "v0.13" "Syncthing"
|
||||
.TH "SYNCTHING-RELAY" "7" "July 17, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-relay \- Relay Protocol v1
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-REST-API" "7" "July 02, 2016" "v0.13" "Syncthing"
|
||||
.TH "SYNCTHING-REST-API" "7" "July 17, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-rest-api \- REST API
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-SECURITY" "7" "July 02, 2016" "v0.13" "Syncthing"
|
||||
.TH "SYNCTHING-SECURITY" "7" "July 17, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-security \- Security Principles
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-STIGNORE" "5" "July 02, 2016" "v0.13" "Syncthing"
|
||||
.TH "SYNCTHING-STIGNORE" "5" "July 17, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-stignore \- Prevent files from being synchronized to other nodes
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "TODO" "7" "July 02, 2016" "v0.13" "Syncthing"
|
||||
.TH "TODO" "7" "July 17, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
Todo \- Keep automatic backups of deleted files by other nodes
|
||||
.
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING" "1" "July 02, 2016" "v0.13" "Syncthing"
|
||||
.TH "SYNCTHING" "1" "July 17, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing \- Syncthing
|
||||
.
|
||||
|
||||
@@ -34,6 +34,7 @@ var excludeCommits = stringSetFromStrings([]string{
|
||||
"3626003f680bad3e63677982576d3a05421e88e9",
|
||||
"342036408e65bd25bb6afbcc705e2e2c013bb01f",
|
||||
"e37cefdbee1c1cd95ad095b5da6d1252723f103b",
|
||||
"bcc5d7c00f52552303b463d43a636f27b7f7e19b",
|
||||
})
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -29,6 +29,7 @@ var copyrightRegexps = []string{
|
||||
`Copyright`,
|
||||
`package auto`,
|
||||
`automatically generated by genxdr`,
|
||||
`generated by protoc`,
|
||||
}
|
||||
|
||||
var copyrightRe = regexp.MustCompile(strings.Join(copyrightRegexps, "|"))
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
// Copyright (C) 2016 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
// +build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -50,7 +50,8 @@ func benchmarkTransfer(t *testing.T, files, sizeExp int) {
|
||||
log.Println("Generating files...")
|
||||
if files == 1 {
|
||||
// Special case. Generate one file with the specified size exactly.
|
||||
fd, err := os.Open("../LICENSE")
|
||||
var fd *os.File
|
||||
fd, err = os.Open("../LICENSE")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
ISC License
|
||||
|
||||
Copyright (c) 2015, Gregory J. Oschwald <oschwald@gmail.com>
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
PERFORMANCE OF THIS SOFTWARE.
|
||||
+288
@@ -0,0 +1,288 @@
|
||||
// Package geoip2 provides a wrapper around the maxminddb package for
|
||||
// easy use with the MaxMind GeoIP2 and GeoLite2 databases. The records for
|
||||
// the IP address is returned from this package as well-formed structures
|
||||
// that match the internal layout of data from MaxMind.
|
||||
package geoip2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/oschwald/maxminddb-golang"
|
||||
)
|
||||
|
||||
// The City structure corresponds to the data in the GeoIP2/GeoLite2 City
|
||||
// databases.
|
||||
type City struct {
|
||||
City struct {
|
||||
GeoNameID uint `maxminddb:"geoname_id"`
|
||||
Names map[string]string `maxminddb:"names"`
|
||||
} `maxminddb:"city"`
|
||||
Continent struct {
|
||||
Code string `maxminddb:"code"`
|
||||
GeoNameID uint `maxminddb:"geoname_id"`
|
||||
Names map[string]string `maxminddb:"names"`
|
||||
} `maxminddb:"continent"`
|
||||
Country struct {
|
||||
GeoNameID uint `maxminddb:"geoname_id"`
|
||||
IsoCode string `maxminddb:"iso_code"`
|
||||
Names map[string]string `maxminddb:"names"`
|
||||
} `maxminddb:"country"`
|
||||
Location struct {
|
||||
AccuracyRadius uint16 `maxminddb:"accuracy_radius"`
|
||||
Latitude float64 `maxminddb:"latitude"`
|
||||
Longitude float64 `maxminddb:"longitude"`
|
||||
MetroCode uint `maxminddb:"metro_code"`
|
||||
TimeZone string `maxminddb:"time_zone"`
|
||||
} `maxminddb:"location"`
|
||||
Postal struct {
|
||||
Code string `maxminddb:"code"`
|
||||
} `maxminddb:"postal"`
|
||||
RegisteredCountry struct {
|
||||
GeoNameID uint `maxminddb:"geoname_id"`
|
||||
IsoCode string `maxminddb:"iso_code"`
|
||||
Names map[string]string `maxminddb:"names"`
|
||||
} `maxminddb:"registered_country"`
|
||||
RepresentedCountry struct {
|
||||
GeoNameID uint `maxminddb:"geoname_id"`
|
||||
IsoCode string `maxminddb:"iso_code"`
|
||||
Names map[string]string `maxminddb:"names"`
|
||||
Type string `maxminddb:"type"`
|
||||
} `maxminddb:"represented_country"`
|
||||
Subdivisions []struct {
|
||||
GeoNameID uint `maxminddb:"geoname_id"`
|
||||
IsoCode string `maxminddb:"iso_code"`
|
||||
Names map[string]string `maxminddb:"names"`
|
||||
} `maxminddb:"subdivisions"`
|
||||
Traits struct {
|
||||
IsAnonymousProxy bool `maxminddb:"is_anonymous_proxy"`
|
||||
IsSatelliteProvider bool `maxminddb:"is_satellite_provider"`
|
||||
} `maxminddb:"traits"`
|
||||
}
|
||||
|
||||
// The Country structure corresponds to the data in the GeoIP2/GeoLite2
|
||||
// Country databases.
|
||||
type Country struct {
|
||||
Continent struct {
|
||||
Code string `maxminddb:"code"`
|
||||
GeoNameID uint `maxminddb:"geoname_id"`
|
||||
Names map[string]string `maxminddb:"names"`
|
||||
} `maxminddb:"continent"`
|
||||
Country struct {
|
||||
GeoNameID uint `maxminddb:"geoname_id"`
|
||||
IsoCode string `maxminddb:"iso_code"`
|
||||
Names map[string]string `maxminddb:"names"`
|
||||
} `maxminddb:"country"`
|
||||
RegisteredCountry struct {
|
||||
GeoNameID uint `maxminddb:"geoname_id"`
|
||||
IsoCode string `maxminddb:"iso_code"`
|
||||
Names map[string]string `maxminddb:"names"`
|
||||
} `maxminddb:"registered_country"`
|
||||
RepresentedCountry struct {
|
||||
GeoNameID uint `maxminddb:"geoname_id"`
|
||||
IsoCode string `maxminddb:"iso_code"`
|
||||
Names map[string]string `maxminddb:"names"`
|
||||
Type string `maxminddb:"type"`
|
||||
} `maxminddb:"represented_country"`
|
||||
Traits struct {
|
||||
IsAnonymousProxy bool `maxminddb:"is_anonymous_proxy"`
|
||||
IsSatelliteProvider bool `maxminddb:"is_satellite_provider"`
|
||||
} `maxminddb:"traits"`
|
||||
}
|
||||
|
||||
// The AnonymousIP structure corresponds to the data in the GeoIP2
|
||||
// Anonymous IP database.
|
||||
type AnonymousIP struct {
|
||||
IsAnonymous bool `maxminddb:"is_anonymous"`
|
||||
IsAnonymousVPN bool `maxminddb:"is_anonymous_vpn"`
|
||||
IsHostingProvider bool `maxminddb:"is_hosting_provider"`
|
||||
IsPublicProxy bool `maxminddb:"is_public_proxy"`
|
||||
IsTorExitNode bool `maxminddb:"is_tor_exit_node"`
|
||||
}
|
||||
|
||||
// The ConnectionType structure corresponds to the data in the GeoIP2
|
||||
// Connection-Type database.
|
||||
type ConnectionType struct {
|
||||
ConnectionType string `maxminddb:"connection_type"`
|
||||
}
|
||||
|
||||
// The Domain structure corresponds to the data in the GeoIP2 Domain database.
|
||||
type Domain struct {
|
||||
Domain string `maxminddb:"domain"`
|
||||
}
|
||||
|
||||
// The ISP structure corresponds to the data in the GeoIP2 ISP database.
|
||||
type ISP struct {
|
||||
AutonomousSystemNumber uint `maxminddb:"autonomous_system_number"`
|
||||
AutonomousSystemOrganization string `maxminddb:"autonomous_system_organization"`
|
||||
ISP string `maxminddb:"isp"`
|
||||
Organization string `maxminddb:"organization"`
|
||||
}
|
||||
|
||||
type databaseType int
|
||||
|
||||
const (
|
||||
isAnonymousIP = 1 << iota
|
||||
isCity
|
||||
isConnectionType
|
||||
isCountry
|
||||
isDomain
|
||||
isEnterprise
|
||||
isISP
|
||||
)
|
||||
|
||||
// Reader holds the maxminddb.Reader structure. It should be created
|
||||
// using the Open function.
|
||||
type Reader struct {
|
||||
mmdbReader *maxminddb.Reader
|
||||
databaseType databaseType
|
||||
}
|
||||
|
||||
// InvalidMethodError is returned when a lookup method is called on a
|
||||
// database that it does not support. For instance, calling the ISP method
|
||||
// on a City database.
|
||||
type InvalidMethodError struct {
|
||||
Method string
|
||||
DatabaseType string
|
||||
}
|
||||
|
||||
func (e InvalidMethodError) Error() string {
|
||||
return fmt.Sprintf(`geoip2: the %s method does not support the %s database`,
|
||||
e.Method, e.DatabaseType)
|
||||
}
|
||||
|
||||
// UnknownDatabaseTypeError is returned when an unknown database type is
|
||||
// opened.
|
||||
type UnknownDatabaseTypeError struct {
|
||||
DatabaseType string
|
||||
}
|
||||
|
||||
func (e UnknownDatabaseTypeError) Error() string {
|
||||
return fmt.Sprintf(`geoip2: reader does not support the "%s" database type`,
|
||||
e.DatabaseType)
|
||||
}
|
||||
|
||||
// Open takes a string path to a file and returns a Reader structure or an
|
||||
// error. The database file is opened using a memory map. Use the Close method
|
||||
// on the Reader object to return the resources to the system.
|
||||
func Open(file string) (*Reader, error) {
|
||||
reader, err := maxminddb.Open(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dbType, err := getDBType(reader)
|
||||
return &Reader{reader, dbType}, err
|
||||
}
|
||||
|
||||
// FromBytes takes a byte slice corresponding to a GeoIP2/GeoLite2 database
|
||||
// file and returns a Reader structure or an error.
|
||||
func FromBytes(bytes []byte) (*Reader, error) {
|
||||
reader, err := maxminddb.FromBytes(bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dbType, err := getDBType(reader)
|
||||
return &Reader{reader, dbType}, err
|
||||
}
|
||||
|
||||
func getDBType(reader *maxminddb.Reader) (databaseType, error) {
|
||||
switch reader.Metadata.DatabaseType {
|
||||
case "GeoIP2-Anonymous-IP":
|
||||
return isAnonymousIP, nil
|
||||
// We allow City lookups on Country for back compat
|
||||
case "GeoLite2-City", "GeoIP2-City", "GeoIP2-Precision-City", "GeoLite2-Country",
|
||||
"GeoIP2-Country":
|
||||
return isCity | isCountry, nil
|
||||
case "GeoIP2-Connection-Type":
|
||||
return isConnectionType, nil
|
||||
case "GeoIP2-Domain":
|
||||
return isDomain, nil
|
||||
case "GeoIP2-Enterprise":
|
||||
return isEnterprise | isCity | isCountry, nil
|
||||
case "GeoIP2-ISP", "GeoIP2-Precision-ISP":
|
||||
return isISP, nil
|
||||
default:
|
||||
return 0, UnknownDatabaseTypeError{reader.Metadata.DatabaseType}
|
||||
}
|
||||
}
|
||||
|
||||
// City takes an IP address as a net.IP struct and returns a City struct
|
||||
// and/or an error. Although this can be used with other databases, this
|
||||
// method generally should be used with the GeoIP2 or GeoLite2 City databases.
|
||||
func (r *Reader) City(ipAddress net.IP) (*City, error) {
|
||||
if isCity&r.databaseType == 0 {
|
||||
return nil, InvalidMethodError{"City", r.Metadata().DatabaseType}
|
||||
}
|
||||
var city City
|
||||
err := r.mmdbReader.Lookup(ipAddress, &city)
|
||||
return &city, err
|
||||
}
|
||||
|
||||
// Country takes an IP address as a net.IP struct and returns a Country struct
|
||||
// and/or an error. Although this can be used with other databases, this
|
||||
// method generally should be used with the GeoIP2 or GeoLite2 Country
|
||||
// databases.
|
||||
func (r *Reader) Country(ipAddress net.IP) (*Country, error) {
|
||||
if isCountry&r.databaseType == 0 {
|
||||
return nil, InvalidMethodError{"Country", r.Metadata().DatabaseType}
|
||||
}
|
||||
var country Country
|
||||
err := r.mmdbReader.Lookup(ipAddress, &country)
|
||||
return &country, err
|
||||
}
|
||||
|
||||
// AnonymousIP takes an IP address as a net.IP struct and returns a
|
||||
// AnonymousIP struct and/or an error.
|
||||
func (r *Reader) AnonymousIP(ipAddress net.IP) (*AnonymousIP, error) {
|
||||
if isAnonymousIP&r.databaseType == 0 {
|
||||
return nil, InvalidMethodError{"AnonymousIP", r.Metadata().DatabaseType}
|
||||
}
|
||||
var anonIP AnonymousIP
|
||||
err := r.mmdbReader.Lookup(ipAddress, &anonIP)
|
||||
return &anonIP, err
|
||||
}
|
||||
|
||||
// ConnectionType takes an IP address as a net.IP struct and returns a
|
||||
// ConnectionType struct and/or an error
|
||||
func (r *Reader) ConnectionType(ipAddress net.IP) (*ConnectionType, error) {
|
||||
if isConnectionType&r.databaseType == 0 {
|
||||
return nil, InvalidMethodError{"ConnectionType", r.Metadata().DatabaseType}
|
||||
}
|
||||
var val ConnectionType
|
||||
err := r.mmdbReader.Lookup(ipAddress, &val)
|
||||
return &val, err
|
||||
}
|
||||
|
||||
// Domain takes an IP address as a net.IP struct and returns a
|
||||
// Domain struct and/or an error
|
||||
func (r *Reader) Domain(ipAddress net.IP) (*Domain, error) {
|
||||
if isDomain&r.databaseType == 0 {
|
||||
return nil, InvalidMethodError{"Domain", r.Metadata().DatabaseType}
|
||||
}
|
||||
var val Domain
|
||||
err := r.mmdbReader.Lookup(ipAddress, &val)
|
||||
return &val, err
|
||||
}
|
||||
|
||||
// ISP takes an IP address as a net.IP struct and returns a ISP struct and/or
|
||||
// an error
|
||||
func (r *Reader) ISP(ipAddress net.IP) (*ISP, error) {
|
||||
if isISP&r.databaseType == 0 {
|
||||
return nil, InvalidMethodError{"ISP", r.Metadata().DatabaseType}
|
||||
}
|
||||
var val ISP
|
||||
err := r.mmdbReader.Lookup(ipAddress, &val)
|
||||
return &val, err
|
||||
}
|
||||
|
||||
// Metadata takes no arguments and returns a struct containing metadata about
|
||||
// the MaxMind database in use by the Reader.
|
||||
func (r *Reader) Metadata() maxminddb.Metadata {
|
||||
return r.mmdbReader.Metadata
|
||||
}
|
||||
|
||||
// Close unmaps the database file from virtual memory and returns the
|
||||
// resources to the system.
|
||||
func (r *Reader) Close() error {
|
||||
return r.mmdbReader.Close()
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
Copyright (c) 2015, Gregory J. Oschwald <oschwald@gmail.com>
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
PERFORMANCE OF THIS SOFTWARE.
|
||||
+559
@@ -0,0 +1,559 @@
|
||||
package maxminddb
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"math"
|
||||
"math/big"
|
||||
"reflect"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type decoder struct {
|
||||
buffer []byte
|
||||
}
|
||||
|
||||
type dataType int
|
||||
|
||||
const (
|
||||
_Extended dataType = iota
|
||||
_Pointer
|
||||
_String
|
||||
_Float64
|
||||
_Bytes
|
||||
_Uint16
|
||||
_Uint32
|
||||
_Map
|
||||
_Int32
|
||||
_Uint64
|
||||
_Uint128
|
||||
_Slice
|
||||
_Container
|
||||
_Marker
|
||||
_Bool
|
||||
_Float32
|
||||
)
|
||||
|
||||
func (d *decoder) decode(offset uint, result reflect.Value) (uint, error) {
|
||||
typeNum, size, newOffset := d.decodeCtrlData(offset)
|
||||
|
||||
if typeNum != _Pointer && result.Kind() == reflect.Uintptr {
|
||||
result.Set(reflect.ValueOf(uintptr(offset)))
|
||||
return d.nextValueOffset(offset, 1), nil
|
||||
}
|
||||
return d.decodeFromType(typeNum, size, newOffset, result)
|
||||
}
|
||||
|
||||
func (d *decoder) decodeCtrlData(offset uint) (dataType, uint, uint) {
|
||||
newOffset := offset + 1
|
||||
ctrlByte := d.buffer[offset]
|
||||
|
||||
typeNum := dataType(ctrlByte >> 5)
|
||||
if typeNum == _Extended {
|
||||
typeNum = dataType(d.buffer[newOffset] + 7)
|
||||
newOffset++
|
||||
}
|
||||
|
||||
var size uint
|
||||
size, newOffset = d.sizeFromCtrlByte(ctrlByte, newOffset, typeNum)
|
||||
return typeNum, size, newOffset
|
||||
}
|
||||
|
||||
func (d *decoder) sizeFromCtrlByte(ctrlByte byte, offset uint, typeNum dataType) (uint, uint) {
|
||||
size := uint(ctrlByte & 0x1f)
|
||||
if typeNum == _Extended {
|
||||
return size, offset
|
||||
}
|
||||
|
||||
var bytesToRead uint
|
||||
if size > 28 {
|
||||
bytesToRead = size - 28
|
||||
}
|
||||
|
||||
newOffset := offset + bytesToRead
|
||||
sizeBytes := d.buffer[offset:newOffset]
|
||||
|
||||
switch {
|
||||
case size == 29:
|
||||
size = 29 + uint(sizeBytes[0])
|
||||
case size == 30:
|
||||
size = 285 + uint(uintFromBytes(0, sizeBytes))
|
||||
case size > 30:
|
||||
size = uint(uintFromBytes(0, sizeBytes)) + 65821
|
||||
}
|
||||
return size, newOffset
|
||||
}
|
||||
|
||||
func (d *decoder) decodeFromType(dtype dataType, size uint, offset uint, result reflect.Value) (uint, error) {
|
||||
for result.Kind() == reflect.Ptr {
|
||||
if result.IsNil() {
|
||||
result.Set(reflect.New(result.Type().Elem()))
|
||||
}
|
||||
result = result.Elem()
|
||||
}
|
||||
|
||||
switch dtype {
|
||||
case _Bool:
|
||||
return d.unmarshalBool(size, offset, result)
|
||||
case _Bytes:
|
||||
return d.unmarshalBytes(size, offset, result)
|
||||
case _Float32:
|
||||
return d.unmarshalFloat32(size, offset, result)
|
||||
case _Float64:
|
||||
return d.unmarshalFloat64(size, offset, result)
|
||||
case _Int32:
|
||||
return d.unmarshalInt32(size, offset, result)
|
||||
case _Map:
|
||||
return d.unmarshalMap(size, offset, result)
|
||||
case _Pointer:
|
||||
return d.unmarshalPointer(size, offset, result)
|
||||
case _Slice:
|
||||
return d.unmarshalSlice(size, offset, result)
|
||||
case _String:
|
||||
return d.unmarshalString(size, offset, result)
|
||||
case _Uint16:
|
||||
return d.unmarshalUint(size, offset, result, 16)
|
||||
case _Uint32:
|
||||
return d.unmarshalUint(size, offset, result, 32)
|
||||
case _Uint64:
|
||||
return d.unmarshalUint(size, offset, result, 64)
|
||||
case _Uint128:
|
||||
return d.unmarshalUint128(size, offset, result)
|
||||
default:
|
||||
return 0, newInvalidDatabaseError("unknown type: %d", dtype)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *decoder) unmarshalBool(size uint, offset uint, result reflect.Value) (uint, error) {
|
||||
if size > 1 {
|
||||
return 0, newInvalidDatabaseError("the MaxMind DB file's data section contains bad data (bool size of %v)", size)
|
||||
}
|
||||
value, newOffset, err := d.decodeBool(size, offset)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
switch result.Kind() {
|
||||
default:
|
||||
return newOffset, newUnmarshalTypeError(value, result.Type())
|
||||
case reflect.Bool:
|
||||
result.SetBool(value)
|
||||
return newOffset, nil
|
||||
case reflect.Interface:
|
||||
result.Set(reflect.ValueOf(value))
|
||||
return newOffset, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d *decoder) unmarshalBytes(size uint, offset uint, result reflect.Value) (uint, error) {
|
||||
value, newOffset, err := d.decodeBytes(size, offset)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
switch result.Kind() {
|
||||
default:
|
||||
return newOffset, newUnmarshalTypeError(value, result.Type())
|
||||
case reflect.Slice:
|
||||
result.SetBytes(value)
|
||||
return newOffset, nil
|
||||
case reflect.Interface:
|
||||
result.Set(reflect.ValueOf(value))
|
||||
return newOffset, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d *decoder) unmarshalFloat32(size uint, offset uint, result reflect.Value) (uint, error) {
|
||||
if size != 4 {
|
||||
return 0, newInvalidDatabaseError("the MaxMind DB file's data section contains bad data (float32 size of %v)", size)
|
||||
}
|
||||
value, newOffset, err := d.decodeFloat32(size, offset)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
switch result.Kind() {
|
||||
default:
|
||||
return newOffset, newUnmarshalTypeError(value, result.Type())
|
||||
case reflect.Float32, reflect.Float64:
|
||||
result.SetFloat(float64(value))
|
||||
return newOffset, nil
|
||||
case reflect.Interface:
|
||||
result.Set(reflect.ValueOf(value))
|
||||
return newOffset, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d *decoder) unmarshalFloat64(size uint, offset uint, result reflect.Value) (uint, error) {
|
||||
|
||||
if size != 8 {
|
||||
return 0, newInvalidDatabaseError("the MaxMind DB file's data section contains bad data (float 64 size of %v)", size)
|
||||
}
|
||||
value, newOffset, err := d.decodeFloat64(size, offset)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
switch result.Kind() {
|
||||
default:
|
||||
return newOffset, newUnmarshalTypeError(value, result.Type())
|
||||
case reflect.Float32, reflect.Float64:
|
||||
if result.OverflowFloat(value) {
|
||||
return 0, newUnmarshalTypeError(value, result.Type())
|
||||
}
|
||||
result.SetFloat(value)
|
||||
return newOffset, nil
|
||||
case reflect.Interface:
|
||||
result.Set(reflect.ValueOf(value))
|
||||
return newOffset, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d *decoder) unmarshalInt32(size uint, offset uint, result reflect.Value) (uint, error) {
|
||||
if size > 4 {
|
||||
return 0, newInvalidDatabaseError("the MaxMind DB file's data section contains bad data (int32 size of %v)", size)
|
||||
}
|
||||
value, newOffset, err := d.decodeInt(size, offset)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
switch result.Kind() {
|
||||
default:
|
||||
return newOffset, newUnmarshalTypeError(value, result.Type())
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
n := int64(value)
|
||||
if result.OverflowInt(n) {
|
||||
return 0, newUnmarshalTypeError(value, result.Type())
|
||||
}
|
||||
result.SetInt(n)
|
||||
return newOffset, nil
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
n := uint64(value)
|
||||
if result.OverflowUint(n) {
|
||||
return 0, newUnmarshalTypeError(value, result.Type())
|
||||
}
|
||||
result.SetUint(n)
|
||||
return newOffset, nil
|
||||
case reflect.Interface:
|
||||
result.Set(reflect.ValueOf(value))
|
||||
return newOffset, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d *decoder) unmarshalMap(size uint, offset uint, result reflect.Value) (uint, error) {
|
||||
switch result.Kind() {
|
||||
default:
|
||||
return 0, newUnmarshalTypeError("map", result.Type())
|
||||
case reflect.Struct:
|
||||
return d.decodeStruct(size, offset, result)
|
||||
case reflect.Map:
|
||||
return d.decodeMap(size, offset, result)
|
||||
case reflect.Interface:
|
||||
rv := reflect.ValueOf(make(map[string]interface{}, size))
|
||||
newOffset, err := d.decodeMap(size, offset, rv)
|
||||
result.Set(rv)
|
||||
return newOffset, err
|
||||
}
|
||||
}
|
||||
|
||||
func (d *decoder) unmarshalPointer(size uint, offset uint, result reflect.Value) (uint, error) {
|
||||
pointer, newOffset := d.decodePointer(size, offset)
|
||||
_, err := d.decode(pointer, result)
|
||||
return newOffset, err
|
||||
}
|
||||
|
||||
func (d *decoder) unmarshalSlice(size uint, offset uint, result reflect.Value) (uint, error) {
|
||||
|
||||
switch result.Kind() {
|
||||
default:
|
||||
return 0, newUnmarshalTypeError("array", result.Type())
|
||||
case reflect.Slice:
|
||||
return d.decodeSlice(size, offset, result)
|
||||
case reflect.Interface:
|
||||
a := []interface{}{}
|
||||
rv := reflect.ValueOf(&a).Elem()
|
||||
newOffset, err := d.decodeSlice(size, offset, rv)
|
||||
result.Set(rv)
|
||||
return newOffset, err
|
||||
}
|
||||
}
|
||||
|
||||
func (d *decoder) unmarshalString(size uint, offset uint, result reflect.Value) (uint, error) {
|
||||
value, newOffset, err := d.decodeString(size, offset)
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
switch result.Kind() {
|
||||
default:
|
||||
return newOffset, newUnmarshalTypeError(value, result.Type())
|
||||
case reflect.String:
|
||||
result.SetString(value)
|
||||
return newOffset, nil
|
||||
case reflect.Interface:
|
||||
result.Set(reflect.ValueOf(value))
|
||||
return newOffset, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d *decoder) unmarshalUint(size uint, offset uint, result reflect.Value, uintType uint) (uint, error) {
|
||||
if size > uintType/8 {
|
||||
return 0, newInvalidDatabaseError("the MaxMind DB file's data section contains bad data (uint%v size of %v)", uintType, size)
|
||||
}
|
||||
|
||||
value, newOffset, err := d.decodeUint(size, offset)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
switch result.Kind() {
|
||||
default:
|
||||
return newOffset, newUnmarshalTypeError(value, result.Type())
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
n := int64(value)
|
||||
if result.OverflowInt(n) {
|
||||
return 0, newUnmarshalTypeError(value, result.Type())
|
||||
}
|
||||
result.SetInt(n)
|
||||
return newOffset, nil
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
if result.OverflowUint(value) {
|
||||
return 0, newUnmarshalTypeError(value, result.Type())
|
||||
}
|
||||
result.SetUint(value)
|
||||
return newOffset, nil
|
||||
case reflect.Interface:
|
||||
result.Set(reflect.ValueOf(value))
|
||||
return newOffset, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d *decoder) unmarshalUint128(size uint, offset uint, result reflect.Value) (uint, error) {
|
||||
if size > 16 {
|
||||
return 0, newInvalidDatabaseError("the MaxMind DB file's data section contains bad data (uint128 size of %v)", size)
|
||||
}
|
||||
value, newOffset, err := d.decodeUint128(size, offset)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// XXX - this should allow *big.Int rather than just bigInt
|
||||
// Currently this is reported as invalid
|
||||
switch result.Kind() {
|
||||
default:
|
||||
return newOffset, newUnmarshalTypeError(value, result.Type())
|
||||
case reflect.Struct:
|
||||
result.Set(reflect.ValueOf(*value))
|
||||
return newOffset, nil
|
||||
case reflect.Interface, reflect.Ptr:
|
||||
result.Set(reflect.ValueOf(value))
|
||||
return newOffset, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d *decoder) decodeBool(size uint, offset uint) (bool, uint, error) {
|
||||
return size != 0, offset, nil
|
||||
}
|
||||
|
||||
func (d *decoder) decodeBytes(size uint, offset uint) ([]byte, uint, error) {
|
||||
newOffset := offset + size
|
||||
bytes := make([]byte, size)
|
||||
copy(bytes, d.buffer[offset:newOffset])
|
||||
return bytes, newOffset, nil
|
||||
}
|
||||
|
||||
func (d *decoder) decodeFloat64(size uint, offset uint) (float64, uint, error) {
|
||||
newOffset := offset + size
|
||||
bits := binary.BigEndian.Uint64(d.buffer[offset:newOffset])
|
||||
return math.Float64frombits(bits), newOffset, nil
|
||||
}
|
||||
|
||||
func (d *decoder) decodeFloat32(size uint, offset uint) (float32, uint, error) {
|
||||
newOffset := offset + size
|
||||
bits := binary.BigEndian.Uint32(d.buffer[offset:newOffset])
|
||||
return math.Float32frombits(bits), newOffset, nil
|
||||
}
|
||||
|
||||
func (d *decoder) decodeInt(size uint, offset uint) (int, uint, error) {
|
||||
newOffset := offset + size
|
||||
var val int32
|
||||
for _, b := range d.buffer[offset:newOffset] {
|
||||
val = (val << 8) | int32(b)
|
||||
}
|
||||
return int(val), newOffset, nil
|
||||
}
|
||||
|
||||
func (d *decoder) decodeMap(size uint, offset uint, result reflect.Value) (uint, error) {
|
||||
if result.IsNil() {
|
||||
result.Set(reflect.MakeMap(result.Type()))
|
||||
}
|
||||
|
||||
for i := uint(0); i < size; i++ {
|
||||
var key string
|
||||
var err error
|
||||
key, offset, err = d.decodeKeyString(offset)
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
value := reflect.New(result.Type().Elem())
|
||||
offset, err = d.decode(offset, value)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
result.SetMapIndex(reflect.ValueOf(key), value.Elem())
|
||||
}
|
||||
return offset, nil
|
||||
}
|
||||
|
||||
func (d *decoder) decodePointer(size uint, offset uint) (uint, uint) {
|
||||
pointerSize := ((size >> 3) & 0x3) + 1
|
||||
newOffset := offset + pointerSize
|
||||
pointerBytes := d.buffer[offset:newOffset]
|
||||
var prefix uint64
|
||||
if pointerSize == 4 {
|
||||
prefix = 0
|
||||
} else {
|
||||
prefix = uint64(size & 0x7)
|
||||
}
|
||||
unpacked := uint(uintFromBytes(prefix, pointerBytes))
|
||||
|
||||
var pointerValueOffset uint
|
||||
switch pointerSize {
|
||||
case 1:
|
||||
pointerValueOffset = 0
|
||||
case 2:
|
||||
pointerValueOffset = 2048
|
||||
case 3:
|
||||
pointerValueOffset = 526336
|
||||
case 4:
|
||||
pointerValueOffset = 0
|
||||
}
|
||||
|
||||
pointer := unpacked + pointerValueOffset
|
||||
|
||||
return pointer, newOffset
|
||||
}
|
||||
|
||||
func (d *decoder) decodeSlice(size uint, offset uint, result reflect.Value) (uint, error) {
|
||||
result.Set(reflect.MakeSlice(result.Type(), int(size), int(size)))
|
||||
for i := 0; i < int(size); i++ {
|
||||
var err error
|
||||
offset, err = d.decode(offset, result.Index(i))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
return offset, nil
|
||||
}
|
||||
|
||||
func (d *decoder) decodeString(size uint, offset uint) (string, uint, error) {
|
||||
newOffset := offset + size
|
||||
return string(d.buffer[offset:newOffset]), newOffset, nil
|
||||
}
|
||||
|
||||
var (
|
||||
fieldMap = map[reflect.Type]map[string]int{}
|
||||
fieldMapMu sync.RWMutex
|
||||
)
|
||||
|
||||
func (d *decoder) decodeStruct(size uint, offset uint, result reflect.Value) (uint, error) {
|
||||
resultType := result.Type()
|
||||
|
||||
fieldMapMu.RLock()
|
||||
fields, ok := fieldMap[resultType]
|
||||
fieldMapMu.RUnlock()
|
||||
if !ok {
|
||||
numFields := resultType.NumField()
|
||||
fields = make(map[string]int, numFields)
|
||||
for i := 0; i < numFields; i++ {
|
||||
fieldType := resultType.Field(i)
|
||||
|
||||
fieldName := fieldType.Name
|
||||
if tag := fieldType.Tag.Get("maxminddb"); tag != "" {
|
||||
fieldName = tag
|
||||
}
|
||||
fields[fieldName] = i
|
||||
}
|
||||
fieldMapMu.Lock()
|
||||
fieldMap[resultType] = fields
|
||||
fieldMapMu.Unlock()
|
||||
}
|
||||
|
||||
for i := uint(0); i < size; i++ {
|
||||
var (
|
||||
err error
|
||||
key string
|
||||
)
|
||||
key, offset, err = d.decodeStructKey(offset)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
i, ok := fields[key]
|
||||
if !ok {
|
||||
offset = d.nextValueOffset(offset, 1)
|
||||
continue
|
||||
}
|
||||
offset, err = d.decode(offset, result.Field(i))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
return offset, nil
|
||||
}
|
||||
|
||||
func (d *decoder) decodeUint(size uint, offset uint) (uint64, uint, error) {
|
||||
newOffset := offset + size
|
||||
val := uintFromBytes(0, d.buffer[offset:newOffset])
|
||||
|
||||
return val, newOffset, nil
|
||||
}
|
||||
|
||||
func (d *decoder) decodeUint128(size uint, offset uint) (*big.Int, uint, error) {
|
||||
newOffset := offset + size
|
||||
val := new(big.Int)
|
||||
val.SetBytes(d.buffer[offset:newOffset])
|
||||
|
||||
return val, newOffset, nil
|
||||
}
|
||||
|
||||
func uintFromBytes(prefix uint64, uintBytes []byte) uint64 {
|
||||
val := prefix
|
||||
for _, b := range uintBytes {
|
||||
val = (val << 8) | uint64(b)
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func (d *decoder) decodeKeyString(offset uint) (string, uint, error) {
|
||||
typeNum, size, newOffset := d.decodeCtrlData(offset)
|
||||
if typeNum == _Pointer {
|
||||
pointer, ptrOffset := d.decodePointer(size, newOffset)
|
||||
key, _, err := d.decodeKeyString(pointer)
|
||||
return key, ptrOffset, err
|
||||
}
|
||||
if typeNum != _String {
|
||||
return "", 0, newInvalidDatabaseError("unexpected type when decoding string: %v", typeNum)
|
||||
}
|
||||
return d.decodeString(size, newOffset)
|
||||
}
|
||||
|
||||
// This function is used to skip ahead to the next value without decoding
|
||||
// the one at the offset passed in. The size bits have different meanings for
|
||||
// different data types
|
||||
func (d *decoder) nextValueOffset(offset uint, numberToSkip uint) uint {
|
||||
if numberToSkip == 0 {
|
||||
return offset
|
||||
}
|
||||
typeNum, size, offset := d.decodeCtrlData(offset)
|
||||
switch typeNum {
|
||||
case _Pointer:
|
||||
_, offset = d.decodePointer(size, offset)
|
||||
case _Map:
|
||||
numberToSkip += 2 * size
|
||||
case _Slice:
|
||||
numberToSkip += size
|
||||
case _Bool:
|
||||
default:
|
||||
offset += size
|
||||
}
|
||||
return d.nextValueOffset(offset, numberToSkip-1)
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package maxminddb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// InvalidDatabaseError is returned when the database contains invalid data
|
||||
// and cannot be parsed.
|
||||
type InvalidDatabaseError struct {
|
||||
message string
|
||||
}
|
||||
|
||||
func newInvalidDatabaseError(format string, args ...interface{}) InvalidDatabaseError {
|
||||
return InvalidDatabaseError{fmt.Sprintf(format, args...)}
|
||||
}
|
||||
|
||||
func (e InvalidDatabaseError) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
// UnmarshalTypeError is returned when the value in the database cannot be
|
||||
// assigned to the specified data type.
|
||||
type UnmarshalTypeError struct {
|
||||
Value string // stringified copy of the database value that caused the error
|
||||
Type reflect.Type // type of the value that could not be assign to
|
||||
}
|
||||
|
||||
func newUnmarshalTypeError(value interface{}, rType reflect.Type) UnmarshalTypeError {
|
||||
return UnmarshalTypeError{
|
||||
Value: fmt.Sprintf("%v", value),
|
||||
Type: rType,
|
||||
}
|
||||
}
|
||||
|
||||
func (e UnmarshalTypeError) Error() string {
|
||||
return fmt.Sprintf("maxminddb: cannot unmarshal %s into type %s", e.Value, e.Type.String())
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// +build appengine
|
||||
|
||||
package maxminddb
|
||||
|
||||
func (d *decoder) decodeStructKey(offset uint) (string, uint, error) {
|
||||
return d.decodeKeyString(offset)
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
// +build !appengine
|
||||
|
||||
package maxminddb
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// decodeStructKey returns a string which points into the database. Don't keep
|
||||
// it around.
|
||||
func (d *decoder) decodeStructKey(offset uint) (string, uint, error) {
|
||||
typeNum, size, newOffset := d.decodeCtrlData(offset)
|
||||
switch typeNum {
|
||||
case _Pointer:
|
||||
pointer, ptrOffset := d.decodePointer(size, newOffset)
|
||||
s, _, err := d.decodeStructKey(pointer)
|
||||
return s, ptrOffset, err
|
||||
case _String:
|
||||
var s string
|
||||
val := (*reflect.StringHeader)(unsafe.Pointer(&s))
|
||||
val.Data = uintptr(unsafe.Pointer(&d.buffer[newOffset]))
|
||||
val.Len = int(size)
|
||||
return s, newOffset + size, nil
|
||||
default:
|
||||
return "", 0, newInvalidDatabaseError("unexpected type when decoding struct key: %v", typeNum)
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
// +build !windows,!appengine
|
||||
|
||||
package maxminddb
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func mmap(fd int, length int) (data []byte, err error) {
|
||||
return unix.Mmap(fd, 0, length, syscall.PROT_READ, syscall.MAP_SHARED)
|
||||
}
|
||||
|
||||
func munmap(b []byte) (err error) {
|
||||
return unix.Munmap(b)
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
package maxminddb
|
||||
|
||||
// Windows support largely borrowed from mmap-go.
|
||||
//
|
||||
// Copyright 2011 Evan Shaw. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"reflect"
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
type memoryMap []byte
|
||||
|
||||
// Windows
|
||||
var handleLock sync.Mutex
|
||||
var handleMap = map[uintptr]windows.Handle{}
|
||||
|
||||
func mmap(fd int, length int) (data []byte, err error) {
|
||||
h, errno := windows.CreateFileMapping(windows.Handle(fd), nil,
|
||||
uint32(windows.PAGE_READONLY), 0, uint32(length), nil)
|
||||
if h == 0 {
|
||||
return nil, os.NewSyscallError("CreateFileMapping", errno)
|
||||
}
|
||||
|
||||
addr, errno := windows.MapViewOfFile(h, uint32(windows.FILE_MAP_READ), 0,
|
||||
0, uintptr(length))
|
||||
if addr == 0 {
|
||||
return nil, os.NewSyscallError("MapViewOfFile", errno)
|
||||
}
|
||||
handleLock.Lock()
|
||||
handleMap[addr] = h
|
||||
handleLock.Unlock()
|
||||
|
||||
m := memoryMap{}
|
||||
dh := m.header()
|
||||
dh.Data = addr
|
||||
dh.Len = length
|
||||
dh.Cap = dh.Len
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *memoryMap) header() *reflect.SliceHeader {
|
||||
return (*reflect.SliceHeader)(unsafe.Pointer(m))
|
||||
}
|
||||
|
||||
func flush(addr, len uintptr) error {
|
||||
errno := windows.FlushViewOfFile(addr, len)
|
||||
return os.NewSyscallError("FlushViewOfFile", errno)
|
||||
}
|
||||
|
||||
func munmap(b []byte) (err error) {
|
||||
m := memoryMap(b)
|
||||
dh := m.header()
|
||||
|
||||
addr := dh.Data
|
||||
length := uintptr(dh.Len)
|
||||
|
||||
flush(addr, length)
|
||||
err = windows.UnmapViewOfFile(addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
handleLock.Lock()
|
||||
defer handleLock.Unlock()
|
||||
handle, ok := handleMap[addr]
|
||||
if !ok {
|
||||
// should be impossible; we would've errored above
|
||||
return errors.New("unknown base address")
|
||||
}
|
||||
delete(handleMap, addr)
|
||||
|
||||
e := windows.CloseHandle(windows.Handle(handle))
|
||||
return os.NewSyscallError("CloseHandle", e)
|
||||
}
|
||||
+246
@@ -0,0 +1,246 @@
|
||||
package maxminddb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
const (
|
||||
// NotFound is returned by LookupOffset when a matched root record offset
|
||||
// cannot be found.
|
||||
NotFound = ^uintptr(0)
|
||||
|
||||
dataSectionSeparatorSize = 16
|
||||
)
|
||||
|
||||
var metadataStartMarker = []byte("\xAB\xCD\xEFMaxMind.com")
|
||||
|
||||
// Reader holds the data corresponding to the MaxMind DB file. Its only public
|
||||
// field is Metadata, which contains the metadata from the MaxMind DB file.
|
||||
type Reader struct {
|
||||
hasMappedFile bool
|
||||
buffer []byte
|
||||
decoder decoder
|
||||
Metadata Metadata
|
||||
ipv4Start uint
|
||||
}
|
||||
|
||||
// Metadata holds the metadata decoded from the MaxMind DB file. In particular
|
||||
// in has the format version, the build time as Unix epoch time, the database
|
||||
// type and description, the IP version supported, and a slice of the natural
|
||||
// languages included.
|
||||
type Metadata struct {
|
||||
BinaryFormatMajorVersion uint `maxminddb:"binary_format_major_version"`
|
||||
BinaryFormatMinorVersion uint `maxminddb:"binary_format_minor_version"`
|
||||
BuildEpoch uint `maxminddb:"build_epoch"`
|
||||
DatabaseType string `maxminddb:"database_type"`
|
||||
Description map[string]string `maxminddb:"description"`
|
||||
IPVersion uint `maxminddb:"ip_version"`
|
||||
Languages []string `maxminddb:"languages"`
|
||||
NodeCount uint `maxminddb:"node_count"`
|
||||
RecordSize uint `maxminddb:"record_size"`
|
||||
}
|
||||
|
||||
// FromBytes takes a byte slice corresponding to a MaxMind DB file and returns
|
||||
// a Reader structure or an error.
|
||||
func FromBytes(buffer []byte) (*Reader, error) {
|
||||
metadataStart := bytes.LastIndex(buffer, metadataStartMarker)
|
||||
|
||||
if metadataStart == -1 {
|
||||
return nil, newInvalidDatabaseError("error opening database: invalid MaxMind DB file")
|
||||
}
|
||||
|
||||
metadataStart += len(metadataStartMarker)
|
||||
metadataDecoder := decoder{buffer[metadataStart:]}
|
||||
|
||||
var metadata Metadata
|
||||
|
||||
rvMetdata := reflect.ValueOf(&metadata)
|
||||
_, err := metadataDecoder.decode(0, rvMetdata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
searchTreeSize := metadata.NodeCount * metadata.RecordSize / 4
|
||||
dataSectionStart := searchTreeSize + dataSectionSeparatorSize
|
||||
dataSectionEnd := uint(metadataStart - len(metadataStartMarker))
|
||||
if dataSectionStart > dataSectionEnd {
|
||||
return nil, newInvalidDatabaseError("the MaxMind DB contains invalid metadata")
|
||||
}
|
||||
d := decoder{
|
||||
buffer[searchTreeSize+dataSectionSeparatorSize : metadataStart-len(metadataStartMarker)],
|
||||
}
|
||||
|
||||
reader := &Reader{
|
||||
buffer: buffer,
|
||||
decoder: d,
|
||||
Metadata: metadata,
|
||||
ipv4Start: 0,
|
||||
}
|
||||
|
||||
reader.ipv4Start, err = reader.startNode()
|
||||
|
||||
return reader, err
|
||||
}
|
||||
|
||||
func (r *Reader) startNode() (uint, error) {
|
||||
if r.Metadata.IPVersion != 6 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
nodeCount := r.Metadata.NodeCount
|
||||
|
||||
node := uint(0)
|
||||
var err error
|
||||
for i := 0; i < 96 && node < nodeCount; i++ {
|
||||
node, err = r.readNode(node, 0)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
return node, err
|
||||
}
|
||||
|
||||
// Lookup takes an IP address as a net.IP structure and a pointer to the
|
||||
// result value to Decode into.
|
||||
func (r *Reader) Lookup(ipAddress net.IP, result interface{}) error {
|
||||
pointer, err := r.lookupPointer(ipAddress)
|
||||
if pointer == 0 || err != nil {
|
||||
return err
|
||||
}
|
||||
return r.retrieveData(pointer, result)
|
||||
}
|
||||
|
||||
// LookupOffset maps an argument net.IP to a corresponding record offset in the
|
||||
// database. NotFound is returned if no such record is found, and a record may
|
||||
// otherwise be extracted by passing the returned offset to Decode. LookupOffset
|
||||
// is an advanced API, which exists to provide clients with a means to cache
|
||||
// previously-decoded records.
|
||||
func (r *Reader) LookupOffset(ipAddress net.IP) (uintptr, error) {
|
||||
pointer, err := r.lookupPointer(ipAddress)
|
||||
if pointer == 0 || err != nil {
|
||||
return NotFound, err
|
||||
}
|
||||
return r.resolveDataPointer(pointer)
|
||||
}
|
||||
|
||||
// Decode the record at |offset| into |result|. The result value pointed to
|
||||
// must be a data value that corresponds to a record in the database. This may
|
||||
// include a struct representation of the data, a map capable of holding the
|
||||
// data or an empty interface{} value.
|
||||
//
|
||||
// If result is a pointer to a struct, the struct need not include a field
|
||||
// for every value that may be in the database. If a field is not present in
|
||||
// the structure, the decoder will not decode that field, reducing the time
|
||||
// required to decode the record.
|
||||
//
|
||||
// As a special case, a struct field of type uintptr will be used to capture
|
||||
// the offset of the value. Decode may later be used to extract the stored
|
||||
// value from the offset. MaxMind DBs are highly normalized: for example in
|
||||
// the City database, all records of the same country will reference a
|
||||
// single representative record for that country. This uintptr behavior allows
|
||||
// clients to leverage this normalization in their own sub-record caching.
|
||||
func (r *Reader) Decode(offset uintptr, result interface{}) error {
|
||||
rv := reflect.ValueOf(result)
|
||||
if rv.Kind() != reflect.Ptr || rv.IsNil() {
|
||||
return errors.New("result param must be a pointer")
|
||||
}
|
||||
|
||||
_, err := r.decoder.decode(uint(offset), reflect.ValueOf(result))
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Reader) lookupPointer(ipAddress net.IP) (uint, error) {
|
||||
if ipAddress == nil {
|
||||
return 0, errors.New("ipAddress passed to Lookup cannot be nil")
|
||||
}
|
||||
|
||||
ipV4Address := ipAddress.To4()
|
||||
if ipV4Address != nil {
|
||||
ipAddress = ipV4Address
|
||||
}
|
||||
if len(ipAddress) == 16 && r.Metadata.IPVersion == 4 {
|
||||
return 0, fmt.Errorf("error looking up '%s': you attempted to look up an IPv6 address in an IPv4-only database", ipAddress.String())
|
||||
}
|
||||
|
||||
return r.findAddressInTree(ipAddress)
|
||||
}
|
||||
|
||||
func (r *Reader) findAddressInTree(ipAddress net.IP) (uint, error) {
|
||||
|
||||
bitCount := uint(len(ipAddress) * 8)
|
||||
|
||||
var node uint
|
||||
if bitCount == 32 {
|
||||
node = r.ipv4Start
|
||||
}
|
||||
|
||||
nodeCount := r.Metadata.NodeCount
|
||||
|
||||
for i := uint(0); i < bitCount && node < nodeCount; i++ {
|
||||
bit := uint(1) & (uint(ipAddress[i>>3]) >> (7 - (i % 8)))
|
||||
|
||||
var err error
|
||||
node, err = r.readNode(node, bit)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
if node == nodeCount {
|
||||
// Record is empty
|
||||
return 0, nil
|
||||
} else if node > nodeCount {
|
||||
return node, nil
|
||||
}
|
||||
|
||||
return 0, newInvalidDatabaseError("invalid node in search tree")
|
||||
}
|
||||
|
||||
func (r *Reader) readNode(nodeNumber uint, index uint) (uint, error) {
|
||||
RecordSize := r.Metadata.RecordSize
|
||||
|
||||
baseOffset := nodeNumber * RecordSize / 4
|
||||
|
||||
var nodeBytes []byte
|
||||
var prefix uint64
|
||||
switch RecordSize {
|
||||
case 24:
|
||||
offset := baseOffset + index*3
|
||||
nodeBytes = r.buffer[offset : offset+3]
|
||||
case 28:
|
||||
prefix = uint64(r.buffer[baseOffset+3])
|
||||
if index != 0 {
|
||||
prefix &= 0x0F
|
||||
} else {
|
||||
prefix = (0xF0 & prefix) >> 4
|
||||
}
|
||||
offset := baseOffset + index*4
|
||||
nodeBytes = r.buffer[offset : offset+3]
|
||||
case 32:
|
||||
offset := baseOffset + index*4
|
||||
nodeBytes = r.buffer[offset : offset+4]
|
||||
default:
|
||||
return 0, newInvalidDatabaseError("unknown record size: %d", RecordSize)
|
||||
}
|
||||
return uint(uintFromBytes(prefix, nodeBytes)), nil
|
||||
}
|
||||
|
||||
func (r *Reader) retrieveData(pointer uint, result interface{}) error {
|
||||
if offset, err := r.resolveDataPointer(pointer); err != nil {
|
||||
return err
|
||||
} else {
|
||||
return r.Decode(offset, result)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Reader) resolveDataPointer(pointer uint) (uintptr, error) {
|
||||
var resolved = uintptr(pointer - r.Metadata.NodeCount - dataSectionSeparatorSize)
|
||||
|
||||
if resolved > uintptr(len(r.buffer)) {
|
||||
return 0, newInvalidDatabaseError("the MaxMind DB file's search tree is corrupt")
|
||||
}
|
||||
return resolved, nil
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
// +build appengine
|
||||
|
||||
package maxminddb
|
||||
|
||||
import "io/ioutil"
|
||||
|
||||
// Open takes a string path to a MaxMind DB file and returns a Reader
|
||||
// structure or an error. The database file is opened using a memory map,
|
||||
// except on Google App Engine where mmap is not supported; there the database
|
||||
// is loaded into memory. Use the Close method on the Reader object to return
|
||||
// the resources to the system.
|
||||
func Open(file string) (*Reader, error) {
|
||||
bytes, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return FromBytes(bytes)
|
||||
}
|
||||
|
||||
// Close unmaps the database file from virtual memory and returns the
|
||||
// resources to the system. If called on a Reader opened using FromBytes
|
||||
// or Open on Google App Engine, this method does nothing.
|
||||
func (r *Reader) Close() error {
|
||||
return nil
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
// +build !appengine
|
||||
|
||||
package maxminddb
|
||||
|
||||
import "os"
|
||||
|
||||
// Open takes a string path to a MaxMind DB file and returns a Reader
|
||||
// structure or an error. The database file is opened using a memory map,
|
||||
// except on Google App Engine where mmap is not supported; there the database
|
||||
// is loaded into memory. Use the Close method on the Reader object to return
|
||||
// the resources to the system.
|
||||
func Open(file string) (*Reader, error) {
|
||||
mapFile, err := os.Open(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if rerr := mapFile.Close(); rerr != nil {
|
||||
err = rerr
|
||||
}
|
||||
}()
|
||||
|
||||
stats, err := mapFile.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fileSize := int(stats.Size())
|
||||
mmap, err := mmap(int(mapFile.Fd()), fileSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reader, err := FromBytes(mmap)
|
||||
if err != nil {
|
||||
if err2 := munmap(mmap); err2 != nil {
|
||||
// failing to unmap the file is probably the more severe error
|
||||
return nil, err2
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reader.hasMappedFile = true
|
||||
return reader, err
|
||||
}
|
||||
|
||||
// Close unmaps the database file from virtual memory and returns the
|
||||
// resources to the system. If called on a Reader opened using FromBytes
|
||||
// or Open on Google App Engine, this method does nothing.
|
||||
func (r *Reader) Close() (err error) {
|
||||
if r.hasMappedFile {
|
||||
err = munmap(r.buffer)
|
||||
r.hasMappedFile = false
|
||||
}
|
||||
return err
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
package maxminddb
|
||||
|
||||
import "net"
|
||||
|
||||
// Internal structure used to keep track of nodes we still need to visit.
|
||||
type netNode struct {
|
||||
ip net.IP
|
||||
bit uint
|
||||
pointer uint
|
||||
}
|
||||
|
||||
// Networks represents a set of subnets that we are iterating over.
|
||||
type Networks struct {
|
||||
reader *Reader
|
||||
nodes []netNode // Nodes we still have to visit.
|
||||
lastNode netNode
|
||||
err error
|
||||
}
|
||||
|
||||
// Networks returns an iterator that can be used to traverse all networks in
|
||||
// the database.
|
||||
//
|
||||
// Please note that a MaxMind DB may map IPv4 networks into several locations
|
||||
// in in an IPv6 database. This iterator will iterate over all of these
|
||||
// locations separately.
|
||||
func (r *Reader) Networks() *Networks {
|
||||
s := 4
|
||||
if r.Metadata.IPVersion == 6 {
|
||||
s = 16
|
||||
}
|
||||
return &Networks{
|
||||
reader: r,
|
||||
nodes: []netNode{
|
||||
netNode{
|
||||
ip: make(net.IP, s),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Next prepares the next network for reading with the Network method. It
|
||||
// returns true if there is another network to be processed and false if there
|
||||
// are no more networks or if there is an error.
|
||||
func (n *Networks) Next() bool {
|
||||
for len(n.nodes) > 0 {
|
||||
node := n.nodes[len(n.nodes)-1]
|
||||
n.nodes = n.nodes[:len(n.nodes)-1]
|
||||
|
||||
for {
|
||||
if node.pointer < n.reader.Metadata.NodeCount {
|
||||
ipRight := make(net.IP, len(node.ip))
|
||||
copy(ipRight, node.ip)
|
||||
if len(ipRight) <= int(node.bit>>3) {
|
||||
n.err = newInvalidDatabaseError(
|
||||
"invalid search tree at %v/%v", ipRight, node.bit)
|
||||
return false
|
||||
}
|
||||
ipRight[node.bit>>3] |= 1 << uint(7-(node.bit%8))
|
||||
|
||||
rightPointer, err := n.reader.readNode(node.pointer, 1)
|
||||
if err != nil {
|
||||
n.err = err
|
||||
return false
|
||||
}
|
||||
|
||||
node.bit++
|
||||
n.nodes = append(n.nodes, netNode{
|
||||
pointer: rightPointer,
|
||||
ip: ipRight,
|
||||
bit: node.bit,
|
||||
})
|
||||
|
||||
node.pointer, err = n.reader.readNode(node.pointer, 0)
|
||||
if err != nil {
|
||||
n.err = err
|
||||
return false
|
||||
}
|
||||
|
||||
} else if node.pointer > n.reader.Metadata.NodeCount {
|
||||
n.lastNode = node
|
||||
return true
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Network returns the current network or an error if there is a problem
|
||||
// decoding the data for the network. It takes a pointer to a result value to
|
||||
// decode the network's data into.
|
||||
func (n *Networks) Network(result interface{}) (*net.IPNet, error) {
|
||||
if err := n.reader.retrieveData(n.lastNode.pointer, result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &net.IPNet{
|
||||
IP: n.lastNode.ip,
|
||||
Mask: net.CIDRMask(int(n.lastNode.bit), len(n.lastNode.ip)*8),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Err returns an error, if any, that was encountered during iteration.
|
||||
func (n *Networks) Err() error {
|
||||
return n.err
|
||||
}
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
package maxminddb
|
||||
|
||||
import "reflect"
|
||||
|
||||
type verifier struct {
|
||||
reader *Reader
|
||||
}
|
||||
|
||||
// Verify checks that the database is valid. It validates the search tree,
|
||||
// the data section, and the metadata section. This verifier is stricter than
|
||||
// the specification and may return errors on databases that are readable.
|
||||
func (r *Reader) Verify() error {
|
||||
v := verifier{r}
|
||||
if err := v.verifyMetadata(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return v.verifyDatabase()
|
||||
}
|
||||
|
||||
func (v *verifier) verifyMetadata() error {
|
||||
metadata := v.reader.Metadata
|
||||
|
||||
if metadata.BinaryFormatMajorVersion != 2 {
|
||||
return testError(
|
||||
"binary_format_major_version",
|
||||
2,
|
||||
metadata.BinaryFormatMajorVersion,
|
||||
)
|
||||
}
|
||||
|
||||
if metadata.BinaryFormatMinorVersion != 0 {
|
||||
return testError(
|
||||
"binary_format_minor_version",
|
||||
0,
|
||||
metadata.BinaryFormatMinorVersion,
|
||||
)
|
||||
}
|
||||
|
||||
if metadata.DatabaseType == "" {
|
||||
return testError(
|
||||
"database_type",
|
||||
"non-empty string",
|
||||
metadata.DatabaseType,
|
||||
)
|
||||
}
|
||||
|
||||
if len(metadata.Description) == 0 {
|
||||
return testError(
|
||||
"description",
|
||||
"non-empty slice",
|
||||
metadata.Description,
|
||||
)
|
||||
}
|
||||
|
||||
if metadata.IPVersion != 4 && metadata.IPVersion != 6 {
|
||||
return testError(
|
||||
"ip_version",
|
||||
"4 or 6",
|
||||
metadata.IPVersion,
|
||||
)
|
||||
}
|
||||
|
||||
if metadata.RecordSize != 24 &&
|
||||
metadata.RecordSize != 28 &&
|
||||
metadata.RecordSize != 32 {
|
||||
return testError(
|
||||
"record_size",
|
||||
"24, 28, or 32",
|
||||
metadata.RecordSize,
|
||||
)
|
||||
}
|
||||
|
||||
if metadata.NodeCount == 0 {
|
||||
return testError(
|
||||
"node_count",
|
||||
"positive integer",
|
||||
metadata.NodeCount,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *verifier) verifyDatabase() error {
|
||||
offsets, err := v.verifySearchTree()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := v.verifyDataSectionSeparator(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return v.verifyDataSection(offsets)
|
||||
}
|
||||
|
||||
func (v *verifier) verifySearchTree() (map[uint]bool, error) {
|
||||
offsets := make(map[uint]bool)
|
||||
|
||||
it := v.reader.Networks()
|
||||
for it.Next() {
|
||||
offset, err := v.reader.resolveDataPointer(it.lastNode.pointer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
offsets[uint(offset)] = true
|
||||
}
|
||||
if err := it.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return offsets, nil
|
||||
}
|
||||
|
||||
func (v *verifier) verifyDataSectionSeparator() error {
|
||||
separatorStart := v.reader.Metadata.NodeCount * v.reader.Metadata.RecordSize / 4
|
||||
|
||||
separator := v.reader.buffer[separatorStart : separatorStart+dataSectionSeparatorSize]
|
||||
|
||||
for _, b := range separator {
|
||||
if b != 0 {
|
||||
return newInvalidDatabaseError("unexpected byte in data separator: %v", separator)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *verifier) verifyDataSection(offsets map[uint]bool) error {
|
||||
pointerCount := len(offsets)
|
||||
|
||||
decoder := v.reader.decoder
|
||||
|
||||
var offset uint
|
||||
bufferLen := uint(len(decoder.buffer))
|
||||
for offset < bufferLen {
|
||||
var data interface{}
|
||||
rv := reflect.ValueOf(&data)
|
||||
newOffset, err := decoder.decode(offset, rv)
|
||||
if err != nil {
|
||||
return newInvalidDatabaseError("received decoding error (%v) at offset of %v", err, offset)
|
||||
}
|
||||
if newOffset <= offset {
|
||||
return newInvalidDatabaseError("data section offset unexpectedly went from %v to %v", offset, newOffset)
|
||||
}
|
||||
|
||||
pointer := offset
|
||||
|
||||
if _, ok := offsets[pointer]; ok {
|
||||
delete(offsets, pointer)
|
||||
} else {
|
||||
return newInvalidDatabaseError("found data (%v) at %v that the search tree does not point to", data, pointer)
|
||||
}
|
||||
|
||||
offset = newOffset
|
||||
}
|
||||
|
||||
if offset != bufferLen {
|
||||
return newInvalidDatabaseError(
|
||||
"unexpected data at the end of the data section (last offset: %v, end: %v)",
|
||||
offset,
|
||||
bufferLen,
|
||||
)
|
||||
}
|
||||
|
||||
if len(offsets) != 0 {
|
||||
return newInvalidDatabaseError(
|
||||
"found %v pointers (of %v) in the search tree that we did not see in the data section",
|
||||
len(offsets),
|
||||
pointerCount,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func testError(
|
||||
field string,
|
||||
expected interface{},
|
||||
actual interface{},
|
||||
) error {
|
||||
return newInvalidDatabaseError(
|
||||
"%v - Expected: %v Actual: %v",
|
||||
field,
|
||||
expected,
|
||||
actual,
|
||||
)
|
||||
}
|
||||
-105
@@ -1,105 +0,0 @@
|
||||
This is an implementation of the [LevelDB key/value database](http:code.google.com/p/leveldb) in the [Go programming language](http:golang.org).
|
||||
|
||||
[](https://travis-ci.org/syndtr/goleveldb)
|
||||
|
||||
Installation
|
||||
-----------
|
||||
|
||||
go get github.com/syndtr/goleveldb/leveldb
|
||||
|
||||
Requirements
|
||||
-----------
|
||||
|
||||
* Need at least `go1.4` or newer.
|
||||
|
||||
Usage
|
||||
-----------
|
||||
|
||||
Create or open a database:
|
||||
```go
|
||||
db, err := leveldb.OpenFile("path/to/db", nil)
|
||||
...
|
||||
defer db.Close()
|
||||
...
|
||||
```
|
||||
Read or modify the database content:
|
||||
```go
|
||||
// Remember that the contents of the returned slice should not be modified.
|
||||
data, err := db.Get([]byte("key"), nil)
|
||||
...
|
||||
err = db.Put([]byte("key"), []byte("value"), nil)
|
||||
...
|
||||
err = db.Delete([]byte("key"), nil)
|
||||
...
|
||||
```
|
||||
|
||||
Iterate over database content:
|
||||
```go
|
||||
iter := db.NewIterator(nil, nil)
|
||||
for iter.Next() {
|
||||
// Remember that the contents of the returned slice should not be modified, and
|
||||
// only valid until the next call to Next.
|
||||
key := iter.Key()
|
||||
value := iter.Value()
|
||||
...
|
||||
}
|
||||
iter.Release()
|
||||
err = iter.Error()
|
||||
...
|
||||
```
|
||||
Seek-then-Iterate:
|
||||
```go
|
||||
iter := db.NewIterator(nil, nil)
|
||||
for ok := iter.Seek(key); ok; ok = iter.Next() {
|
||||
// Use key/value.
|
||||
...
|
||||
}
|
||||
iter.Release()
|
||||
err = iter.Error()
|
||||
...
|
||||
```
|
||||
Iterate over subset of database content:
|
||||
```go
|
||||
iter := db.NewIterator(&util.Range{Start: []byte("foo"), Limit: []byte("xoo")}, nil)
|
||||
for iter.Next() {
|
||||
// Use key/value.
|
||||
...
|
||||
}
|
||||
iter.Release()
|
||||
err = iter.Error()
|
||||
...
|
||||
```
|
||||
Iterate over subset of database content with a particular prefix:
|
||||
```go
|
||||
iter := db.NewIterator(util.BytesPrefix([]byte("foo-")), nil)
|
||||
for iter.Next() {
|
||||
// Use key/value.
|
||||
...
|
||||
}
|
||||
iter.Release()
|
||||
err = iter.Error()
|
||||
...
|
||||
```
|
||||
Batch writes:
|
||||
```go
|
||||
batch := new(leveldb.Batch)
|
||||
batch.Put([]byte("foo"), []byte("value"))
|
||||
batch.Put([]byte("bar"), []byte("another value"))
|
||||
batch.Delete([]byte("baz"))
|
||||
err = db.Write(batch, nil)
|
||||
...
|
||||
```
|
||||
Use bloom filter:
|
||||
```go
|
||||
o := &opt.Options{
|
||||
Filter: filter.NewBloomFilter(10),
|
||||
}
|
||||
db, err := leveldb.OpenFile("path/to/db", o)
|
||||
...
|
||||
defer db.Close()
|
||||
...
|
||||
```
|
||||
Documentation
|
||||
-----------
|
||||
|
||||
You can read package documentation [here](http:godoc.org/github.com/syndtr/goleveldb).
|
||||
+1
@@ -158,6 +158,7 @@ func (b *Batch) append(p *Batch) {
|
||||
b.grow(len(p.data) - batchHdrLen)
|
||||
b.data = append(b.data, p.data[batchHdrLen:]...)
|
||||
b.rLen += p.rLen
|
||||
b.bLen += p.bLen
|
||||
}
|
||||
if p.sync {
|
||||
b.sync = true
|
||||
|
||||
+3
@@ -100,6 +100,9 @@ func TestBatch_Append(t *testing.T) {
|
||||
b2b.Put([]byte("bar"), []byte("barvalue"))
|
||||
b2a.append(b2b)
|
||||
compareBatch(t, b1, b2a)
|
||||
if b1.size() != b2a.size() {
|
||||
t.Fatalf("invalid batch size want %d, got %d", b1.size(), b2a.size())
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatch_Size(t *testing.T) {
|
||||
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
// Copyright (c) 2012, Suryandaru Triandana <syndtr@gmail.com>
|
||||
// All rights reserved.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build !go1.2
|
||||
|
||||
package leveldb
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func BenchmarkDBReadConcurrent(b *testing.B) {
|
||||
p := openDBBench(b, false)
|
||||
p.populate(b.N)
|
||||
p.fill()
|
||||
p.gc()
|
||||
defer p.close()
|
||||
|
||||
b.ResetTimer()
|
||||
b.SetBytes(116)
|
||||
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
iter := p.newIter()
|
||||
defer iter.Release()
|
||||
for pb.Next() && iter.Next() {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkDBReadConcurrent2(b *testing.B) {
|
||||
p := openDBBench(b, false)
|
||||
p.populate(b.N)
|
||||
p.fill()
|
||||
p.gc()
|
||||
defer p.close()
|
||||
|
||||
b.ResetTimer()
|
||||
b.SetBytes(116)
|
||||
|
||||
var dir uint32
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
iter := p.newIter()
|
||||
defer iter.Release()
|
||||
if atomic.AddUint32(&dir, 1)%2 == 0 {
|
||||
for pb.Next() && iter.Next() {
|
||||
}
|
||||
} else {
|
||||
if pb.Next() && iter.Last() {
|
||||
for pb.Next() && iter.Prev() {
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
// Copyright (c) 2012, Suryandaru Triandana <syndtr@gmail.com>
|
||||
// All rights reserved.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build !go1.2
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func BenchmarkLRUCache(b *testing.B) {
|
||||
c := NewCache(NewLRU(10000))
|
||||
|
||||
b.SetParallelism(10)
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
for pb.Next() {
|
||||
key := uint64(r.Intn(1000000))
|
||||
c.Get(0, key, func() (int, Value) {
|
||||
return 1, key
|
||||
}).Release()
|
||||
}
|
||||
})
|
||||
}
|
||||
+2
-2
@@ -172,7 +172,7 @@ func (db *DB) compactionTransact(name string, t compactionTransactInterface) {
|
||||
disableBackoff = db.s.o.GetDisableCompactionBackoff()
|
||||
)
|
||||
for n := 0; ; n++ {
|
||||
// Check wether the DB is closed.
|
||||
// Check whether the DB is closed.
|
||||
if db.isClosed() {
|
||||
db.logf("%s exiting", name)
|
||||
db.compactionExitTransact()
|
||||
@@ -688,7 +688,7 @@ func (db *DB) compTrigger(compC chan<- cCmd) {
|
||||
}
|
||||
}
|
||||
|
||||
// This will trigger auto compation and/or wait for all compaction to be done.
|
||||
// This will trigger auto compaction and/or wait for all compaction to be done.
|
||||
func (db *DB) compTriggerWait(compC chan<- cCmd) (err error) {
|
||||
ch := make(chan error)
|
||||
defer close(ch)
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) 2012, Suryandaru Triandana <syndtr@gmail.com>
|
||||
// All rights reserved.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build nacl
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func newFileLock(path string, readOnly bool) (fl fileLock, err error) {
|
||||
return nil, syscall.ENOTSUP
|
||||
}
|
||||
|
||||
func setFileLock(f *os.File, readOnly, lock bool) error {
|
||||
return syscall.ENOTSUP
|
||||
}
|
||||
|
||||
func rename(oldpath, newpath string) error {
|
||||
return syscall.ENOTSUP
|
||||
}
|
||||
|
||||
func isErrInvalid(err error) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func syncDir(name string) error {
|
||||
return syscall.ENOTSUP
|
||||
}
|
||||
+14
-14
@@ -7,38 +7,38 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
)
|
||||
|
||||
// Hash return hash of the given data.
|
||||
func Hash(data []byte, seed uint32) uint32 {
|
||||
// Similar to murmur hash
|
||||
var m uint32 = 0xc6a4a793
|
||||
var r uint32 = 24
|
||||
h := seed ^ (uint32(len(data)) * m)
|
||||
const (
|
||||
m = uint32(0xc6a4a793)
|
||||
r = uint32(24)
|
||||
)
|
||||
var (
|
||||
h = seed ^ (uint32(len(data)) * m)
|
||||
i int
|
||||
)
|
||||
|
||||
buf := bytes.NewBuffer(data)
|
||||
for buf.Len() >= 4 {
|
||||
var w uint32
|
||||
binary.Read(buf, binary.LittleEndian, &w)
|
||||
h += w
|
||||
for n := len(data) - len(data)%4; i < n; i += 4 {
|
||||
h += binary.LittleEndian.Uint32(data[i:])
|
||||
h *= m
|
||||
h ^= (h >> 16)
|
||||
}
|
||||
|
||||
rest := buf.Bytes()
|
||||
switch len(rest) {
|
||||
switch len(data) - i {
|
||||
default:
|
||||
panic("not reached")
|
||||
case 3:
|
||||
h += uint32(rest[2]) << 16
|
||||
h += uint32(data[i+2]) << 16
|
||||
fallthrough
|
||||
case 2:
|
||||
h += uint32(rest[1]) << 8
|
||||
h += uint32(data[i+1]) << 8
|
||||
fallthrough
|
||||
case 1:
|
||||
h += uint32(rest[0])
|
||||
h += uint32(data[i])
|
||||
h *= m
|
||||
h ^= (h >> r)
|
||||
case 0:
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) 2012, Suryandaru Triandana <syndtr@gmail.com>
|
||||
// All rights reserved.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
var hashTests = []struct {
|
||||
data []byte
|
||||
seed uint32
|
||||
hash uint32
|
||||
}{
|
||||
{nil, 0xbc9f1d34, 0xbc9f1d34},
|
||||
{[]byte{0x62}, 0xbc9f1d34, 0xef1345c4},
|
||||
{[]byte{0xc3, 0x97}, 0xbc9f1d34, 0x5b663814},
|
||||
{[]byte{0xe2, 0x99, 0xa5}, 0xbc9f1d34, 0x323c078f},
|
||||
{[]byte{0xe1, 0x80, 0xb9, 0x32}, 0xbc9f1d34, 0xed21633a},
|
||||
{[]byte{
|
||||
0x01, 0xc0, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x14, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x04, 0x00,
|
||||
0x00, 0x00, 0x00, 0x14,
|
||||
0x00, 0x00, 0x00, 0x18,
|
||||
0x28, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x02, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
}, 0x12345678, 0xf333dabb},
|
||||
}
|
||||
|
||||
func TestHash(t *testing.T) {
|
||||
for i, x := range hashTests {
|
||||
h := Hash(x.data, x.seed)
|
||||
if h != x.hash {
|
||||
t.Fatalf("test-%d: invalid hash, %#x vs %#x", i, h, x.hash)
|
||||
}
|
||||
}
|
||||
}
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
// Copyright (c) 2014, Suryandaru Triandana <syndtr@gmail.com>
|
||||
// All rights reserved.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build go1.3
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Pool struct {
|
||||
sync.Pool
|
||||
}
|
||||
|
||||
func NewPool(cap int) *Pool {
|
||||
return &Pool{}
|
||||
}
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
// Copyright (c) 2014, Suryandaru Triandana <syndtr@gmail.com>
|
||||
// All rights reserved.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build !go1.3
|
||||
|
||||
package util
|
||||
|
||||
type Pool struct {
|
||||
pool chan interface{}
|
||||
}
|
||||
|
||||
func (p *Pool) Get() interface{} {
|
||||
select {
|
||||
case x := <-p.pool:
|
||||
return x
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pool) Put(x interface{}) {
|
||||
select {
|
||||
case p.pool <- x:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func NewPool(cap int) *Pool {
|
||||
return &Pool{pool: make(chan interface{}, cap)}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
Copyright (c) 2009 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
+269
@@ -0,0 +1,269 @@
|
||||
// Copyright 2016 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build darwin dragonfly freebsd netbsd openbsd
|
||||
|
||||
package route
|
||||
|
||||
import "runtime"
|
||||
|
||||
// An Addr represents an address associated with packet routing.
|
||||
type Addr interface {
|
||||
// Family returns an address family.
|
||||
Family() int
|
||||
}
|
||||
|
||||
// A LinkAddr represents a link-layer address.
|
||||
type LinkAddr struct {
|
||||
Index int // interface index when attached
|
||||
Name string // interface name when attached
|
||||
Addr []byte // link-layer address when attached
|
||||
}
|
||||
|
||||
// Family implements the Family method of Addr interface.
|
||||
func (a *LinkAddr) Family() int { return sysAF_LINK }
|
||||
|
||||
func parseLinkAddr(b []byte) (Addr, error) {
|
||||
if len(b) < 8 {
|
||||
return nil, errInvalidAddr
|
||||
}
|
||||
_, a, err := parseKernelLinkAddr(sysAF_LINK, b[4:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
a.(*LinkAddr).Index = int(nativeEndian.Uint16(b[2:4]))
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// parseKernelLinkAddr parses b as a link-layer address in
|
||||
// conventional BSD kernel form.
|
||||
func parseKernelLinkAddr(_ int, b []byte) (int, Addr, error) {
|
||||
// The encoding looks like the following:
|
||||
// +----------------------------+
|
||||
// | Type (1 octet) |
|
||||
// +----------------------------+
|
||||
// | Name length (1 octet) |
|
||||
// +----------------------------+
|
||||
// | Address length (1 octet) |
|
||||
// +----------------------------+
|
||||
// | Selector length (1 octet) |
|
||||
// +----------------------------+
|
||||
// | Data (variable) |
|
||||
// +----------------------------+
|
||||
//
|
||||
// On some platforms, all-bit-one of length field means "don't
|
||||
// care".
|
||||
nlen, alen, slen := int(b[1]), int(b[2]), int(b[3])
|
||||
if nlen == 0xff {
|
||||
nlen = 0
|
||||
}
|
||||
if alen == 0xff {
|
||||
alen = 0
|
||||
}
|
||||
if slen == 0xff {
|
||||
slen = 0
|
||||
}
|
||||
l := 4 + nlen + alen + slen
|
||||
if len(b) < l {
|
||||
return 0, nil, errInvalidAddr
|
||||
}
|
||||
data := b[4:]
|
||||
var name string
|
||||
var addr []byte
|
||||
if nlen > 0 {
|
||||
name = string(data[:nlen])
|
||||
data = data[nlen:]
|
||||
}
|
||||
if alen > 0 {
|
||||
addr = data[:alen]
|
||||
data = data[alen:]
|
||||
}
|
||||
return l, &LinkAddr{Name: name, Addr: addr}, nil
|
||||
}
|
||||
|
||||
// An Inet4Addr represents an internet address for IPv4.
|
||||
type Inet4Addr struct {
|
||||
IP [4]byte // IP address
|
||||
}
|
||||
|
||||
// Family implements the Family method of Addr interface.
|
||||
func (a *Inet4Addr) Family() int { return sysAF_INET }
|
||||
|
||||
// An Inet6Addr represents an internet address for IPv6.
|
||||
type Inet6Addr struct {
|
||||
IP [16]byte // IP address
|
||||
ZoneID int // zone identifier
|
||||
}
|
||||
|
||||
// Family implements the Family method of Addr interface.
|
||||
func (a *Inet6Addr) Family() int { return sysAF_INET6 }
|
||||
|
||||
// parseInetAddr parses b as an internet address for IPv4 or IPv6.
|
||||
func parseInetAddr(af int, b []byte) (Addr, error) {
|
||||
switch af {
|
||||
case sysAF_INET:
|
||||
if len(b) < 16 {
|
||||
return nil, errInvalidAddr
|
||||
}
|
||||
a := &Inet4Addr{}
|
||||
copy(a.IP[:], b[4:8])
|
||||
return a, nil
|
||||
case sysAF_INET6:
|
||||
if len(b) < 28 {
|
||||
return nil, errInvalidAddr
|
||||
}
|
||||
a := &Inet6Addr{ZoneID: int(nativeEndian.Uint32(b[24:28]))}
|
||||
copy(a.IP[:], b[8:24])
|
||||
if a.IP[0] == 0xfe && a.IP[1]&0xc0 == 0x80 || a.IP[0] == 0xff && (a.IP[1]&0x0f == 0x01 || a.IP[1]&0x0f == 0x02) {
|
||||
// KAME based IPv6 protocol stack usually
|
||||
// embeds the interface index in the
|
||||
// interface-local or link-local address as
|
||||
// the kernel-internal form.
|
||||
id := int(bigEndian.Uint16(a.IP[2:4]))
|
||||
if id != 0 {
|
||||
a.ZoneID = id
|
||||
a.IP[2], a.IP[3] = 0, 0
|
||||
}
|
||||
}
|
||||
return a, nil
|
||||
default:
|
||||
return nil, errInvalidAddr
|
||||
}
|
||||
}
|
||||
|
||||
// parseKernelInetAddr parses b as an internet address in conventional
|
||||
// BSD kernel form.
|
||||
func parseKernelInetAddr(af int, b []byte) (int, Addr, error) {
|
||||
// The encoding looks similar to the NLRI encoding.
|
||||
// +----------------------------+
|
||||
// | Length (1 octet) |
|
||||
// +----------------------------+
|
||||
// | Address prefix (variable) |
|
||||
// +----------------------------+
|
||||
//
|
||||
// The differences between the kernel form and the NLRI
|
||||
// encoding are:
|
||||
//
|
||||
// - The length field of the kernel form indicates the prefix
|
||||
// length in bytes, not in bits
|
||||
//
|
||||
// - In the kernel form, zero value of the length field
|
||||
// doesn't mean 0.0.0.0/0 or ::/0
|
||||
//
|
||||
// - The kernel form appends leading bytes to the prefix field
|
||||
// to make the <length, prefix> tuple to be conformed with
|
||||
// the routing message boundary
|
||||
l := int(b[0])
|
||||
if runtime.GOOS == "darwin" {
|
||||
// On Darwn, an address in the kernel form is also
|
||||
// used as a message filler.
|
||||
if l == 0 || len(b) > roundup(l) {
|
||||
l = roundup(l)
|
||||
}
|
||||
} else {
|
||||
l = roundup(l)
|
||||
}
|
||||
if len(b) < l {
|
||||
return 0, nil, errInvalidAddr
|
||||
}
|
||||
// Don't reorder case expressions.
|
||||
// The case expressions for IPv6 must come first.
|
||||
const (
|
||||
off4 = 4 // offset of in_addr
|
||||
off6 = 8 // offset of in6_addr
|
||||
)
|
||||
switch {
|
||||
case b[0] == 28: // size of sockaddr_in6
|
||||
a := &Inet6Addr{}
|
||||
copy(a.IP[:], b[off6:off6+16])
|
||||
return int(b[0]), a, nil
|
||||
case af == sysAF_INET6:
|
||||
a := &Inet6Addr{}
|
||||
if l-1 < off6 {
|
||||
copy(a.IP[:], b[1:l])
|
||||
} else {
|
||||
copy(a.IP[:], b[l-off6:l])
|
||||
}
|
||||
return int(b[0]), a, nil
|
||||
case b[0] == 16: // size of sockaddr_in
|
||||
a := &Inet4Addr{}
|
||||
copy(a.IP[:], b[off4:off4+4])
|
||||
return int(b[0]), a, nil
|
||||
default: // an old fashion, AF_UNSPEC or unknown means AF_INET
|
||||
a := &Inet4Addr{}
|
||||
if l-1 < off4 {
|
||||
copy(a.IP[:], b[1:l])
|
||||
} else {
|
||||
copy(a.IP[:], b[l-off4:l])
|
||||
}
|
||||
return int(b[0]), a, nil
|
||||
}
|
||||
}
|
||||
|
||||
// A DefaultAddr represents an address of various operating
|
||||
// system-specific features.
|
||||
type DefaultAddr struct {
|
||||
af int
|
||||
Raw []byte // raw format of address
|
||||
}
|
||||
|
||||
// Family implements the Family method of Addr interface.
|
||||
func (a *DefaultAddr) Family() int { return a.af }
|
||||
|
||||
func parseDefaultAddr(b []byte) (Addr, error) {
|
||||
if len(b) < 2 || len(b) < int(b[0]) {
|
||||
return nil, errInvalidAddr
|
||||
}
|
||||
a := &DefaultAddr{af: int(b[1]), Raw: b[:b[0]]}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func parseAddrs(attrs uint, fn func(int, []byte) (int, Addr, error), b []byte) ([]Addr, error) {
|
||||
var as [sysRTAX_MAX]Addr
|
||||
af := int(sysAF_UNSPEC)
|
||||
for i := uint(0); i < sysRTAX_MAX && len(b) >= roundup(0); i++ {
|
||||
if attrs&(1<<i) == 0 {
|
||||
continue
|
||||
}
|
||||
if i <= sysRTAX_BRD {
|
||||
switch b[1] {
|
||||
case sysAF_LINK:
|
||||
a, err := parseLinkAddr(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
as[i] = a
|
||||
b = b[roundup(int(b[0])):]
|
||||
case sysAF_INET, sysAF_INET6:
|
||||
af = int(b[1])
|
||||
a, err := parseInetAddr(af, b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
as[i] = a
|
||||
b = b[roundup(int(b[0])):]
|
||||
default:
|
||||
l, a, err := fn(af, b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
as[i] = a
|
||||
ll := roundup(l)
|
||||
if len(b) < ll {
|
||||
b = b[l:]
|
||||
} else {
|
||||
b = b[ll:]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
a, err := parseDefaultAddr(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
as[i] = a
|
||||
b = b[roundup(int(b[0])):]
|
||||
}
|
||||
}
|
||||
return as[:], nil
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
// Copyright 2016 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build darwin dragonfly freebsd netbsd openbsd
|
||||
|
||||
package route
|
||||
|
||||
// This file contains duplicates of encoding/binary package.
|
||||
//
|
||||
// This package is supposed to be used by the net package of standard
|
||||
// library. Therefore a package set used in the package must be the
|
||||
// same as net package.
|
||||
|
||||
var (
|
||||
littleEndian binaryLittleEndian
|
||||
bigEndian binaryBigEndian
|
||||
)
|
||||
|
||||
type binaryByteOrder interface {
|
||||
Uint16([]byte) uint16
|
||||
Uint32([]byte) uint32
|
||||
PutUint16([]byte, uint16)
|
||||
PutUint32([]byte, uint32)
|
||||
Uint64([]byte) uint64
|
||||
}
|
||||
|
||||
type binaryLittleEndian struct{}
|
||||
|
||||
func (binaryLittleEndian) Uint16(b []byte) uint16 {
|
||||
_ = b[1] // bounds check hint to compiler; see golang.org/issue/14808
|
||||
return uint16(b[0]) | uint16(b[1])<<8
|
||||
}
|
||||
|
||||
func (binaryLittleEndian) PutUint16(b []byte, v uint16) {
|
||||
_ = b[1] // early bounds check to guarantee safety of writes below
|
||||
b[0] = byte(v)
|
||||
b[1] = byte(v >> 8)
|
||||
}
|
||||
|
||||
func (binaryLittleEndian) Uint32(b []byte) uint32 {
|
||||
_ = b[3] // bounds check hint to compiler; see golang.org/issue/14808
|
||||
return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24
|
||||
}
|
||||
|
||||
func (binaryLittleEndian) PutUint32(b []byte, v uint32) {
|
||||
_ = b[3] // early bounds check to guarantee safety of writes below
|
||||
b[0] = byte(v)
|
||||
b[1] = byte(v >> 8)
|
||||
b[2] = byte(v >> 16)
|
||||
b[3] = byte(v >> 24)
|
||||
}
|
||||
|
||||
func (binaryLittleEndian) Uint64(b []byte) uint64 {
|
||||
_ = b[7] // bounds check hint to compiler; see golang.org/issue/14808
|
||||
return uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24 |
|
||||
uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56
|
||||
}
|
||||
|
||||
type binaryBigEndian struct{}
|
||||
|
||||
func (binaryBigEndian) Uint16(b []byte) uint16 {
|
||||
_ = b[1] // bounds check hint to compiler; see golang.org/issue/14808
|
||||
return uint16(b[1]) | uint16(b[0])<<8
|
||||
}
|
||||
|
||||
func (binaryBigEndian) PutUint16(b []byte, v uint16) {
|
||||
_ = b[1] // early bounds check to guarantee safety of writes below
|
||||
b[0] = byte(v >> 8)
|
||||
b[1] = byte(v)
|
||||
}
|
||||
|
||||
func (binaryBigEndian) Uint32(b []byte) uint32 {
|
||||
_ = b[3] // bounds check hint to compiler; see golang.org/issue/14808
|
||||
return uint32(b[3]) | uint32(b[2])<<8 | uint32(b[1])<<16 | uint32(b[0])<<24
|
||||
}
|
||||
|
||||
func (binaryBigEndian) PutUint32(b []byte, v uint32) {
|
||||
_ = b[3] // early bounds check to guarantee safety of writes below
|
||||
b[0] = byte(v >> 24)
|
||||
b[1] = byte(v >> 16)
|
||||
b[2] = byte(v >> 8)
|
||||
b[3] = byte(v)
|
||||
}
|
||||
|
||||
func (binaryBigEndian) Uint64(b []byte) uint64 {
|
||||
_ = b[7] // bounds check hint to compiler; see golang.org/issue/14808
|
||||
return uint64(b[7]) | uint64(b[6])<<8 | uint64(b[5])<<16 | uint64(b[4])<<24 |
|
||||
uint64(b[3])<<32 | uint64(b[2])<<40 | uint64(b[1])<<48 | uint64(b[0])<<56
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
// Copyright 2016 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build ignore
|
||||
|
||||
package route
|
||||
|
||||
/*
|
||||
#include <sys/socket.h>
|
||||
#include <sys/sysctl.h>
|
||||
|
||||
#include <net/if.h>
|
||||
#include <net/if_dl.h>
|
||||
#include <net/route.h>
|
||||
*/
|
||||
import "C"
|
||||
|
||||
const (
|
||||
sysAF_UNSPEC = C.AF_UNSPEC
|
||||
sysAF_INET = C.AF_INET
|
||||
sysAF_ROUTE = C.AF_ROUTE
|
||||
sysAF_LINK = C.AF_LINK
|
||||
sysAF_INET6 = C.AF_INET6
|
||||
|
||||
sysNET_RT_DUMP = C.NET_RT_DUMP
|
||||
sysNET_RT_FLAGS = C.NET_RT_FLAGS
|
||||
sysNET_RT_IFLIST = C.NET_RT_IFLIST
|
||||
sysNET_RT_STAT = C.NET_RT_STAT
|
||||
sysNET_RT_TRASH = C.NET_RT_TRASH
|
||||
sysNET_RT_IFLIST2 = C.NET_RT_IFLIST2
|
||||
sysNET_RT_DUMP2 = C.NET_RT_DUMP2
|
||||
sysNET_RT_MAXID = C.NET_RT_MAXID
|
||||
)
|
||||
|
||||
const (
|
||||
sysCTL_MAXNAME = C.CTL_MAXNAME
|
||||
|
||||
sysCTL_UNSPEC = C.CTL_UNSPEC
|
||||
sysCTL_KERN = C.CTL_KERN
|
||||
sysCTL_VM = C.CTL_VM
|
||||
sysCTL_VFS = C.CTL_VFS
|
||||
sysCTL_NET = C.CTL_NET
|
||||
sysCTL_DEBUG = C.CTL_DEBUG
|
||||
sysCTL_HW = C.CTL_HW
|
||||
sysCTL_MACHDEP = C.CTL_MACHDEP
|
||||
sysCTL_USER = C.CTL_USER
|
||||
sysCTL_MAXID = C.CTL_MAXID
|
||||
)
|
||||
|
||||
const (
|
||||
sysRTM_VERSION = C.RTM_VERSION
|
||||
|
||||
sysRTM_ADD = C.RTM_ADD
|
||||
sysRTM_DELETE = C.RTM_DELETE
|
||||
sysRTM_CHANGE = C.RTM_CHANGE
|
||||
sysRTM_GET = C.RTM_GET
|
||||
sysRTM_LOSING = C.RTM_LOSING
|
||||
sysRTM_REDIRECT = C.RTM_REDIRECT
|
||||
sysRTM_MISS = C.RTM_MISS
|
||||
sysRTM_LOCK = C.RTM_LOCK
|
||||
sysRTM_OLDADD = C.RTM_OLDADD
|
||||
sysRTM_OLDDEL = C.RTM_OLDDEL
|
||||
sysRTM_RESOLVE = C.RTM_RESOLVE
|
||||
sysRTM_NEWADDR = C.RTM_NEWADDR
|
||||
sysRTM_DELADDR = C.RTM_DELADDR
|
||||
sysRTM_IFINFO = C.RTM_IFINFO
|
||||
sysRTM_NEWMADDR = C.RTM_NEWMADDR
|
||||
sysRTM_DELMADDR = C.RTM_DELMADDR
|
||||
sysRTM_IFINFO2 = C.RTM_IFINFO2
|
||||
sysRTM_NEWMADDR2 = C.RTM_NEWMADDR2
|
||||
sysRTM_GET2 = C.RTM_GET2
|
||||
|
||||
sysRTA_DST = C.RTA_DST
|
||||
sysRTA_GATEWAY = C.RTA_GATEWAY
|
||||
sysRTA_NETMASK = C.RTA_NETMASK
|
||||
sysRTA_GENMASK = C.RTA_GENMASK
|
||||
sysRTA_IFP = C.RTA_IFP
|
||||
sysRTA_IFA = C.RTA_IFA
|
||||
sysRTA_AUTHOR = C.RTA_AUTHOR
|
||||
sysRTA_BRD = C.RTA_BRD
|
||||
|
||||
sysRTAX_DST = C.RTAX_DST
|
||||
sysRTAX_GATEWAY = C.RTAX_GATEWAY
|
||||
sysRTAX_NETMASK = C.RTAX_NETMASK
|
||||
sysRTAX_GENMASK = C.RTAX_GENMASK
|
||||
sysRTAX_IFP = C.RTAX_IFP
|
||||
sysRTAX_IFA = C.RTAX_IFA
|
||||
sysRTAX_AUTHOR = C.RTAX_AUTHOR
|
||||
sysRTAX_BRD = C.RTAX_BRD
|
||||
sysRTAX_MAX = C.RTAX_MAX
|
||||
)
|
||||
|
||||
const (
|
||||
sizeofIfMsghdrDarwin15 = C.sizeof_struct_if_msghdr
|
||||
sizeofIfaMsghdrDarwin15 = C.sizeof_struct_ifa_msghdr
|
||||
sizeofIfmaMsghdrDarwin15 = C.sizeof_struct_ifma_msghdr
|
||||
sizeofIfMsghdr2Darwin15 = C.sizeof_struct_if_msghdr2
|
||||
sizeofIfmaMsghdr2Darwin15 = C.sizeof_struct_ifma_msghdr2
|
||||
sizeofIfDataDarwin15 = C.sizeof_struct_if_data
|
||||
sizeofIfData64Darwin15 = C.sizeof_struct_if_data64
|
||||
|
||||
sizeofRtMsghdrDarwin15 = C.sizeof_struct_rt_msghdr
|
||||
sizeofRtMsghdr2Darwin15 = C.sizeof_struct_rt_msghdr2
|
||||
sizeofRtMetricsDarwin15 = C.sizeof_struct_rt_metrics
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user