diff --git a/CMakeLists.txt b/CMakeLists.txt index f2e2a7fa99..4acc509a05 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -46,7 +46,7 @@ option(USE_DISCORD_PRESENCE "Enables Discord Rich Presence, show the current gam option(ENABLE_ANALYTICS "Enables opt-in Analytics collection" OFF) # Enable Playback build for Slippi. TODO: turn off for normal builds -option(SLIPPI_PLAYBACK "Enable Playback changes" ON) +option(SLIPPI_PLAYBACK "Enable Playback changes" OFF) option(ENCODE_FRAMEDUMPS "Encode framedumps in AVI format" ON) diff --git a/Data/install_smashenabler.sh b/Data/install_smashenabler.sh new file mode 100644 index 0000000000..fbfdcc9071 --- /dev/null +++ b/Data/install_smashenabler.sh @@ -0,0 +1,257 @@ +#!/bin/sh +# shellcheck shell=dash + +# This is just a little script that can be downloaded from the internet to +# install SmashEnabler. +# +# It runs on Unix shells like {a,ba,da,k,z}sh. It uses the common `local` +# extension. Note: Most shells limit `local` to 1 var per line, contra bash. +# +# Borrow heavily and liberally from rustup's install script. ;P + +set -u + +usage() { + cat 1>&2 <&2 + exit 1 +} + +need_cmd() { + if ! check_cmd "$1"; then + err "need '$1' (command not found)" + fi +} + +check_cmd() { + command -v "$1" > /dev/null 2>&1 +} + +assert_nz() { + if [ -z "$1" ]; then err "assert_nz $2"; fi +} + +# Run a command that should never fail. If the command fails execution +# will immediately terminate with an error showing the failing +# command. +ensure() { + if ! "$@"; then err "command failed: $*"; fi +} + +# This is just for indicating that commands' results are being +# intentionally ignored. Usually, because it's being executed +# as part of error handling. +ignore() { + "$@" +} + +# This wraps curl or wget. Try curl first, if not installed, +# use wget instead. +downloader() { + local _dld + local _ciphersuites + if check_cmd curl; then + _dld=curl + elif check_cmd wget; then + _dld=wget + else + _dld='curl or wget' # to be used in error message of need_cmd + fi + + if [ "$1" = --check ]; then + need_cmd "$_dld" + elif [ "$_dld" = curl ]; then + get_ciphersuites_for_curl + _ciphersuites="$RETVAL" + if [ -n "$_ciphersuites" ]; then + curl --proto '=https' --tlsv1.2 --ciphers "$_ciphersuites" --silent --show-error --fail --location "$1" --output "$2" + else + echo "Warning: Not enforcing strong cipher suites for TLS, this is potentially less secure" + if ! check_help_for "$3" curl --proto --tlsv1.2; then + echo "Warning: Not enforcing TLS v1.2, this is potentially less secure" + curl --silent --show-error --fail --location "$1" --output "$2" + else + curl --proto '=https' --tlsv1.2 --silent --show-error --fail --location "$1" --output "$2" + fi + fi + elif [ "$_dld" = wget ]; then + get_ciphersuites_for_wget + _ciphersuites="$RETVAL" + if [ -n "$_ciphersuites" ]; then + wget --https-only --secure-protocol=TLSv1_2 --ciphers "$_ciphersuites" "$1" -O "$2" + else + echo "Warning: Not enforcing strong cipher suites for TLS, this is potentially less secure" + if ! check_help_for "$3" wget --https-only --secure-protocol; then + echo "Warning: Not enforcing TLS v1.2, this is potentially less secure" + wget "$1" -O "$2" + else + wget --https-only --secure-protocol=TLSv1_2 "$1" -O "$2" + fi + fi + else + err "Unknown downloader" # should not reach here + fi +} + +check_help_for() { + local _arch + local _cmd + local _arg + _arch="$1" + shift + _cmd="$1" + shift + + case "$_arch" in + # If we're running on OS-X, older than 10.13, then we always + # fail to find these options to force fallback + *darwin*) + if check_cmd sw_vers; then + if [ "$(sw_vers -productVersion | cut -d. -f2)" -lt 13 ]; then + # Older than 10.13 + echo "Warning: Detected OS X platform older than 10.13" + return 1 + fi + fi + ;; + + esac + + for _arg in "$@"; do + if ! "$_cmd" --help | grep -q -- "$_arg"; then + return 1 + fi + done + + true # not strictly needed +} + +# Return cipher suite string specified by user, otherwise return strong TLS 1.2-1.3 cipher suites +# if support by local tools is detected. Detection currently supports these curl backends: +# GnuTLS and OpenSSL (possibly also LibreSSL and BoringSSL). Return value can be empty. +get_ciphersuites_for_curl() { + if [ -n "${RUSTUP_TLS_CIPHERSUITES-}" ]; then + # user specified custom cipher suites, assume they know what they're doing + RETVAL="$RUSTUP_TLS_CIPHERSUITES" + return + fi + + local _openssl_syntax="no" + local _gnutls_syntax="no" + local _backend_supported="yes" + if curl -V | grep -q ' OpenSSL/'; then + _openssl_syntax="yes" + elif curl -V | grep -iq ' LibreSSL/'; then + _openssl_syntax="yes" + elif curl -V | grep -iq ' BoringSSL/'; then + _openssl_syntax="yes" + elif curl -V | grep -iq ' GnuTLS/'; then + _gnutls_syntax="yes" + else + _backend_supported="no" + fi + + local _args_supported="no" + if [ "$_backend_supported" = "yes" ]; then + # "unspecified" is for arch, allows for possibility old OS using macports, homebrew, etc. + if check_help_for "notspecified" "curl" "--tlsv1.2" "--ciphers" "--proto"; then + _args_supported="yes" + fi + fi + + local _cs="" + if [ "$_args_supported" = "yes" ]; then + if [ "$_openssl_syntax" = "yes" ]; then + _cs=$(get_strong_ciphersuites_for "openssl") + elif [ "$_gnutls_syntax" = "yes" ]; then + _cs=$(get_strong_ciphersuites_for "gnutls") + fi + fi + + RETVAL="$_cs" +} + +# Return cipher suite string specified by user, otherwise return strong TLS 1.2-1.3 cipher suites +# if support by local tools is detected. Detection currently supports these wget backends: +# GnuTLS and OpenSSL (possibly also LibreSSL and BoringSSL). Return value can be empty. +get_ciphersuites_for_wget() { + if [ -n "${RUSTUP_TLS_CIPHERSUITES-}" ]; then + # user specified custom cipher suites, assume they know what they're doing + RETVAL="$RUSTUP_TLS_CIPHERSUITES" + return + fi + + local _cs="" + if wget -V | grep -q '\-DHAVE_LIBSSL'; then + # "unspecified" is for arch, allows for possibility old OS using macports, homebrew, etc. + if check_help_for "notspecified" "wget" "TLSv1_2" "--ciphers" "--https-only" "--secure-protocol"; then + _cs=$(get_strong_ciphersuites_for "openssl") + fi + elif wget -V | grep -q '\-DHAVE_LIBGNUTLS'; then + # "unspecified" is for arch, allows for possibility old OS using macports, homebrew, etc. + if check_help_for "notspecified" "wget" "TLSv1_2" "--ciphers" "--https-only" "--secure-protocol"; then + _cs=$(get_strong_ciphersuites_for "gnutls") + fi + fi + + RETVAL="$_cs" +} + +# Return strong TLS 1.2-1.3 cipher suites in OpenSSL or GnuTLS syntax. TLS 1.2 +# excludes non-ECDHE and non-AEAD cipher suites. DHE is excluded due to bad +# DH params often found on servers (see RFC 7919). Sequence matches or is +# similar to Firefox 68 ESR with weak cipher suites disabled via about:config. +# $1 must be openssl or gnutls. +get_strong_ciphersuites_for() { + if [ "$1" = "openssl" ]; then + # OpenSSL is forgiving of unknown values, no problems with TLS 1.3 values on versions that don't support it yet. + echo "TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_256_GCM_SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384" + elif [ "$1" = "gnutls" ]; then + # GnuTLS isn't forgiving of unknown values, so this may require a GnuTLS version that supports TLS 1.3 even if wget doesn't. + # Begin with SECURE128 (and higher) then remove/add to build cipher suites. Produces same 9 cipher suites as OpenSSL but in slightly different order. + echo "SECURE128:-VERS-SSL3.0:-VERS-TLS1.0:-VERS-TLS1.1:-VERS-DTLS-ALL:-CIPHER-ALL:-MAC-ALL:-KX-ALL:+AEAD:+ECDHE-ECDSA:+ECDHE-RSA:+AES-128-GCM:+CHACHA20-POLY1305:+AES-256-GCM" + fi +} + +main "$@" || exit 1 \ No newline at end of file diff --git a/Source/Core/InputCommon/CMakeLists.txt b/Source/Core/InputCommon/CMakeLists.txt index d5efe58248..70da1ea48e 100644 --- a/Source/Core/InputCommon/CMakeLists.txt +++ b/Source/Core/InputCommon/CMakeLists.txt @@ -157,7 +157,7 @@ if(LIBEVDEV_FOUND AND LIBUDEV_FOUND) ) endif() -if(UNIX) +if(UNIX OR WIN32) target_sources(inputcommon PRIVATE ControllerInterface/Pipes/Pipes.cpp ControllerInterface/Pipes/Pipes.h diff --git a/Source/Core/InputCommon/ControllerInterface/Pipes/Pipes.cpp b/Source/Core/InputCommon/ControllerInterface/Pipes/Pipes.cpp index 6b44466eb6..47d1d6aa97 100644 --- a/Source/Core/InputCommon/ControllerInterface/Pipes/Pipes.cpp +++ b/Source/Core/InputCommon/ControllerInterface/Pipes/Pipes.cpp @@ -12,7 +12,6 @@ #include #include #include -#include #include #include "Common/FileUtil.h" @@ -41,6 +40,31 @@ static double StringToDouble(const std::string& text) void PopulateDevices() { +#ifdef _WIN32 + PIPE_FD pipes[4]; + // Windows has named pipes, but they're different. They don't exist on the + // local filesystem and are transient. So rather than searching the /Pipes + // directory for pipes, we just always assume there's 4 and then make them + for (uint32_t i = 0; i < 4; i++) + { + std::string pipename = "\\\\.\\pipe\\slippibot" + std::to_string(i + 1); + pipes[i] = CreateNamedPipeA( + pipename.data(), // pipe name + PIPE_ACCESS_INBOUND, // read access, inward only + PIPE_TYPE_BYTE | PIPE_NOWAIT, // byte mode, nonblocking + 1, // number of clients + 256, // output buffer size + 256, // input buffer size + 0, // timeout value + NULL // security attributes + ); + + // We're in nonblocking mode, so this won't wait for clients + ConnectNamedPipe(pipes[i], NULL); + std::string ui_pipe_name = "slippibot" + std::to_string(i + 1); + g_controller_interface.AddDevice(std::make_shared(pipes[i], ui_pipe_name)); + } +#else // Search the Pipes directory for files that we can open in read-only, // non-blocking mode. The device name is the virtual name of the file. File::FSTEntry fst; @@ -55,14 +79,15 @@ void PopulateDevices() const File::FSTEntry& child = fst.children[i]; if (child.isDirectory) continue; - int fd = open(child.physicalName.c_str(), O_RDONLY | O_NONBLOCK); + PIPE_FD fd = open(child.physicalName.c_str(), O_RDONLY | O_NONBLOCK); if (fd < 0) continue; g_controller_interface.AddDevice(std::make_shared(fd, child.virtualName)); } +#endif } -PipeDevice::PipeDevice(int fd, const std::string& name) : m_fd(fd), m_name(name) +PipeDevice::PipeDevice(PIPE_FD fd, const std::string& name) : m_fd(fd), m_name(name) { for (const auto& tok : s_button_tokens) { @@ -83,7 +108,52 @@ PipeDevice::PipeDevice(int fd, const std::string& name) : m_fd(fd), m_name(name) PipeDevice::~PipeDevice() { +#ifdef _WIN32 + CloseHandle(m_fd); +#else close(m_fd); +#endif +} + +s32 PipeDevice::readFromPipe(PIPE_FD file_descriptor, char* in_buffer, size_t size) +{ +#ifdef _WIN32 + + u32 bytes_available = 0; + DWORD bytesread = 0; + bool peek_success = PeekNamedPipe( + file_descriptor, + NULL, + 0, + NULL, + (LPDWORD)&bytes_available, + NULL + ); + + if (!peek_success && (GetLastError() == ERROR_BROKEN_PIPE)) + { + DisconnectNamedPipe(file_descriptor); + ConnectNamedPipe(file_descriptor, NULL); + return -1; + } + + if (peek_success && (bytes_available > 0)) + { + bool success = ReadFile( + file_descriptor, // pipe handle + in_buffer, // buffer to receive reply + (DWORD)std::min(bytes_available, (u32)size), // size of buffer + &bytesread, // number of bytes read + NULL); // not overlapped + if (!success) + { + return -1; + } + } + return (s32)bytesread; +#else + return read(file_descriptor, in_buffer, size); +#endif } void PipeDevice::UpdateInput() @@ -91,11 +161,11 @@ void PipeDevice::UpdateInput() // Read any pending characters off the pipe. If we hit a newline, // then dequeue a command off the front of m_buf and parse it. char buf[32]; - ssize_t bytes_read = read(m_fd, buf, sizeof buf); + s32 bytes_read = readFromPipe(m_fd, buf, sizeof buf); while (bytes_read > 0) { m_buf.append(buf, bytes_read); - bytes_read = read(m_fd, buf, sizeof buf); + bytes_read = readFromPipe(m_fd, buf, sizeof buf); } std::size_t newline = m_buf.find("\n"); while (newline != std::string::npos) diff --git a/Source/Core/InputCommon/ControllerInterface/Pipes/Pipes.h b/Source/Core/InputCommon/ControllerInterface/Pipes/Pipes.h index 3d316f64e4..245eaba51c 100644 --- a/Source/Core/InputCommon/ControllerInterface/Pipes/Pipes.h +++ b/Source/Core/InputCommon/ControllerInterface/Pipes/Pipes.h @@ -7,9 +7,19 @@ #include #include #include +#ifdef _WIN32 +#include +#else +#include +#endif namespace ciface::Pipes { +#ifdef _WIN32 + typedef HANDLE PIPE_FD; +#else + typedef int PIPE_FD; +#endif // To create a piped controller input, create a named pipe in the // Pipes directory and write commands out to it. Commands are separated // by a newline character, with spaces separating command tokens. @@ -25,7 +35,7 @@ void PopulateDevices(); class PipeDevice : public Core::Device { public: - PipeDevice(int fd, const std::string& name); + PipeDevice(PIPE_FD fd, const std::string& name); ~PipeDevice(); void UpdateInput() override; @@ -49,8 +59,9 @@ private: void AddAxis(const std::string& name, double value); void ParseCommand(const std::string& command); void SetAxis(const std::string& entry, double value); + s32 readFromPipe(PIPE_FD file_descriptor, char* in_buffer, size_t size); - const int m_fd; + const PIPE_FD m_fd; const std::string m_name; std::string m_buf; std::map m_buttons;