Archived
1 parent
a7169a6348
commit
b0cd7be39b
41 files changed
+1936
-1503
No files matched your search
@@ -5,13 +5,13 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
AssetsBuildDate = "Thu, 10 Sep 2015 13:04:55 GMT"
|
||||
AssetsBuildDate = "Fri, 18 Sep 2015 06:05:31 GMT"
|
||||
)
|
||||
|
||||
func Assets() map[string][]byte {
|
||||
var assets = make(map[string][]byte, 116)
|
||||
|
||||
assets["assets/css/overrides.css"], _ = base64.StdEncoding.DecodeString("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")
|
||||
assets["assets/css/overrides.css"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/5xXW2/bNhR+9684SzGg9SxZVuykSbBgRYBhA9qiWPsyFH2gRNriQpECScVJhv73HVKiLMmXtgvg2DriuX/nwvl0Mp/DnaqeNN8UFl7evYI0WSzhU8Hg45PMbcHlBt7UtlDaxHjYnf9UcAMfVa1zhryUwe9Kl4A0U2f/sNyCVWBRgGW6NKDW/uGdeuZCEPhQZ4LnTsxbnjNp2AweYkjjJIY/10AgR2M6ng9vYUsMSGWBcmM1z2rLKGy5LfAAalxzwWZO2N+qhpxIUJklHL8kA2KhsLa6ns/LRnes9GaOMueobR5PJtP5ZJIp+gT/TgD/KkIpehtlylpVXsNlUj3e+DdrJW20JiUXT9dw9gcTD8zynMB7VrOzGXSEGbzRnIgZGCJNZJjm65vJ18mkWMygSPFzjp8lflatyqHgv4hgW/KEEn9Uh5MluGRRwVwer2ERpyuvuha/dNpKojdcRlZV7sCKlf5EXBHJRFRptdHMmPZsRvL7jVa1pNfw4nx59ZpmjZqg4TwER7A1PibNQ6UMt1zJayCZUQKz1dC9zvYMZrISBB3OhMrv+zZYbgUL2egkaSaI5Q9BEnu0kXpgei3U9hqYELwy3DQvd/SCU8qkF87xB0ZOyVZyp59LH7PWjFNKt5zaAoPmQtYPQkcYBv+mH27dEFcuXs7VsTknZQ8ZtCuuFjkIaJeZ8/PmUF6w/D5TjwdSnQTN3Rkuq9p+tk8V+/UsEM++zCDWhHI1eO0pZ1+CVqEImiaxvG5gPsWaNzWDF4vF1SVgNflMqsplobPjMWod9Ey9ZBeMuHKDeI2YjktFSUNDXiQN/WhjuOhcGYo4jZiDoIg9jkollalIzg7V4zsmhZph45Ikx+87JRHRxGB13mHv42jne7bF0uyEtIJJJliE6aKuu9Fb66y8tRp/oJ97b10DOv52rZQ9/raTTE9KPvi2k0xDxSuNsW8gs8uVUBs1yMWu1Jt22T17xmgV2sKhbHiB2McjqaKsFoIFNHuisU9oooNdY0B7HI0WdfkdxVsiUFqsXYZiGuEnkHfdzUlXGo3cZC/T84sZpMvE/Vu8uulHxZVBbXpdr/N+UT3CMlBDiFIkpg1Wex4gSGRMiR2Bu22gTUMeObJIAtHjlQi+cRF17tyciocLHY6yHOv1fhw5F1v4iZeV0pZI20MtztTQlApuWeRR7Ri2mlS9UbhtW9QySQbcdDRJG89SrNpj+mhsSiJE1IvKQdXI4jnGOG55Gip6p2pUuOaPjDqewyydmfuN4ZsD5ohxv5WMcgIvd+3u8uKyenzVN++4He4Pm+laaewlGW40UBWYI+P2KAyO2vrxkmlG7g3mGYTCnqcZTmzkEQjPuSmIbveiTuIUGtiZ2LXmQB05oDH6jWdfG8yQh4zopl5a69Aybwcir1m8fO8NMkOqQ4WFMTefTidoAvbQ2jtmmHDDSxC5qcmGTRz/Lmwd4JevExc2Imk/mBfLq10wp5+DkKgR+gVuIaZaVVRtZVQ6jbu4tnWUnnfLnHd0X/PFKvkxHbeYlX1Fq+Tnm47WjktXBkH3//ZguRx6cABzy8temI7KCxnz/Xq12gn1XwiAHqtVm023kXXlb/gzgmexHLGeYtsPRMPS4i3TLuP7Ng52yz3jFxdjC1px3glVMXkwZ7dA9lWhNNe0HX6bX+lqGO525fD3hSNdqO3n4f7Q9kaKe4rS/BkjR4R7kkRrrGlqhwvgsltujnLQQ4PjIhmW3IewyGMkTFOxeM/Cu5hrOdjbmtKLR/v+2PZvjfGO3Q+1tfa2hnbXG1WN6tOXg/GFoBeUReLqyQ+MQjPWTtJgNA6i7J7blopftXQ3k0YCLnfPh9/sEb38rfou6elR6ekh6Wl7CxvZL/gMHLGntOsmI/Dv9gIfV+4wh/Qo6W5wmHi8giuMsy9fvCj4oQqUPeAV28B00Gt3G/nV1WLXL3oX4b4RPTAfKDQ/adszfeYu0cYiZvL+hBndD3arTrpqgfwfAAAA//8BAAD//4H7tyWVEAAA")
|
||||
assets["assets/font/raleway-500.woff"], _ = base64.StdEncoding.DecodeString("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 truncated
|
||||
assets["assets/font/raleway.css"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/3JIy88r0U1LTE5VqOZSUIDycjNzKq0U1IMSc1LLEyvVrWEyxSWVOalWCnn5RbmJOXDR8tTM9IwSKwVTAwOQWHFRspVCTn5yYo4G3ARNHYXSohyNIghXF6hSrzw/LU0TaADQqBINdRBPXdOaq5YLAAAA//8BAAD//xy2WhaSAAAA")
|
||||
assets["assets/img/favicon.png"], _ = base64.StdEncoding.DecodeString("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 truncated
|
||||
@@ -49,7 +49,7 @@ func Assets() map[string][]byte {
|
||||
assets["assets/lang/lang-zh-TW.json"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/8x7WXMcR3L/+36K/jOC8QciuFju2rsPfLBCK612GVqKtCB6ww5GOBrTNUCbM92z3T2EsAg4BiRxH0NJAEiAEHEQBIcAcZCUCBAHGeGPYk8f87RfwZmV1dXVPTOQLEu29SAOqrKurDx+mVk9+DMN/jv3vmaxXt0zbzHNKhd7mKPZec3QB1zNsJlr/X9PK+o3meYyy2Vd5y5p5+qHFb8yFb1cDZfu+hvPgvnD+uFm/XDGvzce3hv9j8rtcxeSmfth9L/YjnaLOa5pW/DXgGbZntbDtJxdLMG6PQWm9Zten1Zy2C3TLrsxrctX++9PEu/m2mXtYzbA54SfjbEvGl/sy84eu+xhV+P+crBwIptzHs6BHcFXM/XTZdlhGNjorz/yJ1eVRu1DWD7Hkr7o8dfh6a5K8ZFdMJijULwaCxYW/Y23KhGeOc8J3+OLL+wDsaR8TyF1mMv3Vz+d8b+uZDpYy65bupVjfP/yd6ZP+8C28mZv2dHx/CplpiceVyhoH+qezk81UmvcqdFelX67X3vfsq2BIl7OdVfvZdqnrGQ7nmn1vkfjhqPaC3/6nX9vBiSrfvounMNpotqk//CRv/rirycryXylPr2HeWaOj9y5H+zNNFZH/aOqpLA09rnHHEsvoIwUdcvQ+uB/BeZqXh+LxQMW79Iue9AFzbbmsKINeoAEeRNEKu/YRf6XO8APT1dCcvmTLiBP0YZj/NjtWZWMH9AMLpEubJIuDlYBFdItzbQ8xzbKOdB4ogENgnsEpdINA6hgu16f6cadcIB+ViiQCTg+rp/O+8s1/2TeH3kSVUZIzoOJSlTbgR+wK2rx98eC5TtgMuqn3wopHnsV7Gxk9KLs2UWQqJxWLvU6ukFyG41t+VPz/sxY+M1STPlbUHndYfly4f8hifpnTFHu5aPD2pR/Um1M70VbG3HfB9eua9c9s2D+RQo2NhEDJRFcYy8r2JzJwcNvYNvB/SfR1rQkKDBgHxgMTc97pMrZJklpu0yYlcbChGwmeeHzT4/VjzfUDmZ5F7T+PmZpZRduQfdIPjzd8dAy61rBtPicUe04evr4ryfT4fzr6OQQ7gEuBDgdrU0D+xsLU43VnWDxtjJ3CW2COLf/+KvwcDfptCzGjZ32O8ex+ZkalcfhwVKGgXbJRDFFsWUFl8E+HbJ2bx/540+ixfloYyx6fNqK3nbMXhPUhfOrVXsyZgCa+jzt355pv7r4y1+TuthoQ0D00QJ5jtkDIuO4l8RkmQH1wyl//H70cj2cOQbh/OuJlJ8PWYF5tOPxrcbiRrqdjPrBy2wfV4DLH3Kuc8GNdh7AccP1kyyNAddn5s2clC+VPkP8iV5kCQnqcu1FhmTwPGnf+SGtY/C8Tlb9/FCn1q9bHrcoObq5Li12GzTgvWRibXCQ2oZwkkExyRBMEtx5FW0O0z2D34bfQkVpQ/dnFZNLG3KTaWWH6Yo9EPuChQOYTvbauTKKdMKPra3gwWywAGL/OiHqtwq2bmif6nQ3cH/RyUSjshLOjmWJ5CURUbZbGMdg5zHXBpXkd4bJfXx4UItO9tRWxW1TZ/qInCTx24Ik67eRKrV6Zh1LR5Ry/Zp1jR9gfgVsDv0pKcB0kDMBuFXSwceCjmgdN855udKlX/zCLF1C83/j3AUtbuqzXU80dmp67PRBpYDCGLD0opm7cQ7FpMScvO0UNV1aWgPvDbzUAFoV1DAxnBv4/ytbSXHG7LVsh2mA99DvuhfAjzGcjVtEvm1hBt+dhvNPgtpjsP9gHoO9KphEMIxhbVdBp4mdS1m438VO/SP0z/8gvTi/s40FgDbB1lywNh5OjAfLz4PZp/64lOOPdBhjgLtnRa4oqb9jGpz2Whkc7VVHyFNQvedXF8S0YLlTQKbFNs7YABIDR4omt/NajwlGAvyjYJ1BfqVg2zfRkMItaDnu7NwuABgMccFH739GyMQdcHHbnK0/xbTKjmkuBEYcdHS5XgzeBR6i+R1WKugIkmwELNxcaz0DWjcgJ8ApgLPirf5o87XfowH6gD65WIK/5ewm4Kofffs/yVJNJys5tsdNOPlkcX8QaRn8/mzQyhgmguKB820iUbFiDCQhaPQIRzLYlOvF+p0rlEEMCEj7I6/rxwvC5QwvgQIEc2+i3UmIKAE5+qcvGivv/PF9iRmRgCsAqHb9dBTsrOyqH04i8Nyd9KcXAHhG69vh4kmjMgzD/ep64+Fo/XAHg1a+ImiyP66Gq4mBbzLt1BUjgLi3CQQIsiu6K2Bh/fC43VzXdK8vPdvBnv/2izSV23I3v79+Gdvxn6RFAwzdh+CDoAfM77r9tmMISi3amol2Jvy9UWW/LYaBvjrpISK6qIykAQoS/NGEg1paKtjE9vDh42j4NB12/p5ZzBHuPZxbD+cksvh9we4Be/th7AREEOmvrASz6+E36+3otG7m3BKRtEJePzkKlmf8xVpmXLcnVificKoSjEzFNH9ghVKCTmSrXUR300sQfnOhsTocd13mlo9Px71Nuh1vl/uohIDcUZZM2lSV8vlkY/GepLTAC6NZRWwEHC8CFOn42PztL9xOAdLB4xFSglHgC2SvMoHjgG7LCJBuGpMohl7EsHLALjuxAQEiuFO07xDQOszCNmk4wOzY4AcQyHDd/cnmlnuPQ1TOnjjYTLrjjJCwK73mLZBHWAahGDR3mF2sSzNsni1in4PZMRhnm1+diU4nwm83/d2lYP1rwKJahz/zCgIFMDuIg2e/hABA8vBjxkqxE6aUyruvw/nFYOfAr74Ujnj+MKb+o+70orH7yHRcDjiD5Yq/8RQtz90tf2Q8oeNEBYzrcwy2bsTEb6eD2SfB3Gtp61JDXMashBJMXFh9C4FeY+GrhEwYoLA2AySy2c5lFQ127i/vpxWNyKS+EElaXxQSreMz29MLnK2tmsWIKzyDd53Cey65YzPAFIqvE6LPzWK5qL3fK1auNOYPBK9TB7zCPB0co65dtQpkMO6MoHF/c5JOPF0xLT7hRw5jePCbWndJJ6DfriseyfM04LrsEkrXn8usTLbr6TG4k8bSJASYjdXb4bYE+FfKBc/UCuwWK6ADNHK6Y2gdgHBzfegmsbcEd22YqDA22C9OSmrsbyw1lmb9FxvRvedgbf2d+/7IHRDJ6l6wN+fvA6eWsDP2BJKrn7DYAr59FCxvJc39SkgDDE7HM9ideDvszroYoMhKMJA1SfAnNl/73mbS0Ao3h3fX2mPWT2wv3uerWnBXmsirH/Oh60egorItn2/OplyFg2T2Go1PNu31aknmchvDh41VaUWuljkuQUOEneqfzRQJuOcZ7+lg50k48UyJZYG21z7bXo8dnWWvr8KNOiZgKspDUS7rdDeoPiRcFNMhgIixlbCvHIUxAOI5SkqWygiytD8JNJZzGA/jzLwGm8LsvrCL4Ma7tM9gpAdiyxDZOXoOI66Of+3UcrqFgykZBaZbc/sgyMsBS/J2Gi0hFOO2ovHwOBrZRCjGMQ2mFqaeSDJkHCCn5Zo/gblB//go3J4CmuDVevDtVvh8M1pc4SuD9IMu1E+XRTonngCt4swBTKzkdTg3eDYqwciw0XLB4DjU42FKR4HpoNWsWPIGeJiC3DJYXgfdbAWmTUvhbieh1bsvYfOq4ZfH1DrglOHto/DZEUBTOFxj9V5U25HZ2fj0CEFTqyVqraDRa3qZUof0Q201ZLNM3F+Dk7lYDbFcPAvhbWqDK4aL7mF5HjtT7I3yqYuiisi40ulWDiBUJstMeVd/YibangI18qt3Ggsv6MiUalY3S0v165RggRFpx3MNqzKsnysAMGXzNNORSm8nVKnE9uoL/wsp/H9fNnM3td4y6gnogFsu4Ui44pIKvMB6zu0F1W8Ie6FFeLcNqhdMj/kz9+OpPn3/SjYnHNUeBKOvG9tz6cTwpwBb7CLf3lIteCYB7KesoA9gcGbyAoj6p0ohrAY6qcbxI6WL8+4TvCau6wqLEyIMB4X7UTKTnzIX1FN4U8xTz94JqtV0r/Z+oZDUZc6gu4xJlluUn1XJwO02luYUYkxFJ0T+/Io/NZ/p1j5hTGTqGmCON4fPoE3lzNrQlSlTKn7J9lgZxC/R3q0Tr0hXZSuc0UqtleZCN1xEzhMmgUomKFl9GB3z2DaOsE2vr0t4keCrMTibP/KifgSCtYhlUcVIkdNV1ERZgiZTluDVSyWG/sFLeMhQChxrO4rv7MZlKPE9CnOlmtWiJO9tAgQqmQseBTabAIxofEzE73ybbQYrg1wxCg5UP3qojk0PUfj+XrI5jPfjJZQMNR9i8GXiTW3tZ2cFK6GZIknPHOkERN1Nu1J2eVmZl1z0Ik9s6OA9RcIiloyulN+Dewj318KVHUoIAHiDC/HfjTRWx9HL8cyDP/I0mKgEyxPy0niyYjR8eAgoQr3APrtfJBoaa3vhxlFTlgEpsHoHG9INxCWyPhE7rHi3oF5e2U0AgG6Az/FMlxJLqZwOd+0QR3GQBOgh9ooWMEGU/B4HD99lqh+0Q3lIihHAxMF54GqD3W+DhTHEsvvk6B8HX+5gLobnXxJGnC6D16RXBWCyQeZ5EhR9J6UcMtz5YWcvlwwOfgRiwnMpup6wxkV4VGB5j2DCDzo7YtzndwENACT317b9vW1AA5SQongHziuTViAWxBn0T83nLXsGHLkZ9cY9Gtb14poWkfi708G4TCB0m9jfEpbvr/kLu+2ReTfQwaB2EQ1aLJ6GTCIaG6IximcWdlvHM1vIsIVd1YwRISoID3UkEO4uguY1xdH71SZk322XHbjCD2wKLP3ZFf/plKownt7by9A2tGACZgun5gEenMEH7tF+69j9IjlG7imsVAGjKIkmjHsBSps58uPfvohqyR4hjiwl9cXh5WBHxtvdhF74CTlekR0YcFxzbM/O2YWW2TaKQBqVJULine truncated
|
||||
assets["assets/lang/prettyprint.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/1xSbW7aQBD931Os/MtI9QUqVWrtUtoQEWQg+T3gxd54PWvtBwiiHCcnycUys7aA5Bfz3puZffPwAazQgPXSSu9PvVXoxU/xkmzr5EeSB12DVYDJ92QHRBTgQY/w1wG0xJ264UX6OHI44R7H0lnuGgKVJDCTtovjUjOyUrYMkMAUa61cE2E2y6+MSDeovKzEXGFdmY43S9686gHHCZdNV1dGpFSoaGGviP6rcGzcW4aWLA4oK35fCJEWgFABjzWB6H8BL9crT8R/OnKAz3z0HdBz0knCrcnmJVFzYyXHEH95kea5e+WbQM7iKG6JWRh7lDURIjdt9/6mWeBI/gQfrSF+6lqc0FjHWfXctTRjVL3P8jIS1oc6kBmR5hbOSk8Gdbn+og41RN2arHwgvTRddCfSsYoiR1AG5wbb7sD5HmU1PBxagpvWUsyDfm6yYsF/d6MwPsRFXETK+ulWWYM68gfy+u0DAAD//wEAAP//j0iqQH0CAAA=")
|
||||
assets["assets/lang/valid-langs.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/yTMvQ6CMBTF8d2naDp7H8FEIcZBo6aSOBiHAhUqTSEtdPDpvacsv38/kpN0EEk7216076LYiZesO7mVjc7s+c/4xuZbZFrDGAd8hk4FGjN0fHA/FoQMlQduvzB2Zr5YGkY6K67Di68BFj0WJ5ymmQq19l5xw0jqhmInJmYZmF9P5XVt9ZTvzR8AAP//AQAA//8eEnv9zQAAAA==")
|
||||
assets["index.html"], _ = base64.StdEncoding.DecodeString("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 truncated
|
||||
assets["index.html"], _ = base64.StdEncoding.DecodeString("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 truncated
|
||||
assets["modal.html"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/3RST0/cPhC9/z7F/HLogkQ2gDjRXaoWqVIlKiHBpceJPUlcHDuyJ9BtlO/eibNNYbUcEv97783Mm9lo8wzKYozbrPUaLVSoKQPG0jhNv7ZZfpGBq3NkDrlGxrxE9aSD77bZMAjVR4JPwKEnuIZVZGSjVjCOB6wn2pUeg06sL95bQneS2KcT+OY/gM3/eS5LUcCt73bB1A3Dye0pXJ5fXMFjQ/Cwc4ob42r43HPjQ1wn+Mx5bEyEB98HRcLXBF99aEHuYl/+JMXAHlhEmEIbwVfp8N3/NtYi3PelNWoWujOKXKQzeF7D5fp8Dd8qQFCS0sK6v4MXjOA8gzaRgyl7Jg0vhhsBSMzKWDqb5X74HhQ68CWjkcURIEPD3F0XRTvHX/tQF6JaSLxiKirPkyGHzcm1QevrZO3+fljNL7ZeXYPFUNNs5jG28o7J8f79GKIh6X4AtBR4/ufDMPW0j+O48ITZXL0lsmFLrwACiZ1ULYmaapsZCZ39ZVQoQya60+Wkuikm6GvuMCS9cfwXsGiulrQLyfv9Gkqvd8khDuiisr2m95n7/NIgZm91Ku9lVl4XLV1m74B3HW2z+bBwSnYgX66pwt5y2sc2gzT8MiStWZSP2PTGGTYtxcWWD66M3ccZliqyyHRzOyV8aNymmJM6Uu6y3W/2yx8AAAD//wEAAP//qvxG6f8DAAA=")
|
||||
assets["syncthing/app.js"], _ = base64.StdEncoding.DecodeString("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")
|
||||
assets["syncthing/core/aboutModalDirective.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/1SOMQ7CMAxF954im1upSnc6cQBG2ENqWktOglwHhFDvTgpSgT/+/yw/F8fMTmxIQ2asYX5ErxPF0fokCE1lSuxAgl7pVgB3TlkPaXAMrbnkQlOKpm7M842uEdQs8af4lLMKed0Z2EP7NymGKzvFo3BZN4NuNei+/06EdztpYNiul75amr56AQAA//8BAAD//xLf4CHFAAAA")
|
||||
@@ -78,7 +78,7 @@ func Assets() map[string][]byte {
|
||||
assets["syncthing/core/selectOnClickDirective.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/3SQQWvEIBCF7/srPBRiIEjPXXooe2+hPZYeRKeprJkpo2YpJf+9atLdJWzeSZ/z+Z5q7JPXrAayyYNswg+a+OWwV4YYmnYnspR1DCa6sQyAz8sXPHhnjk0nPlMGHKGQdyeHlk6t+K1QEUNMjFdGkXd4fLjigqFv6ES+dwCMndAxcmhXUNEyoQhlY1bx8hZQNGoWc+cS9iiWlqqH+PZvy3a/yXL+IcicJZNqumHQEV6LvcVVRs2pz2ThQBgzGuTygvf7jw3y3FQxDDTCk/c1KWxFXQBt7Vyqpt8Yn1bedN5N+105/AMAAP//AQAA//+SF+4JDAIAAA==")
|
||||
assets["syncthing/core/shutdownDialogDirective.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/1SOwQqDQAxE737F3qIg672eCv2F9r6sqQZitsRspRT/vdqC1DnOTDIvSJ85qB9TlxlLmF4SbSDpfUyKUBVule9IMRo9t8KQrUuzXChw6qF297xeUBJXVu79rW9StKzyZ/zMyZSinRycoT5EhuODg+FVeU13imajaI6bN8LZDzYy7B+WtliqtvgAAAD//wEAAP//3qFOo80AAAA=")
|
||||
assets["syncthing/core/shutdownDialogView.html"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/0TOTaqDMBAH8PXzFMNsXL16gZhNj+AJ0jjWQJwJzgQp1ru3lkK3P/h/uEXGkCGNPepcbZSNEdSCVX1LjZFUEVIU7rHIRuu/TBOCJcvU4763wzcFV1lKJqMWnmBrYM3B6DjQN3+u/MQPD442J77DHBRuRAznMpwlF9cV37juc8o3LwAAAP//AQAA//99X8KxnQAAAA==")
|
||||
assets["syncthing/core/syncthingController.js"], _ = base64.StdEncoding.DecodeString("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 truncated
|
||||
assets["syncthing/core/syncthingController.js"], _ = base64.StdEncoding.DecodeString("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 truncated
|
||||
assets["syncthing/core/uniqueFolderDirective.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/7SSwW4yMQyE7zyFD7+0IK3CHc5/b1V74h5tvKzVkIDtQFHFuzdhUaGwtJWqzgVp4vkYW2vDMnnLZhVd8jiuZB8a7SgsTRMZq8kIsowjxkZpmwdSoE3Ch+gdclVDm/I8xQDjCbwdh4sYNXG4MHpzkzJnBlVYPkaHvqo/vXsKL7MLoDRxjTWgX9VgVVlqaJT95ApbVHzzb21ZkMWkIB21Oj6jtoS7hfUJh8JF1J7+z6Ajzev/fyUpv/cCRdMp7BCki8m7UClsrSdnFe8G+pqCuiiTpPubcyrnjvNBwCFfQvCiaXsMiemsPO3CM2ePM/K86zfdtUPoGYBlWwHrGa3b/6J/a3PHrxf4aScS6OF/dM5B9/ThftzwNnu44p05h/moPL4DAAD//wEAAP//r65WJ1IDAAA=")
|
||||
assets["syncthing/core/upgradingDialogDirective.js"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/1yOwQrCMBBE7/2K3LaFkt7tSfAX9B7SNV3YJmW7UUT676YKRZ3jzNudcTFkdmKnNGTGGpZH9DpSDNYnQWgqU2QHEvRKtwLkOYgbCnAixylAa665nFCKpm7M881vEtQs8cv4mIsKeT0YOEL7EylOMzvFs3BJ9xndNqP7K70Q3u2oE8P+Yu2rtemrFwAAAP//AQAA//99zQ2GzwAAAA==")
|
||||
assets["syncthing/core/upgradingDialogView.html"], _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/1zOTarDMAwE4PXLKYQ2WSW5gO0zPCg9gHDcVODaxlIIJc3dm5b+QLfDN8yYSx4pAo8W5zJVGjlNCKKks1jkdMoI7HOySLXmpfNcfQzdXBCUNQaL69oe380WbqCVkkTSsG3omj9TnJFC6Zu7wzV5Pe8cWOCz2pvh4Rz88v8YSAIsxPoifb/j4hozPN+75g4AAP//AQAA///kaeW6xgAAAA==")
|
||||
|
||||
+23
-22
@@ -6,7 +6,12 @@
|
||||
|
||||
package beacon
|
||||
|
||||
import "net"
|
||||
import (
|
||||
"net"
|
||||
stdsync "sync"
|
||||
|
||||
"github.com/thejerf/suture"
|
||||
)
|
||||
|
||||
type recv struct {
|
||||
data []byte
|
||||
@@ -14,34 +19,30 @@ type recv struct {
|
||||
}
|
||||
|
||||
type Interface interface {
|
||||
suture.Service
|
||||
Send(data []byte)
|
||||
Recv() ([]byte, net.Addr)
|
||||
Error() error
|
||||
}
|
||||
|
||||
type readerFrom interface {
|
||||
ReadFrom([]byte) (int, net.Addr, error)
|
||||
}
|
||||
|
||||
func genericReader(conn readerFrom, outbox chan<- recv) {
|
||||
bs := make([]byte, 65536)
|
||||
for {
|
||||
n, addr, err := conn.ReadFrom(bs)
|
||||
if err != nil {
|
||||
l.Warnln("multicast read:", err)
|
||||
return
|
||||
}
|
||||
if debug {
|
||||
l.Debugf("recv %d bytes from %s", n, addr)
|
||||
}
|
||||
type errorHolder struct {
|
||||
err error
|
||||
mut stdsync.Mutex // uses stdlib sync as I want this to be trivially embeddable, and there is no risk of blocking
|
||||
}
|
||||
|
||||
c := make([]byte, n)
|
||||
copy(c, bs)
|
||||
select {
|
||||
case outbox <- recv{c, addr}:
|
||||
default:
|
||||
if debug {
|
||||
l.Debugln("dropping message")
|
||||
}
|
||||
}
|
||||
}
|
||||
func (e *errorHolder) setError(err error) {
|
||||
e.mut.Lock()
|
||||
e.err = err
|
||||
e.mut.Unlock()
|
||||
}
|
||||
|
||||
func (e *errorHolder) Error() error {
|
||||
e.mut.Lock()
|
||||
err := e.err
|
||||
e.mut.Unlock()
|
||||
return err
|
||||
}
|
||||
+43
-27
@@ -19,6 +19,8 @@ type Broadcast struct {
|
||||
port int
|
||||
inbox chan []byte
|
||||
outbox chan recv
|
||||
br *broadcastReader
|
||||
bw *broadcastWriter
|
||||
}
|
||||
|
||||
func NewBroadcast(port int) *Broadcast {
|
||||
@@ -41,14 +43,16 @@ func NewBroadcast(port int) *Broadcast {
|
||||
outbox: make(chan recv, 16),
|
||||
}
|
||||
|
||||
b.Add(&broadcastReader{
|
||||
b.br = &broadcastReader{
|
||||
port: port,
|
||||
outbox: b.outbox,
|
||||
})
|
||||
b.Add(&broadcastWriter{
|
||||
}
|
||||
b.Add(b.br)
|
||||
b.bw = &broadcastWriter{
|
||||
port: port,
|
||||
inbox: b.inbox,
|
||||
})
|
||||
}
|
||||
b.Add(b.bw)
|
||||
|
||||
return b
|
||||
}
|
||||
@@ -62,11 +66,18 @@ func (b *Broadcast) Recv() ([]byte, net.Addr) {
|
||||
return recv.data, recv.src
|
||||
}
|
||||
|
||||
func (b *Broadcast) Error() error {
|
||||
if err := b.br.Error(); err != nil {
|
||||
return err
|
||||
}
|
||||
return b.bw.Error()
|
||||
}
|
||||
|
||||
type broadcastWriter struct {
|
||||
port int
|
||||
inbox chan []byte
|
||||
conn *net.UDPConn
|
||||
failed bool // Have we already logged a failure reason?
|
||||
port int
|
||||
inbox chan []byte
|
||||
conn *net.UDPConn
|
||||
errorHolder
|
||||
}
|
||||
|
||||
func (w *broadcastWriter) Serve() {
|
||||
@@ -78,22 +89,21 @@ func (w *broadcastWriter) Serve() {
|
||||
var err error
|
||||
w.conn, err = net.ListenUDP("udp4", nil)
|
||||
if err != nil {
|
||||
if !w.failed {
|
||||
l.Warnln("Local discovery over IPv4 unavailable:", err)
|
||||
w.failed = true
|
||||
if debug {
|
||||
l.Debugln(err)
|
||||
}
|
||||
w.setError(err)
|
||||
return
|
||||
}
|
||||
defer w.conn.Close()
|
||||
|
||||
w.failed = false
|
||||
|
||||
for bs := range w.inbox {
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugln("Local discovery (broadcast writer):", err)
|
||||
l.Debugln(err)
|
||||
}
|
||||
w.setError(err)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -117,13 +127,16 @@ func (w *broadcastWriter) Serve() {
|
||||
for _, ip := range dsts {
|
||||
dst := &net.UDPAddr{IP: ip, Port: w.port}
|
||||
|
||||
w.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
|
||||
w.conn.SetWriteDeadline(time.Now().Add(time.Second))
|
||||
_, err := w.conn.WriteTo(bs, dst)
|
||||
w.conn.SetWriteDeadline(time.Time{})
|
||||
if err, ok := err.(net.Error); ok && err.Timeout() {
|
||||
// Write timeouts should not happen. We treat it as a fatal
|
||||
// error on the socket.
|
||||
l.Infoln("Local discovery (broadcast writer):", err)
|
||||
w.failed = true
|
||||
if debug {
|
||||
l.Debugln(err)
|
||||
}
|
||||
w.setError(err)
|
||||
return
|
||||
} else if err, ok := err.(net.Error); ok && err.Temporary() {
|
||||
// A transient error. Lets hope for better luck in the future.
|
||||
@@ -133,11 +146,14 @@ func (w *broadcastWriter) Serve() {
|
||||
continue
|
||||
} else if err != nil {
|
||||
// Some other error that we don't expect. Bail and retry.
|
||||
l.Infoln("Local discovery (broadcast writer):", err)
|
||||
w.failed = true
|
||||
if debug {
|
||||
l.Debugln(err)
|
||||
}
|
||||
w.setError(err)
|
||||
return
|
||||
} else if debug {
|
||||
l.Debugf("sent %d bytes to %s", len(bs), dst)
|
||||
w.setError(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -155,7 +171,7 @@ type broadcastReader struct {
|
||||
port int
|
||||
outbox chan recv
|
||||
conn *net.UDPConn
|
||||
failed bool
|
||||
errorHolder
|
||||
}
|
||||
|
||||
func (r *broadcastReader) Serve() {
|
||||
@@ -167,10 +183,10 @@ func (r *broadcastReader) Serve() {
|
||||
var err error
|
||||
r.conn, err = net.ListenUDP("udp4", &net.UDPAddr{Port: r.port})
|
||||
if err != nil {
|
||||
if !r.failed {
|
||||
l.Warnln("Local discovery over IPv4 unavailable:", err)
|
||||
r.failed = true
|
||||
if debug {
|
||||
l.Debugln(err)
|
||||
}
|
||||
r.setError(err)
|
||||
return
|
||||
}
|
||||
defer r.conn.Close()
|
||||
@@ -179,14 +195,14 @@ func (r *broadcastReader) Serve() {
|
||||
for {
|
||||
n, addr, err := r.conn.ReadFrom(bs)
|
||||
if err != nil {
|
||||
if !r.failed {
|
||||
l.Infoln("Local discovery (broadcast reader):", err)
|
||||
r.failed = true
|
||||
if debug {
|
||||
l.Debugln(err)
|
||||
}
|
||||
r.setError(err)
|
||||
return
|
||||
}
|
||||
|
||||
r.failed = false
|
||||
r.setError(nil)
|
||||
|
||||
if debug {
|
||||
l.Debugf("recv %d bytes from %s", n, addr)
|
||||
|
||||
+201
-54
@@ -8,39 +8,200 @@ package beacon
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/thejerf/suture"
|
||||
"golang.org/x/net/ipv6"
|
||||
)
|
||||
|
||||
type Multicast struct {
|
||||
conn *ipv6.PacketConn
|
||||
*suture.Supervisor
|
||||
addr *net.UDPAddr
|
||||
inbox chan []byte
|
||||
outbox chan recv
|
||||
intfs []net.Interface
|
||||
mr *multicastReader
|
||||
mw *multicastWriter
|
||||
}
|
||||
|
||||
func NewMulticast(addr string) (*Multicast, error) {
|
||||
gaddr, err := net.ResolveUDPAddr("udp6", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func NewMulticast(addr string) *Multicast {
|
||||
m := &Multicast{
|
||||
Supervisor: suture.New("multicastBeacon", suture.Spec{
|
||||
// Don't retry too frenetically: an error to open a socket or
|
||||
// whatever is usually something that is either permanent or takes
|
||||
// a while to get solved...
|
||||
FailureThreshold: 2,
|
||||
FailureBackoff: 60 * time.Second,
|
||||
// Only log restarts in debug mode.
|
||||
Log: func(line string) {
|
||||
if debug {
|
||||
l.Debugln(line)
|
||||
}
|
||||
},
|
||||
}),
|
||||
inbox: make(chan []byte),
|
||||
outbox: make(chan recv, 16),
|
||||
}
|
||||
|
||||
conn, err := net.ListenPacket("udp6", addr)
|
||||
m.mr = &multicastReader{
|
||||
addr: addr,
|
||||
outbox: m.outbox,
|
||||
stop: make(chan struct{}),
|
||||
}
|
||||
m.Add(m.mr)
|
||||
|
||||
m.mw = &multicastWriter{
|
||||
addr: addr,
|
||||
inbox: m.inbox,
|
||||
stop: make(chan struct{}),
|
||||
}
|
||||
m.Add(m.mw)
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Multicast) Send(data []byte) {
|
||||
m.inbox <- data
|
||||
}
|
||||
|
||||
func (m *Multicast) Recv() ([]byte, net.Addr) {
|
||||
recv := <-m.outbox
|
||||
return recv.data, recv.src
|
||||
}
|
||||
|
||||
func (m *Multicast) Error() error {
|
||||
if err := m.mr.Error(); err != nil {
|
||||
return err
|
||||
}
|
||||
return m.mw.Error()
|
||||
}
|
||||
|
||||
type multicastWriter struct {
|
||||
addr string
|
||||
inbox <-chan []byte
|
||||
errorHolder
|
||||
stop chan struct{}
|
||||
}
|
||||
|
||||
func (w *multicastWriter) Serve() {
|
||||
if debug {
|
||||
l.Debugln(w, "starting")
|
||||
defer l.Debugln(w, "stopping")
|
||||
}
|
||||
|
||||
gaddr, err := net.ResolveUDPAddr("udp6", w.addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if debug {
|
||||
l.Debugln(err)
|
||||
}
|
||||
w.setError(err)
|
||||
return
|
||||
}
|
||||
|
||||
conn, err := net.ListenPacket("udp6", ":0")
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugln(err)
|
||||
}
|
||||
w.setError(err)
|
||||
return
|
||||
}
|
||||
|
||||
pconn := ipv6.NewPacketConn(conn)
|
||||
|
||||
wcm := &ipv6.ControlMessage{
|
||||
HopLimit: 1,
|
||||
}
|
||||
|
||||
for bs := range w.inbox {
|
||||
intfs, err := net.Interfaces()
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugln(err)
|
||||
}
|
||||
w.setError(err)
|
||||
return
|
||||
}
|
||||
|
||||
var success int
|
||||
|
||||
for _, intf := range intfs {
|
||||
wcm.IfIndex = intf.Index
|
||||
pconn.SetWriteDeadline(time.Now().Add(time.Second))
|
||||
_, err = pconn.WriteTo(bs, wcm, gaddr)
|
||||
pconn.SetWriteDeadline(time.Time{})
|
||||
if err != nil && debug {
|
||||
l.Debugln(err, "on write to", gaddr, intf.Name)
|
||||
} else if debug {
|
||||
l.Debugf("sent %d bytes to %v on %s", len(bs), gaddr, intf.Name)
|
||||
success++
|
||||
}
|
||||
}
|
||||
|
||||
if success > 0 {
|
||||
w.setError(nil)
|
||||
} else {
|
||||
if debug {
|
||||
l.Debugln(err)
|
||||
}
|
||||
w.setError(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *multicastWriter) Stop() {
|
||||
close(w.stop)
|
||||
}
|
||||
|
||||
func (w *multicastWriter) String() string {
|
||||
return fmt.Sprintf("multicastWriter@%p", w)
|
||||
}
|
||||
|
||||
type multicastReader struct {
|
||||
addr string
|
||||
outbox chan<- recv
|
||||
errorHolder
|
||||
stop chan struct{}
|
||||
}
|
||||
|
||||
func (r *multicastReader) Serve() {
|
||||
if debug {
|
||||
l.Debugln(r, "starting")
|
||||
defer l.Debugln(r, "stopping")
|
||||
}
|
||||
|
||||
gaddr, err := net.ResolveUDPAddr("udp6", r.addr)
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugln(err)
|
||||
}
|
||||
r.setError(err)
|
||||
return
|
||||
}
|
||||
|
||||
conn, err := net.ListenPacket("udp6", r.addr)
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugln(err)
|
||||
}
|
||||
r.setError(err)
|
||||
return
|
||||
}
|
||||
|
||||
intfs, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if debug {
|
||||
l.Debugln(err)
|
||||
}
|
||||
r.setError(err)
|
||||
return
|
||||
}
|
||||
|
||||
p := ipv6.NewPacketConn(conn)
|
||||
pconn := ipv6.NewPacketConn(conn)
|
||||
joined := 0
|
||||
for _, intf := range intfs {
|
||||
err := p.JoinGroup(&intf, &net.UDPAddr{IP: gaddr.IP})
|
||||
err := pconn.JoinGroup(&intf, &net.UDPAddr{IP: gaddr.IP})
|
||||
if debug {
|
||||
if err != nil {
|
||||
l.Debugln("IPv6 join", intf.Name, "failed:", err)
|
||||
@@ -52,57 +213,43 @@ func NewMulticast(addr string) (*Multicast, error) {
|
||||
}
|
||||
|
||||
if joined == 0 {
|
||||
return nil, errors.New("no multicast interfaces available")
|
||||
if debug {
|
||||
l.Debugln("no multicast interfaces available")
|
||||
}
|
||||
r.setError(errors.New("no multicast interfaces available"))
|
||||
return
|
||||
}
|
||||
|
||||
b := &Multicast{
|
||||
conn: p,
|
||||
addr: gaddr,
|
||||
inbox: make(chan []byte),
|
||||
outbox: make(chan recv, 16),
|
||||
intfs: intfs,
|
||||
}
|
||||
bs := make([]byte, 65536)
|
||||
for {
|
||||
n, _, addr, err := pconn.ReadFrom(bs)
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugln(err)
|
||||
}
|
||||
r.setError(err)
|
||||
continue
|
||||
}
|
||||
if debug {
|
||||
l.Debugf("recv %d bytes from %s", n, addr)
|
||||
}
|
||||
|
||||
go genericReader(ipv6ReaderAdapter{b.conn}, b.outbox)
|
||||
go b.writer()
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (b *Multicast) Send(data []byte) {
|
||||
b.inbox <- data
|
||||
}
|
||||
|
||||
func (b *Multicast) Recv() ([]byte, net.Addr) {
|
||||
recv := <-b.outbox
|
||||
return recv.data, recv.src
|
||||
}
|
||||
|
||||
func (b *Multicast) writer() {
|
||||
wcm := &ipv6.ControlMessage{
|
||||
HopLimit: 1,
|
||||
}
|
||||
|
||||
for bs := range b.inbox {
|
||||
for _, intf := range b.intfs {
|
||||
wcm.IfIndex = intf.Index
|
||||
_, err := b.conn.WriteTo(bs, wcm, b.addr)
|
||||
if err != nil && debug {
|
||||
l.Debugln(err, "on write to", b.addr)
|
||||
} else if debug {
|
||||
l.Debugf("sent %d bytes to %v on %s", len(bs), b.addr, intf.Name)
|
||||
c := make([]byte, n)
|
||||
copy(c, bs)
|
||||
select {
|
||||
case r.outbox <- recv{c, addr}:
|
||||
default:
|
||||
if debug {
|
||||
l.Debugln("dropping message")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This makes ReadFrom on an *ipv6.PacketConn behave like ReadFrom on a
|
||||
// net.PacketConn.
|
||||
type ipv6ReaderAdapter struct {
|
||||
c *ipv6.PacketConn
|
||||
func (r *multicastReader) Stop() {
|
||||
close(r.stop)
|
||||
}
|
||||
|
||||
func (i ipv6ReaderAdapter) ReadFrom(bs []byte) (int, net.Addr, error) {
|
||||
n, _, src, err := i.c.ReadFrom(bs)
|
||||
return n, src, err
|
||||
func (r *multicastReader) String() string {
|
||||
return fmt.Sprintf("multicastReader@%p", r)
|
||||
}
|
||||
+27
-8
@@ -31,6 +31,21 @@ const (
|
||||
MaxRescanIntervalS = 365 * 24 * 60 * 60
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultDiscoveryServers should be substituted when the configuration
|
||||
// contains <globalAnnounceServer>default</globalAnnounceServer>. This is
|
||||
// done by the "consumer" of the configuration, as we don't want these
|
||||
// saved to the config.
|
||||
DefaultDiscoveryServers = []string{
|
||||
"https://v4-1.discover.syncthing.net/?id=SR7AARM-TCBUZ5O-VFAXY4D-CECGSDE-3Q6IZ4G-XG7AH75-OBIXJQV-QJ6NLQA", // 194.126.249.5, Sweden
|
||||
"https://v4-2.discover.syncthing.net/?id=AQEHEO2-XOS7QRA-X2COH5K-PO6OPVA-EWOSEGO-KZFMD32-XJ4ZV46-CUUVKAS", // 45.55.230.38, USA
|
||||
"https://v4-3.discover.syncthing.net/?id=7WT2BVR-FX62ZOW-TNVVW25-6AHFJGD-XEXQSBW-VO3MPL2-JBTLL4T-P4572Q4", // 128.199.95.124, Singapore
|
||||
"https://v6-1.discover.syncthing.net/?id=SR7AARM-TCBUZ5O-VFAXY4D-CECGSDE-3Q6IZ4G-XG7AH75-OBIXJQV-QJ6NLQA", // 2001:470:28:4d6::5, Sweden
|
||||
"https://v6-2.discover.syncthing.net/?id=AQEHEO2-XOS7QRA-X2COH5K-PO6OPVA-EWOSEGO-KZFMD32-XJ4ZV46-CUUVKAS", // 2604:a880:800:10::182:a001, USA
|
||||
"https://v6-3.discover.syncthing.net/?id=7WT2BVR-FX62ZOW-TNVVW25-6AHFJGD-XEXQSBW-VO3MPL2-JBTLL4T-P4572Q4", // 2400:6180:0:d0::d9:d001, Singapore
|
||||
}
|
||||
)
|
||||
|
||||
type Configuration struct {
|
||||
Version int `xml:"version,attr" json:"version"`
|
||||
Folders []FolderConfiguration `xml:"folder" json:"folders"`
|
||||
@@ -215,7 +230,7 @@ type FolderDeviceConfiguration struct {
|
||||
|
||||
type OptionsConfiguration struct {
|
||||
ListenAddress []string `xml:"listenAddress" json:"listenAddress" default:"tcp://0.0.0.0:22000"`
|
||||
GlobalAnnServers []string `xml:"globalAnnounceServer" json:"globalAnnounceServers" json:"globalAnnounceServer" default:"udp4://announce.syncthing.net:22027, udp6://announce-v6.syncthing.net:22027"`
|
||||
GlobalAnnServers []string `xml:"globalAnnounceServer" json:"globalAnnounceServers" json:"globalAnnounceServer" default:"default"`
|
||||
GlobalAnnEnabled bool `xml:"globalAnnounceEnabled" json:"globalAnnounceEnabled" default:"true"`
|
||||
LocalAnnEnabled bool `xml:"localAnnounceEnabled" json:"localAnnounceEnabled" default:"true"`
|
||||
LocalAnnPort int `xml:"localAnnouncePort" json:"localAnnouncePort" default:"21027"`
|
||||
@@ -498,17 +513,21 @@ func convertV11V12(cfg *Configuration) {
|
||||
}
|
||||
|
||||
// Use new discovery server
|
||||
for i, addr := range cfg.Options.GlobalAnnServers {
|
||||
var newDiscoServers []string
|
||||
var useDefault bool
|
||||
for _, addr := range cfg.Options.GlobalAnnServers {
|
||||
if addr == "udp4://announce.syncthing.net:22026" {
|
||||
cfg.Options.GlobalAnnServers[i] = "udp4://announce.syncthing.net:22027"
|
||||
useDefault = true
|
||||
} else if addr == "udp6://announce-v6.syncthing.net:22026" {
|
||||
cfg.Options.GlobalAnnServers[i] = "udp6://announce-v6.syncthing.net:22027"
|
||||
} else if addr == "udp4://194.126.249.5:22026" {
|
||||
cfg.Options.GlobalAnnServers[i] = "udp4://194.126.249.5:22027"
|
||||
} else if addr == "udp6://[2001:470:28:4d6::5]:22026" {
|
||||
cfg.Options.GlobalAnnServers[i] = "udp6://[2001:470:28:4d6::5]:22027"
|
||||
useDefault = true
|
||||
} else {
|
||||
newDiscoServers = append(newDiscoServers, addr)
|
||||
}
|
||||
}
|
||||
if useDefault {
|
||||
newDiscoServers = append(newDiscoServers, "default")
|
||||
}
|
||||
cfg.Options.GlobalAnnServers = newDiscoServers
|
||||
|
||||
// Use new multicast group
|
||||
if cfg.Options.LocalAnnMCAddr == "[ff32::5222]:21026" {
|
||||
|
||||
@@ -32,7 +32,7 @@ func init() {
|
||||
func TestDefaultValues(t *testing.T) {
|
||||
expected := OptionsConfiguration{
|
||||
ListenAddress: []string{"tcp://0.0.0.0:22000"},
|
||||
GlobalAnnServers: []string{"udp4://announce.syncthing.net:22027", "udp6://announce-v6.syncthing.net:22027"},
|
||||
GlobalAnnServers: []string{"default"},
|
||||
GlobalAnnEnabled: true,
|
||||
LocalAnnEnabled: true,
|
||||
LocalAnnPort: 21027,
|
||||
|
||||
@@ -317,3 +317,15 @@ func (w *Wrapper) Save() error {
|
||||
events.Default.Log(events.ConfigSaved, w.cfg)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *Wrapper) GlobalDiscoveryServers() []string {
|
||||
var servers []string
|
||||
for _, srv := range w.cfg.Options.GlobalAnnServers {
|
||||
if srv == "default" {
|
||||
servers = append(servers, DefaultDiscoveryServers...)
|
||||
} else {
|
||||
servers = append(servers, srv)
|
||||
}
|
||||
}
|
||||
return uniqueStrings(servers)
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package discover
|
||||
|
||||
import (
|
||||
stdsync "sync"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/protocol"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
"github.com/thejerf/suture"
|
||||
)
|
||||
|
||||
// The CachingMux aggregates results from multiple Finders. Each Finder has
|
||||
// an associated cache time and negative cache time. The cache time sets how
|
||||
// long we cache and return successfull lookup results, the negative cache
|
||||
// time sets how long we refrain from asking about the same device ID after
|
||||
// receiving a negative answer. The value of zero disables caching (positive
|
||||
// or negative).
|
||||
type CachingMux struct {
|
||||
*suture.Supervisor
|
||||
finders []cachedFinder
|
||||
caches []*cache
|
||||
mut sync.Mutex
|
||||
}
|
||||
|
||||
// A cachedFinder is a Finder with associated cache timeouts.
|
||||
type cachedFinder struct {
|
||||
Finder
|
||||
cacheTime time.Duration
|
||||
negCacheTime time.Duration
|
||||
}
|
||||
|
||||
func NewCachingMux() *CachingMux {
|
||||
return &CachingMux{
|
||||
Supervisor: suture.NewSimple("discover.cachingMux"),
|
||||
mut: sync.NewMutex(),
|
||||
}
|
||||
}
|
||||
|
||||
// Add registers a new Finder, with associated cache timeouts.
|
||||
func (m *CachingMux) Add(finder Finder, cacheTime, negCacheTime time.Duration) {
|
||||
m.mut.Lock()
|
||||
m.finders = append(m.finders, cachedFinder{finder, cacheTime, negCacheTime})
|
||||
m.caches = append(m.caches, newCache())
|
||||
m.mut.Unlock()
|
||||
|
||||
if svc, ok := finder.(suture.Service); ok {
|
||||
m.Supervisor.Add(svc)
|
||||
}
|
||||
}
|
||||
|
||||
// Lookup attempts to resolve the device ID using any of the added Finders,
|
||||
// while obeying the cache settings.
|
||||
func (m *CachingMux) Lookup(deviceID protocol.DeviceID) (direct []string, relays []Relay, err error) {
|
||||
m.mut.Lock()
|
||||
for i, finder := range m.finders {
|
||||
if cacheEntry, ok := m.caches[i].Get(deviceID); ok {
|
||||
// We have a cache entry. Lets see what it says.
|
||||
|
||||
if cacheEntry.found && time.Since(cacheEntry.when) < finder.cacheTime {
|
||||
// It's a positive, valid entry. Use it.
|
||||
if debug {
|
||||
l.Debugln("cached discovery entry for", deviceID, "at", finder.String())
|
||||
l.Debugln(" ", cacheEntry)
|
||||
}
|
||||
direct = append(direct, cacheEntry.Direct...)
|
||||
relays = append(relays, cacheEntry.Relays...)
|
||||
continue
|
||||
}
|
||||
|
||||
if !cacheEntry.found && time.Since(cacheEntry.when) < finder.negCacheTime {
|
||||
// It's a negative, valid entry. We should not make another
|
||||
// attempt right now.
|
||||
if debug {
|
||||
l.Debugln("negative cache entry for", deviceID, "at", finder.String())
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// It's expired. Ignore and continue.
|
||||
}
|
||||
|
||||
// Perform the actual lookup and cache the result.
|
||||
if td, tr, err := finder.Lookup(deviceID); err == nil {
|
||||
if debug {
|
||||
l.Debugln("lookup for", deviceID, "at", finder.String())
|
||||
l.Debugln(" ", td)
|
||||
l.Debugln(" ", tr)
|
||||
}
|
||||
direct = append(direct, td...)
|
||||
relays = append(relays, tr...)
|
||||
m.caches[i].Set(deviceID, CacheEntry{
|
||||
Direct: td,
|
||||
Relays: tr,
|
||||
when: time.Now(),
|
||||
found: len(td)+len(tr) > 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
m.mut.Unlock()
|
||||
|
||||
if debug {
|
||||
l.Debugln("lookup results for", deviceID)
|
||||
l.Debugln(" ", direct)
|
||||
l.Debugln(" ", relays)
|
||||
}
|
||||
|
||||
return direct, relays, nil
|
||||
}
|
||||
|
||||
func (m *CachingMux) String() string {
|
||||
return "discovery cache"
|
||||
}
|
||||
|
||||
func (m *CachingMux) Error() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *CachingMux) ChildErrors() map[string]error {
|
||||
m.mut.Lock()
|
||||
children := make(map[string]error, len(m.finders))
|
||||
for _, f := range m.finders {
|
||||
children[f.String()] = f.Error()
|
||||
}
|
||||
m.mut.Unlock()
|
||||
return children
|
||||
}
|
||||
|
||||
func (m *CachingMux) Cache() map[protocol.DeviceID]CacheEntry {
|
||||
// Res will be the "total" cache, i.e. the union of our cache and all our
|
||||
// children's caches.
|
||||
res := make(map[protocol.DeviceID]CacheEntry)
|
||||
|
||||
m.mut.Lock()
|
||||
for i := range m.finders {
|
||||
// Each finder[i] has a corresponding cache at cache[i]. Go through it
|
||||
// and populate the total, if it's newer than what's already in there.
|
||||
// We skip any negative cache entries.
|
||||
for k, v := range m.caches[i].Cache() {
|
||||
if v.found && v.when.After(res[k].when) {
|
||||
res[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// Then ask the finder itself for it's cache and do the same. If this
|
||||
// finder is a global discovery client, it will have no cache. If it's
|
||||
// a local discovery client, this will be it's current state.
|
||||
for k, v := range m.finders[i].Cache() {
|
||||
if v.found && v.when.After(res[k].when) {
|
||||
res[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
m.mut.Unlock()
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// A cache can be embedded wherever useful
|
||||
|
||||
type cache struct {
|
||||
entries map[protocol.DeviceID]CacheEntry
|
||||
mut stdsync.Mutex
|
||||
}
|
||||
|
||||
func newCache() *cache {
|
||||
return &cache{
|
||||
entries: make(map[protocol.DeviceID]CacheEntry),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cache) Set(id protocol.DeviceID, ce CacheEntry) {
|
||||
c.mut.Lock()
|
||||
c.entries[id] = ce
|
||||
c.mut.Unlock()
|
||||
}
|
||||
|
||||
func (c *cache) Get(id protocol.DeviceID) (CacheEntry, bool) {
|
||||
c.mut.Lock()
|
||||
ce, ok := c.entries[id]
|
||||
c.mut.Unlock()
|
||||
return ce, ok
|
||||
}
|
||||
|
||||
func (c *cache) Cache() map[protocol.DeviceID]CacheEntry {
|
||||
c.mut.Lock()
|
||||
m := make(map[protocol.DeviceID]CacheEntry, len(c.entries))
|
||||
for k, v := range c.entries {
|
||||
m[k] = v
|
||||
}
|
||||
c.mut.Unlock()
|
||||
return m
|
||||
}
|
||||
@@ -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 discover
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/protocol"
|
||||
)
|
||||
|
||||
type Announcer interface {
|
||||
Announcement() Announce
|
||||
}
|
||||
|
||||
type Factory func(*url.URL, Announcer) (Client, error)
|
||||
|
||||
var (
|
||||
factories = make(map[string]Factory)
|
||||
DefaultErrorRetryInternval = 60 * time.Second
|
||||
DefaultGlobalBroadcastInterval = 1800 * time.Second
|
||||
)
|
||||
|
||||
func Register(proto string, factory Factory) {
|
||||
factories[proto] = factory
|
||||
}
|
||||
|
||||
func New(addr string, announcer Announcer) (Client, error) {
|
||||
uri, err := url.Parse(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
factory, ok := factories[uri.Scheme]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Unsupported scheme: %s", uri.Scheme)
|
||||
}
|
||||
client, err := factory(uri, announcer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
type Client interface {
|
||||
Lookup(device protocol.DeviceID) (Announce, error)
|
||||
StatusOK() bool
|
||||
Address() string
|
||||
Stop()
|
||||
}
|
||||
@@ -1,239 +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 discover
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"testing"
|
||||
|
||||
"github.com/syncthing/protocol"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
)
|
||||
|
||||
var device protocol.DeviceID
|
||||
|
||||
func init() {
|
||||
device, _ = protocol.DeviceIDFromString("P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2")
|
||||
}
|
||||
|
||||
type FakeAnnouncer struct {
|
||||
pkt Announce
|
||||
}
|
||||
|
||||
func (f *FakeAnnouncer) Announcement() Announce {
|
||||
return f.pkt
|
||||
}
|
||||
|
||||
func TestUDP4Success(t *testing.T) {
|
||||
conn, err := net.ListenUDP("udp4", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
port := conn.LocalAddr().(*net.UDPAddr).Port
|
||||
|
||||
address := fmt.Sprintf("udp4://127.0.0.1:%d", port)
|
||||
pkt := Announce{
|
||||
Magic: AnnouncementMagic,
|
||||
This: Device{
|
||||
device[:],
|
||||
[]string{"tcp://123.123.123.123:1234"},
|
||||
nil,
|
||||
},
|
||||
}
|
||||
ann := &FakeAnnouncer{
|
||||
pkt: pkt,
|
||||
}
|
||||
|
||||
client, err := New(address, ann)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
udpclient := client.(*UDPClient)
|
||||
if udpclient.errorRetryInterval != DefaultErrorRetryInternval {
|
||||
t.Fatal("Incorrect retry interval")
|
||||
}
|
||||
|
||||
if udpclient.listenAddress.IP != nil || udpclient.listenAddress.Port != 0 {
|
||||
t.Fatal("Wrong listen IP or port", udpclient.listenAddress)
|
||||
}
|
||||
|
||||
if client.Address() != address {
|
||||
t.Fatal("Incorrect address")
|
||||
}
|
||||
|
||||
buf := make([]byte, 2048)
|
||||
|
||||
// First announcement
|
||||
conn.SetDeadline(time.Now().Add(time.Millisecond * 100))
|
||||
_, err = conn.Read(buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Announcement verification
|
||||
conn.SetDeadline(time.Now().Add(time.Millisecond * 1100))
|
||||
_, addr, err := conn.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Reply to it.
|
||||
_, err = conn.WriteToUDP(pkt.MustMarshalXDR(), addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// We should get nothing else
|
||||
conn.SetDeadline(time.Now().Add(time.Millisecond * 100))
|
||||
_, err = conn.Read(buf)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error")
|
||||
}
|
||||
|
||||
// Status should be ok
|
||||
if !client.StatusOK() {
|
||||
t.Fatal("Wrong status")
|
||||
}
|
||||
|
||||
// Do a lookup in a separate routine
|
||||
addrs := []string{}
|
||||
wg := sync.NewWaitGroup()
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
pkt, err := client.Lookup(device)
|
||||
if err == nil {
|
||||
for _, addr := range pkt.This.Addresses {
|
||||
addrs = append(addrs, addr)
|
||||
}
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
// Receive the lookup and reply
|
||||
conn.SetDeadline(time.Now().Add(time.Millisecond * 100))
|
||||
_, addr, err = conn.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
conn.WriteToUDP(pkt.MustMarshalXDR(), addr)
|
||||
|
||||
// Wait for the lookup to arrive, verify that the number of answers is correct
|
||||
wg.Wait()
|
||||
|
||||
if len(addrs) != 1 || addrs[0] != "tcp://123.123.123.123:1234" {
|
||||
t.Fatal("Wrong number of answers")
|
||||
}
|
||||
|
||||
client.Stop()
|
||||
}
|
||||
|
||||
func TestUDP4Failure(t *testing.T) {
|
||||
conn, err := net.ListenUDP("udp4", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
port := conn.LocalAddr().(*net.UDPAddr).Port
|
||||
|
||||
address := fmt.Sprintf("udp4://127.0.0.1:%d/?listenaddress=127.0.0.1&retry=5", port)
|
||||
|
||||
pkt := Announce{
|
||||
Magic: AnnouncementMagic,
|
||||
This: Device{
|
||||
device[:],
|
||||
[]string{"tcp://123.123.123.123:1234"},
|
||||
nil,
|
||||
},
|
||||
}
|
||||
ann := &FakeAnnouncer{
|
||||
pkt: pkt,
|
||||
}
|
||||
|
||||
client, err := New(address, ann)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
udpclient := client.(*UDPClient)
|
||||
if udpclient.errorRetryInterval != time.Second*5 {
|
||||
t.Fatal("Incorrect retry interval")
|
||||
}
|
||||
|
||||
if !udpclient.listenAddress.IP.Equal(net.IPv4(127, 0, 0, 1)) || udpclient.listenAddress.Port != 0 {
|
||||
t.Fatal("Wrong listen IP or port", udpclient.listenAddress)
|
||||
}
|
||||
|
||||
if client.Address() != address {
|
||||
t.Fatal("Incorrect address")
|
||||
}
|
||||
|
||||
buf := make([]byte, 2048)
|
||||
|
||||
// First announcement
|
||||
conn.SetDeadline(time.Now().Add(time.Millisecond * 100))
|
||||
_, err = conn.Read(buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Announcement verification
|
||||
conn.SetDeadline(time.Now().Add(time.Millisecond * 1100))
|
||||
_, _, err = conn.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Don't reply
|
||||
// We should get nothing else
|
||||
conn.SetDeadline(time.Now().Add(time.Millisecond * 100))
|
||||
_, err = conn.Read(buf)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error")
|
||||
}
|
||||
|
||||
// Status should be failure
|
||||
if client.StatusOK() {
|
||||
t.Fatal("Wrong status")
|
||||
}
|
||||
|
||||
// Do a lookup in a separate routine
|
||||
addrs := []string{}
|
||||
wg := sync.NewWaitGroup()
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
pkt, err := client.Lookup(device)
|
||||
if err == nil {
|
||||
for _, addr := range pkt.This.Addresses {
|
||||
addrs = append(addrs, addr)
|
||||
}
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
// Receive the lookup and don't reply
|
||||
conn.SetDeadline(time.Now().Add(time.Millisecond * 100))
|
||||
_, _, err = conn.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Wait for the lookup to timeout, verify that the number of answers is none
|
||||
wg.Wait()
|
||||
|
||||
if len(addrs) != 0 {
|
||||
t.Fatal("Wrong number of answers")
|
||||
}
|
||||
|
||||
client.Stop()
|
||||
}
|
||||
@@ -1,261 +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 discover
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/protocol"
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
)
|
||||
|
||||
func init() {
|
||||
for _, proto := range []string{"udp", "udp4", "udp6"} {
|
||||
Register(proto, func(uri *url.URL, announcer Announcer) (Client, error) {
|
||||
c := &UDPClient{
|
||||
announcer: announcer,
|
||||
wg: sync.NewWaitGroup(),
|
||||
mut: sync.NewRWMutex(),
|
||||
}
|
||||
err := c.Start(uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type UDPClient struct {
|
||||
url *url.URL
|
||||
|
||||
stop chan struct{}
|
||||
wg sync.WaitGroup
|
||||
listenAddress *net.UDPAddr
|
||||
|
||||
globalBroadcastInterval time.Duration
|
||||
errorRetryInterval time.Duration
|
||||
announcer Announcer
|
||||
|
||||
status bool
|
||||
mut sync.RWMutex
|
||||
}
|
||||
|
||||
func (d *UDPClient) Start(uri *url.URL) error {
|
||||
d.url = uri
|
||||
d.stop = make(chan struct{})
|
||||
|
||||
params := uri.Query()
|
||||
// The address must not have a port, as otherwise both announce and lookup
|
||||
// sockets would try to bind to the same port.
|
||||
addr, err := net.ResolveUDPAddr(d.url.Scheme, params.Get("listenaddress")+":0")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.listenAddress = addr
|
||||
|
||||
broadcastSeconds, err := strconv.ParseUint(params.Get("broadcast"), 0, 0)
|
||||
if err != nil {
|
||||
d.globalBroadcastInterval = DefaultGlobalBroadcastInterval
|
||||
} else {
|
||||
d.globalBroadcastInterval = time.Duration(broadcastSeconds) * time.Second
|
||||
}
|
||||
|
||||
retrySeconds, err := strconv.ParseUint(params.Get("retry"), 0, 0)
|
||||
if err != nil {
|
||||
d.errorRetryInterval = DefaultErrorRetryInternval
|
||||
} else {
|
||||
d.errorRetryInterval = time.Duration(retrySeconds) * time.Second
|
||||
}
|
||||
|
||||
d.wg.Add(1)
|
||||
go d.broadcast()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *UDPClient) broadcast() {
|
||||
defer d.wg.Done()
|
||||
|
||||
conn, err := net.ListenUDP(d.url.Scheme, d.listenAddress)
|
||||
for err != nil {
|
||||
if debug {
|
||||
l.Debugf("discover %s: broadcast listen: %v; trying again in %v", d.url, err, d.errorRetryInterval)
|
||||
}
|
||||
select {
|
||||
case <-d.stop:
|
||||
return
|
||||
case <-time.After(d.errorRetryInterval):
|
||||
}
|
||||
conn, err = net.ListenUDP(d.url.Scheme, d.listenAddress)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
remote, err := net.ResolveUDPAddr(d.url.Scheme, d.url.Host)
|
||||
for err != nil {
|
||||
if debug {
|
||||
l.Debugf("discover %s: broadcast resolve: %v; trying again in %v", d.url, err, d.errorRetryInterval)
|
||||
}
|
||||
select {
|
||||
case <-d.stop:
|
||||
return
|
||||
case <-time.After(d.errorRetryInterval):
|
||||
}
|
||||
remote, err = net.ResolveUDPAddr(d.url.Scheme, d.url.Host)
|
||||
}
|
||||
|
||||
timer := time.NewTimer(0)
|
||||
|
||||
eventSub := events.Default.Subscribe(events.ExternalPortMappingChanged)
|
||||
defer events.Default.Unsubscribe(eventSub)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-d.stop:
|
||||
return
|
||||
|
||||
case <-eventSub.C():
|
||||
ok := d.sendAnnouncement(remote, conn)
|
||||
|
||||
d.mut.Lock()
|
||||
d.status = ok
|
||||
d.mut.Unlock()
|
||||
|
||||
case <-timer.C:
|
||||
ok := d.sendAnnouncement(remote, conn)
|
||||
|
||||
d.mut.Lock()
|
||||
d.status = ok
|
||||
d.mut.Unlock()
|
||||
|
||||
if ok {
|
||||
timer.Reset(d.globalBroadcastInterval)
|
||||
} else {
|
||||
timer.Reset(d.errorRetryInterval)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *UDPClient) sendAnnouncement(remote net.Addr, conn *net.UDPConn) bool {
|
||||
if debug {
|
||||
l.Debugf("discover %s: broadcast: Sending self announcement to %v", d.url, remote)
|
||||
}
|
||||
|
||||
ann := d.announcer.Announcement()
|
||||
pkt, err := ann.MarshalXDR()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
myID := protocol.DeviceIDFromBytes(ann.This.ID)
|
||||
|
||||
_, err = conn.WriteTo(pkt, remote)
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugf("discover %s: broadcast: Failed to send self announcement: %s", d.url, err)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Verify that the announce server responds positively for our device ID
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
ann, err = d.Lookup(myID)
|
||||
if err != nil && debug {
|
||||
l.Debugf("discover %s: broadcast: Self-lookup failed: %v", d.url, err)
|
||||
} else if debug {
|
||||
l.Debugf("discover %s: broadcast: Self-lookup returned: %v", d.url, ann.This.Addresses)
|
||||
}
|
||||
return len(ann.This.Addresses) > 0
|
||||
}
|
||||
|
||||
func (d *UDPClient) Lookup(device protocol.DeviceID) (Announce, error) {
|
||||
extIP, err := net.ResolveUDPAddr(d.url.Scheme, d.url.Host)
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugf("discover %s: Lookup(%s): %s", d.url, device, err)
|
||||
}
|
||||
return Announce{}, err
|
||||
}
|
||||
|
||||
conn, err := net.DialUDP(d.url.Scheme, d.listenAddress, extIP)
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugf("discover %s: Lookup(%s): %s", d.url, device, err)
|
||||
}
|
||||
return Announce{}, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
err = conn.SetDeadline(time.Now().Add(5 * time.Second))
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugf("discover %s: Lookup(%s): %s", d.url, device, err)
|
||||
}
|
||||
return Announce{}, err
|
||||
}
|
||||
|
||||
buf := Query{QueryMagic, device[:]}.MustMarshalXDR()
|
||||
_, err = conn.Write(buf)
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugf("discover %s: Lookup(%s): %s", d.url, device, err)
|
||||
}
|
||||
return Announce{}, err
|
||||
}
|
||||
|
||||
buf = make([]byte, 2048)
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
if err, ok := err.(net.Error); ok && err.Timeout() {
|
||||
// Expected if the server doesn't know about requested device ID
|
||||
return Announce{}, err
|
||||
}
|
||||
if debug {
|
||||
l.Debugf("discover %s: Lookup(%s): %s", d.url, device, err)
|
||||
}
|
||||
return Announce{}, err
|
||||
}
|
||||
|
||||
var pkt Announce
|
||||
err = pkt.UnmarshalXDR(buf[:n])
|
||||
if err != nil && err != io.EOF {
|
||||
if debug {
|
||||
l.Debugf("discover %s: Lookup(%s): %s\n%s", d.url, device, err, hex.Dump(buf[:n]))
|
||||
}
|
||||
return Announce{}, err
|
||||
}
|
||||
|
||||
if debug {
|
||||
l.Debugf("discover %s: Lookup(%s) result: %v relays: %v", d.url, device, pkt.This.Addresses, pkt.This.Relays)
|
||||
}
|
||||
return pkt, nil
|
||||
}
|
||||
|
||||
func (d *UDPClient) Stop() {
|
||||
if d.stop != nil {
|
||||
close(d.stop)
|
||||
d.wg.Wait()
|
||||
}
|
||||
}
|
||||
|
||||
func (d *UDPClient) StatusOK() bool {
|
||||
d.mut.RLock()
|
||||
defer d.mut.RUnlock()
|
||||
return d.status
|
||||
}
|
||||
|
||||
func (d *UDPClient) Address() string {
|
||||
return d.url.String()
|
||||
}
|
||||
+29
-520
@@ -1,4 +1,4 @@
|
||||
// Copyright (C) 2014 The Syncthing Authors.
|
||||
// Copyright (C) 2015 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,
|
||||
@@ -7,539 +7,48 @@
|
||||
package discover
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/protocol"
|
||||
"github.com/syncthing/syncthing/lib/beacon"
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
"github.com/thejerf/suture"
|
||||
)
|
||||
|
||||
type Discoverer struct {
|
||||
myID protocol.DeviceID
|
||||
listenAddrs []string
|
||||
relayStatusProvider relayStatusProvider
|
||||
localBcastIntv time.Duration
|
||||
localBcastStart time.Time
|
||||
cacheLifetime time.Duration
|
||||
negCacheCutoff time.Duration
|
||||
beacons []beacon.Interface
|
||||
extAddr externalAddr
|
||||
localBcastTick <-chan time.Time
|
||||
forcedBcastTick chan time.Time
|
||||
|
||||
registryLock sync.RWMutex
|
||||
addressRegistry map[protocol.DeviceID][]CacheEntry
|
||||
relayRegistry map[protocol.DeviceID][]CacheEntry
|
||||
lastLookup map[protocol.DeviceID]time.Time
|
||||
|
||||
clients []Client
|
||||
mut sync.RWMutex
|
||||
}
|
||||
|
||||
type relayStatusProvider interface {
|
||||
ClientStatus() map[string]bool
|
||||
}
|
||||
|
||||
type externalAddr interface {
|
||||
ExternalAddresses() []string
|
||||
// A Finder provides lookup services of some kind.
|
||||
type Finder interface {
|
||||
Lookup(deviceID protocol.DeviceID) (direct []string, relays []Relay, err error)
|
||||
Error() error
|
||||
String() string
|
||||
Cache() map[protocol.DeviceID]CacheEntry
|
||||
}
|
||||
|
||||
type CacheEntry struct {
|
||||
Address string
|
||||
Seen time.Time
|
||||
Direct []string `json:"direct"`
|
||||
Relays []Relay `json:"relays"`
|
||||
when time.Time // When did we get the result
|
||||
found bool // Is it a success (cacheTime applies) or a failure (negCacheTime applies)?
|
||||
}
|
||||
|
||||
var (
|
||||
ErrIncorrectMagic = errors.New("incorrect magic number")
|
||||
)
|
||||
|
||||
func NewDiscoverer(id protocol.DeviceID, addresses []string, relayStatusProvider relayStatusProvider) *Discoverer {
|
||||
return &Discoverer{
|
||||
myID: id,
|
||||
listenAddrs: addresses,
|
||||
relayStatusProvider: relayStatusProvider,
|
||||
localBcastIntv: 30 * time.Second,
|
||||
cacheLifetime: 5 * time.Minute,
|
||||
negCacheCutoff: 3 * time.Minute,
|
||||
addressRegistry: make(map[protocol.DeviceID][]CacheEntry),
|
||||
relayRegistry: make(map[protocol.DeviceID][]CacheEntry),
|
||||
lastLookup: make(map[protocol.DeviceID]time.Time),
|
||||
registryLock: sync.NewRWMutex(),
|
||||
mut: sync.NewRWMutex(),
|
||||
}
|
||||
// A FinderService is a Finder that has background activity and must be run as
|
||||
// a suture.Service.
|
||||
type FinderService interface {
|
||||
Finder
|
||||
suture.Service
|
||||
}
|
||||
|
||||
func (d *Discoverer) StartLocal(localPort int, localMCAddr string) {
|
||||
if localPort > 0 {
|
||||
d.startLocalIPv4Broadcasts(localPort)
|
||||
}
|
||||
|
||||
if len(localMCAddr) > 0 {
|
||||
d.startLocalIPv6Multicasts(localMCAddr)
|
||||
}
|
||||
|
||||
if len(d.beacons) == 0 {
|
||||
l.Warnln("Local discovery unavailable")
|
||||
return
|
||||
}
|
||||
|
||||
d.localBcastTick = time.Tick(d.localBcastIntv)
|
||||
d.forcedBcastTick = make(chan time.Time)
|
||||
d.localBcastStart = time.Now()
|
||||
go d.sendLocalAnnouncements()
|
||||
type FinderMux interface {
|
||||
Finder
|
||||
ChildStatus() map[string]error
|
||||
}
|
||||
|
||||
func (d *Discoverer) startLocalIPv4Broadcasts(localPort int) {
|
||||
bb := beacon.NewBroadcast(localPort)
|
||||
d.beacons = append(d.beacons, bb)
|
||||
go d.recvAnnouncements(bb)
|
||||
bb.ServeBackground()
|
||||
// The RelayStatusProvider answers questions about current relay status.
|
||||
type RelayStatusProvider interface {
|
||||
Relays() []string
|
||||
RelayStatus(uri string) (time.Duration, bool)
|
||||
}
|
||||
|
||||
func (d *Discoverer) startLocalIPv6Multicasts(localMCAddr string) {
|
||||
mb, err := beacon.NewMulticast(localMCAddr)
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugln("beacon.NewMulticast:", err)
|
||||
}
|
||||
l.Infoln("Local discovery over IPv6 unavailable")
|
||||
return
|
||||
}
|
||||
d.beacons = append(d.beacons, mb)
|
||||
go d.recvAnnouncements(mb)
|
||||
}
|
||||
|
||||
func (d *Discoverer) StartGlobal(servers []string, extAddr externalAddr) {
|
||||
d.mut.Lock()
|
||||
defer d.mut.Unlock()
|
||||
|
||||
if len(d.clients) > 0 {
|
||||
d.stopGlobal()
|
||||
}
|
||||
|
||||
d.extAddr = extAddr
|
||||
wg := sync.NewWaitGroup()
|
||||
clients := make(chan Client, len(servers))
|
||||
for _, address := range servers {
|
||||
wg.Add(1)
|
||||
go func(addr string) {
|
||||
defer wg.Done()
|
||||
client, err := New(addr, d)
|
||||
if err != nil {
|
||||
l.Infoln("Error creating discovery client", addr, err)
|
||||
return
|
||||
}
|
||||
clients <- client
|
||||
}(address)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(clients)
|
||||
|
||||
for client := range clients {
|
||||
d.clients = append(d.clients, client)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Discoverer) StopGlobal() {
|
||||
d.mut.Lock()
|
||||
defer d.mut.Unlock()
|
||||
d.stopGlobal()
|
||||
}
|
||||
|
||||
func (d *Discoverer) stopGlobal() {
|
||||
for _, client := range d.clients {
|
||||
client.Stop()
|
||||
}
|
||||
d.clients = []Client{}
|
||||
}
|
||||
|
||||
func (d *Discoverer) ExtAnnounceOK() map[string]bool {
|
||||
d.mut.RLock()
|
||||
defer d.mut.RUnlock()
|
||||
|
||||
ret := make(map[string]bool)
|
||||
for _, client := range d.clients {
|
||||
ret[client.Address()] = client.StatusOK()
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// Lookup returns a list of addresses the device is available at, as well as
|
||||
// a list of relays the device is supposed to be available on sorted by the
|
||||
// sum of latencies between this device, and the device in question.
|
||||
func (d *Discoverer) Lookup(device protocol.DeviceID) ([]string, []string) {
|
||||
d.registryLock.RLock()
|
||||
cachedAddresses := d.filterCached(d.addressRegistry[device])
|
||||
cachedRelays := d.filterCached(d.relayRegistry[device])
|
||||
lastLookup := d.lastLookup[device]
|
||||
d.registryLock.RUnlock()
|
||||
|
||||
d.mut.RLock()
|
||||
defer d.mut.RUnlock()
|
||||
|
||||
relays := make([]string, len(cachedRelays))
|
||||
for i := range cachedRelays {
|
||||
relays[i] = cachedRelays[i].Address
|
||||
}
|
||||
|
||||
if len(cachedAddresses) > 0 {
|
||||
// There are cached address entries.
|
||||
addrs := make([]string, len(cachedAddresses))
|
||||
for i := range cachedAddresses {
|
||||
addrs[i] = cachedAddresses[i].Address
|
||||
}
|
||||
return addrs, relays
|
||||
}
|
||||
|
||||
if time.Since(lastLookup) < d.negCacheCutoff {
|
||||
// We have recently tried to lookup this address and failed. Lets
|
||||
// chill for a while.
|
||||
return nil, relays
|
||||
}
|
||||
|
||||
if len(d.clients) != 0 && time.Since(d.localBcastStart) > d.localBcastIntv {
|
||||
// Only perform external lookups if we have at least one external
|
||||
// server client and one local announcement interval has passed. This is
|
||||
// to avoid finding local peers on their remote address at startup.
|
||||
results := make(chan Announce, len(d.clients))
|
||||
wg := sync.NewWaitGroup()
|
||||
for _, client := range d.clients {
|
||||
wg.Add(1)
|
||||
go func(c Client) {
|
||||
defer wg.Done()
|
||||
ann, err := c.Lookup(device)
|
||||
if err == nil {
|
||||
results <- ann
|
||||
}
|
||||
|
||||
}(client)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(results)
|
||||
|
||||
cachedAddresses := []CacheEntry{}
|
||||
availableRelays := []Relay{}
|
||||
seenAddresses := make(map[string]struct{})
|
||||
seenRelays := make(map[string]struct{})
|
||||
now := time.Now()
|
||||
|
||||
var addrs []string
|
||||
for result := range results {
|
||||
for _, addr := range result.This.Addresses {
|
||||
_, ok := seenAddresses[addr]
|
||||
if !ok {
|
||||
cachedAddresses = append(cachedAddresses, CacheEntry{
|
||||
Address: addr,
|
||||
Seen: now,
|
||||
})
|
||||
seenAddresses[addr] = struct{}{}
|
||||
addrs = append(addrs, addr)
|
||||
}
|
||||
}
|
||||
|
||||
for _, relay := range result.This.Relays {
|
||||
_, ok := seenRelays[relay.Address]
|
||||
if !ok {
|
||||
availableRelays = append(availableRelays, relay)
|
||||
seenRelays[relay.Address] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
relays = RelayAddressesSortedByLatency(availableRelays)
|
||||
cachedRelays := make([]CacheEntry, len(relays))
|
||||
for i := range relays {
|
||||
cachedRelays[i] = CacheEntry{
|
||||
Address: relays[i],
|
||||
Seen: now,
|
||||
}
|
||||
}
|
||||
|
||||
d.registryLock.Lock()
|
||||
d.addressRegistry[device] = cachedAddresses
|
||||
d.relayRegistry[device] = cachedRelays
|
||||
d.lastLookup[device] = time.Now()
|
||||
d.registryLock.Unlock()
|
||||
|
||||
return addrs, relays
|
||||
}
|
||||
|
||||
return nil, relays
|
||||
}
|
||||
|
||||
func (d *Discoverer) Hint(device string, addrs []string) {
|
||||
resAddrs := resolveAddrs(addrs)
|
||||
var id protocol.DeviceID
|
||||
id.UnmarshalText([]byte(device))
|
||||
d.registerDevice(nil, Device{
|
||||
Addresses: resAddrs,
|
||||
ID: id[:],
|
||||
})
|
||||
}
|
||||
|
||||
func (d *Discoverer) All() map[protocol.DeviceID][]CacheEntry {
|
||||
d.registryLock.RLock()
|
||||
devices := make(map[protocol.DeviceID][]CacheEntry, len(d.addressRegistry))
|
||||
for device, addrs := range d.addressRegistry {
|
||||
addrsCopy := make([]CacheEntry, len(addrs))
|
||||
copy(addrsCopy, addrs)
|
||||
devices[device] = addrsCopy
|
||||
}
|
||||
d.registryLock.RUnlock()
|
||||
return devices
|
||||
}
|
||||
|
||||
func (d *Discoverer) Announcement() Announce {
|
||||
return d.announcementPkt(true)
|
||||
}
|
||||
|
||||
func (d *Discoverer) announcementPkt(allowExternal bool) Announce {
|
||||
var addrs []string
|
||||
if allowExternal && d.extAddr != nil {
|
||||
addrs = d.extAddr.ExternalAddresses()
|
||||
} else {
|
||||
addrs = resolveAddrs(d.listenAddrs)
|
||||
}
|
||||
|
||||
var relayAddrs []string
|
||||
if d.relayStatusProvider != nil {
|
||||
status := d.relayStatusProvider.ClientStatus()
|
||||
for uri, ok := range status {
|
||||
if ok {
|
||||
relayAddrs = append(relayAddrs, uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Announce{
|
||||
Magic: AnnouncementMagic,
|
||||
This: Device{d.myID[:], addrs, measureLatency(relayAddrs)},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Discoverer) sendLocalAnnouncements() {
|
||||
var pkt = d.announcementPkt(false)
|
||||
msg := pkt.MustMarshalXDR()
|
||||
|
||||
for {
|
||||
for _, b := range d.beacons {
|
||||
b.Send(msg)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-d.localBcastTick:
|
||||
case <-d.forcedBcastTick:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Discoverer) recvAnnouncements(b beacon.Interface) {
|
||||
for {
|
||||
buf, addr := b.Recv()
|
||||
|
||||
var pkt Announce
|
||||
err := pkt.UnmarshalXDR(buf)
|
||||
if err != nil && err != io.EOF {
|
||||
if debug {
|
||||
l.Debugf("discover: Failed to unmarshal local announcement from %s:\n%s", addr, hex.Dump(buf))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if debug {
|
||||
l.Debugf("discover: Received local announcement from %s for %s", addr, protocol.DeviceIDFromBytes(pkt.This.ID))
|
||||
}
|
||||
|
||||
var newDevice bool
|
||||
if bytes.Compare(pkt.This.ID, d.myID[:]) != 0 {
|
||||
newDevice = d.registerDevice(addr, pkt.This)
|
||||
}
|
||||
|
||||
if newDevice {
|
||||
select {
|
||||
case d.forcedBcastTick <- time.Now():
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Discoverer) registerDevice(addr net.Addr, device Device) bool {
|
||||
var id protocol.DeviceID
|
||||
copy(id[:], device.ID)
|
||||
|
||||
d.registryLock.Lock()
|
||||
defer d.registryLock.Unlock()
|
||||
|
||||
current := d.filterCached(d.addressRegistry[id])
|
||||
|
||||
orig := current
|
||||
|
||||
for _, deviceAddr := range device.Addresses {
|
||||
uri, err := url.Parse(deviceAddr)
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugf("discover: Failed to parse address %s: %s", deviceAddr, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
host, port, err := net.SplitHostPort(uri.Host)
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugf("discover: Failed to split address host %s: %s", deviceAddr, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if host == "" {
|
||||
uri.Host = net.JoinHostPort(addr.(*net.UDPAddr).IP.String(), port)
|
||||
deviceAddr = uri.String()
|
||||
}
|
||||
|
||||
for i := range current {
|
||||
if current[i].Address == deviceAddr {
|
||||
current[i].Seen = time.Now()
|
||||
goto done
|
||||
}
|
||||
}
|
||||
current = append(current, CacheEntry{
|
||||
Address: deviceAddr,
|
||||
Seen: time.Now(),
|
||||
})
|
||||
done:
|
||||
}
|
||||
|
||||
if debug {
|
||||
l.Debugf("discover: Caching %s addresses: %v", id, current)
|
||||
}
|
||||
|
||||
d.addressRegistry[id] = current
|
||||
|
||||
if len(current) > len(orig) {
|
||||
addrs := make([]string, len(current))
|
||||
for i := range current {
|
||||
addrs[i] = current[i].Address
|
||||
}
|
||||
events.Default.Log(events.DeviceDiscovered, map[string]interface{}{
|
||||
"device": id.String(),
|
||||
"addrs": addrs,
|
||||
})
|
||||
}
|
||||
|
||||
return len(current) > len(orig)
|
||||
}
|
||||
|
||||
func (d *Discoverer) filterCached(c []CacheEntry) []CacheEntry {
|
||||
for i := 0; i < len(c); {
|
||||
if ago := time.Since(c[i].Seen); ago > d.cacheLifetime {
|
||||
if debug {
|
||||
l.Debugf("discover: Removing cached entry %s - seen %v ago", c[i].Address, ago)
|
||||
}
|
||||
c[i] = c[len(c)-1]
|
||||
c = c[:len(c)-1]
|
||||
} else {
|
||||
i++
|
||||
}
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func addrToAddr(addr *net.TCPAddr) string {
|
||||
if len(addr.IP) == 0 || addr.IP.IsUnspecified() {
|
||||
return fmt.Sprintf(":%d", addr.Port)
|
||||
} else if bs := addr.IP.To4(); bs != nil {
|
||||
return fmt.Sprintf("%s:%d", bs.String(), addr.Port)
|
||||
} else if bs := addr.IP.To16(); bs != nil {
|
||||
return fmt.Sprintf("[%s]:%d", bs.String(), addr.Port)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func resolveAddrs(addrs []string) []string {
|
||||
var raddrs []string
|
||||
for _, addrStr := range addrs {
|
||||
uri, err := url.Parse(addrStr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
addrRes, err := net.ResolveTCPAddr("tcp", uri.Host)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
addr := addrToAddr(addrRes)
|
||||
if len(addr) > 0 {
|
||||
uri.Host = addr
|
||||
raddrs = append(raddrs, uri.String())
|
||||
}
|
||||
}
|
||||
return raddrs
|
||||
}
|
||||
|
||||
func measureLatency(relayAdresses []string) []Relay {
|
||||
relays := make([]Relay, 0, len(relayAdresses))
|
||||
for i, addr := range relayAdresses {
|
||||
relay := Relay{
|
||||
Address: addr,
|
||||
Latency: int32(time.Hour / time.Millisecond),
|
||||
}
|
||||
relays = append(relays, relay)
|
||||
|
||||
if latency, err := osutil.GetLatencyForURL(addr); err == nil {
|
||||
if debug {
|
||||
l.Debugf("Relay %s latency %s", addr, latency)
|
||||
}
|
||||
relays[i].Latency = int32(latency / time.Millisecond)
|
||||
} else {
|
||||
l.Debugf("Failed to get relay %s latency %s", addr, err)
|
||||
}
|
||||
}
|
||||
return relays
|
||||
}
|
||||
|
||||
// RelayAddressesSortedByLatency adds local latency to the relay, and sorts them
|
||||
// by sum latency, and returns the addresses.
|
||||
func RelayAddressesSortedByLatency(input []Relay) []string {
|
||||
relays := make([]Relay, len(input))
|
||||
copy(relays, input)
|
||||
for i, relay := range relays {
|
||||
if latency, err := osutil.GetLatencyForURL(relay.Address); err == nil {
|
||||
relays[i].Latency += int32(latency / time.Millisecond)
|
||||
} else {
|
||||
relays[i].Latency += int32(time.Hour / time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Sort(relayList(relays))
|
||||
|
||||
addresses := make([]string, 0, len(relays))
|
||||
for _, relay := range relays {
|
||||
addresses = append(addresses, relay.Address)
|
||||
}
|
||||
return addresses
|
||||
}
|
||||
|
||||
type relayList []Relay
|
||||
|
||||
func (l relayList) Len() int {
|
||||
return len(l)
|
||||
}
|
||||
|
||||
func (l relayList) Less(a, b int) bool {
|
||||
return l[a].Latency < l[b].Latency
|
||||
}
|
||||
|
||||
func (l relayList) Swap(a, b int) {
|
||||
l[a], l[b] = l[b], l[a]
|
||||
// The AddressLister answers questions about what addresses we are listening
|
||||
// on.
|
||||
type AddressLister interface {
|
||||
ExternalAddresses() []string
|
||||
AllAddresses() []string
|
||||
}
|
||||
@@ -1,163 +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 discover
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"testing"
|
||||
|
||||
"github.com/syncthing/protocol"
|
||||
)
|
||||
|
||||
type DummyClient struct {
|
||||
url *url.URL
|
||||
lookups []protocol.DeviceID
|
||||
lookupRet Announce
|
||||
stops int
|
||||
statusRet bool
|
||||
statusChecks int
|
||||
}
|
||||
|
||||
func (c *DummyClient) Lookup(device protocol.DeviceID) (Announce, error) {
|
||||
c.lookups = append(c.lookups, device)
|
||||
return c.lookupRet, nil
|
||||
}
|
||||
|
||||
func (c *DummyClient) StatusOK() bool {
|
||||
c.statusChecks++
|
||||
return c.statusRet
|
||||
}
|
||||
|
||||
func (c *DummyClient) Stop() {
|
||||
c.stops++
|
||||
}
|
||||
|
||||
func (c *DummyClient) Address() string {
|
||||
return c.url.String()
|
||||
}
|
||||
|
||||
func TestGlobalDiscovery(t *testing.T) {
|
||||
c1 := &DummyClient{
|
||||
statusRet: false,
|
||||
lookupRet: Announce{
|
||||
Magic: AnnouncementMagic,
|
||||
This: Device{
|
||||
ID: protocol.LocalDeviceID[:],
|
||||
Addresses: []string{"test.com:1234"},
|
||||
Relays: nil,
|
||||
},
|
||||
Extra: nil,
|
||||
},
|
||||
}
|
||||
|
||||
c2 := &DummyClient{
|
||||
statusRet: true,
|
||||
lookupRet: Announce{
|
||||
Magic: AnnouncementMagic,
|
||||
This: Device{
|
||||
ID: protocol.LocalDeviceID[:],
|
||||
Addresses: nil,
|
||||
Relays: nil,
|
||||
},
|
||||
Extra: nil,
|
||||
},
|
||||
}
|
||||
|
||||
c3 := &DummyClient{
|
||||
statusRet: true,
|
||||
lookupRet: Announce{
|
||||
Magic: AnnouncementMagic,
|
||||
This: Device{
|
||||
ID: protocol.LocalDeviceID[:],
|
||||
Addresses: []string{"best.com:2345"},
|
||||
Relays: nil,
|
||||
},
|
||||
Extra: nil,
|
||||
},
|
||||
}
|
||||
|
||||
clients := []*DummyClient{c1, c2}
|
||||
|
||||
Register("test1", func(uri *url.URL, ann Announcer) (Client, error) {
|
||||
c := clients[0]
|
||||
clients = clients[1:]
|
||||
c.url = uri
|
||||
return c, nil
|
||||
})
|
||||
|
||||
Register("test2", func(uri *url.URL, ann Announcer) (Client, error) {
|
||||
c3.url = uri
|
||||
return c3, nil
|
||||
})
|
||||
|
||||
d := NewDiscoverer(device, []string{}, nil)
|
||||
d.localBcastStart = time.Time{}
|
||||
servers := []string{
|
||||
"test1://123.123.123.123:1234",
|
||||
"test1://23.23.23.23:234",
|
||||
"test2://234.234.234.234.2345",
|
||||
}
|
||||
d.StartGlobal(servers, nil)
|
||||
|
||||
if len(d.clients) != 3 {
|
||||
t.Fatal("Wrong number of clients")
|
||||
}
|
||||
|
||||
status := d.ExtAnnounceOK()
|
||||
|
||||
for _, c := range []*DummyClient{c1, c2, c3} {
|
||||
if status[c.url.String()] != c.statusRet || c.statusChecks != 1 {
|
||||
t.Fatal("Wrong status")
|
||||
}
|
||||
}
|
||||
|
||||
addrs, _ := d.Lookup(device)
|
||||
if len(addrs) != 2 {
|
||||
t.Fatal("Wrong number of addresses", addrs)
|
||||
}
|
||||
|
||||
for _, addr := range []string{"test.com:1234", "best.com:2345"} {
|
||||
found := false
|
||||
for _, laddr := range addrs {
|
||||
if laddr == addr {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("Couldn't find", addr)
|
||||
}
|
||||
}
|
||||
|
||||
for _, c := range []*DummyClient{c1, c2, c3} {
|
||||
if len(c.lookups) != 1 || c.lookups[0] != device {
|
||||
t.Fatal("Wrong lookups")
|
||||
}
|
||||
}
|
||||
|
||||
addrs, _ = d.Lookup(device)
|
||||
if len(addrs) != 2 {
|
||||
t.Fatal("Wrong number of addresses", addrs)
|
||||
}
|
||||
|
||||
// Answer should be cached, so number of lookups should have not increased
|
||||
for _, c := range []*DummyClient{c1, c2, c3} {
|
||||
if len(c.lookups) != 1 || c.lookups[0] != device {
|
||||
t.Fatal("Wrong lookups")
|
||||
}
|
||||
}
|
||||
|
||||
d.StopGlobal()
|
||||
|
||||
for _, c := range []*DummyClient{c1, c2, c3} {
|
||||
if c.stops != 1 {
|
||||
t.Fatal("Wrong number of stops")
|
||||
}
|
||||
}
|
||||
}
|
||||
+69
-2
@@ -1,8 +1,75 @@
|
||||
// Copyright (C) 2014 The Syncthing Authors.
|
||||
// Copyright (C) 2015 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 implements the device discovery protocol.
|
||||
/*
|
||||
Package discover implements the local and global device discovery protocols.
|
||||
|
||||
Global Discovery
|
||||
================
|
||||
|
||||
Announcements
|
||||
-------------
|
||||
|
||||
A device should announce itself at startup. It does this by an HTTPS POST to
|
||||
the announce server URL (with the path usually being "/", but this is of
|
||||
course up to the discovery server). The POST has a JSON payload listing direct
|
||||
connection addresses (if any) and relay addresses (if any).
|
||||
|
||||
{
|
||||
direct: ["tcp://192.0.2.45:22000", "tcp://:22202"],
|
||||
relays: [{"url": "relay://192.0.2.99:22028", "latency": 142}]
|
||||
}
|
||||
|
||||
It's OK for either of the "direct" or "relays" fields to be either the empty
|
||||
list ([]), null, or missing entirely. An announcment with both fields missing
|
||||
or empty is however not useful...
|
||||
|
||||
Any empty or unspecified IP addresses (i.e. addresses like tcp://:22000,
|
||||
tcp://0.0.0.0:22000, tcp://[::]:22000) are interpreted as referring to the
|
||||
source IP address of the announcement.
|
||||
|
||||
The device ID of the announcing device is not part of the announcement.
|
||||
Instead, the server requires that the client perform certificate
|
||||
authentication. The device ID is deduced from the presented certificate.
|
||||
|
||||
The server response is empty, with code 200 (OK) on success. If no certificate
|
||||
was presented, status 403 (Forbidden) is returned. If the posted data doesn't
|
||||
conform to the expected format, 400 (Bad Request) is returned.
|
||||
|
||||
In successfull responses, the server may return a "Reannounce-After" header
|
||||
containing the number of seconds after which the client should perform a new
|
||||
announcement.
|
||||
|
||||
In error responses, the server may return a "Retry-After" header containing
|
||||
the number of seconds after which the client should retry.
|
||||
|
||||
Performing announcements significantly more often than indicated by the
|
||||
Reannounce-After or Retry-After headers may result in the client being
|
||||
throttled. In such cases the server may respond with status code 429 (Too Many
|
||||
Requests).
|
||||
|
||||
Queries
|
||||
=======
|
||||
|
||||
Queries are performed as HTTPS GET requests to the announce server URL. The
|
||||
requested device ID is passed as the query parameter "device", in canonical
|
||||
string form, i.e. https://announce.syncthing.net/?device=ABC12345-....
|
||||
|
||||
Successfull responses will have status code 200 (OK) and carry a JSON payload
|
||||
of the same format as the announcement above. The response will not contain
|
||||
empty or unspecified addresses.
|
||||
|
||||
If the "device" query parameter is missing or malformed, the status code 400
|
||||
(Bad Request) is returned.
|
||||
|
||||
If the device ID is of a valid format but not found in the registry, 404 (Not
|
||||
Found) is returned.
|
||||
|
||||
If the client has exceeded a rate limit, the server may respond with 429 (Too
|
||||
Many Requests).
|
||||
|
||||
*/
|
||||
package discover
|
||||
@@ -0,0 +1,385 @@
|
||||
package discover
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
stdsync "sync"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/protocol"
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
)
|
||||
|
||||
type globalClient struct {
|
||||
server string
|
||||
addrList AddressLister
|
||||
relayStat RelayStatusProvider
|
||||
announceClient httpClient
|
||||
queryClient httpClient
|
||||
noAnnounce bool
|
||||
stop chan struct{}
|
||||
errorHolder
|
||||
}
|
||||
|
||||
type httpClient interface {
|
||||
Get(url string) (*http.Response, error)
|
||||
Post(url, ctype string, data io.Reader) (*http.Response, error)
|
||||
}
|
||||
|
||||
const (
|
||||
defaultReannounceInterval = 30 * time.Minute
|
||||
announceErrorRetryInterval = 5 * time.Minute
|
||||
)
|
||||
|
||||
type announcement struct {
|
||||
Direct []string `json:"direct"`
|
||||
Relays []Relay `json:"relays"`
|
||||
}
|
||||
|
||||
type serverOptions struct {
|
||||
insecure bool // don't check certificate
|
||||
noAnnounce bool // don't announce
|
||||
id string // expected server device ID
|
||||
}
|
||||
|
||||
func NewGlobal(server string, cert tls.Certificate, addrList AddressLister, relayStat RelayStatusProvider) (FinderService, error) {
|
||||
server, opts, err := parseOptions(server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var devID protocol.DeviceID
|
||||
if opts.id != "" {
|
||||
devID, err = protocol.DeviceIDFromString(opts.id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// The http.Client used for announcements. It needs to have our
|
||||
// certificate to prove our identity, and may or may not verify the server
|
||||
// certificate depending on the insecure setting.
|
||||
var announceClient httpClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: opts.insecure,
|
||||
Certificates: []tls.Certificate{cert},
|
||||
},
|
||||
},
|
||||
}
|
||||
if opts.id != "" {
|
||||
announceClient = newIDCheckingHTTPClient(announceClient, devID)
|
||||
}
|
||||
|
||||
// The http.Client used for queries. We don't need to present our
|
||||
// certificate here, so lets not include it. May be insecure if requested.
|
||||
var queryClient httpClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: opts.insecure,
|
||||
},
|
||||
},
|
||||
}
|
||||
if opts.id != "" {
|
||||
queryClient = newIDCheckingHTTPClient(queryClient, devID)
|
||||
}
|
||||
|
||||
cl := &globalClient{
|
||||
server: server,
|
||||
addrList: addrList,
|
||||
relayStat: relayStat,
|
||||
announceClient: announceClient,
|
||||
queryClient: queryClient,
|
||||
noAnnounce: opts.noAnnounce,
|
||||
stop: make(chan struct{}),
|
||||
}
|
||||
cl.setError(errors.New("not announced"))
|
||||
|
||||
return cl, nil
|
||||
}
|
||||
|
||||
// Lookup returns the list of addresses where the given device is available;
|
||||
// direct, and via relays.
|
||||
func (c *globalClient) Lookup(device protocol.DeviceID) (direct []string, relays []Relay, err error) {
|
||||
qURL, err := url.Parse(c.server)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
q := qURL.Query()
|
||||
q.Set("device", device.String())
|
||||
qURL.RawQuery = q.Encode()
|
||||
|
||||
resp, err := c.queryClient.Get(qURL.String())
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugln("globalClient.Lookup", qURL.String(), err)
|
||||
}
|
||||
return nil, nil, err
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
resp.Body.Close()
|
||||
if debug {
|
||||
l.Debugln("globalClient.Lookup", qURL.String(), resp.Status)
|
||||
}
|
||||
return nil, nil, errors.New(resp.Status)
|
||||
}
|
||||
|
||||
// TODO: Handle 429 and Retry-After?
|
||||
|
||||
var ann announcement
|
||||
err = json.NewDecoder(resp.Body).Decode(&ann)
|
||||
resp.Body.Close()
|
||||
return ann.Direct, ann.Relays, err
|
||||
}
|
||||
|
||||
func (c *globalClient) String() string {
|
||||
return "global@" + c.server
|
||||
}
|
||||
|
||||
func (c *globalClient) Serve() {
|
||||
if c.noAnnounce {
|
||||
// We're configured to not do announcements, only lookups. To maintain
|
||||
// the same interface, we just pause here if Serve() is run.
|
||||
<-c.stop
|
||||
return
|
||||
}
|
||||
|
||||
timer := time.NewTimer(0)
|
||||
defer timer.Stop()
|
||||
|
||||
eventSub := events.Default.Subscribe(events.ExternalPortMappingChanged | events.RelayStateChanged)
|
||||
defer events.Default.Unsubscribe(eventSub)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-eventSub.C():
|
||||
c.sendAnnouncement(timer)
|
||||
|
||||
case <-timer.C:
|
||||
c.sendAnnouncement(timer)
|
||||
|
||||
case <-c.stop:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *globalClient) sendAnnouncement(timer *time.Timer) {
|
||||
|
||||
var ann announcement
|
||||
if c.addrList != nil {
|
||||
ann.Direct = c.addrList.ExternalAddresses()
|
||||
}
|
||||
|
||||
if c.relayStat != nil {
|
||||
for _, relay := range c.relayStat.Relays() {
|
||||
latency, ok := c.relayStat.RelayStatus(relay)
|
||||
if ok {
|
||||
ann.Relays = append(ann.Relays, Relay{
|
||||
URL: relay,
|
||||
Latency: int32(latency / time.Millisecond),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(ann.Direct)+len(ann.Relays) == 0 {
|
||||
c.setError(errors.New("nothing to announce"))
|
||||
if debug {
|
||||
l.Debugln("Nothing to announce")
|
||||
}
|
||||
timer.Reset(announceErrorRetryInterval)
|
||||
return
|
||||
}
|
||||
|
||||
// The marshal doesn't fail, I promise.
|
||||
postData, _ := json.Marshal(ann)
|
||||
|
||||
if debug {
|
||||
l.Debugf("Announcement: %s", postData)
|
||||
}
|
||||
|
||||
resp, err := c.announceClient.Post(c.server, "application/json", bytes.NewReader(postData))
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugln("announce POST:", err)
|
||||
}
|
||||
c.setError(err)
|
||||
timer.Reset(announceErrorRetryInterval)
|
||||
return
|
||||
}
|
||||
if debug {
|
||||
l.Debugln("announce POST:", resp.Status)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode > 299 {
|
||||
if debug {
|
||||
l.Debugln("announce POST:", resp.Status)
|
||||
}
|
||||
c.setError(errors.New(resp.Status))
|
||||
|
||||
if h := resp.Header.Get("Retry-After"); h != "" {
|
||||
// The server has a recommendation on when we should
|
||||
// retry. Follow it.
|
||||
if secs, err := strconv.Atoi(h); err == nil && secs > 0 {
|
||||
if debug {
|
||||
l.Debugln("announce Retry-After:", secs, err)
|
||||
}
|
||||
timer.Reset(time.Duration(secs) * time.Second)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
timer.Reset(announceErrorRetryInterval)
|
||||
return
|
||||
}
|
||||
|
||||
c.setError(nil)
|
||||
|
||||
if h := resp.Header.Get("Reannounce-After"); h != "" {
|
||||
// The server has a recommendation on when we should
|
||||
// reannounce. Follow it.
|
||||
if secs, err := strconv.Atoi(h); err == nil && secs > 0 {
|
||||
if debug {
|
||||
l.Debugln("announce Reannounce-After:", secs, err)
|
||||
}
|
||||
timer.Reset(time.Duration(secs) * time.Second)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
timer.Reset(defaultReannounceInterval)
|
||||
}
|
||||
|
||||
func (c *globalClient) Stop() {
|
||||
close(c.stop)
|
||||
}
|
||||
|
||||
func (c *globalClient) Cache() map[protocol.DeviceID]CacheEntry {
|
||||
// The globalClient doesn't do caching
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseOptions parses and strips away any ?query=val options, setting the
|
||||
// corresponding field in the serverOptions struct. Unknown query options are
|
||||
// ignored and removed.
|
||||
func parseOptions(dsn string) (server string, opts serverOptions, err error) {
|
||||
p, err := url.Parse(dsn)
|
||||
if err != nil {
|
||||
return "", serverOptions{}, err
|
||||
}
|
||||
|
||||
// Grab known options from the query string
|
||||
q := p.Query()
|
||||
opts.id = q.Get("id")
|
||||
opts.insecure = opts.id != "" || queryBool(q, "insecure")
|
||||
opts.noAnnounce = queryBool(q, "noannounce")
|
||||
|
||||
// Check for disallowed combinations
|
||||
if p.Scheme == "http" {
|
||||
if !opts.insecure {
|
||||
return "", serverOptions{}, errors.New("http without insecure not supported")
|
||||
}
|
||||
if !opts.noAnnounce {
|
||||
return "", serverOptions{}, errors.New("http without noannounce not supported")
|
||||
}
|
||||
} else if p.Scheme != "https" {
|
||||
return "", serverOptions{}, errors.New("unsupported scheme " + p.Scheme)
|
||||
}
|
||||
|
||||
// Remove the query string
|
||||
p.RawQuery = ""
|
||||
server = p.String()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// queryBool returns the query parameter parsed as a boolean. An empty value
|
||||
// ("?foo") is considered true, as is any value string except false
|
||||
// ("?foo=false").
|
||||
func queryBool(q url.Values, key string) bool {
|
||||
if _, ok := q[key]; !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return q.Get(key) != "false"
|
||||
}
|
||||
|
||||
type idCheckingHTTPClient struct {
|
||||
httpClient
|
||||
id protocol.DeviceID
|
||||
}
|
||||
|
||||
func newIDCheckingHTTPClient(client httpClient, id protocol.DeviceID) *idCheckingHTTPClient {
|
||||
return &idCheckingHTTPClient{
|
||||
httpClient: client,
|
||||
id: id,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *idCheckingHTTPClient) check(resp *http.Response) error {
|
||||
if resp.TLS == nil {
|
||||
return errors.New("security: not TLS")
|
||||
}
|
||||
|
||||
if len(resp.TLS.PeerCertificates) == 0 {
|
||||
return errors.New("security: no certificates")
|
||||
}
|
||||
|
||||
id := protocol.NewDeviceID(resp.TLS.PeerCertificates[0].Raw)
|
||||
if !id.Equals(c.id) {
|
||||
return errors.New("security: incorrect device id")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *idCheckingHTTPClient) Get(url string) (*http.Response, error) {
|
||||
resp, err := c.httpClient.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := c.check(resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *idCheckingHTTPClient) Post(url, ctype string, data io.Reader) (*http.Response, error) {
|
||||
resp, err := c.httpClient.Post(url, ctype, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := c.check(resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
type errorHolder struct {
|
||||
err error
|
||||
mut stdsync.Mutex // uses stdlib sync as I want this to be trivially embeddable, and there is no risk of blocking
|
||||
}
|
||||
|
||||
func (e *errorHolder) setError(err error) {
|
||||
e.mut.Lock()
|
||||
e.err = err
|
||||
e.mut.Unlock()
|
||||
}
|
||||
|
||||
func (e *errorHolder) Error() error {
|
||||
e.mut.Lock()
|
||||
err := e.err
|
||||
e.mut.Unlock()
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
package discover
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/protocol"
|
||||
"github.com/syncthing/syncthing/lib/tlsutil"
|
||||
)
|
||||
|
||||
func TestParseOptions(t *testing.T) {
|
||||
testcases := []struct {
|
||||
in string
|
||||
out string
|
||||
opts serverOptions
|
||||
}{
|
||||
{"https://example.com/", "https://example.com/", serverOptions{}},
|
||||
{"https://example.com/?insecure", "https://example.com/", serverOptions{insecure: true}},
|
||||
{"https://example.com/?insecure=true", "https://example.com/", serverOptions{insecure: true}},
|
||||
{"https://example.com/?insecure=yes", "https://example.com/", serverOptions{insecure: true}},
|
||||
{"https://example.com/?insecure=false&noannounce", "https://example.com/", serverOptions{noAnnounce: true}},
|
||||
{"https://example.com/?id=abc", "https://example.com/", serverOptions{id: "abc", insecure: true}},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
res, opts, err := parseOptions(tc.in)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected err %v for %v", err, tc.in)
|
||||
continue
|
||||
}
|
||||
if res != tc.out {
|
||||
t.Errorf("Incorrect server, %v!= %v for %v", res, tc.out, tc.in)
|
||||
}
|
||||
if opts != tc.opts {
|
||||
t.Errorf("Incorrect options, %v!= %v for %v", opts, tc.opts, tc.in)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGlobalOverHTTP(t *testing.T) {
|
||||
// HTTP works for queries, but is obviously insecure and we can't do
|
||||
// announces over it (as we don't present a certificate). As such, http://
|
||||
// is only allowed in combination with the "insecure" and "noannounce"
|
||||
// parameters.
|
||||
|
||||
if _, err := NewGlobal("http://192.0.2.42/", tls.Certificate{}, nil, nil); err == nil {
|
||||
t.Fatal("http is not allowed without insecure and noannounce")
|
||||
}
|
||||
|
||||
if _, err := NewGlobal("http://192.0.2.42/?insecure", tls.Certificate{}, nil, nil); err == nil {
|
||||
t.Fatal("http is not allowed without noannounce")
|
||||
}
|
||||
|
||||
if _, err := NewGlobal("http://192.0.2.42/?noannounce", tls.Certificate{}, nil, nil); err == nil {
|
||||
t.Fatal("http is not allowed without insecure")
|
||||
}
|
||||
|
||||
// Now lets check that lookups work over HTTP, given the correct options.
|
||||
|
||||
list, err := net.Listen("tcp4", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer list.Close()
|
||||
|
||||
s := new(fakeDiscoveryServer)
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", s.handler)
|
||||
go http.Serve(list, mux)
|
||||
|
||||
direct, relays, err := testLookup("http://" + list.Addr().String() + "?insecure&noannounce")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if len(direct) != 1 || direct[0] != "tcp://192.0.2.42::22000" {
|
||||
t.Errorf("incorrect direct list: %+v", direct)
|
||||
}
|
||||
if len(relays) != 1 || relays[0] != (Relay{URL: "relay://192.0.2.43:443", Latency: 42}) {
|
||||
t.Errorf("incorrect relays list: %+v", direct)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGlobalOverHTTPS(t *testing.T) {
|
||||
dir, err := ioutil.TempDir("", "syncthing")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Generate a server certificate, using fewer bits than usual to hurry the
|
||||
// process along a bit.
|
||||
cert, err := tlsutil.NewCertificate(dir+"/cert.pem", dir+"/key.pem", "syncthing", 1024)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
list, err := tls.Listen("tcp4", "127.0.0.1:0", &tls.Config{Certificates: []tls.Certificate{cert}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer list.Close()
|
||||
|
||||
s := new(fakeDiscoveryServer)
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", s.handler)
|
||||
go http.Serve(list, mux)
|
||||
|
||||
// With default options the lookup code expects the server certificate to
|
||||
// check out according to the usual CA chains etc. That won't be the case
|
||||
// here so we expect the lookup to fail.
|
||||
|
||||
url := "https://" + list.Addr().String()
|
||||
if _, _, err := testLookup(url); err == nil {
|
||||
t.Fatalf("unexpected nil error when we should have got a certificate error")
|
||||
}
|
||||
|
||||
// With "insecure" set, whatever certificate is on the other side should
|
||||
// be accepted.
|
||||
|
||||
url = "https://" + list.Addr().String() + "?insecure"
|
||||
if direct, relays, err := testLookup(url); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
} else {
|
||||
if len(direct) != 1 || direct[0] != "tcp://192.0.2.42::22000" {
|
||||
t.Errorf("incorrect direct list: %+v", direct)
|
||||
}
|
||||
if len(relays) != 1 || relays[0] != (Relay{URL: "relay://192.0.2.43:443", Latency: 42}) {
|
||||
t.Errorf("incorrect relays list: %+v", direct)
|
||||
}
|
||||
}
|
||||
|
||||
// With "id" set to something incorrect, the checks should fail again.
|
||||
|
||||
url = "https://" + list.Addr().String() + "?id=" + protocol.LocalDeviceID.String()
|
||||
if _, _, err := testLookup(url); err == nil {
|
||||
t.Fatalf("unexpected nil error for incorrect discovery server ID")
|
||||
}
|
||||
|
||||
// With the correct device ID, the check should pass and we should get a
|
||||
// lookup response.
|
||||
|
||||
id := protocol.NewDeviceID(cert.Certificate[0])
|
||||
url = "https://" + list.Addr().String() + "?id=" + id.String()
|
||||
if direct, relays, err := testLookup(url); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
} else {
|
||||
if len(direct) != 1 || direct[0] != "tcp://192.0.2.42::22000" {
|
||||
t.Errorf("incorrect direct list: %+v", direct)
|
||||
}
|
||||
if len(relays) != 1 || relays[0] != (Relay{URL: "relay://192.0.2.43:443", Latency: 42}) {
|
||||
t.Errorf("incorrect relays list: %+v", direct)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGlobalAnnounce(t *testing.T) {
|
||||
dir, err := ioutil.TempDir("", "syncthing")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Generate a server certificate, using fewer bits than usual to hurry the
|
||||
// process along a bit.
|
||||
cert, err := tlsutil.NewCertificate(dir+"/cert.pem", dir+"/key.pem", "syncthing", 1024)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
list, err := tls.Listen("tcp4", "127.0.0.1:0", &tls.Config{Certificates: []tls.Certificate{cert}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer list.Close()
|
||||
|
||||
s := new(fakeDiscoveryServer)
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", s.handler)
|
||||
go http.Serve(list, mux)
|
||||
|
||||
url := "https://" + list.Addr().String() + "?insecure"
|
||||
disco, err := NewGlobal(url, cert, new(fakeAddressLister), new(fakeRelayStatus))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
go disco.Serve()
|
||||
defer disco.Stop()
|
||||
|
||||
// The discovery thing should attempt an announcement immediately. We wait
|
||||
// for it to succeed, a while.
|
||||
t0 := time.Now()
|
||||
for err := disco.Error(); err != nil; err = disco.Error() {
|
||||
if time.Since(t0) > 10*time.Second {
|
||||
t.Fatal("announce failed:", err)
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
if !strings.Contains(string(s.announce), "tcp://0.0.0.0:22000") {
|
||||
t.Errorf("announce missing direct address: %s", s.announce)
|
||||
}
|
||||
if !strings.Contains(string(s.announce), "relay://192.0.2.42:443") {
|
||||
t.Errorf("announce missing relay address: %s", s.announce)
|
||||
}
|
||||
}
|
||||
|
||||
func testLookup(url string) ([]string, []Relay, error) {
|
||||
disco, err := NewGlobal(url, tls.Certificate{}, nil, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
go disco.Serve()
|
||||
defer disco.Stop()
|
||||
|
||||
return disco.Lookup(protocol.LocalDeviceID)
|
||||
}
|
||||
|
||||
type fakeDiscoveryServer struct {
|
||||
announce []byte
|
||||
}
|
||||
|
||||
func (s *fakeDiscoveryServer) handler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "POST" {
|
||||
s.announce, _ = ioutil.ReadAll(r.Body)
|
||||
w.WriteHeader(204)
|
||||
} else {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"direct":["tcp://192.0.2.42::22000"], "relays":[{"url": "relay://192.0.2.43:443", "latency": 42}]}`))
|
||||
}
|
||||
}
|
||||
|
||||
type fakeAddressLister struct{}
|
||||
|
||||
func (f *fakeAddressLister) ExternalAddresses() []string {
|
||||
return []string{"tcp://0.0.0.0:22000"}
|
||||
}
|
||||
func (f *fakeAddressLister) AllAddresses() []string {
|
||||
return []string{"tcp://0.0.0.0:22000", "tcp://192.168.0.1:22000"}
|
||||
}
|
||||
|
||||
type fakeRelayStatus struct{}
|
||||
|
||||
func (f *fakeRelayStatus) Relays() []string {
|
||||
return []string{"relay://192.0.2.42:443"}
|
||||
}
|
||||
func (f *fakeRelayStatus) RelayStatus(uri string) (time.Duration, bool) {
|
||||
return 42 * time.Millisecond, true
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
// 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 discover
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/protocol"
|
||||
"github.com/syncthing/syncthing/lib/beacon"
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/thejerf/suture"
|
||||
)
|
||||
|
||||
type localClient struct {
|
||||
*suture.Supervisor
|
||||
myID protocol.DeviceID
|
||||
addrList AddressLister
|
||||
relayStat RelayStatusProvider
|
||||
name string
|
||||
|
||||
beacon beacon.Interface
|
||||
localBcastStart time.Time
|
||||
localBcastTick <-chan time.Time
|
||||
forcedBcastTick chan time.Time
|
||||
|
||||
*cache
|
||||
}
|
||||
|
||||
const (
|
||||
BroadcastInterval = 30 * time.Second
|
||||
CacheLifeTime = 3 * BroadcastInterval
|
||||
)
|
||||
|
||||
var (
|
||||
ErrIncorrectMagic = errors.New("incorrect magic number")
|
||||
)
|
||||
|
||||
func NewLocal(id protocol.DeviceID, addr string, addrList AddressLister, relayStat RelayStatusProvider) (FinderService, error) {
|
||||
c := &localClient{
|
||||
Supervisor: suture.NewSimple("local"),
|
||||
myID: id,
|
||||
addrList: addrList,
|
||||
relayStat: relayStat,
|
||||
localBcastTick: time.Tick(BroadcastInterval),
|
||||
forcedBcastTick: make(chan time.Time),
|
||||
localBcastStart: time.Now(),
|
||||
cache: newCache(),
|
||||
}
|
||||
|
||||
host, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(host) == 0 {
|
||||
// A broadcast client
|
||||
c.name = "IPv4 local"
|
||||
bcPort, err := strconv.Atoi(port)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.startLocalIPv4Broadcasts(bcPort)
|
||||
} else {
|
||||
// A multicast client
|
||||
c.name = "IPv6 local"
|
||||
c.startLocalIPv6Multicasts(addr)
|
||||
}
|
||||
|
||||
go c.sendLocalAnnouncements()
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *localClient) startLocalIPv4Broadcasts(localPort int) {
|
||||
c.beacon = beacon.NewBroadcast(localPort)
|
||||
c.Add(c.beacon)
|
||||
go c.recvAnnouncements(c.beacon)
|
||||
}
|
||||
|
||||
func (c *localClient) startLocalIPv6Multicasts(localMCAddr string) {
|
||||
c.beacon = beacon.NewMulticast(localMCAddr)
|
||||
c.Add(c.beacon)
|
||||
go c.recvAnnouncements(c.beacon)
|
||||
}
|
||||
|
||||
// Lookup returns a list of addresses the device is available at. Local
|
||||
// discovery never returns relays.
|
||||
func (c *localClient) Lookup(device protocol.DeviceID) (direct []string, relays []Relay, err error) {
|
||||
if cache, ok := c.Get(device); ok {
|
||||
if time.Since(cache.when) < CacheLifeTime {
|
||||
direct = cache.Direct
|
||||
relays = cache.Relays
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *localClient) String() string {
|
||||
return c.name
|
||||
}
|
||||
|
||||
func (c *localClient) Error() error {
|
||||
return c.beacon.Error()
|
||||
}
|
||||
|
||||
func (c *localClient) announcementPkt() Announce {
|
||||
addrs := c.addrList.AllAddresses()
|
||||
|
||||
var relays []Relay
|
||||
for _, relay := range c.relayStat.Relays() {
|
||||
latency, ok := c.relayStat.RelayStatus(relay)
|
||||
if ok {
|
||||
relays = append(relays, Relay{
|
||||
URL: relay,
|
||||
Latency: int32(latency / time.Millisecond),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return Announce{
|
||||
Magic: AnnouncementMagic,
|
||||
This: Device{
|
||||
ID: c.myID[:],
|
||||
Addresses: addrs,
|
||||
Relays: relays,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *localClient) sendLocalAnnouncements() {
|
||||
var pkt = c.announcementPkt()
|
||||
msg := pkt.MustMarshalXDR()
|
||||
|
||||
for {
|
||||
c.beacon.Send(msg)
|
||||
|
||||
select {
|
||||
case <-c.localBcastTick:
|
||||
case <-c.forcedBcastTick:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *localClient) recvAnnouncements(b beacon.Interface) {
|
||||
for {
|
||||
buf, addr := b.Recv()
|
||||
|
||||
var pkt Announce
|
||||
err := pkt.UnmarshalXDR(buf)
|
||||
if err != nil && err != io.EOF {
|
||||
if debug {
|
||||
l.Debugf("discover: Failed to unmarshal local announcement from %s:\n%s", addr, hex.Dump(buf))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if debug {
|
||||
l.Debugf("discover: Received local announcement from %s for %s", addr, protocol.DeviceIDFromBytes(pkt.This.ID))
|
||||
}
|
||||
|
||||
var newDevice bool
|
||||
if bytes.Compare(pkt.This.ID, c.myID[:]) != 0 {
|
||||
newDevice = c.registerDevice(addr, pkt.This)
|
||||
}
|
||||
|
||||
if newDevice {
|
||||
select {
|
||||
case c.forcedBcastTick <- time.Now():
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *localClient) registerDevice(src net.Addr, device Device) bool {
|
||||
var id protocol.DeviceID
|
||||
copy(id[:], device.ID)
|
||||
|
||||
// Remember whether we already had a valid cache entry for this device.
|
||||
|
||||
ce, existsAlready := c.Get(id)
|
||||
isNewDevice := !existsAlready || time.Since(ce.when) > CacheLifeTime
|
||||
|
||||
// Any empty or unspecified addresses should be set to the source address
|
||||
// of the announcement. We also skip any addresses we can't parse.
|
||||
|
||||
var validAddresses []string
|
||||
for _, addr := range device.Addresses {
|
||||
u, err := url.Parse(addr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
tcpAddr, err := net.ResolveTCPAddr("tcp", u.Host)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(tcpAddr.IP) == 0 || tcpAddr.IP.IsUnspecified() {
|
||||
host, _, err := net.SplitHostPort(src.String())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
u.Host = fmt.Sprintf("%s:%d", host, tcpAddr.Port)
|
||||
validAddresses = append(validAddresses, u.String())
|
||||
} else {
|
||||
validAddresses = append(validAddresses, addr)
|
||||
}
|
||||
}
|
||||
|
||||
c.Set(id, CacheEntry{
|
||||
Direct: validAddresses,
|
||||
Relays: device.Relays,
|
||||
when: time.Now(),
|
||||
found: true,
|
||||
})
|
||||
|
||||
if isNewDevice {
|
||||
events.Default.Log(events.DeviceDiscovered, map[string]interface{}{
|
||||
"device": id.String(),
|
||||
"addrs": device.Addresses,
|
||||
"relays": device.Relays,
|
||||
})
|
||||
}
|
||||
|
||||
return isNewDevice
|
||||
}
|
||||
|
||||
func addrToAddr(addr *net.TCPAddr) string {
|
||||
if len(addr.IP) == 0 || addr.IP.IsUnspecified() {
|
||||
return fmt.Sprintf(":%c", addr.Port)
|
||||
} else if bs := addr.IP.To4(); bs != nil {
|
||||
return fmt.Sprintf("%s:%c", bs.String(), addr.Port)
|
||||
} else if bs := addr.IP.To16(); bs != nil {
|
||||
return fmt.Sprintf("[%s]:%c", bs.String(), addr.Port)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func resolveAddrs(addrs []string) []string {
|
||||
var raddrs []string
|
||||
for _, addrStr := range addrs {
|
||||
uri, err := url.Parse(addrStr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
addrRes, err := net.ResolveTCPAddr("tcp", uri.Host)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
addr := addrToAddr(addrRes)
|
||||
if len(addr) > 0 {
|
||||
uri.Host = addr
|
||||
raddrs = append(raddrs, uri.String())
|
||||
}
|
||||
}
|
||||
return raddrs
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
//go:generate -command genxdr go run ../../Godeps/_workspace/src/github.com/calmh/xdr/cmd/genxdr/main.go
|
||||
//go:generate genxdr -o packets_xdr.go packets.go
|
||||
//go:generate genxdr -o localpackets_xdr.go localpackets.go
|
||||
|
||||
package discover
|
||||
|
||||
@@ -26,8 +26,8 @@ type Announce struct {
|
||||
}
|
||||
|
||||
type Relay struct {
|
||||
Address string // max:256
|
||||
Latency int32
|
||||
URL string `json:"url"` // max:2083
|
||||
Latency int32 `json:"latency"`
|
||||
}
|
||||
|
||||
type Device struct {
|
||||
@@ -192,10 +192,10 @@ Relay 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 Address |
|
||||
| Length of URL |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
/ /
|
||||
\ Address (variable length) \
|
||||
\ URL (variable length) \
|
||||
/ /
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Latency |
|
||||
@@ -203,7 +203,7 @@ Relay Structure:
|
||||
|
||||
|
||||
struct Relay {
|
||||
string Address<256>;
|
||||
string URL<256>;
|
||||
int Latency;
|
||||
}
|
||||
|
||||
@@ -234,10 +234,10 @@ func (o Relay) AppendXDR(bs []byte) ([]byte, error) {
|
||||
}
|
||||
|
||||
func (o Relay) EncodeXDRInto(xw *xdr.Writer) (int, error) {
|
||||
if l := len(o.Address); l > 256 {
|
||||
return xw.Tot(), xdr.ElementSizeExceeded("Address", l, 256)
|
||||
if l := len(o.URL); l > 256 {
|
||||
return xw.Tot(), xdr.ElementSizeExceeded("URL", l, 256)
|
||||
}
|
||||
xw.WriteString(o.Address)
|
||||
xw.WriteString(o.URL)
|
||||
xw.WriteUint32(uint32(o.Latency))
|
||||
return xw.Tot(), xw.Error()
|
||||
}
|
||||
@@ -254,7 +254,7 @@ func (o *Relay) UnmarshalXDR(bs []byte) error {
|
||||
}
|
||||
|
||||
func (o *Relay) DecodeXDRFrom(xr *xdr.Reader) error {
|
||||
o.Address = xr.ReadStringMax(256)
|
||||
o.URL = xr.ReadStringMax(256)
|
||||
o.Latency = int32(xr.ReadUint32())
|
||||
return xr.Error()
|
||||
}
|
||||
@@ -40,6 +40,7 @@ const (
|
||||
FolderErrors
|
||||
FolderScanProgress
|
||||
ExternalPortMappingChanged
|
||||
RelayStateChanged
|
||||
|
||||
AllEvents = (1 << iota) - 1
|
||||
)
|
||||
@@ -90,6 +91,8 @@ func (t EventType) String() string {
|
||||
return "FolderScanProgress"
|
||||
case ExternalPortMappingChanged:
|
||||
return "ExternalPortMappingChanged"
|
||||
case RelayStateChanged:
|
||||
return "RelayStateChanged"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
@@ -236,6 +237,7 @@ func parseIgnoreFile(fd io.Reader, currentFile string, seen map[string]bool) ([]
|
||||
includeFile := filepath.Join(filepath.Dir(currentFile), line[len("#include "):])
|
||||
includes, err := loadIgnoreFile(includeFile, seen)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return err
|
||||
}
|
||||
patterns = append(patterns, includes...)
|
||||
|
||||
+137
-11
@@ -12,18 +12,23 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/relaysrv/client"
|
||||
"github.com/syncthing/relaysrv/protocol"
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/discover"
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
|
||||
"github.com/thejerf/suture"
|
||||
)
|
||||
|
||||
const (
|
||||
eventBroadcasterCheckInterval = 10 * time.Second
|
||||
)
|
||||
|
||||
type Svc struct {
|
||||
*suture.Supervisor
|
||||
cfg *config.Wrapper
|
||||
@@ -71,7 +76,12 @@ func NewSvc(cfg *config.Wrapper, tlsCfg *tls.Config) *Svc {
|
||||
stop: make(chan struct{}),
|
||||
}
|
||||
|
||||
eventBc := &eventBroadcaster{
|
||||
svc: svc,
|
||||
}
|
||||
|
||||
svc.Add(receiver)
|
||||
svc.Add(eventBc)
|
||||
|
||||
return svc
|
||||
}
|
||||
@@ -132,7 +142,7 @@ func (s *Svc) CommitConfiguration(from, to config.Configuration) bool {
|
||||
continue
|
||||
}
|
||||
|
||||
dynRelays := make([]discover.Relay, 0, len(ann.Relays))
|
||||
var dynRelayAddrs []string
|
||||
for _, relayAnn := range ann.Relays {
|
||||
ruri, err := url.Parse(relayAnn.URL)
|
||||
if err != nil {
|
||||
@@ -144,13 +154,11 @@ func (s *Svc) CommitConfiguration(from, to config.Configuration) bool {
|
||||
if debug {
|
||||
l.Debugln("Found", ruri, "via", uri)
|
||||
}
|
||||
dynRelays = append(dynRelays, discover.Relay{
|
||||
Address: ruri.String(),
|
||||
})
|
||||
dynRelayAddrs = append(dynRelayAddrs, ruri.String())
|
||||
}
|
||||
|
||||
dynRelayAddrs := discover.RelayAddressesSortedByLatency(dynRelays)
|
||||
if len(dynRelayAddrs) > 0 {
|
||||
dynRelayAddrs = relayAddressesSortedByLatency(dynRelayAddrs)
|
||||
closestRelay := dynRelayAddrs[0]
|
||||
if debug {
|
||||
l.Debugln("Picking", closestRelay, "as closest dynamic relay from", uri)
|
||||
@@ -193,7 +201,14 @@ func (s *Svc) CommitConfiguration(from, to config.Configuration) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *Svc) ClientStatus() map[string]bool {
|
||||
type Status struct {
|
||||
URL string
|
||||
OK bool
|
||||
Latency int
|
||||
}
|
||||
|
||||
// Relays return the list of relays that currently have an OK status.
|
||||
func (s *Svc) Relays() []string {
|
||||
if s == nil {
|
||||
// A nil client does not have a status, really. Yet we may be called
|
||||
// this way, for raisins...
|
||||
@@ -201,12 +216,34 @@ func (s *Svc) ClientStatus() map[string]bool {
|
||||
}
|
||||
|
||||
s.mut.RLock()
|
||||
status := make(map[string]bool, len(s.clients))
|
||||
for uri, client := range s.clients {
|
||||
status[uri] = client.StatusOK()
|
||||
relays := make([]string, 0, len(s.clients))
|
||||
for uri := range s.clients {
|
||||
relays = append(relays, uri)
|
||||
}
|
||||
s.mut.RUnlock()
|
||||
return status
|
||||
|
||||
sort.Strings(relays)
|
||||
|
||||
return relays
|
||||
}
|
||||
|
||||
// RelayStatus returns the latency and OK status for a given relay.
|
||||
func (s *Svc) RelayStatus(uri string) (time.Duration, bool) {
|
||||
if s == nil {
|
||||
// A nil client does not have a status, really. Yet we may be called
|
||||
// this way, for raisins...
|
||||
return time.Hour, false
|
||||
}
|
||||
|
||||
s.mut.RLock()
|
||||
client, ok := s.clients[uri]
|
||||
s.mut.RUnlock()
|
||||
|
||||
if !ok || !client.StatusOK() {
|
||||
return time.Hour, false
|
||||
}
|
||||
|
||||
return client.Latency(), true
|
||||
}
|
||||
|
||||
// Accept returns a new *tls.Conn. The connection is already handshaken.
|
||||
@@ -266,6 +303,55 @@ func (r *invitationReceiver) Stop() {
|
||||
close(r.stop)
|
||||
}
|
||||
|
||||
// The eventBroadcaster sends a RelayStateChanged event when the relay status
|
||||
// changes. We need this somewhat ugly polling mechanism as there's currently
|
||||
// no way to get the event feed directly from the relay lib. This may be
|
||||
// somethign to revisit later, possibly.
|
||||
type eventBroadcaster struct {
|
||||
svc *Svc
|
||||
stop chan struct{}
|
||||
}
|
||||
|
||||
func (e *eventBroadcaster) Serve() {
|
||||
timer := time.NewTicker(eventBroadcasterCheckInterval)
|
||||
defer timer.Stop()
|
||||
|
||||
var prevOKRelays []string
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-timer.C:
|
||||
curOKRelays := e.svc.Relays()
|
||||
|
||||
changed := len(curOKRelays) != len(prevOKRelays)
|
||||
if !changed {
|
||||
for i := range curOKRelays {
|
||||
if curOKRelays[i] != prevOKRelays[i] {
|
||||
changed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if changed {
|
||||
events.Default.Log(events.RelayStateChanged, map[string][]string{
|
||||
"old": prevOKRelays,
|
||||
"new": curOKRelays,
|
||||
})
|
||||
}
|
||||
|
||||
prevOKRelays = curOKRelays
|
||||
|
||||
case <-e.stop:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *eventBroadcaster) Stop() {
|
||||
close(e.stop)
|
||||
}
|
||||
|
||||
// This is the announcement recieved from the relay server;
|
||||
// {"relays": [{"url": "relay://10.20.30.40:5060"}, ...]}
|
||||
type dynamicAnnouncement struct {
|
||||
@@ -273,3 +359,43 @@ type dynamicAnnouncement struct {
|
||||
URL string
|
||||
}
|
||||
}
|
||||
|
||||
// relayAddressesSortedByLatency adds local latency to the relay, and sorts them
|
||||
// by sum latency, and returns the addresses.
|
||||
func relayAddressesSortedByLatency(input []string) []string {
|
||||
relays := make(relayList, len(input))
|
||||
for i, relay := range input {
|
||||
if latency, err := osutil.GetLatencyForURL(relay); err == nil {
|
||||
relays[i] = relayWithLatency{relay, int(latency / time.Millisecond)}
|
||||
} else {
|
||||
relays[i] = relayWithLatency{relay, int(time.Hour / time.Millisecond)}
|
||||
}
|
||||
}
|
||||
|
||||
sort.Sort(relays)
|
||||
|
||||
addresses := make([]string, len(relays))
|
||||
for i, relay := range relays {
|
||||
addresses[i] = relay.relay
|
||||
}
|
||||
return addresses
|
||||
}
|
||||
|
||||
type relayWithLatency struct {
|
||||
relay string
|
||||
latency int
|
||||
}
|
||||
|
||||
type relayList []relayWithLatency
|
||||
|
||||
func (l relayList) Len() int {
|
||||
return len(l)
|
||||
}
|
||||
|
||||
func (l relayList) Less(a, b int) bool {
|
||||
return l[a].latency < l[b].latency
|
||||
}
|
||||
|
||||
func (l relayList) Swap(a, b int) {
|
||||
l[a], l[b] = l[b], l[a]
|
||||
}
|
||||
Reference in new issue
Block a user