diff --git a/src/3rdParty/psicash/Debug2015/psicash.lib b/src/3rdParty/psicash/Debug2015/psicash.lib index 6962eb738..cff55aedb 100644 Binary files a/src/3rdParty/psicash/Debug2015/psicash.lib and b/src/3rdParty/psicash/Debug2015/psicash.lib differ diff --git a/src/3rdParty/psicash/Debug2015/psicash.pdb b/src/3rdParty/psicash/Debug2015/psicash.pdb index c22fa2a10..47af54c53 100644 Binary files a/src/3rdParty/psicash/Debug2015/psicash.pdb and b/src/3rdParty/psicash/Debug2015/psicash.pdb differ diff --git a/src/3rdParty/psicash/Release2015/psicash.lib b/src/3rdParty/psicash/Release2015/psicash.lib index 585ade43e..05e76a2a3 100644 Binary files a/src/3rdParty/psicash/Release2015/psicash.lib and b/src/3rdParty/psicash/Release2015/psicash.lib differ diff --git a/src/3rdParty/psicash/git.txt b/src/3rdParty/psicash/git.txt index 788190e98..5d17bada2 100644 --- a/src/3rdParty/psicash/git.txt +++ b/src/3rdParty/psicash/git.txt @@ -1 +1 @@ -v2.3.0-0-gdd95614 +v2.3.1-0-ge96df62 diff --git a/src/3rdParty/psiphon-tunnel-core.exe b/src/3rdParty/psiphon-tunnel-core.exe index 67e38027e..d8fe7c392 100644 Binary files a/src/3rdParty/psiphon-tunnel-core.exe and b/src/3rdParty/psiphon-tunnel-core.exe differ diff --git a/src/coretransport.cpp b/src/coretransport.cpp index 6c733ae25..57cb559f3 100644 --- a/src/coretransport.cpp +++ b/src/coretransport.cpp @@ -38,8 +38,6 @@ using namespace std::experimental; #define AUTOMATICALLY_ASSIGNED_PORT_NUMBER 0 -#define EXE_NAME _T("psiphon-tunnel-core.exe") -#define URL_PROXY_EXE_NAME _T("psiphon-url-proxy.exe") #define MAX_LEGACY_SERVER_ENTRIES 30 #define LEGACY_SERVER_ENTRY_LIST_NAME (string(LOCAL_SETTINGS_REGISTRY_VALUE_SERVERS) + "OSSH").c_str() @@ -328,58 +326,103 @@ void CoreTransport::TransportConnectHelper() bool CoreTransport::SpawnCoreProcess(const tstring& configFilename, const tstring& serverListFilename) { - filesystem::path tempPath; - if (!GetSysTempPath(tempPath)) { - my_print(NOT_SENSITIVE, true, _T("%s:%d - GetSysTempPath failed: %d"), __TFUNCTION__, __LINE__, GetLastError()); - return false; - } + // When Settings::ExposeLocalProxiesToLAN is true, we are exposing the local proxy + // to the LAN (i.e., listen on all IP addresses; allow connections from external + // clients) there is a Windows Firewall prompt to allow the subprocess -- by path + // and name -- to accept external connections. In that case there are two things + // we don't want to do: + // 1. Show a new prompt every time there's a connection attempt and a new + // subprocess is spawned. This means that we can't use a random name every time. + // 2. Show a prompt with an filename that is unintelligible to the user. This means + // that we don't want to use a random name at all. + bool useFixedSubprocessName = Settings::ExposeLocalProxiesToLAN(); + + bool startSuccess = false; + for (int i = 0; i < 5; i++) { + if (i > 0 && useFixedSubprocessName) { + // We've already had our one attempt + break; + } - filesystem::path exePath; - if (RequestingUrlProxyWithoutTunnel()) - { - exePath = tempPath / URL_PROXY_EXE_NAME; + tstring exePath; + if (useFixedSubprocessName) { + filesystem::path tempPath; + if (!GetSysTempPath(tempPath)) { + my_print(NOT_SENSITIVE, true, _T("%s:%d - GetSysTempPath failed: %d"), __TFUNCTION__, __LINE__, GetLastError()); + return false; + } - // In RequestingUrlProxyWithoutTunnel mode, we allow for multiple instances - // so we don't fail extract if the file already exists -- and don't try to - // kill any associated process holding a lock on it. - if (!ExtractExecutable(IDR_PSIPHON_TUNNEL_CORE_EXE, exePath, true)) + exePath = tempPath / "psiphon-tunnel-core.exe"; + } + else { + // We will be using a random file name for the executable. This will help + // prevent blocking of "psiphon-tunnel-core.exe". See: + // https://github.com/Psiphon-Inc/psiphon-issues/issues/828 + // The goal is to make the running of this file as unblockable as possible, + // for example by a Windows Group Policy. Originally we always used the the + // same filename and it was trivially blocked from running. We are now using a + // filename with random length and random characters, under a random depth of + // subdirectories, which should be extremely difficult to create a glob-based + // matching rule for. + // The extension is also only included half the time. We will make multiple + // attempts to ensure that various filename configurations are tried. + if (!GetUniqueTempFilename(_T(".exe"), exePath, i)) { + my_print(NOT_SENSITIVE, true, _T("%s:%d - GetUniqueTempFilename failed: %d"), __TFUNCTION__, __LINE__, GetLastError()); + // This is unlikely to be recoverable with more attempts + return false; + } + } + + if (RequestingUrlProxyWithoutTunnel()) { - my_print(NOT_SENSITIVE, true, _T("%s:%d - ExtractExecutable failed: %d"), __TFUNCTION__, __LINE__, GetLastError()); + // In RequestingUrlProxyWithoutTunnel mode, we allow for multiple instances + // so we don't fail extract if the file already exists -- and don't try to + // kill any associated process holding a lock on it. + if (!ExtractExecutable(IDR_PSIPHON_TUNNEL_CORE_EXE, exePath, true)) + { + my_print(NOT_SENSITIVE, true, _T("%s:%d - ExtractExecutable failed: %d"), __TFUNCTION__, __LINE__, GetLastError()); - // This string contains PII (the username in the temp path) but won't be logged - auto errorDetail = WStringToUTF8(exePath.tstring() + L"\n\n" + SystemErrorMessage(GetLastError())); - UI_Notice("PsiphonUI::FileError", errorDetail); + // This string contains PII (the username in the temp path) but won't be logged + auto errorDetail = WStringToUTF8(exePath + L"\n\n" + SystemErrorMessage(GetLastError())); + UI_Notice("PsiphonUI::FileError", errorDetail); - return false; + continue; + } } - } - else - { - exePath = tempPath / EXE_NAME; - - if (!ExtractExecutable(IDR_PSIPHON_TUNNEL_CORE_EXE, exePath)) + else { - my_print(NOT_SENSITIVE, true, _T("%s:%d - ExtractExecutable failed: %d"), __TFUNCTION__, __LINE__, GetLastError()); + if (!ExtractExecutable(IDR_PSIPHON_TUNNEL_CORE_EXE, exePath)) + { + my_print(NOT_SENSITIVE, true, _T("%s:%d - ExtractExecutable failed: %d"), __TFUNCTION__, __LINE__, GetLastError()); - // This string contains PII (the username in the temp path) but won't be logged - auto errorDetail = WStringToUTF8(exePath.tstring() + L"\n\n" + SystemErrorMessage(GetLastError())); - UI_Notice("PsiphonUI::FileError", errorDetail); + // This string contains PII (the username in the temp path) but won't be logged + auto errorDetail = WStringToUTF8(exePath + L"\n\n" + SystemErrorMessage(GetLastError())); + UI_Notice("PsiphonUI::FileError", errorDetail); - return false; + continue; + } } - } - tstringstream commandLineFlags; - commandLineFlags << _T(" --config \"") << configFilename << _T("\""); + tstringstream commandLineFlags; + commandLineFlags << _T(" --config \"") << configFilename << _T("\""); - if (!RequestingUrlProxyWithoutTunnel()) - { - commandLineFlags << _T(" --serverList \"") << serverListFilename << _T("\""); + if (!RequestingUrlProxyWithoutTunnel()) + { + commandLineFlags << _T(" --serverList \"") << serverListFilename << _T("\""); + } + + m_psiphonTunnelCore = make_unique(this, exePath); + if (!m_psiphonTunnelCore->SpawnSubprocess(commandLineFlags.str())) { + my_print(NOT_SENSITIVE, false, _T("%s:%d - SpawnSubprocess failed"), __TFUNCTION__, __LINE__); + continue; + } + + startSuccess = true; + break; } - m_psiphonTunnelCore = make_unique(this, exePath); - if (!m_psiphonTunnelCore->SpawnSubprocess(commandLineFlags.str())) { - my_print(NOT_SENSITIVE, false, _T("%s:%d - SpawnSubprocess failed"), __TFUNCTION__, __LINE__); + if (!startSuccess) { + my_print(NOT_SENSITIVE, true, _T("%s:%d - process spawning failed utterly: %d"), __TFUNCTION__, __LINE__, GetLastError()); return false; } diff --git a/src/feedback_upload.cpp b/src/feedback_upload.cpp index 3d30dd6df..3a6cb5345 100644 --- a/src/feedback_upload.cpp +++ b/src/feedback_upload.cpp @@ -35,8 +35,6 @@ using namespace std::experimental; -#define EXE_NAME _T("feedback-upload.exe") - // IWorkerThread boilerplate void FeedbackUpload::StartSendFeedback() @@ -168,31 +166,42 @@ void FeedbackUpload::SendFeedbackHelper() bool FeedbackUpload::SpawnFeedbackUploadProcess(const tstring& configFilename, const string& diagnosticData) { - filesystem::path tempPath; - if (!GetSysTempPath(tempPath)) { - my_print(NOT_SENSITIVE, true, _T("%s:%d - GetSysTempPath failed: %d"), __TFUNCTION__, __LINE__, GetLastError()); - return false; - } + bool startSuccess = false; + for (int i = 0; i < 5; i++) { + tstring exePath; + if (!GetUniqueTempFilename(_T(".exe"), exePath, i)) { + my_print(NOT_SENSITIVE, true, _T("%s:%d - GetUniqueTempFilename failed: %d"), __TFUNCTION__, __LINE__, GetLastError()); + // This is unlikely to be recoverable with more attempts + return false; + } - auto exePath = tempPath / EXE_NAME; + if (!ExtractExecutable(IDR_PSIPHON_TUNNEL_CORE_EXE, exePath)) + { + my_print(NOT_SENSITIVE, true, _T("%s:%d - ExtractExecutable failed: %d"), __TFUNCTION__, __LINE__, GetLastError()); - if (!ExtractExecutable(IDR_PSIPHON_TUNNEL_CORE_EXE, exePath)) - { - my_print(NOT_SENSITIVE, true, _T("%s:%d - ExtractExecutable failed: %d"), __TFUNCTION__, __LINE__, GetLastError()); + // This string contains PII (the username in the temp path) but won't be logged + auto errorDetail = WStringToUTF8(exePath + L"\n\n" + SystemErrorMessage(GetLastError())); + UI_Notice("PsiphonUI::FileError", errorDetail); - // This string contains PII (the username in the temp path) but won't be logged - auto errorDetail = WStringToUTF8(exePath.tstring() + L"\n\n" + SystemErrorMessage(GetLastError())); - UI_Notice("PsiphonUI::FileError", errorDetail); + continue; + } - return false; - } + tstringstream commandLineFlags; + commandLineFlags << _T(" --config \"") << configFilename << _T("\" --feedbackUpload"); - tstringstream commandLineFlags; - commandLineFlags << _T(" --config \"") << configFilename << _T("\" --feedbackUpload"); + m_psiphonTunnelCore = make_unique(this, exePath); + if (!m_psiphonTunnelCore->SpawnSubprocess(commandLineFlags.str())) { + my_print(NOT_SENSITIVE, false, _T("%s:%d - SpawnSubprocess failed"), __TFUNCTION__, __LINE__); + // The PsiphonTunnelCore (Subprocess) destructor will clean up the executable file + continue; + } + + startSuccess = true; + break; + } - m_psiphonTunnelCore = make_unique(this, exePath); - if (!m_psiphonTunnelCore->SpawnSubprocess(commandLineFlags.str())) { - my_print(NOT_SENSITIVE, false, _T("%s:%d - SpawnSubprocess failed"), __TFUNCTION__, __LINE__); + if (!startSuccess) { + my_print(NOT_SENSITIVE, true, _T("%s:%d - process spawning failed utterly: %d"), __TFUNCTION__, __LINE__, GetLastError()); return false; } diff --git a/src/local_proxy.cpp b/src/local_proxy.cpp index 3d1bf3523..f0b4e2542 100644 --- a/src/local_proxy.cpp +++ b/src/local_proxy.cpp @@ -30,7 +30,6 @@ #define POLIPO_CONNECTION_TIMEOUT_SECONDS 20 -#define POLIPO_EXE_NAME _T("psiphon3-polipo.exe") LocalProxy::LocalProxy( @@ -89,49 +88,59 @@ void LocalProxy::UpdateSessionInfo(const SessionInfo& sessionInfo) bool LocalProxy::DoStart() { - if (m_polipoPath.size() == 0) - { - filesystem::path tempPath; - if (!GetSysTempPath(tempPath)) { - my_print(NOT_SENSITIVE, true, _T("%s:%d - GetSysTempPath failed: %d"), __TFUNCTION__, __LINE__, GetLastError()); + // Ensure we start from a disconnected/clean state + Cleanup(false); + + int localHttpProxyPort = Settings::LocalHttpProxyPort(); + + bool startSuccess = false; + for (int i = 0; i < 5; i++) { + if (!GetUniqueTempFilename(_T(".exe"), m_polipoPath, i)) { + my_print(NOT_SENSITIVE, true, _T("%s:%d - GetUniqueTempFilename failed: %d"), __TFUNCTION__, __LINE__, GetLastError()); + // This is unlikely to be recoverable with more attempts return false; } - m_polipoPath = tempPath / POLIPO_EXE_NAME; - if (!ExtractExecutable(IDR_POLIPO_EXE, m_polipoPath)) { - return false; + continue; } - } - // Ensure we start from a disconnected/clean state - Cleanup(false); - - int localHttpProxyPort = Settings::LocalHttpProxyPort(); - if (localHttpProxyPort == 0) - { - // Choose the port automatically - localHttpProxyPort = 1024; - if (!TestForOpenPort(localHttpProxyPort, 60000, m_stopInfo)) + if (localHttpProxyPort == 0) { - my_print(NOT_SENSITIVE, false, _T("HTTP proxy could not find an available port.")); - return false; + // Choose the port automatically + localHttpProxyPort = 1024; + if (!TestForOpenPort(localHttpProxyPort, 60000, m_stopInfo)) + { + my_print(NOT_SENSITIVE, false, _T("HTTP proxy could not find an available port.")); + // This is unlikely to be recoverable with more attempts + return false; + } } - } - else - { - // Require the specified port - if (!TestForOpenPort(localHttpProxyPort, 0, m_stopInfo)) + else { - my_print(NOT_SENSITIVE, false, _T("Port is not available for HTTP proxy to listen on: %d"), localHttpProxyPort); - return false; + // Require the specified port + if (!TestForOpenPort(localHttpProxyPort, 0, m_stopInfo)) + { + my_print(NOT_SENSITIVE, false, _T("Port is not available for HTTP proxy to listen on: %d"), localHttpProxyPort); + // This is unlikely to be recoverable with more attempts + return false; + } + } + + if (!StartPolipo(localHttpProxyPort)) + { + // The executable file is deleted by Cleanup + Cleanup(false); + continue; } + + startSuccess = true; + break; } - if (!StartPolipo(localHttpProxyPort)) - { - Cleanup(false); + if (!startSuccess) { + my_print(NOT_SENSITIVE, true, _T("%s:%d - startup failed utterly: %d"), __TFUNCTION__, __LINE__, GetLastError()); return false; } @@ -204,6 +213,21 @@ void LocalProxy::DoStop(bool cleanly) void LocalProxy::Cleanup(bool doStats) { + auto deleteExe = finally([=]() { + if (!m_polipoPath.empty() && !DeleteFile(m_polipoPath.c_str())) + { + // We sometimes see random DeleteFile failures with ERROR_ACCESS_DENIED. + // This may be due to ongoing virus scanning of a newly written executable. + // Sleeping seems effective in allowing a subsequent DeleteFile to succeed, + // although the exact time needed to sleep surely varies by system. + Sleep(1000); + if (!DeleteFile(m_polipoPath.c_str())) { + my_print(NOT_SENSITIVE, true, _T("%s:%d - DeleteFile failed: %d"), __TFUNCTION__, __LINE__, GetLastError()); + } + } + m_polipoPath.clear(); + }); + // Give the process an opportunity for graceful shutdown, then terminate if (m_polipoProcessInfo.hProcess != 0 && m_polipoProcessInfo.hProcess != INVALID_HANDLE_VALUE) diff --git a/src/psiphon_tunnel_core_utilities.cpp b/src/psiphon_tunnel_core_utilities.cpp index 42dc673e1..ede98e75e 100644 --- a/src/psiphon_tunnel_core_utilities.cpp +++ b/src/psiphon_tunnel_core_utilities.cpp @@ -163,7 +163,7 @@ bool WriteParameterFiles(const WriteParameterFilesIn& in, WriteParameterFilesOut // as each other. So we'll give each one a unique, temporary directory. tstring tempDir; - if (!GetUniqueTempDir(tempDir, true)) + if (!GetUniqueTempDir(tempDir)) { my_print(NOT_SENSITIVE, false, _T("%s - GetUniqueTempDir failed (%d)"), __TFUNCTION__, GetLastError()); return false; diff --git a/src/subprocess.cpp b/src/subprocess.cpp index d4781b3cb..31f308f17 100644 --- a/src/subprocess.cpp +++ b/src/subprocess.cpp @@ -27,9 +27,10 @@ #include "utilities.h" -Subprocess::Subprocess(const tstring& exePath, ISubprocessOutputHandler* outputHandler) +Subprocess::Subprocess(const tstring& exePath, ISubprocessOutputHandler* outputHandler, bool deleteExe/*=true*/) : m_parentOutputPipe(INVALID_HANDLE_VALUE), - m_parentInputPipe(INVALID_HANDLE_VALUE) + m_parentInputPipe(INVALID_HANDLE_VALUE), + m_deleteExe(deleteExe) { if (outputHandler == NULL) { throw std::exception(__FUNCTION__ ":" STRINGIZE(__LINE__) "outputHandler null"); @@ -95,6 +96,7 @@ bool Subprocess::SpawnSubprocess(const tstring &commandLineFlags) { my_print(NOT_SENSITIVE, false, _T("%s - CreateProcess failed (%d)"), __TFUNCTION__, GetLastError()); CloseHandle(m_parentOutputPipe); + m_parentOutputPipe = INVALID_HANDLE_VALUE; CloseHandle(startupInfo.hStdInput); CloseHandle(startupInfo.hStdOutput); CloseHandle(startupInfo.hStdError); @@ -133,6 +135,7 @@ bool Subprocess::SpawnSubprocess(const tstring &commandLineFlags) if (!CloseHandle(m_parentOutputPipe)) { my_print(NOT_SENSITIVE, false, _T("%s:%d - CloseHandle failed (%d)"), __TFUNCTION__, __LINE__, GetLastError()); } + m_parentOutputPipe = INVALID_HANDLE_VALUE; return false; } @@ -171,6 +174,7 @@ bool Subprocess::CloseInputPipes() if (!CloseHandle(m_parentOutputPipe)) { my_print(NOT_SENSITIVE, false, _T("%s:%d - CloseHandle failed (%d)"), __FUNCTION__, __LINE__, GetLastError()); } + m_parentOutputPipe = INVALID_HANDLE_VALUE; } return success; @@ -181,6 +185,21 @@ bool Subprocess::Cleanup() { AutoMUTEX lock(m_mutex); + auto deleteExe = finally([=]() { + if (m_deleteExe && !m_exePath.empty() && !DeleteFile(m_exePath.c_str())) + { + // We sometimes see random DeleteFile failures with ERROR_ACCESS_DENIED. + // This may be due to ongoing virus scanning of a newly written executable. + // Sleeping seems effective in allowing a subsequent DeleteFile to succeed, + // although the exact time needed to sleep surely varies by system. + Sleep(1000); + if (!DeleteFile(m_exePath.c_str())) { + my_print(NOT_SENSITIVE, true, _T("%s:%d - DeleteFile failed: %d"), __TFUNCTION__, __LINE__, GetLastError()); + } + } + m_exePath.clear(); + }); + (void)CloseInputPipes(); // Give the process an opportunity for graceful shutdown, then terminate diff --git a/src/subprocess.h b/src/subprocess.h index 2ac51a631..1439da7b7 100644 --- a/src/subprocess.h +++ b/src/subprocess.h @@ -53,8 +53,9 @@ class Subprocess public: /** Initialize a new instance. Throws std::exception if outputHandler is null. + If deleteExe is true, the file at exePath will be deleted on cleanup. */ - Subprocess(const tstring& exePath, ISubprocessOutputHandler* outputHandler); + Subprocess(const tstring& exePath, ISubprocessOutputHandler* outputHandler, bool deleteExe=true); virtual ~Subprocess(); /** @@ -127,4 +128,5 @@ class Subprocess string m_parentOutputPipeBuffer; HANDLE m_mutex; ISubprocessOutputHandler* m_outputHandler; + bool m_deleteExe; }; diff --git a/src/utilities.cpp b/src/utilities.cpp index c29ae4cbb..26345c1f0 100644 --- a/src/utilities.cpp +++ b/src/utilities.cpp @@ -43,6 +43,10 @@ using namespace std::experimental; extern HINSTANCE g_hInst; +/// To be used for random number operations within this file +thread_local static std::mt19937 g_RNG{std::random_device{}()}; + + // Adapted from here: // http://stackoverflow.com/questions/865152/how-can-i-get-a-process-handle-by-its-name-in-c void TerminateProcessByName(const TCHAR* executableName) @@ -249,6 +253,7 @@ bool GetSysTempPath(filesystem::path& o_path) ret = GetTempPath(MAX_PATH, tempPath); if (ret > MAX_PATH - 14 || ret == 0) { + my_print(NOT_SENSITIVE, true, _T("%s:%d - GetTempPath failed: %d"), __TFUNCTION__, __LINE__, GetLastError()); return false; } @@ -257,58 +262,115 @@ bool GetSysTempPath(filesystem::path& o_path) } -// Makes an absolute path to a unique temp directory. -// If `create` is true, the directory will also be created. -// Returns true on success, false otherwise. Caller can check GetLastError() on failure. -bool GetUniqueTempDir(tstring& o_path, bool create) +/// Generates a random alphanumeric string with the given length +std::tstring RandomString(size_t length) +{ + static auto& chars = "0123456789" + "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + // -1 for the null terminator, -1 because 0-based indexing + static const auto lastCharIndex = sizeof(chars) - 2; + + // The 0-to-lastCharIndex range is inclusive + std::uniform_int_distribution pick(0, lastCharIndex); + + std::tstring s; + s.reserve(length); + + while (length--) { + s += chars[pick(g_RNG)]; + } + + return s; +} + + +/// Generates a random alphanumeric string with length randomly chosen between the given bounds (inclusive) +std::tstring RandomLengthString(size_t shortestLength, size_t longestLength) +{ + std::uniform_int_distribution pick(shortestLength, longestLength); + return RandomString(pick(g_RNG)); +} + + +/// Generates a random file or directory name with random (but appropriate) length +std::tstring RandomLengthFilename() { + // This is entirely arbitrary. Any shorter than this and there starts being the + // possibility of conflict; any longer and it starts encroaching on MAX_PATH + // (especially if a random filename is combined with a random directory name). + return RandomLengthString(6, 60); +} + + +bool GetUniqueTempDir(tstring& o_path, int depth/*=1*/) +{ + // This is arbitrary, but sane. The length variability gets smaller as the depth + // gets deeper, and at a very large number we might hit MAX_PATH. + if (depth > 10) { + my_print(NOT_SENSITIVE, true, _T("%s:%d - depth too large: %d"), __TFUNCTION__, __LINE__, depth); + return false; + } + o_path.clear(); filesystem::path tempPath; if (!GetSysTempPath(tempPath)) { + my_print(NOT_SENSITIVE, true, _T("%s:%d - GetSysTempPath failed: %d"), __TFUNCTION__, __LINE__, GetLastError()); return false; } - tstring guid; - if (!MakeGUID(guid)) - { - return false; - } + if (depth > 0) { + // These is entirely arbitrary. Directories shorter than 6 run the risk of + // name conflicts. We also don't want paths that are too long, to make sure + // we stay under MAX_PATH (note that there will still be a filename under + // this path). + const int minSubDirLen = 6; + const int maxTotalSubDirsLen = 80; - auto tempDir = filesystem::path(tempPath).append(guid); + // Ensure that the max is at least a little bigger than the min. + int maxSubDirLen = max(maxTotalSubDirsLen/depth, minSubDirLen+1); - if (create && !CreateDirectory(tempDir.tstring().c_str(), NULL)) { - return false; + for (int i = 0; i < depth; i++) { + tempPath /= RandomLengthString(minSubDirLen, maxSubDirLen); + + if (!CreateDirectory(tempPath.tstring().c_str(), NULL)) { + my_print(NOT_SENSITIVE, true, _T("%s:%d - CreateDirectory failed: %d"), __TFUNCTION__, __LINE__, GetLastError()); + return false; + } + } } - o_path = tempDir.tstring(); + o_path = tempPath.tstring(); return true; } -bool GetUniqueTempFilename(const tstring& extension, tstring& o_filepath) +bool GetUniqueTempFilename(const tstring& extension, tstring& o_filepath, int attempt/*=-1*/) { o_filepath.clear(); - filesystem::path tempPath; - if (!GetSysTempPath(tempPath)) - { - return false; - } + // Many ordinary temp files are under directories, so it makes sense for us to + // also use subdirecties, but going much deeper than 3 has diminishing returns. + int tempDirDepth = std::uniform_int_distribution(0, 3)(g_RNG); - tstring filename; - if (!MakeGUID(filename)) - { + tstring tempPath; + if (!GetUniqueTempDir(tempPath, tempDirDepth)) { + my_print(NOT_SENSITIVE, true, _T("%s:%d - GetUniqueTempDir failed: %d"), __TFUNCTION__, __LINE__, GetLastError()); return false; } + tstring filename = RandomLengthString(6, 60); + if (!extension.empty()) { - if (extension[0] == _T('.')) { - filename += extension; - } - else { - filename += _T(".") + extension; + if (attempt < 0 || attempt % 2 == 0) { + if (extension[0] == _T('.')) { + filename += extension; + } + else { + filename += _T(".") + extension; + } } } @@ -331,29 +393,6 @@ bool GetOwnExecutablePath(tstring& o_path) { } -// Makes a GUID string. Returns true on success, false otherwise. -bool MakeGUID(tstring& o_guid) { - o_guid.clear(); - - GUID g; - if (CoCreateGuid(&g) != S_OK) - { - return false; - } - - TCHAR guidString[128]; - - if (StringFromGUID2(g, guidString, sizeof(guidString) / sizeof(TCHAR)) <= 0) - { - return false; - } - - o_guid = guidString; - - return true; -} - - // Caller can check GetLastError() on failure bool GetShortPathName(const tstring& path, tstring& o_shortPath) { diff --git a/src/utilities.h b/src/utilities.h index 4ab5b71ea..d2c9fe8a0 100644 --- a/src/utilities.h +++ b/src/utilities.h @@ -47,17 +47,22 @@ bool DirectoryExists(LPCTSTR szPath); // dir with no suffixes is always created.) bool GetPsiphonDataPath(const vector& pathSuffixes, bool ensureExists, tstring& o_path); +/// Returns the system temp path to be used for Psiphon files. bool GetSysTempPath(filesystem::path& o_path); -// Makes an absolute path to a unique temp directory. -// If `create` is true, the directory will also be created. -// Returns true on success, false otherwise. Caller can check GetLastError() on failure. -bool GetUniqueTempDir(tstring& o_path, bool create); +/// Makes an absolute path to a unique temp directory. The directory is created. +/// `depth` indicates how many subdirectories under the temp root should be used. +/// If `depth` is zero, then the temp directory itself is returned. +/// `depth` must not be larger than 10, as randomness is lost and the path can get too long. +/// Returns true on success, false otherwise. Caller can check GetLastError() on failure. +bool GetUniqueTempDir(tstring& o_path, int depth=1); -// Makes an absolute path for a unique filename. If `extension` is nonempty, -// the filename will have that extension. -// Returns true on success, false otherwise. Caller can check GetLastError() on failure. -bool GetUniqueTempFilename(const tstring& extension, tstring& o_filepath); +/// Makes an absolute path for a unique filename. It may include one or more subdirectories. +/// If `extension` is nonempty, the filename will have that extension. +/// If `attempt` is greater than zero, the extension will alternate between being included and not. +/// (This is intended to be used when creating executable files.) +/// Returns true on success, false otherwise. Caller can check GetLastError() on failure. +bool GetUniqueTempFilename(const tstring& extension, tstring& o_filepath, int attempt=-1); /// Retrieves the path of the current executable. Returns false on error. bool GetOwnExecutablePath(tstring& o_path); @@ -185,9 +190,6 @@ std::tstring SystemErrorMessage(DWORD errorCode); tstring GetISO8601DatetimeString(); -// Makes a GUID string. Returns true on success, false otherwise. -bool MakeGUID(tstring& o_guid); - /// Randomly shuffle a vector of values. template void ShuffleVector(IteratorType begin, IteratorType end) { diff --git a/src/webui/_locales/fr/messages.json b/src/webui/_locales/fr/messages.json index c4747f3a0..eed784cc1 100644 --- a/src/webui/_locales/fr/messages.json +++ b/src/webui/_locales/fr/messages.json @@ -856,7 +856,7 @@ "description": "Body of modal dialog in which the user can log in to an existing account or click a link to create a new one." }, "psicash#modal-login-username-field": { - "message": "PsiCash username", + "message": "Nom d’utilisateur PsiCash", "description": "Label for a username field in the PsiCash account login modal dialog." }, "psicash#modal-login-password-field": { @@ -864,7 +864,7 @@ "description": "Label for a password field in the PsiCash account login modal dialog." }, "psicash#modal-login-forgot-password-link": { - "message": "Forgot your password or username?", + "message": "Mot de passe ou nom d’utilisateur oublié?", "description": "Link for text in the PsiCash account login modal dialog. Clicking it will bring the user to a page where they can recover their account." }, "psicash#modal-login-submit-button": { @@ -972,11 +972,11 @@ "description": "Title of modal error message shown to user when Psiphon disallows some non-web internet traffic. The user will be told to 'upgrade' their connection with Speed Boost (giving more speed and app compatibility). The word 'Psiphon' must not be translated or transliterated." }, "notice#disallowed-traffic-alert-body": { - "message": "

Les applis ne fonctionnent-elles pas ?

\n

Une partie du trafic Internet n’est pas prise en charge sans une Survitesse active. Activez la Survitesse avec du PsiCash afin de libérer le plein potentiel de votre expérience Psiphon.

", + "message": "

Les applis ne fonctionnent-elles pas?

\n

Une partie du trafic Internet n’est pas prise en charge sans une Survitesse active. Activez la Survitesse avec du PsiCash afin de libérer le plein potentiel de votre expérience Psiphon.

", "description": "Body text of modal error message shown to user when Psiphon disallows some non-web internet traffic. The user needs to purchase Speed Boost in order to full app compatibilty and more speed. The words 'Psiphon' and 'PsiCash' must not be translated or transliterated. 'Speed Boost' is a reward that can be purchased with PsiCash credit. It provides unlimited network connection speed through Psiphon. Other words that can be used to help with translation are: 'turbo' (like cars), 'accelerate', 'warp speed', 'blast off', or anything that indicates a fast or unrestricted speed." }, "appbackend#disallowed-traffic-notification-title": { - "message": "Les applis ne fonctionnent-elles pas ?", + "message": "Les applis ne fonctionnent-elles pas?", "description": "Appears in the notification area balloon when Psiphon disallows some non-web internet traffic. The user needs to purchase Speed Boost in order to full app compatibilty and more speed." }, "appbackend#disallowed-traffic-notification-body": { diff --git a/src/webui/_locales/ru/messages.json b/src/webui/_locales/ru/messages.json index f2cbf0bbf..14e4d0c53 100644 --- a/src/webui/_locales/ru/messages.json +++ b/src/webui/_locales/ru/messages.json @@ -576,7 +576,7 @@ "description": "Text in the button that dismisses a modal dialog. It is often the only button (no 'OK' or 'Cancel', etc.)." }, "general#modal-feedback-button": { - "message": "Send Feedback", + "message": "Оставить обратную связь", "description": "Text in a button that switches to the feedback tab and dismisses a modal dialog." }, "general#notice-modal-tech-preamble": { diff --git a/src/webui/_locales/tr/messages.json b/src/webui/_locales/tr/messages.json index 672f7e96c..5416d05bb 100644 --- a/src/webui/_locales/tr/messages.json +++ b/src/webui/_locales/tr/messages.json @@ -400,7 +400,7 @@ "description": "Help text describing why the user might need to modify the upstream proxy settings in order to successfully connect to the Psiphon server." }, "settings#upstream-proxy#proxy-reqs": { - "message": "Yalnız HTTPS destekleyen HTTP vekil sunucularına izin verilir.", + "message": "Yalnızca HTTPS destekleyen HTTP vekil sunucularına izin verilir.", "description": "Help text beside the upstream proxy settings indicating what type of upstream proxies can by used by Psiphon (different proxies are of different types and have different capabilities, and Psiphon has certain requirements)." }, "settings#upstream-proxy#hostname-label": { @@ -656,11 +656,11 @@ "description": "Body text of a modal dialog shown when a PsiCash transaction (like Speed Boost purchase) fails. The word 'PsiCash' must not be translated or transliterated." }, "psicash#transaction-InsufficientBalance-title": { - "message": "PsiCash bakiyesi yetersiz", + "message": "PsiCash birikiminiz yetersiz", "description": "Title of a modal dialog shown when a PsiCash transaction (like Speed Boost purchase) fails. The user doesn't have enough PsiCash to make the attempted purchase. The word 'PsiCash' must not be translated or transliterated." }, "psicash#transaction-InsufficientBalance-body": { - "message": "Bu satın alma için yeterli PsiCash bakiyeniz yok.", + "message": "Bu satın alma için yeterli PsiCash birikiminiz yok.", "description": "Body text of a modal dialog shown when a PsiCash transaction (like Speed Boost purchase) fails. The user doesn't have enough PsiCash to make the attempted purchase. The word 'PsiCash' must not be translated or transliterated." }, "psicash#transaction-TransactionAmountMismatch-title": { @@ -748,7 +748,7 @@ "description": "Text for a link displayed when the user does not have a PsiCash account. This is an strong suggestion, hence the exclamation point -- use whatever makes sense in your language. It encourages them to make one. The word 'PsiCash' must not be translated or transliterated." }, "psicash#pane-balance-header": { - "message": "Bakiye:", + "message": "Birikiminiz:", "description": "Text before showing the user's PsiCash balance. So it will appear like: 'Balance: Ⓟ100'." }, "psicash#pane-log-in": { @@ -764,7 +764,7 @@ "description": "Text for a link to a web site where users can manage their PsiCash account." }, "psicash#pane-create-account-plea": { - "message": "Bir PsiCash hesabı açarak PsiCash bakiyenizi koruyabilirsiniz. Uygulama deposunu temizlerseniz ya da Psiphon uygulamasını başka bir aygıtta kullanırsanız bakiyenizi görebilirsiniz. Ayrıntılı bilgi alın.", + "message": "Bir PsiCash hesabı açarak PsiCash birikiminizi koruyabilirsiniz. Uygulama deposunu temizlerseniz ya da Psiphon uygulamasını başka bir aygıtta kullanırsanız birikiminizi görebilirsiniz. Ayrıntılı bilgi alın.", "description": "Text shown below the PsiCash balance when the user does not have a PsiCash account. 'Learn more' is a link to an FAQ entry explaining the benefits of creating an account. The word 'PsiCash' must not be translated or transliterated." }, "psicash#pane-l2tp-incompatible": { @@ -816,7 +816,7 @@ "description": "Header of a modal dialog that appears when the user tries to buy PsiCash with real money but doens't have an account. The word 'PsiCash' must not be translated or transliterated." }, "psicash#modal-create-psicash-account-body": { - "message": "PsiCash satın almadan önce bir PsiCash hesabı oluşturmanızı önemle öneririz. Bir hesap açarak, bakiyenizi aygıtlarınız arasında paylalabilir ve satın aldığınız PsiCash bakiyenizi koruyabilirsiniz.", + "message": "PsiCash satın almadan önce bir PsiCash hesabı oluşturmanızı önemle öneririz. Bir hesap açarak, birikiminizi aygıtlarınız arasında paylalabilir ve satın aldığınız PsiCash birikiminizi koruyabilirsiniz.", "description": "Body of a modal dialog that appears when the user tries to buy PsiCash with real money but doens't have an account. The word 'PsiCash' must not be translated or transliterated." }, "psicash#modal-create-psicash-account-create-button": { @@ -904,11 +904,11 @@ "description": "Title of a modal dialog shown when the PsiCash library fails to initialize. The word 'PsiCash' must not be translated or transliterated." }, "psicash#init-error-body-unrecovered": { - "message": "PsiCash hazırlanamadı. Bu durum büyük olasılıkla disk alanının yetersiz olması gibi bir dosya sistemi sorunundan kaynaklanıyor. Bakiyeniz ve diğer durum bilgileriniz kayboldu. PsiCash kullanılamayacak. Sorunu çözmek için uygulamayı yeniden başlatmayı deneyebilirsiniz.", + "message": "PsiCash hazırlanamadı. Bu durum büyük olasılıkla disk alanının yetersiz olması gibi bir dosya sistemi sorunundan kaynaklanıyor. Birikiminiz ve diğer durum bilgileriniz kayboldu. PsiCash kullanılamayacak. Sorunu çözmek için uygulamayı yeniden başlatmayı deneyebilirsiniz.", "description": "Body text of a modal dialog shown when the PsiCash library fails to initialize. The word 'PsiCash' must not be translated or transliterated." }, "psicash#init-error-body-recovered": { - "message": "PsiCash hazırlanamadı. Bu durum büyük olasılıkla disk alanının yetersiz olması gibi bir dosya sistemi sorunundan kaynaklanıyor. Bakiyeniz ve diğer durum bilgileriniz sıfırlandı.", + "message": "PsiCash hazırlanamadı. Bu durum büyük olasılıkla disk alanının yetersiz olması gibi bir dosya sistemi sorunundan kaynaklanıyor. Birikiminiz ve diğer durum bilgileriniz sıfırlandı.", "description": "Body text of a modal dialog shown when the PsiCash library fails to initialize. The word 'PsiCash' must not be translated or transliterated." }, "psicash#psiphon-speed": { @@ -956,7 +956,7 @@ "description": "Title of modal dialog shown when the PsiCash account login attempt succeeds, if additional information needs to be conveyed. The word 'PsiCash' must not be translated or transliterated." }, "psicash#login#last-tracker-merge-body": { - "message": "PsiCash hesabınıza oturum açtınız. Bu aygıttaki bakiyeniz hesabınıza aktarıldı. Ancak bu bakiye aktarımı işlemi son kez yapıldı.", + "message": "PsiCash hesabınıza oturum açtınız. Bu aygıttaki birikiminiz hesabınıza aktarıldı. Ancak bu birikim aktarım işlemi son kez yapıldı.", "description": "Body text of a modal dialog shown when a PsiCash login succeeds. There is a fixed number of times that a user can merge a pre-account balance into a PsiCash account. This message indicates that the user has hit that limit and the merge that occurred is the last one allowed. The word 'PsiCash' must not be translated or transliterated." }, "psicash#alert#tokens-expired": { diff --git a/src/webui/_locales/uk/messages.json b/src/webui/_locales/uk/messages.json index 9eca770b0..343b84363 100644 --- a/src/webui/_locales/uk/messages.json +++ b/src/webui/_locales/uk/messages.json @@ -576,7 +576,7 @@ "description": "Text in the button that dismisses a modal dialog. It is often the only button (no 'OK' or 'Cancel', etc.)." }, "general#modal-feedback-button": { - "message": "Send Feedback", + "message": "Надіслати відгук", "description": "Text in a button that switches to the feedback tab and dismisses a modal dialog." }, "general#notice-modal-tech-preamble": { @@ -756,7 +756,7 @@ "description": "Text on a button that will lead the user to log into their PsiCash account. This is an strong suggestion, hence the exclamation point -- use whatever makes sense in your language." }, "psicash#pane-log-out": { - "message": "Log Out", + "message": "Вийти", "description": "Text on a button that will log the user out of their PsiCash account." }, "psicash#pane-manage-web": { @@ -804,7 +804,7 @@ "description": "Text encouraging the user to buy PsiCash (with real money). The word 'PsiCash' must not be translated or transliterated." }, "psicash#overlay-logging-in": { - "message": "Logging in…", + "message": "Вхід...", "description": "Text on a screen shown while the user's log-in request is in progress." }, "psicash#overlay-logging-out": { diff --git a/src/webui/_locales/zh-TW/messages.json b/src/webui/_locales/zh-TW/messages.json index 18c5655ea..da256384b 100644 --- a/src/webui/_locales/zh-TW/messages.json +++ b/src/webui/_locales/zh-TW/messages.json @@ -68,7 +68,7 @@ "description": "Text on the big connection button telling the user that if they click it, Psiphon will start trying to connect to the network. This is shown to the user while Psiphon is disconnected." }, "connection#egress-region-combo-label": { - "message": "Select server region", + "message": "選擇伺服器地區", "description": "The label for the 'Psiphon Server Region' combo box control located on the connection tab." }, "settings#error-alert": { diff --git a/src/webui/_locales/zh/messages.json b/src/webui/_locales/zh/messages.json index dc9dc6a9f..01f56ae43 100644 --- a/src/webui/_locales/zh/messages.json +++ b/src/webui/_locales/zh/messages.json @@ -856,7 +856,7 @@ "description": "Body of modal dialog in which the user can log in to an existing account or click a link to create a new one." }, "psicash#modal-login-username-field": { - "message": "PsiCash username", + "message": "PsiCash 用户名", "description": "Label for a username field in the PsiCash account login modal dialog." }, "psicash#modal-login-password-field": { diff --git a/src/webui/js/locales.js b/src/webui/js/locales.js index 7018ba3c7..c1106b142 100644 --- a/src/webui/js/locales.js +++ b/src/webui/js/locales.js @@ -3583,9 +3583,9 @@ "psicash#modal-psicash-logout-offline-cancel-button": "Annuler", "psicash#modal-login-header": "Compte PsiCash", "psicash#modal-login-body": " ou connectez-vous ci-dessous.", - "psicash#modal-login-username-field": "PsiCash username", + "psicash#modal-login-username-field": "Nom d’utilisateur PsiCash", "psicash#modal-login-password-field": "Mot de passe", - "psicash#modal-login-forgot-password-link": "Forgot your password or username?", + "psicash#modal-login-forgot-password-link": "Mot de passe ou nom d’utilisateur oublié?", "psicash#modal-login-submit-button": "Envoyer", "psicash#modal-login-cancel-button": "Annuler", "psicash#alert#buyspeedboost-purchase-complete": "La Survitesse est en fonction.", @@ -3612,8 +3612,8 @@ "psicash#alert#tokens-expired": "La connexion PsiCash est expirée. Veuillez vous reconnecter.", "psicash#alert#logged-out": "La déconnexion du compte PsiCash est terminée.", "notice#disallowed-traffic-alert-title": "Surclassez votre connexion Psiphon", - "notice#disallowed-traffic-alert-body": "

Les applis ne fonctionnent-elles pas ?

\n

Une partie du trafic Internet n’est pas prise en charge sans une Survitesse active. Activez la Survitesse avec du PsiCash afin de libérer le plein potentiel de votre expérience Psiphon.

", - "appbackend#disallowed-traffic-notification-title": "Les applis ne fonctionnent-elles pas ?", + "notice#disallowed-traffic-alert-body": "

Les applis ne fonctionnent-elles pas?

\n

Une partie du trafic Internet n’est pas prise en charge sans une Survitesse active. Activez la Survitesse avec du PsiCash afin de libérer le plein potentiel de votre expérience Psiphon.

", + "appbackend#disallowed-traffic-notification-title": "Les applis ne fonctionnent-elles pas?", "appbackend#disallowed-traffic-notification-body": "Activez la Survitesse afin de libérer le plein potentiel de votre expérience Psiphon.", "settings#disallowed-traffic-alert#heading": "Alerte de trafic refusé", "settings#disallowed-traffic-alert#help-text": "Certains types de trafic Internet ne sont pas pris en charge sans une Survitesse active. Quand un tel trafic est refusé, une alerte est affichée. (La réactivation demandera une reconnexion.)", @@ -7916,7 +7916,7 @@ "about#license-agree": "By using Psiphon you signal that you agree to the terms of the End-User License and Privacy Policy.", "about#client-version": "Версия клиента Psiphon для Windows:", "general#modal-close-button": "Закрыть", - "general#modal-feedback-button": "Send Feedback", + "general#modal-feedback-button": "Оставить обратную связь", "general#notice-modal-tech-preamble": "Техническая информация об ошибке:", "notice#systemproxysettings-setproxy-error-title": "Ошибка системного прокси", "notice#systemproxysettings-setproxy-error-body": "

Psiphon не удалось установить настройки системного прокси

. \n

Это могло произойти из-за конфликта с антивирусным программным обеспечением. Возможно, вам потребуется вручную настроить приложения или системный прокси для использования локальных прокси Psiphon.

", @@ -9426,7 +9426,7 @@ "settings#upstream-proxy#heading": "Giden vekil sunucu", "settings#upstream-proxy#by-default": "Bilgisayarınızda zaten yapılandırılmış bir vekil sunucu varsa, Psiphon bir tünel açarken varsayılan olarak bu vekil sunucuyu kullanır. Bu davranışı değiştirmek için kullanılacak bir vekil sunucu belirtebilirsiniz ya da \"giden vekil sunucu\" ayarını kaldırabilirsiniz.", "settings#upstream-proxy#reason": "Giden vekil sunucular bazen okullar, üniversiteler ya da kurumlar tarafından zorunlu olarak kullanılır. Ağ hizmeti sağlayıcınız giden vekil sunucu ayarları belirtmiş ise, bağlantı kurabilmeniz için bu ayarları el ile yazmanız gerekebilir.", - "settings#upstream-proxy#proxy-reqs": "Yalnız HTTPS destekleyen HTTP vekil sunucularına izin verilir.", + "settings#upstream-proxy#proxy-reqs": "Yalnızca HTTPS destekleyen HTTP vekil sunucularına izin verilir.", "settings#upstream-proxy#hostname-label": "Sunucu adı", "settings#upstream-proxy#port-label": "Kapı no", "settings#upstream-proxy#username-label": "Kullanıcı adı", @@ -9490,8 +9490,8 @@ "psicash#transaction-error-body": "PsiCash işleminiz beklenmedik şekilde tamamlanamadı.", "psicash#transaction-ExistingTransaction-title": "Zaten PsiCash satın alınmış", "psicash#transaction-ExistingTransaction-body": "Zaten bu türde bir PsiCash satın almışsınız. Öncekinin süresi dolmadan bu türde başka bir satın alma yapamazsınız. PsiCash durumunuz şimdi yenilenecek.", - "psicash#transaction-InsufficientBalance-title": "PsiCash bakiyesi yetersiz", - "psicash#transaction-InsufficientBalance-body": "Bu satın alma için yeterli PsiCash bakiyeniz yok.", + "psicash#transaction-InsufficientBalance-title": "PsiCash birikiminiz yetersiz", + "psicash#transaction-InsufficientBalance-body": "Bu satın alma için yeterli PsiCash birikiminiz yok.", "psicash#transaction-TransactionAmountMismatch-title": "PsiCash satın alma fiyatı farklı", "psicash#transaction-TransactionAmountMismatch-body": "PsiCash satın alma fiyatları değişmiş. PsiCash durumunuz şimdi yenilenecek.", "psicash#transaction-TransactionTypeNotFound-title": "PsiCash satın alma türü bulunamadı", @@ -9513,11 +9513,11 @@ "psicash#1-month": "1 ay", "psicash#ui-buyingboost-buttontext": "Speed Boost! başlatılıyor", "psicash#pane-create-account": "PsiCash birikiminizi korumak için bir hesap açın!", - "psicash#pane-balance-header": "Bakiye:", + "psicash#pane-balance-header": "Birikiminiz:", "psicash#pane-log-in": "Oturum açın!", "psicash#pane-log-out": "Oturumu kapat", "psicash#pane-manage-web": "Web üzerinden yönetin", - "psicash#pane-create-account-plea": "Bir PsiCash hesabı açarak PsiCash bakiyenizi koruyabilirsiniz. Uygulama deposunu temizlerseniz ya da Psiphon uygulamasını başka bir aygıtta kullanırsanız bakiyenizi görebilirsiniz. Ayrıntılı bilgi alın.", + "psicash#pane-create-account-plea": "Bir PsiCash hesabı açarak PsiCash birikiminizi koruyabilirsiniz. Uygulama deposunu temizlerseniz ya da Psiphon uygulamasını başka bir aygıtta kullanırsanız birikiminizi görebilirsiniz. Ayrıntılı bilgi alın.", "psicash#pane-l2tp-incompatible": "Psiphon L2TP/IPSec aktarım kipi ile bağlıyken Speed Boost kullanılamaz.", "psicash#pane-must-log-in": "PsiCash kullanmak için oturum açmalısınız.", "psicash#pane-create-new-account": "Yeni hesap ekle", @@ -9530,7 +9530,7 @@ "psicash#overlay-logging-in": "Oturum açılıyor…", "psicash#overlay-logging-out": "Oturum kapatılıyor…", "psicash#modal-create-psicash-account-header": "PsiCash hesabı açın", - "psicash#modal-create-psicash-account-body": "PsiCash satın almadan önce bir PsiCash hesabı oluşturmanızı önemle öneririz. Bir hesap açarak, bakiyenizi aygıtlarınız arasında paylalabilir ve satın aldığınız PsiCash bakiyenizi koruyabilirsiniz.", + "psicash#modal-create-psicash-account-body": "PsiCash satın almadan önce bir PsiCash hesabı oluşturmanızı önemle öneririz. Bir hesap açarak, birikiminizi aygıtlarınız arasında paylalabilir ve satın aldığınız PsiCash birikiminizi koruyabilirsiniz.", "psicash#modal-create-psicash-account-create-button": "Hesap ya da oturum açın", "psicash#modal-create-psicash-account-continue-button": "Hesap açmadan devam et", "psicash#modal-psicash-logout-offline-header": "PsiCash hesabı oturumunu kapat", @@ -9552,8 +9552,8 @@ "psicash#mustconnect-modal#title": "Psiphon bağlantısı gerekli", "psicash#mustconnect-modal#body": "PsiCash kullanabilmeniz için, Psiphon ağına bağlı olmalısınız.", "psicash#init-error-title": "PsiCash başlatma sorunu", - "psicash#init-error-body-unrecovered": "PsiCash hazırlanamadı. Bu durum büyük olasılıkla disk alanının yetersiz olması gibi bir dosya sistemi sorunundan kaynaklanıyor. Bakiyeniz ve diğer durum bilgileriniz kayboldu. PsiCash kullanılamayacak. Sorunu çözmek için uygulamayı yeniden başlatmayı deneyebilirsiniz.", - "psicash#init-error-body-recovered": "PsiCash hazırlanamadı. Bu durum büyük olasılıkla disk alanının yetersiz olması gibi bir dosya sistemi sorunundan kaynaklanıyor. Bakiyeniz ve diğer durum bilgileriniz sıfırlandı.", + "psicash#init-error-body-unrecovered": "PsiCash hazırlanamadı. Bu durum büyük olasılıkla disk alanının yetersiz olması gibi bir dosya sistemi sorunundan kaynaklanıyor. Birikiminiz ve diğer durum bilgileriniz kayboldu. PsiCash kullanılamayacak. Sorunu çözmek için uygulamayı yeniden başlatmayı deneyebilirsiniz.", + "psicash#init-error-body-recovered": "PsiCash hazırlanamadı. Bu durum büyük olasılıkla disk alanının yetersiz olması gibi bir dosya sistemi sorunundan kaynaklanıyor. Birikiminiz ve diğer durum bilgileriniz sıfırlandı.", "psicash#psiphon-speed": "Psiphon
hızı", "psicash#psiphon-speed-nobreak": "Psiphon hızı", "psicash#corner-logged-out": "PsiCash oturumunuzu kapattınız", @@ -9565,7 +9565,7 @@ "psicash#login#server-error-body": "Oturum açılmaya çalışılırken PsiCash sunucusundan bir hata yanıtı alındı. Lütfen bir süre sonra yeniden deneyin.", "psicash#login#badrequest-error-body": "PsiCash sunucusu oturum açma isteğinin geçersiz olduğunu bildirdi. Lütfen oturum açma bilgilerinizi yazarak yeniden deneyin.", "psicash#login#success-modal-title": "PsiCash oturumu açıldı", - "psicash#login#last-tracker-merge-body": "PsiCash hesabınıza oturum açtınız. Bu aygıttaki bakiyeniz hesabınıza aktarıldı. Ancak bu bakiye aktarımı işlemi son kez yapıldı.", + "psicash#login#last-tracker-merge-body": "PsiCash hesabınıza oturum açtınız. Bu aygıttaki birikiminiz hesabınıza aktarıldı. Ancak bu birikim aktarım işlemi son kez yapıldı.", "psicash#alert#tokens-expired": "PsiCash oturumu zaman aşımına uğramış. Lütfen yeniden oturum açın.", "psicash#alert#logged-out": "PsiCash hesabının oturumu kapatıldı.", "notice#disallowed-traffic-alert-title": "Psiphon bağlantınızı yükseltin", @@ -9729,7 +9729,7 @@ "about#license-agree": "By using Psiphon you signal that you agree to the terms of the End-User License and Privacy Policy.", "about#client-version": "Psiphon для клієнтської версії Windows:", "general#modal-close-button": "Закрити", - "general#modal-feedback-button": "Send Feedback", + "general#modal-feedback-button": "Надіслати відгук", "general#notice-modal-tech-preamble": "Ось технічна інформація про помилку:", "notice#systemproxysettings-setproxy-error-title": "Помилка системного проксі", "notice#systemproxysettings-setproxy-error-body": "

Psiphon не зміг встановити налаштування системного проксі.

\n

Це може бути пов'язано з конфліктом з антивірусним програмним забезпеченням. Можливо, вам доведеться вручну налаштувати параметри програми або системного проксі-сервера для використання локальних проксі-серверів Psiphon.

", @@ -9774,7 +9774,7 @@ "psicash#pane-create-account": "Create an account to protect your PsiCash!", "psicash#pane-balance-header": "Balance:", "psicash#pane-log-in": "Log In!", - "psicash#pane-log-out": "Log Out", + "psicash#pane-log-out": "Вийти", "psicash#pane-manage-web": "Manage on the web", "psicash#pane-create-account-plea": "Creating a PsiCash account will help preserve your PsiCash if you clear the app's storage or use Psiphon on another device. Learn more.", "psicash#pane-l2tp-incompatible": "Speed Boost cannot be used while Psiphon is connected in L2TP/IPSec transport mode.", @@ -9786,7 +9786,7 @@ "psicash#pane-need-more-psicash-header": "You need more PsiCash!", "psicash#pane-need-more-psicash-body": "In order activate Speed Boost, you need to buy more PsiCash.", "psicash#pane-need-psicash-visit-store": "Need PsiCash? Visit our store to buy more!", - "psicash#overlay-logging-in": "Logging in…", + "psicash#overlay-logging-in": "Вхід...", "psicash#overlay-logging-out": "Logging out…", "psicash#modal-create-psicash-account-header": "Create a PsiCash Account", "psicash#modal-create-psicash-account-body": "We strongly encourage you to create a PsiCash account before buying PsiCash. Having an account allows you to share your balance between devices and protect your purchases.", @@ -10835,7 +10835,7 @@ "psicash#modal-psicash-logout-offline-cancel-button": "取消", "psicash#modal-login-header": "PsiCash账户", "psicash#modal-login-body": "或者在下方登录", - "psicash#modal-login-username-field": "PsiCash username", + "psicash#modal-login-username-field": "PsiCash 用户名", "psicash#modal-login-password-field": "密码", "psicash#modal-login-forgot-password-link": "忘记用户名或者密码?", "psicash#modal-login-submit-button": "提交", @@ -10897,7 +10897,7 @@ "connection#wait-btn": "請稍候...", "connection#stopped-msg": "賽風連接已中斷", "connection#connect-btn": "連接", - "connection#egress-region-combo-label": "Select server region", + "connection#egress-region-combo-label": "選擇伺服器地區", "settings#error-alert": "出錯了!請在繼續前修正錯誤設定值。", "settings#reset-button": "重置為默認值", "settings#apply-button": "應用更改", diff --git a/src/webui/main-inline.html b/src/webui/main-inline.html index c418bf752..e5ad6224f 100644 --- a/src/webui/main-inline.html +++ b/src/webui/main-inline.html @@ -13459,12 +13459,12 @@ "object"!=typeof JSON&&(JSON={}),function(){"use strict";var rx_one=/^[\],:{}\s]*$/,rx_two=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,rx_three=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,rx_four=/(?:^|:|,)(?:\s*\[)+/g,rx_escapable=/[\\"\u0000-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,rx_dangerous=/[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,gap,indent,meta,rep;function f(t){return t<10?"0"+t:t}function this_value(){return this.valueOf()}function quote(t){return rx_escapable.lastIndex=0,rx_escapable.test(t)?'"'+t.replace(rx_escapable,function(t){var e=meta[t];return"string"==typeof e?e:"\\u"+("0000"+t.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+t+'"'}function str(t,e){var r,n,o,u,f,a=gap,i=e[t];switch(i&&"object"==typeof i&&"function"==typeof i.toJSON&&(i=i.toJSON(t)),"function"==typeof rep&&(i=rep.call(e,t,i)),typeof i){case"string":return quote(i);case"number":return isFinite(i)?String(i):"null";case"boolean":case"null":return String(i);case"object":if(!i)return"null";if(gap+=indent,f=[],"[object Array]"===Object.prototype.toString.apply(i)){for(u=i.length,r=0;r