Compare commits

..

19 Commits

Author SHA1 Message Date
Jakob Borg
c513171014 gui: Update translations 2016-05-26 09:49:07 +02:00
Jakob Borg
da5010d37a cmd/syncthing: Use API to generate API Key and folder ID (fixes #3179)
Expose a random string generator in the API and use it when the GUI
needs random strings for API key and folder ID.

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3192
2016-05-26 07:25:34 +00:00
Jakob Borg
e6b78e5d56 lib/rand: Break out random functions into separate package
The intention for this package is to provide a combination of the
security of crypto/rand and the convenience of math/rand. It should be
the first choice of random data unless ultimate performance is required
and the usage is provably irrelevant from a security standpoint.

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3186
2016-05-26 07:02:56 +00:00
Audrius Butkevicius
410d700ae3 cmd/syncthing: Do not modify events (fixes #3002)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3190
2016-05-26 06:54:44 +00:00
Audrius Butkevicius
fc173bf679 lib/model: Fix wild completion percentages
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3188
2016-05-26 06:53:27 +00:00
Jakob Borg
72154aa668 lib/upgrade: Prefer a minor upgrade over a major (fixes #3163)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3184
2016-05-25 14:01:52 +00:00
Jakob Borg
31b5156191 lib/util: Add secure random numbers source (fixes #3178)
The math/rand package contains lots of convenient functions, for example
to get an integer in a specified range without running into issues
caused by just truncating a number from a different distribution and so
on. But it's insecure, and we use if for things that benefit from being
more secure like session IDs, CSRF tokens and API keys.

This implements a math/rand.Source that reads from crypto/rand.Reader,
this bridging the gap between them. It also updates our RandomString to
use the new source, thus giving us secure session IDs and CSRF tokens.

Some future work remains:

 - Fix API keys by making the generation in the UI use this code as well

 - Refactor out these things into an actual random package, and audit
   our use of randomness everywhere

I'll leave both of those for the future in order to not muddy the waters
on this diff...

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3180
2016-05-25 06:38:38 +00:00
Lars K.W. Gohlke
ebce5d07ac lib/connections: Shorten connection limiting lines
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3177
2016-05-24 21:57:56 +00:00
Audrius Butkevicius
915e1ac7de lib/model: Handle (?d) deletes of directories (fixes #3164)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3170
2016-05-23 23:32:08 +00:00
Lars K.W. Gohlke
b78bfc0a43 build.go: add gometalinter to lint runs
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3085
2016-05-23 21:19:08 +00:00
Lars K.W. Gohlke
30436741a7 build: Also vet and lint build script
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3159
2016-05-23 12:23:55 +00:00
Jakob Borg
98734375f2 cmd/syncthing: Correctly set, parse and compare modified time HTTP headers (fixes #3165)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3167
2016-05-23 12:16:14 +00:00
norgeous
37816e3818 gui: Remove extra href on folder panel titles
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3139
2016-05-22 16:17:33 +00:00
Jakob Borg
4bc2b3f369 gui: Set CSRF stuff earlier (fixes #3138)
We need to set these properties *before* Angular starts making requests,
and doing that from the response to a request is too late. The obvious
choice (to me) would be to use the angular $cookies service, but that
service isn't available until after initialization so we can't use it.
Instead, add a special file that is loaded by index.html and includes
the info we need before the JS app even starts running.

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3152
2016-05-22 10:26:09 +00:00
Audrius Butkevicius
00be2bf18d lib/model: Track puller creation times (fixes #3145)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3150
2016-05-22 10:16:09 +00:00
Jakob Borg
44290a66b7 lib/model: Leave temp file in place when final rename fails (fixes #3146)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3148
2016-05-22 09:06:07 +00:00
Jakob Borg
f6cc344623 vendor: Replace github.com/jackpal/gateway with github.com/calmh/gateway (fixes #3142)
Switch to my forked version which contains a fix for this issue. I'll
track upstream in the future if things update there, and attempt to
contribute back fixes...

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3149
2016-05-22 09:04:27 +00:00
Jakob Borg
a89d487510 vendor: Bump github.com/AudriusButkevicius/go-nat-pmp 2016-05-22 17:46:36 +09:00
Jakob Borg
a0ec4467fd cmd/syncthing: Emit new RemoteDownloadProgress event to track remote download progress
Without this the summary service doesn't know to recalculate completion
percentage for remote devices when DownloadProgress messages come in.
That means that completion percentage isn't updated in the GUI while
transfers of large files are ongoing. With this change, it updates
correctly.

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3144
2016-05-22 07:52:08 +00:00
54 changed files with 981 additions and 336 deletions

View File

@@ -217,11 +217,19 @@ func main() {
clean()
case "vet":
vet("build.go")
vet("cmd", "lib")
case "lint":
lint(".")
lint("./cmd/...")
lint("./lib/...")
if isGometalinterInstalled() {
dirs := []string{".", "./cmd/...", "./lib/..."}
gometalinter("deadcode", dirs, "test/util.go")
gometalinter("structcheck", dirs)
gometalinter("varcheck", dirs)
}
default:
log.Fatalf("Unknown command %q", cmd)
@@ -279,6 +287,7 @@ func setup() {
runPrint("go", "get", "-v", "github.com/axw/gocov/gocov")
runPrint("go", "get", "-v", "github.com/AlekSi/gocov-xml")
runPrint("go", "get", "-v", "bitbucket.org/tebeka/go2xunit")
runPrint("go", "get", "-v", "github.com/alecthomas/gometalinter")
}
func test(pkgs ...string) {
@@ -860,7 +869,6 @@ func vet(dirs ...string) {
// A genuine error exit from the vet tool.
log.Fatal(err)
}
}
func lint(pkg string) {
@@ -909,3 +917,34 @@ func exitStatus(err error) int {
return -1
}
func isGometalinterInstalled() bool {
if _, err := runError("gometalinter", "--disable-all"); err != nil {
log.Println("gometalinter is not installed")
return false
}
return true
}
func gometalinter(linter string, dirs []string, excludes ...string) {
params := []string{"--disable-all"}
params = append(params, fmt.Sprintf("--deadline=%ds", 60))
params = append(params, "--enable="+linter)
for _, exclude := range excludes {
params = append(params, "--exclude="+exclude)
}
for _, dir := range dirs {
params = append(params, dir)
}
bs, err := runError("gometalinter", params...)
if len(bs) > 0 {
log.Printf("%s", bs)
}
if err != nil {
log.Printf("%v", err)
}
}

View File

@@ -35,11 +35,11 @@ import (
"github.com/syncthing/syncthing/lib/model"
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/rand"
"github.com/syncthing/syncthing/lib/stats"
"github.com/syncthing/syncthing/lib/sync"
"github.com/syncthing/syncthing/lib/tlsutil"
"github.com/syncthing/syncthing/lib/upgrade"
"github.com/syncthing/syncthing/lib/util"
"github.com/vitrun/qart/qr"
"golang.org/x/crypto/bcrypt"
)
@@ -250,6 +250,7 @@ func (s *apiService) Serve() {
getRestMux.HandleFunc("/rest/svc/deviceid", s.getDeviceID) // id
getRestMux.HandleFunc("/rest/svc/lang", s.getLang) // -
getRestMux.HandleFunc("/rest/svc/report", s.getReport) // -
getRestMux.HandleFunc("/rest/svc/random/string", s.getRandomString) // [length]
getRestMux.HandleFunc("/rest/system/browse", s.getSystemBrowse) // current
getRestMux.HandleFunc("/rest/system/config", s.getSystemConfig) // -
getRestMux.HandleFunc("/rest/system/config/insync", s.getSystemConfigInsync) // -
@@ -298,13 +299,16 @@ func (s *apiService) Serve() {
// Serve compiled in assets unless an asset directory was set (for development)
assets := &embeddedStatic{
theme: s.cfg.GUI().Theme,
lastModified: time.Now(),
lastModified: time.Now().Truncate(time.Second), // must truncate, for the wire precision is 1s
mut: sync.NewRWMutex(),
assetDir: s.assetDir,
assets: auto.Assets(),
}
mux.Handle("/", assets)
// Handle the special meta.js path
mux.HandleFunc("/meta.js", s.getJSMetadata)
s.cfg.Subscribe(assets)
guiCfg := s.cfg.GUI()
@@ -525,6 +529,14 @@ func (s *apiService) restPing(w http.ResponseWriter, r *http.Request) {
sendJSON(w, map[string]string{"ping": "pong"})
}
func (s *apiService) getJSMetadata(w http.ResponseWriter, r *http.Request) {
meta, _ := json.Marshal(map[string]string{
"deviceID": s.id.String(),
})
w.Header().Set("Content-Type", "application/javascript")
fmt.Fprintf(w, "var metadata = %s;\n", meta)
}
func (s *apiService) getSystemVersion(w http.ResponseWriter, r *http.Request) {
sendJSON(w, map[string]string{
"version": Version,
@@ -739,7 +751,7 @@ func (s *apiService) postSystemConfig(w http.ResponseWriter, r *http.Request) {
if curAcc := s.cfg.Options().URAccepted; to.Options.URAccepted > curAcc {
// UR was enabled
to.Options.URAccepted = usageReportVersion
to.Options.URUniqueID = util.RandomString(8)
to.Options.URUniqueID = rand.String(8)
} else if to.Options.URAccepted < curAcc {
// UR was disabled
to.Options.URAccepted = -1
@@ -919,6 +931,16 @@ func (s *apiService) getReport(w http.ResponseWriter, r *http.Request) {
sendJSON(w, reportData(s.cfg, s.model))
}
func (s *apiService) getRandomString(w http.ResponseWriter, r *http.Request) {
length := 32
if val, _ := strconv.Atoi(r.URL.Query().Get("length")); val > 0 {
length = val
}
str := rand.String(length)
sendJSON(w, map[string]string{"random": str})
}
func (s *apiService) getDBIgnores(w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()
@@ -1224,7 +1246,8 @@ func (s embeddedStatic) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}
if modifiedSince, err := time.Parse(r.Header.Get("If-Modified-Since"), http.TimeFormat); err == nil && modified.Before(modifiedSince) {
modifiedSince, err := http.ParseTime(r.Header.Get("If-Modified-Since"))
if err == nil && !modified.After(modifiedSince) {
w.WriteHeader(http.StatusNotModified)
return
}
@@ -1243,7 +1266,7 @@ func (s embeddedStatic) ServeHTTP(w http.ResponseWriter, r *http.Request) {
gr.Close()
}
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(bs)))
w.Header().Set("Last-Modified", modified.Format(http.TimeFormat))
w.Header().Set("Last-Modified", modified.UTC().Format(http.TimeFormat))
w.Header().Set("Cache-Control", "public")
w.Write(bs)

View File

@@ -9,15 +9,14 @@ package main
import (
"bytes"
"encoding/base64"
"math/rand"
"net/http"
"strings"
"time"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/rand"
"github.com/syncthing/syncthing/lib/sync"
"github.com/syncthing/syncthing/lib/util"
"golang.org/x/crypto/bcrypt"
)
@@ -114,7 +113,7 @@ func basicAuthAndSessionMiddleware(cookieName string, cfg config.GUIConfiguratio
return
passwordOK:
sessionid := util.RandomString(32)
sessionid := rand.String(32)
sessionsMut.Lock()
sessions[sessionid] = true
sessionsMut.Unlock()

View File

@@ -15,8 +15,8 @@ import (
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/rand"
"github.com/syncthing/syncthing/lib/sync"
"github.com/syncthing/syncthing/lib/util"
)
// csrfTokens is a list of valid tokens. It is sorted so that the most
@@ -87,7 +87,7 @@ func validCsrfToken(token string) bool {
}
func newCsrfToken() string {
token := util.RandomString(32)
token := rand.String(32)
csrfMut.Lock()
csrfTokens = append([]string{token}, csrfTokens...)

View File

@@ -9,6 +9,7 @@ package main
import (
"bytes"
"compress/gzip"
"encoding/json"
"fmt"
"io/ioutil"
"net"
@@ -570,3 +571,52 @@ func TestCSRFRequired(t *testing.T) {
t.Fatal("Getting /rest/system/config with API key should succeed, not", resp.Status)
}
}
func TestRandomString(t *testing.T) {
const testAPIKey = "foobarbaz"
cfg := new(mockedConfig)
cfg.gui.APIKey = testAPIKey
baseURL, err := startHTTP(cfg)
if err != nil {
t.Fatal(err)
}
cli := &http.Client{
Timeout: time.Second,
}
// The default should be to return a 32 character random string
for _, url := range []string{"/rest/svc/random/string", "/rest/svc/random/string?length=-1", "/rest/svc/random/string?length=yo"} {
req, _ := http.NewRequest("GET", baseURL+url, nil)
req.Header.Set("X-API-Key", testAPIKey)
resp, err := cli.Do(req)
if err != nil {
t.Fatal(err)
}
var res map[string]string
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
t.Fatal(err)
}
if len(res["random"]) != 32 {
t.Errorf("Expected 32 random characters, got %q of length %d", res["random"], len(res["random"]))
}
}
// We can ask for a different length if we like
req, _ := http.NewRequest("GET", baseURL+"/rest/svc/random/string?length=27", nil)
req.Header.Set("X-API-Key", testAPIKey)
resp, err := cli.Do(req)
if err != nil {
t.Fatal(err)
}
var res map[string]string
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
t.Fatal(err)
}
if len(res["random"]) != 27 {
t.Errorf("Expected 27 random characters, got %q of length %d", res["random"], len(res["random"]))
}
}

View File

@@ -39,10 +39,10 @@ import (
"github.com/syncthing/syncthing/lib/model"
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/rand"
"github.com/syncthing/syncthing/lib/symlinks"
"github.com/syncthing/syncthing/lib/tlsutil"
"github.com/syncthing/syncthing/lib/upgrade"
"github.com/syncthing/syncthing/lib/util"
"github.com/thejerf/suture"
)
@@ -761,7 +761,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
if opts.URUniqueID == "" {
// Previously the ID was generated from the node ID. We now need
// to generate a new one.
opts.URUniqueID = util.RandomString(8)
opts.URUniqueID = rand.String(8)
cfg.SetOptions(opts)
cfg.Save()
}
@@ -947,7 +947,7 @@ func defaultConfig(myName string) config.Configuration {
if !noDefaultFolder {
l.Infoln("Default folder created and/or linked to new config")
folderID := util.RandomString(5) + "-" + util.RandomString(5)
folderID := rand.String(5) + "-" + rand.String(5)
defaultFolder = config.NewFolderConfiguration(folderID, locations[locDefFolder])
defaultFolder.Label = "Default Folder (" + folderID + ")"
defaultFolder.RescanIntervalS = 60

View File

@@ -59,7 +59,7 @@ func (c *folderSummaryService) Stop() {
// listenForUpdates subscribes to the event bus and makes note of folders that
// need their data recalculated.
func (c *folderSummaryService) listenForUpdates() {
sub := events.Default.Subscribe(events.LocalIndexUpdated | events.RemoteIndexUpdated | events.StateChanged)
sub := events.Default.Subscribe(events.LocalIndexUpdated | events.RemoteIndexUpdated | events.StateChanged | events.RemoteDownloadProgress)
defer events.Default.Unsubscribe(sub)
for {

View File

@@ -132,11 +132,14 @@ func (s *verboseService) formatEvent(ev events.Event) string {
case events.FolderSummary:
data := ev.Data.(map[string]interface{})
sum := data["summary"].(map[string]interface{})
delete(sum, "invalid")
delete(sum, "ignorePatterns")
delete(sum, "stateChanged")
return fmt.Sprintf("Summary for folder %q is %v", data["folder"], data["summary"])
sum := make(map[string]interface{})
for k, v := range data["summary"].(map[string]interface{}) {
if k == "invalid" || k == "ignorePatterns" || k == "stateChanged" {
continue
}
sum[k] = v
}
return fmt.Sprintf("Summary for folder %q is %v", data["folder"], sum)
case events.FolderScanProgress:
data := ev.Data.(map[string]interface{})

View File

@@ -38,7 +38,7 @@ ul+h5 {
text-overflow: ellipsis;
overflow: hidden;
}
.panel-title a:hover {
a.panel-heading:hover {
text-decoration: none;
}

View File

@@ -32,7 +32,7 @@
"Comment, when used at the start of a line": "Komentář, pokud použito na začátku řádku",
"Compression": "Komprese",
"Connection Error": "Chyba připojení",
"Connection Type": "Connection Type",
"Connection Type": "Typ připojení",
"Copied from elsewhere": "Zkopírováno odjinud",
"Copied from original": "Zkopírováno z originálu",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 následující přispěvatelé:",
@@ -75,7 +75,7 @@
"Folder Label": "Jmenovka adresáře",
"Folder Master": "Master adresář",
"Folder Path": "Cesta k adresáři",
"Folder Type": "Folder Type",
"Folder Type": "Typ adresáře",
"Folders": "Adresáře",
"GUI": "GUI",
"GUI Authentication Password": "Přihlašovací heslo pro GUI",
@@ -100,7 +100,7 @@
"Last File Received": "Poslední přijatý soubor",
"Last seen": "Naposledy spatřen",
"Later": "Později",
"Listeners": "Listeners",
"Listeners": "Naslouchající",
"Local Discovery": "Místní oznamování",
"Local State": "Místní status",
"Local State (Total)": "Místní status (Celkem)",
@@ -117,7 +117,7 @@
"Newest First": "Od nejnovějšího",
"No": "Ne",
"No File Versioning": "Bez verzování souborů",
"Normal": "Normal",
"Normal": "Normální",
"Notice": "Oznámení",
"OK": "OK",
"Off": "Vypnuta",

View File

@@ -0,0 +1,249 @@
{
"A device with that ID is already added.": "이 기기 ID는 이미 추가되었습니다.",
"A negative number of days doesn't make sense.": "음수로는 지정할 수 없습니다.",
"A new major version may not be compatible with previous versions.": "새로운 메이저 버전은 이전 버전과 호환되지 않을 수 있습니다.",
"API Key": "API 키",
"About": " 정보",
"Actions": "동작",
"Add": "추가",
"Add Device": "기기 추가",
"Add Folder": "폴더 추가",
"Add Remote Device": "다른 기기 추가",
"Add new folder?": "새로운 폴더를 추가하시겠습니까?",
"Address": "주소",
"Addresses": "주소",
"Advanced": "고급",
"Advanced Configuration": "고급 설정",
"Advanced settings": "고급 설정",
"All Data": "전체 데이터",
"Allow Anonymous Usage Reporting?": "익명 사용 보고서를 보내시겠습니까?",
"Alphabetic": "알파벳순",
"An external command handles the versioning. It has to remove the file from the synced folder.": "외부 커맨드가 파일 버전을 관리합니다. 동기화된 폴더에서 파일을 삭제해야 합니다.",
"Anonymous Usage Reporting": "익명 사용 보고서",
"Any devices configured on an introducer device will be added to this device as well.": "유도 장치에 추가된 기기들은 이 기기에도 동시에 추가됩니다.",
"Automatic upgrades": "자동 업데이트",
"Be careful!": "주의!",
"Bugs": "버그",
"CPU Utilization": "CPU 사용률",
"Changelog": "바뀐 점",
"Clean out after": "삭제 후",
"Close": "닫기",
"Command": "커맨드",
"Comment, when used at the start of a line": "명령행에서 시작을 할수 있어요.",
"Compression": "압축",
"Connection Error": "연결 에러",
"Connection Type": "연결 종류",
"Copied from elsewhere": "다른 곳에서 복사됨",
"Copied from original": "원본에서 복사됨",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
"Copyright © 2015 the following Contributors:": "Copyright © 2015 the following Contributors:",
"Danger!": "경고!",
"Delete": "삭제",
"Deleted": "삭제됨",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "다른 기기 {{device}} ({{address}}) 에서 접속을 요청했습니다. 새 장치를 추가하시겠습니까?",
"Device ID": "기기 ID",
"Device Identification": "기기 식별자",
"Device Name": "기기 이름",
"Device {%device%} ({%address%}) wants to connect. Add new device?": "다른 기기 {{device}} ({{address}}) 에서 접속을 요청했습니다. 새 장치를 추가하시겠습니까?",
"Devices": "기기",
"Disconnected": "연결 끊김",
"Discovery": "탐색",
"Documentation": "문서",
"Download Rate": "다운로드 속도",
"Downloaded": "다운로드됨",
"Downloading": "다운로드 중",
"Edit": "편집",
"Edit Device": "기기 편집",
"Edit Folder": "폴더 편집",
"Editing": "편집",
"Enable NAT traversal": "NAT traversal 활성화",
"Enable Relaying": "Relaying 활성화",
"Enable UPnP": "UPnP 활성화",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "주소 자동 검색을 하기 위해서는 \"ip:port\" 형식의 주소들을 쉼표로 구분해서 입력하거나 \"dynamic\"을 입력하세요.",
"Enter ignore patterns, one per line.": "무시할 패턴을 한 줄에 하나씩 입력하세요.",
"Error": "오류",
"External File Versioning": "외부 파일 버전 관리",
"Failed Items": "실패한 항목",
"File Pull Order": "파일 동기화 순서",
"File Versioning": "파일 버전 관리",
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "파일을 동기화할 때 파일 권한이 무시됩니다. FAT 파일 시스템에서 사용하세요.",
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "파일이 Syncthing에 의해서 교체되거나 삭제되면 .stversions 폴더로 이동됩니다.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "파일이 Syncthing에 의해서 교체되거나 삭제되면 .stversions 폴더에 있는 날짜가 바뀐 버전으로 이동됩니다.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "다른 장치가 파일을 편집할 수 없으며 반드시 이 장치의 내용을 기준으로 동기화합니다.",
"Folder": "폴더",
"Folder ID": "폴더 ID",
"Folder Label": "폴더 라벨",
"Folder Master": "폴더 소유자",
"Folder Path": "폴더 경로",
"Folder Type": "폴더 유형",
"Folders": "폴더",
"GUI": "GUI",
"GUI Authentication Password": "GUI 인증 비밀번호",
"GUI Authentication User": "GUI 인증 사용자",
"GUI Listen Addresses": "GUI 주소",
"Generate": "생성",
"Global Discovery": "글로벌 탐색",
"Global Discovery Server": "글로벌 탐색 서버",
"Global Discovery Servers": "글로벌 탐색 서버",
"Global State": "글로벌 서버 상태",
"Help": "도움말",
"Home page": "홈페이지",
"Ignore": "무시",
"Ignore Patterns": "패턴 무시",
"Ignore Permissions": "권한 무시",
"Incoming Rate Limit (KiB/s)": "다운로드 속도 제한 (KiB/S)",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "잘못된 설정은 폴더의 컨텐츠를 훼손하거나 Syncthing의 오작동을 일으킬 수 있습니다.",
"Introducer": "유도",
"Inversion of the given condition (i.e. do not exclude)": "주어진 조건의 반대(전혀 배제하지 않음)",
"Keep Versions": "버전 보관",
"Largest First": "큰 파일 순",
"Last File Received": "마지막으로 받은 파일",
"Last seen": "마지막 접속",
"Later": "나중에",
"Listeners": "수신자",
"Local Discovery": "로컬 노드 검색",
"Local State": "로컬 상태",
"Local State (Total)": "로컬 상태 (합계)",
"Major Upgrade": "메이저 업데이트",
"Master": "마스터",
"Maximum Age": "최대 보존 기간",
"Metadata Only": "메타데이터만",
"Minimum Free Disk Space": "최소 여유 디스크 용량",
"Move to top of queue": "대기열 상단으로 이동",
"Multi level wildcard (matches multiple directory levels)": "다중 레벨 와일드 카드 (여러 단계의 디렉토리와 일치하는 경우)",
"Never": "사용 안 함",
"New Device": "새 기기",
"New Folder": "새 폴더",
"Newest First": "새로운 파일순",
"No": "아니오",
"No File Versioning": "파일 버전 관리 안 함",
"Normal": "일반",
"Notice": "공지",
"OK": "확인",
"Off": "꺼짐",
"Oldest First": "오래된 파일순",
"Optional descriptive label for the folder. Can be different on each device.": "폴더 라벨은 편의를 위한 것입니다. 기기마다 다르게 설정할 수 있습니다.",
"Options": "옵션",
"Out of Sync": "동기화 오류",
"Out of Sync Items": "동기화되지 않은 항목",
"Outgoing Rate Limit (KiB/s)": "업로드 속도 제한 (KiB/s)",
"Override Changes": "덮어쓰기",
"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": "로컬 컴퓨터에 있는 폴더의 경로를 지정합니다. 존재하지 않는 폴더일 경우 자동으로 생성됩니다. 물결 기호 (~)는 아래와 같은 폴더를 나타냅니다.",
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "버전을 보관할 경로 (비워둘 시 기본값 .stversions 폴더로 지정됨)",
"Pause": "일시 중지",
"Paused": "일시 중지됨",
"Please consult the release notes before performing a major upgrade.": "메이저 업데이트를 하기 전에 먼저 릴리즈 노트를 살펴보세요.",
"Please set a GUI Authentication User and Password in the Settings dialog.": "설정에서 GUI 인증용 User와 암호를 입력해주세요.",
"Please wait": "기다려 주십시오",
"Preview": "미리보기",
"Preview Usage Report": "사용 보고서 미리보기",
"Quick guide to supported patterns": "지원하는 패턴에 대한 빠른 도움말",
"RAM Utilization": "RAM 사용량",
"Random": "무작위",
"Relay Servers": "중계 서버",
"Relayed via": "중계 중인 서버 주소",
"Relays": "중계",
"Release Notes": "릴리즈 노트",
"Remote Devices": "원격 기기",
"Remove": "삭제",
"Required identifier for the folder. Must be the same on all cluster devices.": "폴더 식별자가 필요합니다. 모든 장치에서 동일해야 합니다.",
"Rescan": "재탐색",
"Rescan All": "전체 재탐색",
"Rescan Interval": "재탐색 간격",
"Restart": "재시작",
"Restart Needed": "재시작 필요함",
"Restarting": "재시작 중",
"Resume": "재개",
"Reused": "재개",
"Save": "저장",
"Scan Time Remaining": "탐색 남은 시간",
"Scanning": "탐색중",
"Select the devices to share this folder with.": "이 폴더를 공유할 장치를 선택합니다.",
"Select the folders to share with this device.": "이 장치와 공유할 폴더를 선택합니다.",
"Settings": "설정",
"Share": "공유",
"Share Folder": "폴더 공유",
"Share Folders With Device": "폴더를 공유할 기기",
"Share With Devices": "공유할 기기",
"Share this folder?": "이 폴더를 공유하시겠습니까?",
"Shared With": "~와 공유",
"Short identifier for the folder. Must be the same on all cluster devices.": "간단한 폴더 식별자입니다. 모든 장치에서 동일해야 합니다.",
"Show ID": "내 기기 ID",
"Show QR": "QR 코드 보기",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "장치에 대한 아이디로 표시됩니다. 옵션에 얻은 기본이름으로 다른장치에 통보합니다.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "아이디가 비어있는 경우 기본 값으로 다른 장치에 업데이트 됩니다.",
"Shutdown": "종료",
"Shutdown Complete": "종료 완료",
"Simple File Versioning": "간단한 파일 버전 관리",
"Single level wildcard (matches within a directory only)": "단일 레벨 와일드카드 (하나의 디렉토리만 일치하는 경우)",
"Smallest First": "작은 파일순",
"Source Code": "소스 코드",
"Staggered File Versioning": "타임스탬프 기준 파일 버전 관리",
"Start Browser": "브라우저 열기",
"Statistics": "통계",
"Stopped": "중지됨",
"Support": "지원",
"Sync Protocol Listen Addresses": "동기화 프로토콜 수신 주소",
"Syncing": "동기화 중",
"Syncthing has been shut down.": "Syncthing이 종료되었습니다.",
"Syncthing includes the following software or portions thereof:": "Syncthing은 다음과 같은 소프트웨어나 그 일부를 포함합니다:",
"Syncthing is restarting.": "Syncthing이 재시작 중입니다.",
"Syncthing is upgrading.": "Syncthing이 업데이트 중입니다.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing이 중지되었거나 인터넷 연결에 문제가 있는 것 같습니다. 재시도 중입니다...",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing에서 요청을 처리하는 중에 문제가 발생했습니다. 계속 문제가 발생하면 페이지를 다시 불러오거나 Syncthing을 재시작해 보세요.",
"The Syncthing admin interface is configured to allow remote access without a password.": "Syncthing 관리자 인터페이스가 암호 없이 원격 접속이 허가되도록 설정되었습니다.",
"The aggregated statistics are publicly available at {%url%}.": "수집된 통계는 {{URL}} 에서 공개적으로 볼 수 있습니다.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "설정이 저장되었지만 활성화되지 않았습니다. 설정을 활성화 하려면 Syncthing을 다시 시작하세요.",
"The device ID cannot be blank.": "기기 ID는 비워 둘 수 없습니다.",
"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).": "여기에 입력한 기기 ID가 다른 장치의 \"동작 - ID 보기\"에 표시됩니다. 공백과 하이픈은 세지 않습니다. 즉 무시됩니다.",
"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).": "여기에 입력한 기기 ID가 다른 장치의 \"편집 - ID 보기\"에 표시됩니다. 공백과 하이픈은 세지 않습니다. 즉 무시됩니다.",
"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.": "암호화된 사용 보고서는 매일 전송됩니다 사용 중인 플랫폼과 폴더 크기, 앱 버전이 포함되어 있습니다. 전송되는 데이터가 변경되면 다시 이 대화 상자가 나타납니다.",
"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.": "입력한 기기 ID가 올바르지 않습니다. 52/56자의 알파벳과 숫자로 구성되어 있으며, 공백과 하이픈은 포함되지 않습니다.",
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "첫 명령행 옵션은 폴더 경로이고 두 번째 명령행 옵션은 폴더의 상대 경로입니다.",
"The folder ID cannot be blank.": "폴더 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.": "폴더 ID는 문자, 숫자, 마침표(.), 붙임표(-), 밑줄 문자(_)로만 구성되어 있는 짧은 식별자(64자 이하)여야 합니다.",
"The folder ID must be unique.": "폴더 ID는 중복될 수 없습니다.",
"The folder path cannot be blank.": "폴더 경로는 비워 둘 수 없습니다.",
"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.": "다음과 같은 간격이 사용됩니다: 첫 한 시간 동안은 버전이 매 30초마다 유지되며, 첫 하루 동안은 매 시간, 첫 한 달 동안은 매 일마다 유지됩니다. 그리고 최대 날짜까지는 버전이 매 주마다 유지됩니다.",
"The following items could not be synchronized.": "이 항목들은 동기화 할 수 없습니다.",
"The maximum age must be a number and cannot be blank.": "최대 보존 기간은 숫자여야 하며 비워 둘 수 없습니다.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "버전을 유지할 최대 시간을 지정합니다. 일단위이며 버전을 계속 유지하려면 0을 입력하세요,",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "최소 여유 디스크 용량의 퍼센티지 설정은 0부터 100 까지 가능합니다.",
"The number of days must be a number and cannot be blank.": "날짜는 숫자여야 하며 비워 둘 수 없습니다.",
"The number of days to keep files in the trash can. Zero means forever.": "설정된 날짜 동안 파일이 휴지통에 보관됩니다. 0은 무제한입니다.",
"The number of old versions to keep, per file.": "각 파일별로 유지할 이전 버전의 개수를 지정합니다.",
"The number of versions must be a number and cannot be blank.": "버전 개수는 숫자여야 하며 비워 둘 수 없습니다.",
"The path cannot be blank.": "경로는 비워 둘 수 없습니다.",
"The rate limit must be a non-negative number (0: no limit)": "대역폭 제한 설정은 반드시 양수로 입력해야 합니다 (0: 무제한)",
"The rescan interval must be a non-negative number of seconds.": "재검색 간격은 초단위이며 양수로 입력해야 합니다.",
"They are retried automatically and will be synced when the error is resolved.": "오류가 해결되면 자동적으로 동기화 됩니다.",
"This Device": "현재 기기",
"This can easily give hackers access to read and change any files on your computer.": "이 설정은 해커가 손쉽게 사용자 컴퓨터의 모든 파일을 읽고 변경할 수 있도록 할 수 있습니다.",
"This is a major version upgrade.": "이 업데이트는 메이저 버전입니다.",
"Trash Can File Versioning": "휴지통을 통한 파일 버전 관리",
"Unknown": "알 수 없음",
"Unshared": "공유되지 않음",
"Unused": "사용되지 않음",
"Up to Date": "최신 데이터",
"Updated": "업데이트 완료",
"Upgrade": "업데이트",
"Upgrade To {%version%}": "{{version}} 으로 업데이트",
"Upgrading": "업데이트 중",
"Upload Rate": "업로드 속도",
"Uptime": "가동 시간",
"Use HTTPS for GUI": "GUI에서 HTTPS 프로토콜 사용",
"Version": "버전",
"Versions Path": "버전 저장 경로",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "최대 보존 기간보다 오래되었거나 지정한 개수를 넘긴 버전은 자동으로 삭제됩니다.",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "경고, 이 경로는 현재 존재하는 폴더 \"{{otherFolder}}\" 의 하위 폴더 입니다.",
"When adding a new device, keep in mind that this device must be added on the other side too.": "새 장치를 추가할 시 추가한 기기 쪽에서도 이 장치를 추가해야 합니다.",
"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.": "새 폴더를 추가할 시 폴더 ID는 장치간에 폴더를 묶을 때 사용됩니다. 대소문자를 구분하며 모든 장치에서 같은 ID를 사용해야 합니다.",
"Yes": "예",
"You must keep at least one version.": "최소 한 개의 버전은 유지해야 합니다.",
"days": "일",
"full documentation": "전체 문서",
"items": "항목",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} 에서 폴더 \\\"{{folder}}\\\" 를 공유하길 원합니다.",
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} 에서 폴더 \"{{folderLabel}}\" ({{folder}})를 공유하길 원합니다.",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} 에서 폴더 \"{{folderLabel}}\" ({{folder}})를 공유하길 원합니다."
}

View File

@@ -32,7 +32,7 @@
"Comment, when used at the start of a line": "Komentarz, jeżeli użyty na początku linii",
"Compression": "Kompresja",
"Connection Error": "Błąd połączenia",
"Connection Type": "Connection Type",
"Connection Type": "Rodzaj połączenia",
"Copied from elsewhere": "Skopiowane z innego miejsca ",
"Copied from original": "Skopiowane z oryginału",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016: ",
@@ -75,7 +75,7 @@
"Folder Label": "Etykieta folderu",
"Folder Master": "Główny folder",
"Folder Path": "Ścieżka folderu",
"Folder Type": "Folder Type",
"Folder Type": "Rodzaj folderu",
"Folders": "Foldery",
"GUI": "GUI",
"GUI Authentication Password": "Hasło",
@@ -100,12 +100,12 @@
"Last File Received": "Ostatni otrzymany plik",
"Last seen": "Ostatnio widziany",
"Later": "Później",
"Listeners": "Listeners",
"Listeners": "Nasłuchujący",
"Local Discovery": "Lokalne odnajdywanie",
"Local State": "Status lokalny",
"Local State (Total)": "Status lokalny (suma)",
"Major Upgrade": "Ważna aktualizacja",
"Master": "Master",
"Master": "Mistrz",
"Maximum Age": "Maksymalny wiek",
"Metadata Only": "Tylko metadane",
"Minimum Free Disk Space": "Minimum wolnego miejsca na dysku",
@@ -117,7 +117,7 @@
"Newest First": "Najnowsze na początku",
"No": "Nie",
"No File Versioning": "Bez wersjonowania pliku",
"Normal": "Normal",
"Normal": "Zwykły",
"Notice": "Wskazówka",
"OK": "OK",
"Off": "Wyłącz",

View File

@@ -103,7 +103,7 @@
"Listeners": "Listeners",
"Local Discovery": "Локальное обнаружение",
"Local State": "Локальное состояние",
"Local State (Total)": "Локально (всего)",
"Local State (Total)": "Локальное состояние (всего)",
"Major Upgrade": "Обновление основной версии",
"Master": "Master",
"Maximum Age": "Максимальный срок",

View File

@@ -32,7 +32,7 @@
"Comment, when used at the start of a line": "Kommentar, vid början av en rad.",
"Compression": "Komprimering",
"Connection Error": "Anslutningsproblem",
"Connection Type": "Connection Type",
"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:",
@@ -75,7 +75,7 @@
"Folder Label": "Katalog etikett",
"Folder Master": "Huvudlagring",
"Folder Path": "Sökväg",
"Folder Type": "Folder Type",
"Folder Type": "Katalogtyp",
"Folders": "Kataloger",
"GUI": "GUI",
"GUI Authentication Password": "GUI-lösenord",
@@ -100,12 +100,12 @@
"Last File Received": "Senast Mottagna Fil",
"Last seen": "Senast online",
"Later": "Senare",
"Listeners": "Listeners",
"Listeners": "Lyssnare",
"Local Discovery": "Lokal uppslagning",
"Local State": "Lokal status",
"Local State (Total)": "Lokal status (Total)",
"Major Upgrade": "Stor uppgradering",
"Master": "Master",
"Master": "Huvud",
"Maximum Age": "Högsta åldersgräns",
"Metadata Only": "Endast metadata",
"Minimum Free Disk Space": "Minimum ledigt diskutrymme",

View File

@@ -32,7 +32,7 @@
"Comment, when used at the start of a line": "Коментар, якщо використовується на початку рядка",
"Compression": "Стиснення",
"Connection Error": "Помилка з’єднання",
"Connection Type": "Connection Type",
"Connection Type": "Тип з*єднання",
"Copied from elsewhere": "Скопійовано з іншого місця",
"Copied from original": "Скопійовано з оригіналу",
"Copyright © 2014-2016 the following Contributors:": "© 2014-2016 Всі права застережено, вклад внесли:",
@@ -47,7 +47,7 @@
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Пристрій {{device}} ({{address}}) намагається під’єднатися. Додати новий пристрій?",
"Devices": "Пристрої",
"Disconnected": "З’єднання відсутнє",
"Discovery": "Сервери обходу NAT",
"Discovery": "Сервери координації NAT",
"Documentation": "Документація",
"Download Rate": "Швидкість завантаження",
"Downloaded": "Завантажено",
@@ -75,7 +75,7 @@
"Folder Label": "Мітка директорії",
"Folder Master": "Вважати за оригінал",
"Folder Path": "Шлях до директорії",
"Folder Type": "Folder Type",
"Folder Type": "Тип директорії",
"Folders": "Директорії",
"GUI": "Графічний інтерфейс",
"GUI Authentication Password": "Пароль для доступу до панелі управління",
@@ -84,7 +84,7 @@
"Generate": "Згенерувати",
"Global Discovery": "Глобальне виявлення (internet)",
"Global Discovery Server": "Сервер для глобального виявлення",
"Global Discovery Servers": "Сервери глобального виявлення \n(координації обходу NAT)",
"Global Discovery Servers": "Сервери глобального виявлення \n(координації NAT)",
"Global State": "Глобальний статус",
"Help": "Допомога",
"Home page": "Домашня сторінка",
@@ -100,12 +100,12 @@
"Last File Received": "Останній завантажений файл",
"Last seen": "З’являвся останній раз",
"Later": "Пізніше",
"Listeners": "Listeners",
"Listeners": "Приймачі (TCP & Relay)",
"Local Discovery": "Локальне виявлення (LAN)",
"Local State": "Локальний статус",
"Local State (Total)": "Локальний статус (загалом)",
"Major Upgrade": "Мажорне оновлення",
"Master": "Master",
"Master": "Головний",
"Maximum Age": "Максимальний вік",
"Metadata Only": "Тільки метадані",
"Minimum Free Disk Space": "Мінімальний вільний простір на диску",
@@ -117,7 +117,7 @@
"Newest First": "Спершу новіші",
"No": "Ні",
"No File Versioning": "Версіонування вимкнено",
"Normal": "Normal",
"Normal": "Нормальний",
"Notice": "Повідомлення",
"OK": "Гаразд",
"Off": "Вимкнути",

View File

@@ -32,7 +32,7 @@
"Comment, when used at the start of a line": "注释,在行首使用",
"Compression": "压缩",
"Connection Error": "连接出错",
"Connection Type": "Connection Type",
"Connection Type": "连接类型",
"Copied from elsewhere": "从其他设备复制",
"Copied from original": "从源复制",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 以下贡献者:",
@@ -75,7 +75,7 @@
"Folder Label": "文件夹标签",
"Folder Master": "主文件夹",
"Folder Path": "文件夹路径",
"Folder Type": "Folder Type",
"Folder Type": "文件夹类型",
"Folders": "文件夹",
"GUI": "图形用户界面",
"GUI Authentication Password": "图形管理界面密码",
@@ -100,12 +100,12 @@
"Last File Received": "最后接收的文件",
"Last seen": "最后可见",
"Later": "稍后",
"Listeners": "Listeners",
"Listeners": "侦听程序",
"Local Discovery": "在局域网上寻找设备",
"Local State": "本地状态",
"Local State (Total)": "本地状态汇总",
"Major Upgrade": "重大更新",
"Master": "Master",
"Master": "主要",
"Maximum Age": "历史版本最长保留时间",
"Metadata Only": "仅元数据",
"Minimum Free Disk Space": "最低可用磁盘空间",
@@ -117,7 +117,7 @@
"Newest First": "新文件优先",
"No": "否",
"No File Versioning": "不启用版本控制",
"Normal": "Normal",
"Normal": "正常",
"Notice": "提示",
"OK": "确定",
"Off": "关闭",

View File

@@ -1 +1 @@
var langPrettyprint = {"bg":"Bulgarian","ca":"Catalan","ca@valencia":"Catalan (Valencian)","cs":"Czech","da":"Danish","de":"German","el":"Greek","en":"English","en-GB":"English (United Kingdom)","es":"Spanish","es-ES":"Spanish (Spain)","fi":"Finnish","fr":"French","fr-CA":"French (Canada)","fy":"Western Frisian","hu":"Hungarian","id":"Indonesian","it":"Italian","ja":"Japanese","lt":"Lithuanian","nb":"Norwegian Bokmål","nl":"Dutch","nn":"Norwegian Nynorsk","pl":"Polish","pt-BR":"Portuguese (Brazil)","pt-PT":"Portuguese (Portugal)","ru":"Russian","sv":"Swedish","tr":"Turkish","uk":"Ukrainian","vi":"Vietnamese","zh-CN":"Chinese (China)","zh-TW":"Chinese (Taiwan)"}
var langPrettyprint = {"bg":"Bulgarian","ca":"Catalan","ca@valencia":"Catalan (Valencian)","cs":"Czech","da":"Danish","de":"German","el":"Greek","en":"English","en-GB":"English (United Kingdom)","es":"Spanish","es-ES":"Spanish (Spain)","fi":"Finnish","fr":"French","fr-CA":"French (Canada)","fy":"Western Frisian","hu":"Hungarian","id":"Indonesian","it":"Italian","ja":"Japanese","ko-KR":"Korean (Korea)","lt":"Lithuanian","nb":"Norwegian Bokmål","nl":"Dutch","nn":"Norwegian Nynorsk","pl":"Polish","pt-BR":"Portuguese (Brazil)","pt-PT":"Portuguese (Portugal)","ru":"Russian","sv":"Swedish","tr":"Turkish","uk":"Ukrainian","vi":"Vietnamese","zh-CN":"Chinese (China)","zh-TW":"Chinese (Taiwan)"}

View File

@@ -1 +1 @@
var validLangs = ["bg","ca","ca@valencia","cs","da","de","el","en","en-GB","es","es-ES","fi","fr","fr-CA","fy","hu","id","it","ja","lt","nb","nl","nn","pl","pt-BR","pt-PT","ru","sv","tr","uk","vi","zh-CN","zh-TW"]
var validLangs = ["bg","ca","ca@valencia","cs","da","de","el","en","en-GB","es","es-ES","fi","fr","fr-CA","fy","hu","id","it","ja","ko-KR","lt","nb","nl","nn","pl","pt-BR","pt-PT","ru","sv","tr","uk","vi","zh-CN","zh-TW"]

View File

@@ -227,15 +227,13 @@
<h3 translate>Folders</h3>
<div class="panel-group" id="folders">
<div class="panel panel-default" ng-repeat="folder in folderList()">
<div class="panel-heading" data-toggle="collapse" data-parent="#folders" href="#folder-{{$index}}" style="cursor: pointer">
<a class="panel-heading" data-toggle="collapse" data-parent="#folders" href="#folder-{{$index}}" style="cursor: pointer; display: block;">
<div class="panel-progress" ng-show="folderStatus(folder) == 'syncing'" ng-attr-style="width: {{syncPercentage(folder.id)}}%"></div>
<div class="panel-progress" ng-show="folderStatus(folder) == 'scanning' && scanProgress[folder.id] != undefined" ng-attr-style="width: {{scanPercentage(folder.id)}}%"></div>
<h4 class="panel-title">
<span class="fa hidden-xs fa-fw" ng-class="[folder.type == 'readonly' ? 'fa-lock' : 'fa-folder']"></span>
<a href="#folder-{{$index}}">
<span ng-show="folder.label.length == 0">{{folder.id}}</span>
<span tooltip data-original-title="{{folder.id}}" ng-show="folder.label.length != 0">{{folder.label}}</span>
</a>
<span ng-show="folder.label.length == 0">{{folder.id}}</span>
<span tooltip data-original-title="{{folder.id}}" ng-show="folder.label.length != 0">{{folder.label}}</span>
<span class="pull-right text-{{folderClass(folder)}}" ng-switch="folderStatus(folder)">
<span ng-switch-when="unknown"><span class="hidden-xs" translate>Unknown</span><span class="visible-xs">&#9724;</span></span>
<span ng-switch-when="unshared"><span class="hidden-xs" translate>Unshared</span><span class="visible-xs">&#9724;</span></span>
@@ -255,7 +253,7 @@
<span ng-switch-when="outofsync"><span class="hidden-xs" translate>Out of Sync</span><span class="visible-xs">&#9724;</span></span>
</span>
</h4>
</div>
</a>
<div id="folder-{{$index}}" class="panel-collapse collapse">
<div class="panel-body">
<table class="table table-condensed table-striped">
@@ -401,11 +399,11 @@
<div class="col-md-6">
<h3 translate>This Device</h3>
<div class="panel panel-default" ng-repeat="deviceCfg in [thisDevice()]">
<div class="panel-heading" data-toggle="collapse" href="#device-this" style="cursor: pointer">
<a class="panel-heading" data-toggle="collapse" href="#device-this" style="cursor: pointer; display: block;">
<h4 class="panel-title">
<identicon data-value="deviceCfg.deviceID"></identicon>&emsp;{{deviceName(deviceCfg)}}
</h4>
</div>
</a>
<div id="device-this" class="panel-collapse collapse in">
<div class="panel-body">
<table class="table table-condensed table-striped">
@@ -476,10 +474,10 @@
<h3 translate>Remote Devices</h3>
<div class="panel-group" id="devices">
<div class="panel panel-default" ng-repeat="deviceCfg in otherDevices()">
<div class="panel-heading" data-toggle="collapse" data-parent="#devices" href="#device-{{$index}}" style="cursor: pointer">
<a class="panel-heading" data-toggle="collapse" data-parent="#devices" href="#device-{{$index}}" style="cursor: pointer; display: block;">
<div class="panel-progress" ng-show="deviceStatus(deviceCfg) == 'syncing'" ng-attr-style="width: {{completion[deviceCfg.deviceID]._total | number:0}}%"></div>
<h4 class="panel-title">
<identicon data-value="deviceCfg.deviceID"></identicon>&emsp;<a href="#device-{{$index}}">{{deviceName(deviceCfg)}}</a>
<identicon data-value="deviceCfg.deviceID"></identicon>&emsp;{{deviceName(deviceCfg)}}
<span ng-switch="deviceStatus(deviceCfg)" class="pull-right text-{{deviceClass(deviceCfg)}}">
<span ng-switch-when="insync"><span class="hidden-xs" translate>Up to Date</span><span class="visible-xs">&#9724;</span></span>
<span ng-switch-when="syncing">
@@ -490,7 +488,7 @@
<span ng-switch-when="unused"><span class="hidden-xs" translate>Unused</span><span class="visible-xs">&#9724;</span></span>
</span>
</h4>
</div>
</a>
<div id="device-{{$index}}" class="panel-collapse collapse">
<div class="panel-body">
<table class="table table-condensed table-striped">
@@ -664,6 +662,7 @@
<script src="assets/lang/valid-langs.js"></script>
<script src="assets/lang/prettyprint.js"></script>
<script src="meta.js"></script>
<script src="syncthing/app.js"></script>
<!-- / gui application code -->

View File

@@ -23,31 +23,9 @@ var syncthing = angular.module('syncthing', [
var urlbase = 'rest';
syncthing.config(function ($httpProvider, $translateProvider, LocaleServiceProvider) {
$httpProvider.interceptors.push(function xHeadersResponseInterceptor() {
var deviceId = null;
return {
response: function onResponse(response) {
var headers = response.headers();
// angular template cache sends no headers
if(Object.keys(headers).length === 0) {
return response;
}
if (!deviceId) {
deviceId = headers['x-syncthing-id'];
if (deviceId) {
var deviceIdShort = deviceId.substring(0, 5);
$httpProvider.defaults.xsrfHeaderName = 'X-CSRF-Token-' + deviceIdShort;
$httpProvider.defaults.xsrfCookieName = 'CSRF-Token-' + deviceIdShort;
}
}
return response;
}
};
});
var deviceIDShort = metadata.deviceID.substr(0, 5);
$httpProvider.defaults.xsrfHeaderName = 'X-CSRF-Token-' + deviceIDShort;
$httpProvider.defaults.xsrfCookieName = 'CSRF-Token-' + deviceIDShort;
// language and localisation
@@ -59,7 +37,6 @@ syncthing.config(function ($httpProvider, $translateProvider, LocaleServiceProvi
LocaleServiceProvider.setAvailableLocales(validLangs);
LocaleServiceProvider.setDefaultLocale('en');
});
// @TODO: extract global level functions into separate service(s)
@@ -112,19 +89,6 @@ function decimals(val, num) {
return decs;
}
function randomString(len) {
var chars = '01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-';
return randomStringFromCharset(len, chars);
}
function randomStringFromCharset(len, charset) {
var result = '';
for (var i = 0; i < len; i++) {
result += charset[Math.round(Math.random() * (charset.length - 1))];
}
return result;
}
function isEmptyObject(obj) {
var name;
for (name in obj) {

View File

@@ -1196,7 +1196,6 @@ angular.module('syncthing.core')
$scope.addFolder = function () {
$scope.currentFolder = {
selectedDevices: {},
id: $scope.createRandomFolderId(),
type: "readwrite",
rescanIntervalS: 60,
minDiskFreePct: 1,
@@ -1213,7 +1212,10 @@ angular.module('syncthing.core')
};
$scope.editingExisting = false;
$scope.folderEditor.$setPristine();
$('#editFolder').modal();
$http.get(urlbase + '/svc/random/string?length=10').success(function (data) {
$scope.currentFolder.id = data.random.substr(0, 5) + '-' + data.random.substr(5, 5);
$('#editFolder').modal();
});
};
$scope.addFolderAndShare = function (folder, folderLabel, device) {
@@ -1406,7 +1408,9 @@ angular.module('syncthing.core')
};
$scope.setAPIKey = function (cfg) {
cfg.apiKey = randomString(32);
$http.get(urlbase + '/svc/random/string?length=32').success(function (data) {
cfg.apiKey = data.random;
});
};
$scope.showURPreview = function () {
@@ -1544,11 +1548,6 @@ angular.module('syncthing.core')
return 'text';
};
$scope.createRandomFolderId = function(){
var charset = '2345679abcdefghijkmnopqrstuvwxyzACDEFGHJKLMNPQRSTUVWXYZ';
return randomStringFromCharset(5, charset) + "-" + randomStringFromCharset(5, charset);
};
$scope.themeName = function (theme) {
return theme.replace('-', ' ').replace(/(?:^|\s)\S/g, function (a) {
return a.toUpperCase();

View File

@@ -19,6 +19,7 @@ import (
"strings"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/rand"
"github.com/syncthing/syncthing/lib/util"
)
@@ -254,7 +255,7 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) {
}
if cfg.GUI.APIKey == "" {
cfg.GUI.APIKey = util.RandomString(32)
cfg.GUI.APIKey = rand.String(32)
}
}

View File

@@ -90,11 +90,13 @@ func NewService(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *
// The rate variables are in KiB/s in the UI (despite the camel casing
// of the name). We multiply by 1024 here to get B/s.
if service.cfg.Options().MaxSendKbps > 0 {
service.writeRateLimit = ratelimit.NewBucketWithRate(float64(1024*service.cfg.Options().MaxSendKbps), int64(5*1024*service.cfg.Options().MaxSendKbps))
options := service.cfg.Options()
if options.MaxSendKbps > 0 {
service.writeRateLimit = ratelimit.NewBucketWithRate(float64(1024*options.MaxSendKbps), int64(5*1024*options.MaxSendKbps))
}
if service.cfg.Options().MaxRecvKbps > 0 {
service.readRateLimit = ratelimit.NewBucketWithRate(float64(1024*service.cfg.Options().MaxRecvKbps), int64(5*1024*service.cfg.Options().MaxRecvKbps))
if options.MaxRecvKbps > 0 {
service.readRateLimit = ratelimit.NewBucketWithRate(float64(1024*options.MaxRecvKbps), int64(5*1024*options.MaxRecvKbps))
}
// There are several moving parts here; one routine per listening address

View File

@@ -36,6 +36,7 @@ const (
FolderRejected
ConfigSaved
DownloadProgress
RemoteDownloadProgress
FolderSummary
FolderCompletion
FolderErrors
@@ -80,6 +81,8 @@ func (t EventType) String() string {
return "ConfigSaved"
case DownloadProgress:
return "DownloadProgress"
case RemoteDownloadProgress:
return "RemoteDownloadProgress"
case FolderSummary:
return "FolderSummary"
case FolderCompletion:

View File

@@ -246,8 +246,7 @@ func parseIgnoreFile(fd io.Reader, currentFile string, seen map[string]bool) ([]
addPattern := func(line string) error {
pattern := Pattern{
pattern: line,
result: defaultResult,
result: defaultResult,
}
// Allow prefixes to be specified in any order, but only once.
@@ -275,6 +274,8 @@ func parseIgnoreFile(fd io.Reader, currentFile string, seen map[string]bool) ([]
line = strings.ToLower(line)
}
pattern.pattern = line
var err error
if strings.HasPrefix(line, "/") {
// Pattern is rooted in the current dir only

View File

@@ -665,3 +665,41 @@ func TestCommas(t *testing.T) {
}
}
}
func TestIssue3164(t *testing.T) {
stignore := `
(?d)(?i)*.part
(?d)(?i)/foo
(?d)(?i)**/bar
`
pats := New(true)
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
if err != nil {
t.Fatal(err)
}
expanded := pats.Patterns()
t.Log(expanded)
expected := []string{
"(?d)(?i)*.part",
"(?d)(?i)**/*.part",
"(?d)(?i)*.part/**",
"(?d)(?i)**/*.part/**",
"(?d)(?i)/foo",
"(?d)(?i)/foo/**",
"(?d)(?i)**/bar",
"(?d)(?i)bar",
"(?d)(?i)**/bar/**",
"(?d)(?i)bar/**",
}
if len(expanded) != len(expected) {
t.Errorf("Unmatched count: %d != %d", len(expanded), len(expected))
}
for i := range expanded {
if expanded[i] != expected[i] {
t.Errorf("Pattern %d does not match: %s != %s", i, expanded[i], expected[i])
}
}
}

View File

@@ -23,9 +23,8 @@ type deviceFolderFileDownloadState struct {
// deviceFolderDownloadState holds current download state of all files that
// a remote device is currently downloading in a specific folder.
type deviceFolderDownloadState struct {
mut sync.RWMutex
files map[string]deviceFolderFileDownloadState
numberOfBlocksInProgress int
mut sync.RWMutex
files map[string]deviceFolderFileDownloadState
}
// Has returns whether a block at that specific index, and that specific version of the file
@@ -57,7 +56,6 @@ func (p *deviceFolderDownloadState) Update(updates []protocol.FileDownloadProgre
for _, update := range updates {
local, ok := p.files[update.Name]
if update.UpdateType == protocol.UpdateTypeForget && ok && local.version.Equal(update.Version) {
p.numberOfBlocksInProgress -= len(local.blockIndexes)
delete(p.files, update.Name)
} else if update.UpdateType == protocol.UpdateTypeAppend {
if !ok {
@@ -66,25 +64,25 @@ func (p *deviceFolderDownloadState) Update(updates []protocol.FileDownloadProgre
version: update.Version,
}
} else if !local.version.Equal(update.Version) {
p.numberOfBlocksInProgress -= len(local.blockIndexes)
local.blockIndexes = append(local.blockIndexes[:0], update.BlockIndexes...)
local.version = update.Version
} else {
local.blockIndexes = append(local.blockIndexes, update.BlockIndexes...)
}
p.files[update.Name] = local
p.numberOfBlocksInProgress += len(update.BlockIndexes)
}
}
}
// NumberOfBlocksInProgress returns the number of blocks the device has downloaded
// for a specific folder.
func (p *deviceFolderDownloadState) NumberOfBlocksInProgress() int {
// GetBlockCounts returns a map filename -> number of blocks downloaded.
func (p *deviceFolderDownloadState) GetBlockCounts() map[string]int {
p.mut.RLock()
n := p.numberOfBlocksInProgress
res := make(map[string]int, len(p.files))
for name, state := range p.files {
res[name] = len(state.blockIndexes)
}
p.mut.RUnlock()
return n
return res
}
// deviceDownloadState represents the state of all in progress downloads
@@ -134,20 +132,21 @@ func (t *deviceDownloadState) Has(folder, file string, version protocol.Vector,
return f.Has(file, version, index)
}
// NumberOfBlocksInProgress returns the number of blocks the device has downloaded
// for all folders.
func (t *deviceDownloadState) NumberOfBlocksInProgress() int {
// GetBlockCounts returns a map filename -> number of blocks downloaded for the
// given folder.
func (t *deviceDownloadState) GetBlockCounts(folder string) map[string]int {
if t == nil {
return 0
return nil
}
n := 0
t.mut.RLock()
for _, folder := range t.folders {
n += folder.NumberOfBlocksInProgress()
for name, state := range t.folders {
if name == folder {
return state.GetBlockCounts()
}
}
t.mut.RUnlock()
return n
return nil
}
func newDeviceDownloadState() *deviceDownloadState {

View File

@@ -374,17 +374,26 @@ func (m *Model) Completion(device protocol.DeviceID, folder string) float64 {
return 100 // Folder is empty, so we have all of it
}
var need int64
m.pmut.RLock()
counts := m.deviceDownloads[device].GetBlockCounts(folder)
m.pmut.RUnlock()
var need, fileNeed, downloaded int64
rf.WithNeedTruncated(device, func(f db.FileIntf) bool {
need += f.Size()
ft := f.(db.FileInfoTruncated)
// This might might be more than it really is, because some blocks can be of a smaller size.
downloaded = int64(counts[ft.Name] * protocol.BlockSize)
fileNeed = ft.Size() - downloaded
if fileNeed < 0 {
fileNeed = 0
}
need += fileNeed
return true
})
// This might might be more than it really is, because some blocks can be of a smaller size.
m.pmut.RLock()
need -= int64(m.deviceDownloads[device].NumberOfBlocksInProgress() * protocol.BlockSize)
m.pmut.RUnlock()
needRatio := float64(need) / float64(tot)
completionPct := 100 * (1 - needRatio)
l.Debugf("%v Completion(%s, %q): %f (%d / %d = %f)", m, device, folder, completionPct, need, tot, needRatio)
@@ -1083,7 +1092,14 @@ func (m *Model) DownloadProgress(device protocol.DeviceID, folder string, update
m.pmut.RLock()
m.deviceDownloads[device].Update(folder, updates)
state := m.deviceDownloads[device].GetBlockCounts(folder)
m.pmut.RUnlock()
events.Default.Log(events.RemoteDownloadProgress, map[string]interface{}{
"device": device.String(),
"folder": folder,
"state": state,
})
}
func (m *Model) ResumeDevice(device protocol.DeviceID) {

View File

@@ -245,6 +245,18 @@ func TestSendDownloadProgressMessages(t *testing.T) {
expect(1, state1, protocol.UpdateTypeAppend, v2, []int32{1, 2}, true)
expectEmpty()
// Returns forget and append if sharedPullerState creation timer changes.
state1.available = []int32{1}
state1.availableUpdated = tick()
state1.created = tick()
p.sendDownloadProgressMessages()
expect(0, state1, protocol.UpdateTypeForget, v2, nil, false)
expect(1, state1, protocol.UpdateTypeAppend, v2, []int32{1}, true)
expectEmpty()
// Sends an empty update if new file exists, but does not have any blocks yet. (To indicate that the old blocks are no longer available)
state1.file.Version = v1
state1.available = nil

View File

@@ -676,8 +676,9 @@ func (f *rwFolder) deleteDir(file protocol.FileInfo, matcher *ignore.Matcher) {
if dir != nil {
files, _ := dir.Readdirnames(-1)
for _, dirFile := range files {
if defTempNamer.IsTemporary(dirFile) || (matcher != nil && matcher.Match(filepath.Join(file.Name, dirFile)).IsDeletable()) {
osutil.InWritableDir(osutil.Remove, filepath.Join(realName, dirFile))
fullDirFile := filepath.Join(file.Name, dirFile)
if defTempNamer.IsTemporary(dirFile) || (matcher != nil && matcher.Match(fullDirFile).IsDeletable()) {
osutil.RemoveAll(fullDirFile)
}
}
dir.Close()
@@ -1011,6 +1012,7 @@ func (f *rwFolder) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocks
version: curFile.Version,
mut: sync.NewRWMutex(),
sparse: f.allowSparse,
created: time.Now(),
}
l.Debugf("%v need file %s; copy %d, reused %v", f, file.Name, len(blocks), reused)
@@ -1297,8 +1299,9 @@ func (f *rwFolder) performFinish(state *sharedPullerState) error {
}
}
// Replace the original content with the new one
if err := osutil.Rename(state.tempName, state.realName); err != nil {
// Replace the original content with the new one. If it didn't work,
// leave the temp file in place for reuse.
if err := osutil.TryRename(state.tempName, state.realName); err != nil {
return err
}

View File

@@ -18,6 +18,7 @@ type sentFolderFileDownloadState struct {
blockIndexes []int32
version protocol.Vector
updated time.Time
created time.Time
}
// sentFolderDownloadState represents a state of what we've announced as available
@@ -41,6 +42,7 @@ func (s *sentFolderDownloadState) update(pullers []*sharedPullerState) []protoco
pullerBlockIndexes := puller.Available()
pullerVersion := puller.file.Version
pullerBlockIndexesUpdated := puller.AvailableUpdated()
pullerCreated := puller.created
localFile, ok := s.files[name]
@@ -52,6 +54,7 @@ func (s *sentFolderDownloadState) update(pullers []*sharedPullerState) []protoco
blockIndexes: pullerBlockIndexes,
updated: pullerBlockIndexesUpdated,
version: pullerVersion,
created: pullerCreated,
}
updates = append(updates, protocol.FileDownloadProgressUpdate{
@@ -70,9 +73,9 @@ func (s *sentFolderDownloadState) update(pullers []*sharedPullerState) []protoco
continue
}
if !pullerVersion.Equal(localFile.version) {
// The version has changed, clean up whatever we had for the old
// file, and advertise the new file.
if !pullerVersion.Equal(localFile.version) || !pullerCreated.Equal(localFile.created) {
// The version has changed or the puller was reconstrcuted due to failure.
// Clean up whatever we had for the old file, and advertise the new file.
updates = append(updates, protocol.FileDownloadProgressUpdate{
Name: name,
Version: localFile.version,
@@ -87,6 +90,7 @@ func (s *sentFolderDownloadState) update(pullers []*sharedPullerState) []protoco
localFile.blockIndexes = pullerBlockIndexes
localFile.updated = pullerBlockIndexesUpdated
localFile.version = pullerVersion
localFile.created = pullerCreated
continue
}

View File

@@ -29,6 +29,7 @@ type sharedPullerState struct {
ignorePerms bool
version protocol.Vector // The current (old) version
sparse bool
created time.Time
// Mutable, must be locked for access
err error // The first error we hit

View File

@@ -15,6 +15,7 @@ import (
"path/filepath"
"runtime"
"strings"
"syscall"
"github.com/calmh/du"
"github.com/syncthing/syncthing/lib/sync"
@@ -107,6 +108,78 @@ func Remove(path string) error {
return os.Remove(path)
}
// RemoveAll is a copy of os.RemoveAll, but uses osutil.Remove.
// RemoveAll removes path and any children it contains.
// It removes everything it can but returns the first error
// it encounters. If the path does not exist, RemoveAll
// returns nil (no error).
func RemoveAll(path string) error {
// Simple case: if Remove works, we're done.
err := Remove(path)
if err == nil || os.IsNotExist(err) {
return nil
}
// Otherwise, is this a directory we need to recurse into?
dir, serr := os.Lstat(path)
if serr != nil {
if serr, ok := serr.(*os.PathError); ok && (os.IsNotExist(serr.Err) || serr.Err == syscall.ENOTDIR) {
return nil
}
return serr
}
if !dir.IsDir() {
// Not a directory; return the error from Remove.
return err
}
// Directory.
fd, err := os.Open(path)
if err != nil {
if os.IsNotExist(err) {
// Race. It was deleted between the Lstat and Open.
// Return nil per RemoveAll's docs.
return nil
}
return err
}
// Remove contents & return first error.
err = nil
for {
names, err1 := fd.Readdirnames(100)
for _, name := range names {
err1 := RemoveAll(path + string(os.PathSeparator) + name)
if err == nil {
err = err1
}
}
if err1 == io.EOF {
break
}
// If Readdirnames returned an error, use it.
if err == nil {
err = err1
}
if len(names) == 0 {
break
}
}
// Close directory, because windows won't remove opened directory.
fd.Close()
// Remove directory.
err1 := Remove(path)
if err1 == nil || os.IsNotExist(err1) {
return nil
}
if err == nil {
err = err1
}
return err
}
func ExpandTilde(path string) (string, error) {
if path == "~" {
return getHomeDir()

View File

@@ -13,7 +13,7 @@ import (
"time"
"github.com/AudriusButkevicius/go-nat-pmp"
"github.com/jackpal/gateway"
"github.com/calmh/gateway"
"github.com/syncthing/syncthing/lib/nat"
)

75
lib/rand/random.go Normal file
View File

@@ -0,0 +1,75 @@
// Copyright (C) 2014 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 rand implements functions similar to math/rand in the standard
// library, but on top of a secure random number generator.
package rand
import (
"crypto/md5"
cryptoRand "crypto/rand"
"encoding/binary"
"io"
mathRand "math/rand"
)
// Reader is the standard crypto/rand.Reader, re-exported for convenience
var Reader = cryptoRand.Reader
// randomCharset contains the characters that can make up a randomString().
const randomCharset = "2345679abcdefghijkmnopqrstuvwxyzACDEFGHJKLMNPQRSTUVWXYZ"
var (
// defaultSecureSource is a concurrency safe math/rand.Source with a
// cryptographically sound base.
defaltSecureSource = newSecureSource()
// defaultSecureRand is a math/rand.Rand based on the secure source.
defaultSecureRand = mathRand.New(defaltSecureSource)
)
// String returns a strongly random string of characters (taken from
// randomCharset) of the specified length. The returned string contains ~5.8
// bits of entropy per character, due to the character set used.
func String(l int) string {
bs := make([]byte, l)
for i := range bs {
bs[i] = randomCharset[defaultSecureRand.Intn(len(randomCharset))]
}
return string(bs)
}
// Int63 returns a strongly random int63
func Int63() int64 {
return defaltSecureSource.Int63()
}
// Int64 returns a strongly random int64
func Int64() int64 {
var bs [8]byte
_, err := io.ReadFull(cryptoRand.Reader, bs[:])
if err != nil {
panic("randomness failure: " + err.Error())
}
return int64(binary.BigEndian.Uint64(bs[:]))
}
// Intn returns, as an int, a non-negative strongly random number in [0,n).
// It panics if n <= 0.
func Intn(n int) int {
return defaultSecureRand.Intn(n)
}
// SeedFromBytes calculates a weak 64 bit hash from the given byte slice,
// suitable for use a predictable random seed.
func SeedFromBytes(bs []byte) int64 {
h := md5.New()
h.Write(bs)
s := h.Sum(nil)
// The MD5 hash of the byte slice is 16 bytes long. We interpret it as two
// uint64s and XOR them together.
return int64(binary.BigEndian.Uint64(s[0:]) ^ binary.BigEndian.Uint64(s[8:]))
}

View File

@@ -4,7 +4,7 @@
// 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 util
package rand
import "testing"
@@ -27,7 +27,7 @@ func TestSeedFromBytes(t *testing.T) {
func TestRandomString(t *testing.T) {
for _, l := range []int{0, 1, 2, 3, 4, 8, 42} {
s := RandomString(l)
s := String(l)
if len(s) != l {
t.Errorf("Incorrect length %d != %d", len(s), l)
}
@@ -35,7 +35,7 @@ func TestRandomString(t *testing.T) {
strings := make([]string, 1000)
for i := range strings {
strings[i] = RandomString(8)
strings[i] = String(8)
for j := range strings {
if i == j {
continue
@@ -50,7 +50,7 @@ func TestRandomString(t *testing.T) {
func TestRandomInt64(t *testing.T) {
ints := make([]int64, 1000)
for i := range ints {
ints[i] = RandomInt64()
ints[i] = Int64()
for j := range ints {
if i == j {
continue

57
lib/rand/securesource.go Normal file
View File

@@ -0,0 +1,57 @@
// 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 rand
import (
"bufio"
"crypto/rand"
"encoding/binary"
"io"
"sync"
)
// The secureSource is a math/rand.Source that reads bytes from
// crypto/rand.Reader. It means we can use the convenience functions
// provided by math/rand.Rand on top of a secure source of numbers. It is
// concurrency safe for ease of use.
type secureSource struct {
rd io.Reader
mut sync.Mutex
}
func newSecureSource() *secureSource {
return &secureSource{
// Using buffering on top of the rand.Reader increases our
// performance by about 20%, even though it means we must use
// locking.
rd: bufio.NewReader(rand.Reader),
}
}
func (s *secureSource) Seed(int64) {
panic("SecureSource is not seedable")
}
func (s *secureSource) Int63() int64 {
var buf [8]byte
// Read eight bytes of entropy from the buffered, secure random number
// generator. The buffered reader isn't concurrency safe, so we lock
// around that.
s.mut.Lock()
_, err := io.ReadFull(s.rd, buf[:])
s.mut.Unlock()
if err != nil {
panic("randomness failure: " + err.Error())
}
// Grab those bytes as an uint64
v := binary.BigEndian.Uint64(buf[:])
// Mask of the high bit and return the resulting int63
return int64(v & (1<<63 - 1))
}

View File

@@ -0,0 +1,96 @@
// 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 rand
import "testing"
func TestSecureSource(t *testing.T) {
// This is not a test to verify that the random numbers are secure,
// merely that the numbers look random at all and that we've haven't
// broken the implementation by masking off half the bits or always
// returning "4" (chosen by fair dice roll),
const nsamples = 10000
// Create a new source and sample values from it.
s := newSecureSource()
res0 := make([]int64, nsamples)
for i := range res0 {
res0[i] = s.Int63()
}
// Do it again
s = newSecureSource()
res1 := make([]int64, nsamples)
for i := range res1 {
res1[i] = s.Int63()
}
// There should (statistically speaking) be no repetition of the values,
// neither within the samples from a source nor between sources.
for _, v0 := range res0 {
for _, v1 := range res1 {
if v0 == v1 {
t.Errorf("Suspicious coincidence, %d repeated between res0/res1", v0)
}
}
}
for i, v0 := range res0 {
for _, v1 := range res0[i+1:] {
if v0 == v1 {
t.Errorf("Suspicious coincidence, %d repeated within res0", v0)
}
}
}
for i, v0 := range res1 {
for _, v1 := range res1[i+1:] {
if v0 == v1 {
t.Errorf("Suspicious coincidence, %d repeated within res1", v0)
}
}
}
// Count how many times each bit was set. On average each bit ought to
// be set in half of the samples, except the topmost bit which must
// never be set (int63). We raise an alarm if a single bit is set in
// fewer than 1/3 of the samples or more often than 2/3 of the samples.
var bits [64]int
for _, v := range res0 {
for i := range bits {
if v&1 == 1 {
bits[i]++
}
v >>= 1
}
}
for bit, count := range bits {
switch bit {
case 63:
// The topmost bit is never set
if count != 0 {
t.Errorf("The topmost bit was set %d times in %d samples (should be 0)", count, nsamples)
}
default:
if count < nsamples/3 {
t.Errorf("Bit %d was set only %d times out of %d", bit, count, nsamples)
}
if count > nsamples/3*2 {
t.Errorf("Bit %d was set fully %d times out of %d", bit, count, nsamples)
}
}
}
}
var sink int64
func BenchmarkSecureSource(b *testing.B) {
s := newSecureSource()
for i := 0; i < b.N; i++ {
sink = s.Int63()
}
b.ReportAllocs()
}

View File

@@ -10,7 +10,6 @@ import (
"bufio"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
@@ -19,10 +18,11 @@ import (
"fmt"
"io"
"math/big"
mr "math/rand"
"net"
"os"
"time"
"github.com/syncthing/syncthing/lib/rand"
)
var (
@@ -48,7 +48,7 @@ func NewCertificate(certFile, keyFile, tlsDefaultCommonName string, tlsRSABits i
notAfter := time.Date(2049, 12, 31, 23, 59, 59, 0, time.UTC)
template := x509.Certificate{
SerialNumber: new(big.Int).SetInt64(mr.Int63()),
SerialNumber: new(big.Int).SetInt64(rand.Int63()),
Subject: pkix.Name{
CommonName: tlsDefaultCommonName,
},

View File

@@ -129,11 +129,31 @@ func SelectLatestRelease(version string, rels []Release) (Release, error) {
return Release{}, ErrNoVersionToSelect
}
sort.Sort(SortByRelease(rels))
// Sort the releases, lowest version number first
sort.Sort(sort.Reverse(SortByRelease(rels)))
// Check for a beta build
beta := strings.Contains(version, "-")
var selected Release
for _, rel := range rels {
switch CompareVersions(rel.Tag, version) {
case Older, MajorOlder:
// This is older than what we're already running
continue
case MajorNewer:
// We've found a new major version. That's fine, but if we've
// already found a minor upgrade that is acceptable we should go
// with that one first and then revisit in the future.
if selected.Tag != "" && CompareVersions(selected.Tag, version) == Newer {
return selected, nil
}
// else it may be viable, do the needful below
default:
// New minor release, do the usual processing
}
if rel.Prerelease && !beta {
continue
}
@@ -144,11 +164,16 @@ func SelectLatestRelease(version string, rels []Release) (Release, error) {
l.Debugf("expected release asset %q", expectedRelease)
l.Debugln("considering release", assetName)
if strings.HasPrefix(assetName, expectedRelease) {
return rel, nil
selected = rel
}
}
}
return Release{}, ErrNoReleaseDownload
if selected.Tag == "" {
return Release{}, ErrNoReleaseDownload
}
return selected, nil
}
// Upgrade to the given release, saving the previous binary with a ".old" extension.

View File

@@ -11,6 +11,7 @@ package upgrade
import (
"encoding/json"
"os"
"strings"
"testing"
)
@@ -58,16 +59,15 @@ func TestCompareVersions(t *testing.T) {
}
}
var upgrades = map[string]string{
"v0.10.21": "v0.10.30",
"v0.10.29": "v0.10.30",
"v0.10.31": "v0.10.30",
"v0.10.0-alpha": "v0.11.0-beta0",
"v0.10.0-beta": "v0.11.0-beta0",
"v0.11.0-beta0+40-g53cb66e-dirty": "v0.11.0-beta0",
}
func TestGithubRelease(t *testing.T) {
var upgrades = map[string]string{
"v0.10.21": "v0.10.30",
"v0.10.29": "v0.10.30",
"v0.10.0-alpha": "v0.10.30",
"v0.10.0-beta": "v0.10.30",
"v0.11.0-beta0+40-g53cb66e-dirty": "v0.11.0-beta0",
}
fd, err := os.Open("testdata/github-releases.json")
if err != nil {
t.Errorf("Missing github-release test data")
@@ -94,3 +94,48 @@ func TestErrorRelease(t *testing.T) {
t.Error("Should return an error when no release were available")
}
}
func TestSelectedRelease(t *testing.T) {
testcases := []struct {
current string
candidates []string
selected string
}{
// Within the same "major" (minor, in this case) select the newest
{"v0.12.24", []string{"v0.12.23", "v0.12.24", "v0.12.25", "v0.12.26"}, "v0.12.26"},
// Do no select beta versions when we are not a beta
{"v0.12.24", []string{"v0.12.26", "v0.12.27-beta.42"}, "v0.12.26"},
// Do select beta versions when we are a beta
{"v0.12.24-beta.0", []string{"v0.12.26", "v0.12.27-beta.42"}, "v0.12.27-beta.42"},
// Select the best within the current major when there is a minor upgrade available
{"v0.12.24", []string{"v0.12.23", "v0.12.24", "v0.12.25", "v0.13.0"}, "v0.12.25"},
{"v1.12.24", []string{"v1.12.23", "v1.12.24", "v1.14.2", "v2.0.0"}, "v1.14.2"},
// Select the next major when we are at the best minor
{"v0.12.25", []string{"v0.12.23", "v0.12.24", "v0.12.25", "v0.13.0"}, "v0.13.0"},
{"v1.14.2", []string{"v0.12.23", "v0.12.24", "v1.14.2", "v2.0.0"}, "v2.0.0"},
}
for i, tc := range testcases {
// Prepare a list of candidate releases
var rels []Release
for _, c := range tc.candidates {
rels = append(rels, Release{
Tag: c,
Prerelease: strings.Contains(c, "-"),
Assets: []Asset{
// There must be a matching asset or it will not get selected
{Name: releaseName(c)},
},
})
}
// Check the selection
sel, err := SelectLatestRelease(tc.current, rels)
if err != nil {
t.Fatal("Unexpected error:", err)
}
if sel.Tag != tc.selected {
t.Errorf("Test case %d: expected %s to be selected, but got %s", i, tc.selected, sel.Tag)
}
}
}

View File

@@ -1,54 +0,0 @@
// Copyright (C) 2014 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 util
import (
"crypto/md5"
cryptoRand "crypto/rand"
"encoding/binary"
"io"
mathRand "math/rand"
)
// randomCharset contains the characters that can make up a randomString().
const randomCharset = "2345679abcdefghijkmnopqrstuvwxyzACDEFGHJKLMNPQRSTUVWXYZ"
func init() {
// The default RNG should be seeded with something good.
mathRand.Seed(RandomInt64())
}
// RandomString returns a string of random characters (taken from
// randomCharset) of the specified length.
func RandomString(l int) string {
bs := make([]byte, l)
for i := range bs {
bs[i] = randomCharset[mathRand.Intn(len(randomCharset))]
}
return string(bs)
}
// RandomInt64 returns a strongly random int64, slowly
func RandomInt64() int64 {
var bs [8]byte
_, err := io.ReadFull(cryptoRand.Reader, bs[:])
if err != nil {
panic("randomness failure: " + err.Error())
}
return SeedFromBytes(bs[:])
}
// SeedFromBytes calculates a weak 64 bit hash from the given byte slice,
// suitable for use a predictable random seed.
func SeedFromBytes(bs []byte) int64 {
h := md5.New()
h.Write(bs)
s := h.Sum(nil)
// The MD5 hash of the byte slice is 16 bytes long. We interpret it as two
// uint64s and XOR them together.
return int64(binary.BigEndian.Uint64(s[0:]) ^ binary.BigEndian.Uint64(s[8:]))
}

View File

@@ -1,48 +0,0 @@
go-nat-pmp
==========
A Go language client for the NAT-PMP internet protocol for port mapping and discovering the external
IP address of a firewall.
NAT-PMP is supported by Apple brand routers and open source routers like Tomato and DD-WRT.
See http://tools.ietf.org/html/draft-cheshire-nat-pmp-03
Get the package
---------------
go get -u github.com/jackpal/go-nat-pmp
Usage
-----
import natpmp "github.com/jackpal/go-nat-pmp"
client := natpmp.NewClient(gatewayIP)
response, err := client.GetExternalAddress()
if err != nil {
return
}
print("External IP address:", response.ExternalIPAddress)
Notes
-----
There doesn't seem to be an easy way to programmatically determine the address of the default gateway.
(Linux and OSX have a "routes" kernel API that can be examined to get this information, but there is
no Go package for getting this information.)
Clients
-------
This library is used in the Taipei Torrent BitTorrent client http://github.com/jackpal/Taipei-Torrent
Complete documentation
----------------------
http://godoc.org/github.com/jackpal/go-nat-pmp
License
-------
This project is licensed under the Apache License 2.0.

View File

@@ -4,8 +4,6 @@ import (
"fmt"
"net"
"time"
"github.com/jackpal/gateway"
)
// Implement the NAT-PMP protocol, typically supported by Apple routers and open source
@@ -34,17 +32,6 @@ func NewClient(gateway net.IP, timeout time.Duration) (nat *Client) {
return &Client{gateway, timeout}
}
// Create a NAT-PMP client for the NAT-PMP server at the default gateway.
func NewClientForDefaultGateway(timeout time.Duration) (nat *Client, err error) {
var g net.IP
g, err = gateway.DiscoverGateway()
if err != nil {
return
}
nat = NewClient(g, timeout)
return
}
// Results of the NAT-PMP GetExternalAddress operation
type GetExternalAddressResult struct {
SecondsSinceStartOfEpoc uint32

View File

@@ -1,11 +0,0 @@
package natpmp
import "testing"
func TestNatPMP(t *testing.T) {
client, err := NewClientForDefaultGateway(0)
if err != nil {
t.Errorf("NewClientForDefaultGateway() = %v,%v", client, err)
return
}
}

View File

@@ -2,25 +2,13 @@ package gateway
import (
"bytes"
"io/ioutil"
"errors"
"net"
"os/exec"
)
func DiscoverGateway() (ip net.IP, err error) {
routeCmd := exec.Command("route", "print", "0.0.0.0")
stdOut, err := routeCmd.StdoutPipe()
if err != nil {
return
}
if err = routeCmd.Start(); err != nil {
return
}
output, err := ioutil.ReadAll(stdOut)
if err != nil {
return
}
var errNoGateway = errors.New("no gateway found")
func parseRoutePrint(output []byte) (net.IP, error) {
// Windows route output format is always like this:
// ===========================================================================
// Active Routes:
@@ -33,11 +21,18 @@ func DiscoverGateway() (ip net.IP, err error) {
outputLines := bytes.Split(output, []byte("\n"))
for idx, line := range outputLines {
if bytes.Contains(line, []byte("Active Routes:")) {
if len(outputLines) <= idx+2 {
return nil, errNoGateway
}
ipFields := bytes.Fields(outputLines[idx+2])
ip = net.ParseIP(string(ipFields[2]))
break
if len(ipFields) < 3 {
return nil, errNoGateway
}
ip := net.ParseIP(string(ipFields[2]))
return ip, nil
}
}
err = routeCmd.Wait()
return
return nil, errNoGateway
}

16
vendor/github.com/calmh/gateway/gateway_windows.go generated vendored Normal file
View File

@@ -0,0 +1,16 @@
package gateway
import (
"net"
"os/exec"
)
func DiscoverGateway() (ip net.IP, err error) {
routeCmd := exec.Command("route", "print", "0.0.0.0")
output, err := routeCmd.CombinedOutput()
if err != nil {
return nil, err
}
return parseRoutePrint(output)
}

View File

@@ -1,7 +0,0 @@
# gateway
A very simple library for discovering the IP address of the local LAN gateway.
Provides implementations for Linux, OS X (Darwin) and Windows.
Pull requests for other OSs happily considered!

View File

@@ -1,10 +0,0 @@
package gateway
import "testing"
func TestGateway(t *testing.T) {
ip, err := DiscoverGateway()
if err != nil {
t.Errorf("DiscoverGateway() = %v,%v", ip, err)
}
}

15
vendor/manifest vendored
View File

@@ -4,7 +4,7 @@
{
"importpath": "github.com/AudriusButkevicius/go-nat-pmp",
"repository": "https://github.com/AudriusButkevicius/go-nat-pmp",
"revision": "e9d7ecafd6f4cd4f59fc45bb9a47466ce637d0fe",
"revision": "452c97607362b2ab5a7839b8d1704f0396b640ca",
"branch": "master"
},
{
@@ -19,6 +19,13 @@
"revision": "3c0690cca16228b97741327b1b6781397afbdb24",
"branch": "master"
},
{
"importpath": "github.com/calmh/gateway",
"repository": "https://github.com/calmh/gateway",
"revision": "edad739645120eeb82866bc1901d3317b57909b1",
"branch": "master",
"notests": true
},
{
"importpath": "github.com/calmh/luhn",
"repository": "https://github.com/calmh/luhn",
@@ -49,12 +56,6 @@
"revision": "5f1c01d9f64b941dd9582c638279d046eda6ca31",
"branch": "master"
},
{
"importpath": "github.com/jackpal/gateway",
"repository": "https://github.com/jackpal/gateway",
"revision": "32194371ec3f370166ee10a5ee079206532fdd74",
"branch": "master"
},
{
"importpath": "github.com/juju/ratelimit",
"repository": "https://github.com/juju/ratelimit",