blob: 308679df3740b46f7d6543977ec6335a8dcbeff5 [file] [log] [blame] [edit]
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "ProxyManager.h"
#include <algorithm>
#include <vector>
#include <wininet.h>
#include "json.h"
#include "logging.h"
#include "messages.h"
#include "HookProcessor.h"
#include "StringUtilities.h"
#define WD_PROXY_TYPE_DIRECT "direct"
#define WD_PROXY_TYPE_SYSTEM "system"
#define WD_PROXY_TYPE_MANUAL "manual"
#define WD_PROXY_TYPE_AUTOCONFIGURE "pac"
#define WD_PROXY_TYPE_AUTODETECT "autodetect"
#define HTTP_PROXY_MARKER "http="
#define HTTPS_PROXY_MARKER "https="
#define FTP_PROXY_MARKER "ftp="
#define SOCKS_PROXY_MARKER "socks="
#define BYPASS_PROXY_MARKER "|bypass="
namespace webdriver {
ProxyManager::ProxyManager(void) {
this->proxy_type_ = "";
this->http_proxy_ = "";
this->ftp_proxy_ = "";
this->ssl_proxy_ = "";
this->socks_proxy_ = "";
this->socks_user_name_ = "";
this->socks_password_ = "";
this->proxy_bypass_ = "";
this->proxy_autoconfigure_url_ = "";
this->is_proxy_modified_ = false;
this->is_proxy_authorization_modified_ = false;
this->use_per_process_proxy_ = false;
this->current_autoconfig_url_ = L"";
this->current_proxy_auto_detect_flags_ = 0;
this->current_proxy_server_ = L"";
this->current_socks_user_name_ = L"";
this->current_socks_password_ = L"";
this->current_proxy_type_ = 0;
this->current_proxy_bypass_list_ = L"";
}
ProxyManager::~ProxyManager(void) {
this->RestoreProxySettings();
}
void ProxyManager::Initialize(ProxySettings settings) {
LOG(TRACE) << "ProxyManager::Initialize";
// The wire protocol specifies lower case for the proxy type, but
// language bindings have been sending upper case forever. Handle
// both cases, thus we will normalize to a lower-case string for
// proxy type.
this->proxy_type_ = settings.proxy_type;
std::transform(this->proxy_type_.begin(),
this->proxy_type_.end(),
this->proxy_type_.begin(),
::tolower);
this->http_proxy_ = settings.http_proxy;
this->ftp_proxy_ = settings.ftp_proxy;
this->ssl_proxy_ = settings.ssl_proxy;
this->socks_proxy_ = settings.socks_proxy;
this->socks_user_name_ = settings.socks_user_name;
this->socks_password_ = settings.socks_password;
this->proxy_bypass_ = settings.proxy_bypass;
this->proxy_autoconfigure_url_ = settings.proxy_autoconfig_url;
if (this->proxy_type_ == WD_PROXY_TYPE_SYSTEM ||
this->proxy_type_ == WD_PROXY_TYPE_DIRECT ||
this->proxy_type_ == WD_PROXY_TYPE_MANUAL) {
this->use_per_process_proxy_ = settings.use_per_process_proxy;
} else {
// By definition, per-process proxy settings can only be used with the
// system proxy, direct connection, or with a manually specified proxy.
this->use_per_process_proxy_ = false;
}
}
void ProxyManager::SetProxySettings(HWND browser_window_handle) {
LOG(TRACE) << "ProxyManager::SetProxySettings";
if (this->proxy_type_.size() > 0 && this->proxy_type_ != WD_PROXY_TYPE_SYSTEM) {
if (this->use_per_process_proxy_) {
LOG(DEBUG) << "Setting proxy for individual IE instance.";
this->SetPerProcessProxySettings(browser_window_handle);
} else {
if (!this->is_proxy_modified_) {
LOG(DEBUG) << "Setting system proxy.";
this->GetCurrentProxySettings();
this->SetGlobalProxySettings();
} else {
LOG(DEBUG) << "Proxy settings already set by IE driver.";
}
}
this->is_proxy_modified_ = true;
} else {
LOG(DEBUG) << "Using existing system proxy settings.";
}
}
Json::Value ProxyManager::GetProxyAsJson() {
LOG(TRACE) << "ProxyManager::GetProxyAsJson";
Json::Value proxy_value;
proxy_value["proxyType"] = this->proxy_type_;
if (this->proxy_type_ == WD_PROXY_TYPE_MANUAL) {
if (this->http_proxy_.size() > 0) {
proxy_value["httpProxy"] = this->http_proxy_;
}
if (this->ftp_proxy_.size() > 0) {
proxy_value["ftpProxy"] = this->ftp_proxy_;
}
if (this->ssl_proxy_.size() > 0) {
proxy_value["sslProxy"] = this->ssl_proxy_;
}
if (this->socks_proxy_.size() > 0) {
proxy_value["socksProxy"] = this->socks_proxy_;
if (this->socks_user_name_.size() > 0) {
proxy_value["socksUsername"] = this->socks_user_name_;
}
if (this->socks_password_.size() > 0) {
proxy_value["socksPassword"] = this->socks_password_;
}
}
} else if (this->proxy_type_ == WD_PROXY_TYPE_AUTOCONFIGURE) {
proxy_value["proxyAutoconfigUrl"] = this->proxy_autoconfigure_url_;
}
return proxy_value;
}
std::wstring ProxyManager::BuildProxySettingsString() {
LOG(TRACE) << "ProxyManager::BuildProxySettingsString";
std::string proxy_string = "";
if (this->proxy_type_ == WD_PROXY_TYPE_MANUAL) {
if (this->http_proxy_.size() > 0) {
proxy_string.append(HTTP_PROXY_MARKER).append(this->http_proxy_);
}
if (this->ftp_proxy_.size() > 0) {
if (proxy_string.size() > 0) {
proxy_string.append(" ");
}
proxy_string.append(FTP_PROXY_MARKER).append(this->ftp_proxy_);
}
if (this->ssl_proxy_.size() > 0) {
if (proxy_string.size() > 0) {
proxy_string.append(" ");
}
proxy_string.append(HTTPS_PROXY_MARKER).append(this->ssl_proxy_);
}
if (this->socks_proxy_.size() > 0) {
if (proxy_string.size() > 0) {
proxy_string.append(" ");
}
proxy_string.append(SOCKS_PROXY_MARKER).append(this->socks_proxy_);
}
if (this->proxy_bypass_.size() > 0) {
proxy_string.append(BYPASS_PROXY_MARKER).append(this->proxy_bypass_);
}
} else if (this->proxy_type_ == WD_PROXY_TYPE_AUTOCONFIGURE) {
proxy_string = this->proxy_autoconfigure_url_;
} else {
proxy_string = this->proxy_type_;
}
LOG(DEBUG) << "Built proxy settings string: '" << proxy_string << "'";
return StringUtilities::ToWString(proxy_string);
}
void ProxyManager::RestoreProxySettings() {
LOG(TRACE) << "ProxyManager::RestoreProxySettings";
bool settings_restored = (!this->use_per_process_proxy_ && this->is_proxy_modified_) || this->is_proxy_authorization_modified_;
if (!this->use_per_process_proxy_ && this->is_proxy_modified_) {
INTERNET_PER_CONN_OPTION_LIST option_list;
std::vector<INTERNET_PER_CONN_OPTION> restore_options(5);
unsigned long list_size = sizeof(INTERNET_PER_CONN_OPTION_LIST);
std::vector<wchar_t> autoconfig_url_buffer;
StringUtilities::ToBuffer(this->current_autoconfig_url_, &autoconfig_url_buffer);
restore_options[0].dwOption = INTERNET_PER_CONN_AUTOCONFIG_URL;
restore_options[0].Value.pszValue = &autoconfig_url_buffer[0];
restore_options[1].dwOption = INTERNET_PER_CONN_AUTODISCOVERY_FLAGS;
restore_options[1].Value.dwValue = this->current_proxy_auto_detect_flags_;
restore_options[2].dwOption = INTERNET_PER_CONN_FLAGS;
restore_options[2].Value.dwValue = this->current_proxy_type_;
std::vector<wchar_t> proxy_bypass_buffer;
StringUtilities::ToBuffer(this->current_proxy_bypass_list_, &proxy_bypass_buffer);
restore_options[3].dwOption = INTERNET_PER_CONN_PROXY_BYPASS;
restore_options[3].Value.pszValue = &proxy_bypass_buffer[0];
std::vector<wchar_t> proxy_server_buffer;
StringUtilities::ToBuffer(this->current_proxy_server_, &proxy_server_buffer);
restore_options[4].dwOption = INTERNET_PER_CONN_PROXY_SERVER;
restore_options[4].Value.pszValue = &proxy_server_buffer[0];
option_list.dwSize = sizeof(INTERNET_PER_CONN_OPTION_LIST);
option_list.pszConnection = NULL;
option_list.dwOptionCount = static_cast<int>(restore_options.size());
option_list.dwOptionError = 0;
option_list.pOptions = &restore_options[0];
BOOL success = ::InternetSetOption(NULL,
INTERNET_OPTION_PER_CONNECTION_OPTION,
&option_list,
list_size);
if (!success) {
LOGERR(WARN) << "InternetSetOption failed setting INTERNET_OPTION_PER_CONNECTION_OPTION";
}
this->is_proxy_modified_ = false;
}
if (this->is_proxy_authorization_modified_) {
this->SetProxyAuthentication(this->current_socks_user_name_,
this->current_socks_password_);
this->is_proxy_authorization_modified_ = false;
}
if (settings_restored) {
BOOL notify_success = ::InternetSetOption(NULL,
INTERNET_OPTION_PROXY_SETTINGS_CHANGED,
NULL,
0);
if (!notify_success) {
LOGERR(WARN) << "InternetSetOption failed setting INTERNET_OPTION_PROXY_SETTINGS_CHANGED";
}
}
}
void ProxyManager::SetPerProcessProxySettings(HWND browser_window_handle) {
LOG(TRACE) << "ProxyManager::SetPerProcessProxySettings";
std::wstring proxy = this->BuildProxySettingsString();
HookSettings hook_settings;
hook_settings.window_handle = browser_window_handle;
hook_settings.hook_procedure_name = "SetProxyWndProc";
hook_settings.hook_procedure_type = WH_CALLWNDPROC;
hook_settings.communication_type = OneWay;
HookProcessor hook;
if (!hook.CanSetWindowsHook(browser_window_handle)) {
LOG(WARN) << "Proxy will not be set! There is a mismatch in the "
<< "bitness between the driver and browser. In particular, "
<< "be sure you are not attempting to use a 64-bit "
<< "IEDriverServer.exe against IE 10 or 11, even on 64-bit "
<< "Windows.";
}
hook.Initialize(hook_settings);
hook.PushData(proxy);
LRESULT result = ::SendMessage(browser_window_handle,
WD_CHANGE_PROXY,
NULL,
NULL);
if (this->socks_proxy_.size() > 0 &&
(this->socks_user_name_.size() > 0 || this->socks_password_.size() > 0)) {
LOG(WARN) << "Windows APIs provide no way to set proxy user name and "
<< "password on a per-process basis. Setting global setting.";
this->SetProxyAuthentication(StringUtilities::ToWString(this->socks_user_name_),
StringUtilities::ToWString(this->socks_password_));
this->is_proxy_authorization_modified_ = true;
// Notify WinINet clients that the proxy options have changed.
BOOL success = ::InternetSetOption(NULL,
INTERNET_OPTION_PROXY_SETTINGS_CHANGED,
NULL,
0);
if (!success) {
LOGERR(WARN) << "InternetSetOption failed setting INTERNET_OPTION_PROXY_SETTINGS_CHANGED";
}
}
}
void ProxyManager::SetGlobalProxySettings() {
LOG(TRACE) << "ProxyManager::SetGlobalProxySettings";
std::wstring proxy_settings_string = this->BuildProxySettingsString();
std::vector<std::wstring> proxy_settings;
StringUtilities::Split(proxy_settings_string,
_TEXT(BYPASS_PROXY_MARKER),
&proxy_settings);
std::wstring proxy = proxy_settings[0];
std::wstring proxy_bypass = L"";
if (proxy_settings.size() > 1) {
proxy_bypass = proxy_settings[1];
}
INTERNET_PER_CONN_OPTION_LIST option_list;
unsigned long list_size = sizeof(INTERNET_PER_CONN_OPTION_LIST);
option_list.dwSize = sizeof(INTERNET_PER_CONN_OPTION_LIST);
INTERNET_PER_CONN_OPTION proxy_options[3];
if (this->proxy_type_ == WD_PROXY_TYPE_DIRECT) {
proxy_options[0].dwOption = INTERNET_PER_CONN_FLAGS;
proxy_options[0].Value.dwValue = PROXY_TYPE_DIRECT;
option_list.dwOptionCount = 1;
} else if (this->proxy_type_ == WD_PROXY_TYPE_AUTOCONFIGURE) {
proxy_options[0].dwOption = INTERNET_PER_CONN_AUTOCONFIG_URL;
proxy_options[0].Value.pszValue = const_cast<wchar_t*>(proxy.c_str());
proxy_options[1].dwOption = INTERNET_PER_CONN_FLAGS;
proxy_options[1].Value.dwValue = PROXY_TYPE_AUTO_PROXY_URL;
option_list.dwOptionCount = 2;
} else if (this->proxy_type_ == WD_PROXY_TYPE_AUTODETECT) {
proxy_options[0].dwOption = INTERNET_PER_CONN_AUTODISCOVERY_FLAGS;
proxy_options[0].Value.dwValue = AUTO_PROXY_FLAG_ALWAYS_DETECT;
proxy_options[1].dwOption = INTERNET_PER_CONN_FLAGS;
proxy_options[1].Value.dwValue = PROXY_TYPE_AUTO_DETECT;
option_list.dwOptionCount = 2;
} else {
proxy_options[0].dwOption = INTERNET_PER_CONN_PROXY_SERVER;
proxy_options[0].Value.pszValue = const_cast<wchar_t*>(proxy.c_str());
proxy_options[1].dwOption = INTERNET_PER_CONN_FLAGS;
proxy_options[1].Value.dwValue = PROXY_TYPE_PROXY | PROXY_TYPE_DIRECT;
proxy_options[2].dwOption = INTERNET_PER_CONN_PROXY_BYPASS;
proxy_options[2].Value.pszValue = const_cast<wchar_t*>(proxy_bypass.c_str());;
option_list.dwOptionCount = 3;
}
option_list.pOptions = proxy_options;
option_list.pszConnection = NULL;
option_list.dwOptionError = 0;
BOOL success = ::InternetSetOption(NULL,
INTERNET_OPTION_PER_CONNECTION_OPTION,
&option_list,
list_size);
if (!success) {
LOGERR(WARN) << "InternetSetOption failed setting INTERNET_OPTION_PER_CONNECTION_OPTION";
}
// Only set proxy authentication for SOCKS proxies, and
// where the user name and password have been specified.
if (this->socks_proxy_.size() > 0 &&
(this->socks_user_name_.size() > 0 || this->socks_password_.size() > 0)) {
this->SetProxyAuthentication(StringUtilities::ToWString(this->socks_user_name_),
StringUtilities::ToWString(this->socks_password_));
this->is_proxy_authorization_modified_ = true;
}
success = ::InternetSetOption(NULL,
INTERNET_OPTION_PROXY_SETTINGS_CHANGED,
NULL,
0);
if (!success) {
LOGERR(WARN) << "InternetSetOption failed setting INTERNET_OPTION_PROXY_SETTINGS_CHANGED";
}
}
void ProxyManager::SetProxyAuthentication(const std::wstring& user_name,
const std::wstring& password) {
LOG(TRACE) << "ProxyManager::SetProxyAuthentication";
BOOL success = ::InternetSetOption(NULL,
INTERNET_OPTION_PROXY_USERNAME,
const_cast<wchar_t*>(user_name.c_str()),
static_cast<int>(user_name.size()) + 1);
if (!success) {
LOGERR(WARN) << "InternetSetOption failed setting INTERNET_OPTION_PROXY_USERNAME";
}
success = ::InternetSetOption(NULL,
INTERNET_OPTION_PROXY_PASSWORD,
const_cast<wchar_t*>(password.c_str()),
static_cast<int>(password.size()) + 1);
if (!success) {
LOGERR(WARN) << "InternetSetOption failed setting INTERNET_OPTION_PROXY_PASSWORD";
}
}
void ProxyManager::GetCurrentProxySettings() {
LOG(TRACE) << "ProxyManager::GetCurrentProxySettings";
this->GetCurrentProxyType();
INTERNET_PER_CONN_OPTION_LIST option_list;
std::vector<INTERNET_PER_CONN_OPTION> options_to_get(4);
unsigned long list_size = sizeof(INTERNET_PER_CONN_OPTION_LIST);
options_to_get[0].dwOption = INTERNET_PER_CONN_AUTOCONFIG_URL;
options_to_get[1].dwOption = INTERNET_PER_CONN_AUTODISCOVERY_FLAGS;
options_to_get[2].dwOption = INTERNET_PER_CONN_PROXY_BYPASS;
options_to_get[3].dwOption = INTERNET_PER_CONN_PROXY_SERVER;
option_list.dwSize = sizeof(INTERNET_PER_CONN_OPTION_LIST);
option_list.pszConnection = NULL;
option_list.dwOptionCount = static_cast<int>(options_to_get.size());
option_list.dwOptionError = 0;
option_list.pOptions = &options_to_get[0];
BOOL success = ::InternetQueryOption(NULL,
INTERNET_OPTION_PER_CONNECTION_OPTION,
&option_list,
&list_size);
if (!success) {
LOGERR(WARN) << "InternetQueryOption failed getting proxy settings";
}
if(options_to_get[0].Value.pszValue != NULL) {
this->current_autoconfig_url_ = options_to_get[0].Value.pszValue;
::GlobalFree(options_to_get[0].Value.pszValue);
}
this->current_proxy_auto_detect_flags_ = options_to_get[1].Value.dwValue;
if(options_to_get[2].Value.pszValue != NULL) {
this->current_proxy_bypass_list_ = options_to_get[2].Value.pszValue;
::GlobalFree(options_to_get[2].Value.pszValue);
}
if(options_to_get[3].Value.pszValue != NULL) {
this->current_proxy_server_ = options_to_get[3].Value.pszValue;
::GlobalFree(options_to_get[3].Value.pszValue);
}
this->GetCurrentProxyAuthentication();
}
void ProxyManager::GetCurrentProxyAuthentication() {
LOG(TRACE) << "ProxyManager::GetCurrentProxyAuthentication";
DWORD user_name_length = 0;
BOOL success = ::InternetQueryOption(NULL,
INTERNET_OPTION_PROXY_USERNAME,
NULL,
&user_name_length);
if (user_name_length > 0) {
std::vector<wchar_t> user_name(user_name_length);
success = ::InternetQueryOption(NULL,
INTERNET_OPTION_PROXY_USERNAME,
&user_name[0],
&user_name_length);
this->current_socks_user_name_ = &user_name[0];
}
DWORD password_length = 0;
success = ::InternetQueryOption(NULL,
INTERNET_OPTION_PROXY_PASSWORD,
NULL,
&password_length);
if (password_length > 0) {
std::vector<wchar_t> password(password_length);
success = ::InternetQueryOption(NULL,
INTERNET_OPTION_PROXY_PASSWORD,
&password[0],
&password_length);
this->current_socks_password_ = &password[0];
}
}
void ProxyManager::GetCurrentProxyType() {
LOG(TRACE) << "ProxyManager::GetCurrentProxyType";
INTERNET_PER_CONN_OPTION_LIST option_list;
std::vector<INTERNET_PER_CONN_OPTION> proxy_type_options(1);
unsigned long list_size = sizeof(INTERNET_PER_CONN_OPTION_LIST);
proxy_type_options[0].dwOption = INTERNET_PER_CONN_FLAGS_UI;
option_list.dwSize = sizeof(INTERNET_PER_CONN_OPTION_LIST);
option_list.pszConnection = NULL;
option_list.dwOptionCount = static_cast<int>(proxy_type_options.size());
option_list.dwOptionError = 0;
option_list.pOptions = &proxy_type_options[0];
// First check for INTERNET_PER_CONN_FLAGS_UI, then if that fails
// check again using INTERNET_PER_CONN_FLAGS. This is documented at
// http://msdn.microsoft.com/en-us/library/windows/desktop/aa385145%28v=vs.85%29.aspx
BOOL success = ::InternetQueryOption(NULL,
INTERNET_OPTION_PER_CONNECTION_OPTION,
&option_list,
&list_size);
if (success) {
this->current_proxy_type_ = proxy_type_options[0].Value.dwValue;
return;
}
proxy_type_options[0].dwOption = INTERNET_PER_CONN_FLAGS;
success = ::InternetQueryOption(NULL,
INTERNET_OPTION_PER_CONNECTION_OPTION,
&option_list,
&list_size);
if (success) {
this->current_proxy_type_ = proxy_type_options[0].Value.dwValue;
}
}
} // namespace webdriver
#ifdef __cplusplus
extern "C" {
#endif
LRESULT CALLBACK SetProxyWndProc(int nCode, WPARAM wParam, LPARAM lParam) {
CWPSTRUCT* call_window_proc_struct = reinterpret_cast<CWPSTRUCT*>(lParam);
if (WM_COPYDATA == call_window_proc_struct->message) {
COPYDATASTRUCT* data = reinterpret_cast<COPYDATASTRUCT*>(call_window_proc_struct->lParam);
webdriver::HookProcessor::CopyDataToBuffer(data->cbData, data->lpData);
} else if (WD_CHANGE_PROXY == call_window_proc_struct->message) {
// Allocate a buffer of the length of the data in the shared memory buffer,
// plus one extra wide char, to account for the null terminator.
int multibyte_buffer_size = webdriver::HookProcessor::GetDataBufferSize() + sizeof(wchar_t);
std::vector<char> multibyte_buffer(multibyte_buffer_size);
std::vector<char> multibyte_buffer_bypass(multibyte_buffer_size);
std::wstring proxy_settings = webdriver::HookProcessor::CopyWStringFromBuffer();
std::wstring proxy = proxy_settings;
std::wstring proxy_bypass = L"";
// Use the _TEXT macro to convert to a wstring literal
std::wstring bypass_marker = _TEXT(BYPASS_PROXY_MARKER);
size_t bypass_marker_index = proxy_settings.find(bypass_marker);
if (bypass_marker_index != std::wstring::npos) {
proxy = proxy_settings.substr(0, bypass_marker_index);
proxy_bypass = proxy_settings.substr(bypass_marker_index + bypass_marker.size());
}
INTERNET_PROXY_INFO proxy_info;
if (proxy == L"direct") {
proxy_info.dwAccessType = INTERNET_OPEN_TYPE_DIRECT;
proxy_info.lpszProxy = L"";
} else {
// UrlMkSetSessionOption only appears to work on either ASCII or
// multi-byte strings, not Unicode strings. Since the INTERNET_PROXY_INFO
// struct hard-codes to LPCTSTR, and that translates into LPCWSTR for the
// compiler settings we use, we must use the multi-byte version here.
// Note that for the count of input characters, we can use -1, since
// we've forced the string to be null-terminated.
::WideCharToMultiByte(CP_UTF8,
0,
proxy.c_str(),
-1,
&multibyte_buffer[0],
static_cast<int>(multibyte_buffer.size()),
NULL,
NULL);
proxy_info.dwAccessType = INTERNET_OPEN_TYPE_PROXY;
proxy_info.lpszProxy = reinterpret_cast<LPCTSTR>(&multibyte_buffer[0]);
}
::WideCharToMultiByte(CP_UTF8,
0,
proxy_bypass.c_str(),
-1,
&multibyte_buffer_bypass[0],
static_cast<int>(multibyte_buffer_bypass.size()),
NULL,
NULL);
proxy_info.lpszProxyBypass = reinterpret_cast<LPCTSTR>(&multibyte_buffer_bypass[0]);
DWORD proxy_info_size = sizeof(proxy_info);
HRESULT hr = ::UrlMkSetSessionOption(INTERNET_OPTION_PROXY,
reinterpret_cast<void*>(&proxy_info),
proxy_info_size,
0);
}
return ::CallNextHookEx(NULL, nCode, wParam, lParam);
}
#ifdef __cplusplus
}
#endif