├── .github
├── ISSUE_TEMPLATE
│ ├── bug-report.md
│ ├── change-an-existing-feature.md
│ ├── documentation-request.md
│ ├── question.md
│ ├── request-a-new-feature.md
│ └── support-request.md
└── workflows
│ └── MakeRelease.yaml
├── .gitignore
├── .gitmodules
├── ARRCON
├── ARRCON.cpp
├── ARRCON.ico
├── CMakeLists.txt
├── ExceptionBuilder.hpp
├── config.hpp
├── helpers
│ ├── FileLocator.hpp
│ ├── bukkit-colors.h
│ └── print_input_prompt.h
├── logging.hpp
└── net
│ ├── rcon.hpp
│ └── target_info.hpp
├── CMakeLists.txt
├── CMakePresets.json
├── LICENSE
├── README.md
└── SECURITY.md
/.github/ISSUE_TEMPLATE/bug-report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug Report
3 | about: Issue template for reporting bugs with the ARRCON project.
4 | title: "[BUG] …"
5 | labels: bug
6 | assignees: radj307
7 |
8 | ---
9 |
10 |
14 |
15 | ### \# System Information
16 |
25 | - OS:
26 | - Version:
27 | - Shell (Windows-Only):
28 |
29 | ### \# Bug Description
30 |
36 |
37 | ### \# Reproduction Steps
38 |
44 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/change-an-existing-feature.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Change an Existing Feature
3 | about: Suggest changes to an existing ARRCON feature.
4 | title: "[CHANGE] "
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 |
13 | ### \# Existing Feature Request
14 | - Is this feature related to an existing bug report?
15 |
24 |
25 | - What does this feature currently do?
26 |
29 |
30 | - What should this feature do?
31 |
34 |
35 | - Additional Information
36 |
39 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/documentation-request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Documentation Request
3 | about: Request changes or additions to documentation.
4 | title: "[DOC] "
5 | labels: documentation
6 | assignees: ''
7 |
8 | ---
9 |
10 | ### \# Documentation Request
11 | - What is the documentation for?
12 |
15 |
16 | - Is there existing documentation?
17 |
22 |
23 | - What changes would you want to see?
24 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/question.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Question
3 | about: Ask a question about anything related to the ARRCON project.
4 | title: "[QUESTION] "
5 | labels: question
6 | assignees: radj307
7 |
8 | ---
9 |
10 | ### \# Question
11 |
17 |
18 |
19 |
20 |
24 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/request-a-new-feature.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Request a New Feature
3 | about: Suggest new features to add to ARRCON.
4 | title: "[NEW]"
5 | labels: enhancement, new feature request
6 | assignees: ''
7 |
8 | ---
9 |
10 |
13 | ### \# New Feature Request
14 | - Is this feature related to an existing bug report?
15 |
24 |
25 | - What does this feature do?
26 |
29 |
30 | - Are there alternative solutions that already exist?
31 |
35 |
36 | - Description
37 |
41 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/support-request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Support Request
3 | about: Report an unsupported game or application.
4 | title: 'Unsupported Title:
'
5 | labels: bug, enhancement, support
6 | assignees: radj307
7 |
8 | ---
9 |
10 | # Support Request
11 |
12 | ## Which Game/Application are you requesting support for?
13 |
14 |
15 | ## What is unique to this title that prevents ARRCON from working with it?
16 |
17 |
--------------------------------------------------------------------------------
/.github/workflows/MakeRelease.yaml:
--------------------------------------------------------------------------------
1 | name: Make Release
2 |
3 | on:
4 | push:
5 | tags: [ '[0-9]+.[0-9]+.[0-9]+-?**' ]
6 |
7 | jobs:
8 | build-windows:
9 | runs-on: windows-latest
10 |
11 | steps:
12 | - uses: actions/checkout@v4
13 | with:
14 | submodules: recursive
15 | fetch-depth: 0
16 |
17 | - name: Install Requirements
18 | run: choco install ninja
19 |
20 | - uses: ilammy/msvc-dev-cmd@v1
21 |
22 | - name: CMake Configure
23 | run: cmake -B build -DCMAKE_BUILD_TYPE=Release -G Ninja
24 |
25 | - name: CMake Build
26 | run: cmake --build build --config Release
27 |
28 | - name: Create Archive
29 | run: |
30 | cd build/ARRCON
31 | Compress-Archive ARRCON.exe ARRCON-$(.\ARRCON -vq)-Windows.zip
32 | mv *.zip ../..
33 | shell: pwsh
34 |
35 | - name: Upload Artifact
36 | uses: actions/upload-artifact@v4
37 | with:
38 | name: build-windows
39 | path: 'ARRCON*.zip'
40 |
41 |
42 | build-linux:
43 | runs-on: ubuntu-latest
44 |
45 | steps:
46 | - uses: actions/checkout@v4
47 | with:
48 | submodules: recursive
49 | fetch-depth: 0
50 |
51 | - name: Install Requirements
52 | run: sudo apt-get install -y gcc-10 cmake ninja-build
53 |
54 | - name: CMake Configure
55 | run: cmake -B build -DCMAKE_BUILD_TYPE=Release -G Ninja
56 | env:
57 | CC: gcc-10
58 | CXX: g++-10
59 |
60 | - name: CMake Build
61 | run: cmake --build build --config Release
62 |
63 | - name: Create Archive
64 | run: |
65 | cd build/ARRCON
66 | zip -T9 ARRCON-$(./ARRCON -vq)-Linux.zip ARRCON
67 | mv *.zip ../..
68 |
69 | - name: Upload Artifact
70 | uses: actions/upload-artifact@v4
71 | with:
72 | name: build-linux
73 | path: 'ARRCON*.zip'
74 |
75 |
76 | build-macos:
77 | runs-on: macos-latest
78 |
79 | steps:
80 | - uses: actions/checkout@v4
81 | with:
82 | submodules: recursive
83 | fetch-depth: 0
84 |
85 | - name: Install Ninja & LLVM/Clang 16
86 | id: install-deps
87 | run: |
88 | brew install ninja llvm@16
89 | echo "clang_path=$(brew --prefix llvm@16)/bin/clang" >> "$GITHUB_OUTPUT"
90 |
91 | - name: CMake Configure
92 | run: cmake -B build -DCMAKE_BUILD_TYPE=Release -G Ninja
93 | env:
94 | CC: ${{ steps.install-deps.outputs.clang_path }}
95 | CXX: ${{ steps.install-deps.outputs.clang_path }}++
96 |
97 | - name: CMake Build
98 | run: cmake --build build --config Release
99 |
100 | - name: Create Archive
101 | run: |
102 | cd build/ARRCON
103 | zip -T9 ARRCON-$(./ARRCON -vq)-MacOS.zip ARRCON
104 | mv *.zip ../..
105 |
106 | - name: Upload Artifact
107 | uses: actions/upload-artifact@v4
108 | with:
109 | name: build-macos
110 | path: 'ARRCON*.zip'
111 |
112 |
113 | make-release:
114 | runs-on: ubuntu-latest
115 | needs: [ build-windows, build-linux, build-macos ]
116 | if: ${{ always() && contains(needs.*.result, 'success') }}
117 | # ^ Run after all other jobs finish & at least one was successful
118 |
119 | steps:
120 | - name: Download Artifacts
121 | uses: actions/download-artifact@v4
122 |
123 | - name: Stage Files
124 | run: mv ./build-*/* ./
125 |
126 | - name: Create Release
127 | uses: softprops/action-gh-release@v1
128 | with:
129 | draft: true
130 | tag_name: ${{ github.ref_name }}
131 | generate_release_notes: true
132 | fail_on_unmatched_files: true
133 | files: '*.zip'
134 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | #Ignore thumbnails created by Windows
3 | Thumbs.db
4 | #Ignore files built by Visual Studio
5 | *.obj
6 | *.exe
7 | *.pdb
8 | *.user
9 | *.aps
10 | *.pch
11 | *.vspscc
12 | *_i.c
13 | *_p.c
14 | *.ncb
15 | *.suo
16 | *.tlb
17 | *.tlh
18 | *.bak
19 | *.cache
20 | *.ilk
21 | *.log
22 | [Bb]in
23 | [Dd]ebug*/
24 | *.lib
25 | *.sbr
26 | obj/
27 | [Rr]elease*/
28 | _ReSharper*/
29 | [Tt]est[Rr]esult*
30 | .vs/
31 | #Nuget packages folder
32 | packages/
33 | out/
34 | /ARRCON/version.h
35 | /ARRCON/versioninfo.rc
36 | /ARRCON/ARRCON.rc
37 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "307lib"]
2 | path = 307lib
3 | url = https://github.com/radj307/307lib
4 | branch = main
5 |
--------------------------------------------------------------------------------
/ARRCON/ARRCON.cpp:
--------------------------------------------------------------------------------
1 | // CMake
2 | #include "version.h"
3 | #include "copyright.h"
4 |
5 | // ARRCON
6 | #include "net/rcon.hpp"
7 | #include "config.hpp"
8 | #include "helpers/print_input_prompt.h"
9 | #include "helpers/bukkit-colors.h"
10 | #include "helpers/FileLocator.hpp"
11 |
12 | // 307lib
13 | #include //< for commandline argument parser & manager
14 | #include //< for color::sync
15 | #include //< for env::PATH
16 | #include //< for hasPendingDataSTDIN
17 | #include
18 |
19 | // STL
20 | #include //< for std::filesystem
21 | #include //< for standard io streams
22 |
23 | // Global defaults
24 | static constexpr char const* const DEFAULT_TARGET_HOST{ "127.0.0.1" };
25 | static constexpr char const* const DEFAULT_TARGET_PORT{ "27015" };
26 |
27 | struct print_help {
28 | std::string exeName;
29 |
30 | print_help(const std::string& exeName) : exeName{ exeName } {}
31 |
32 | friend std::ostream& operator<<(std::ostream& os, const print_help& h)
33 | {
34 | return os << h.exeName << " v" << ARRCON_VERSION_EXTENDED << " (" << ARRCON_COPYRIGHT << ")\n"
35 | << " A Robust Remote-CONsole (RCON) client designed for use with the Source RCON Protocol.\n"
36 | << " It is also compatible with similar protocols such as the one used by Minecraft.\n"
37 | << '\n'
38 | << " Report compatibility issues here: https://github.com/radj307/ARRCON/issues/new?template=support-request.md\n"
39 | << '\n'
40 | << "USAGE:" << '\n'
41 | << " " << h.exeName << " [OPTIONS] [COMMANDS]\n"
42 | << '\n'
43 | << " Some arguments take additional inputs, labeled with ." << '\n'
44 | << " Inputs that contain spaces must be enclosed with single (\') or double(\") quotation marks." << '\n'
45 | << '\n'
46 | << "TARGET SPECIFIER OPTIONS:\n"
47 | << " -H, --host RCON Server IP/Hostname. (Default: \"" << DEFAULT_TARGET_HOST << "\")" << '\n'
48 | << " -P, --port RCON Server Port. (Default: \"" << DEFAULT_TARGET_PORT << "\")" << '\n'
49 | << " -p, --pass RCON Server Password. (Default: \"\")" << '\n'
50 | << " -R, --recall Recalls saved [Host|Port|Pass] values from the hosts file." << '\n'
51 | << " --save Saves the specified [Host|Port|Pass] as \"\" in the hosts file." << '\n'
52 | << " --remove Removes an entry from the hosts file." << '\n'
53 | << " -l, --list Lists the servers currently saved in the host file." << '\n'
54 | << '\n'
55 | << "OPTIONS:\n"
56 | << " -h, --help Shows this help display, then exits." << '\n'
57 | << " -v, --version Prints the current version number, then exits." << '\n'
58 | << " -q, --quiet Silent/Quiet mode; prevents or minimizes console output. Use \"-qn\" for scripts." << '\n'
59 | << " -i, --interactive Starts an interactive command shell after sending any scripted commands." << '\n'
60 | << " -e, --echo Enables command echo in oneshot mode." << '\n'
61 | << " -w, --wait Sets the number of milliseconds to wait between sending each queued command. Default: 0" << '\n'
62 | << " -t, --timeout Sets the number of milliseconds to wait for a response before timing out. Default: 3000" << '\n'
63 | << " -n, --no-color Disables colorized console output." << '\n'
64 | << " -Q, --no-prompt Disables the prompt in interactive mode." << '\n'
65 | << " --no-exit Disables handling the \"exit\" keyword in interactive mode." << '\n'
66 | << " --allow-empty Enables sending empty (whitespace-only) commands to the server in interactive mode." << '\n'
67 | << " --print-env Prints all recognized environment variables, their values, and descriptions." << '\n'
68 | // << " --write-ini (Over)write the INI file with the default configuration values & exit." << '\n'
69 | // << " --update-ini Writes the current configuration values to the INI file, and adds missing keys." << '\n'
70 | // << " -f, --file Load the specified file and run each line as a command." << '\n'
71 | ;
72 | }
73 | };
74 |
75 | // terminal color synchronizer
76 | color::sync csync{};
77 |
78 | int main_impl(const int, char**);
79 |
80 | int main(const int argc, char** argv)
81 | {
82 | try {
83 | return main_impl(argc, argv);
84 | } catch (std::exception const& ex) {
85 | std::cerr << csync.get_fatal() << ex.what() << std::endl;
86 | return 1;
87 | } catch (...) {
88 | std::cerr << csync.get_fatal() << "An undefined error occurred!" << std::endl;
89 | return 1;
90 | }
91 | }
92 |
93 | int main_impl(const int argc, char** argv)
94 | {
95 | const opt3::ArgManager args{ argc, argv,
96 | // define capturing args:
97 | opt3::make_template(opt3::CaptureStyle::Required, opt3::ConflictStyle::Conflict, 'H', "host", "hostname"),
98 | opt3::make_template(opt3::CaptureStyle::Required, opt3::ConflictStyle::Conflict, 'P', "port"),
99 | opt3::make_template(opt3::CaptureStyle::Required, opt3::ConflictStyle::Conflict, 'p', "pass", "password"),
100 | opt3::make_template(opt3::CaptureStyle::Required, opt3::ConflictStyle::Conflict, 'S', 'R', "saved", "recall"),
101 | opt3::make_template(opt3::CaptureStyle::Required, opt3::ConflictStyle::Conflict, "save", "save-host"),
102 | opt3::make_template(opt3::CaptureStyle::Required, opt3::ConflictStyle::Conflict, "rm", "remove", "rm-host" "remove-host"),
103 | opt3::make_template(opt3::CaptureStyle::Required, opt3::ConflictStyle::Conflict, 'w', "wait"),
104 | opt3::make_template(opt3::CaptureStyle::Required, opt3::ConflictStyle::Conflict, 't', "timeout"),
105 | opt3::make_template(opt3::CaptureStyle::Required, opt3::ConflictStyle::Conflict, 'f', "file"),
106 | };
107 |
108 | // get the executable's location & name
109 | const auto& [programPath, programName] { env::PATH().resolve_split(argv[0]) };
110 | FileLocator locator{ programPath, std::filesystem::path{ programName }.replace_extension() };
111 |
112 | /// setup the log
113 | // log file stream
114 | std::ofstream logfs{ locator.from_extension(".log") };
115 | // log manager object
116 | Logger logManager{ logfs.rdbuf() };
117 | logManager.print_header();
118 | // write commandline to log
119 | {
120 | const auto argVec{ opt3::vectorize(argc, argv) };
121 | std::clog
122 | << MessageHeader(LogLevel::Debug) << "Commandline Arguments: \""
123 | << str::stringify_join(argVec.begin(), argVec.end(), ' ') << '\"'
124 | << std::endl;
125 | }
126 |
127 | try {
128 | // -h|--help
129 | if (args.empty() || args.check_any('h', "help")) {
130 | std::cout << print_help(programName.generic_string());
131 | return 0;
132 | }
133 |
134 | // -q|--quiet
135 | const bool quiet{ args.check_any('q', "quiet") };
136 |
137 | // -v|--version
138 | if (args.check_any('v', "version")) {
139 | if (!quiet) std::cout << "ARRCON v";
140 | std::cout << ARRCON_VERSION_EXTENDED;
141 | if (!quiet) std::cout << std::endl << ARRCON_COPYRIGHT;
142 | std::cout << std::endl;
143 | return 0;
144 | }
145 |
146 | // -n|--no-color
147 | csync.setEnabled(!args.check_any('n', "no-color"));
148 |
149 | std::string programNameStr{ std::filesystem::path(programName).replace_extension().generic_string() };
150 |
151 | // --print-env
152 | if (args.check("print-env")) {
153 | const auto
154 | config_dir{ env::getvar(programNameStr + "_CONFIG_DIR") },
155 | hostname{ env::getvar(programNameStr + "_HOST") },
156 | port{ env::getvar(programNameStr + "_PORT") },
157 | password{ env::getvar(programNameStr + "_PASS") };
158 | std::cout << std::boolalpha
159 | << "Environment Variables" << '\n'
160 | << " " << csync(color::yellow) << programNameStr << "_CONFIG_DIR" << csync() << '\n'
161 | << " Is Defined: " << config_dir.has_value() << '\n'
162 | << " Current Value: " << config_dir.value_or("") << '\n'
163 | << " Description:\n"
164 | << " Overrides the config file search location.\n"
165 | << " When this is set, config files in other directories on the search path are ignored.\n"
166 | << '\n'
167 | << " " << csync(color::yellow) << programNameStr << "_HOST" << csync() << '\n'
168 | << " Is Defined: " << hostname.has_value() << '\n'
169 | << " Current Value: " << hostname.value_or("") << '\n'
170 | << " Description:\n"
171 | << " Overrides the target hostname, unless one is specified on the commandline with [-H|--host].\n"
172 | //<< " When this is set, the " << csync(color::yellow) << "sDefaultHost" << csync() << " key in the INI will be ignored.\n"
173 | << '\n'
174 | << " " << csync(color::yellow) << programNameStr << "_PORT" << csync() << '\n'
175 | << " Is Defined: " << port.has_value() << '\n'
176 | << " Current Value: " << port.value_or("") << '\n'
177 | << " Description:\n"
178 | << " Overrides the target port, unless one is specified on the commandline with [-P|--port].\n"
179 | //<< " When this is set, the " << csync(color::yellow) << "sDefaultPort" << csync() << " key in the INI will be ignored.\n"
180 | << '\n'
181 | << " " << csync(color::yellow) << programNameStr << "_PASS" << csync() << '\n'
182 | << " Is Defined: " << password.has_value() << '\n'
183 | << " Description:\n"
184 | << " Overrides the target password, unless one is specified on the commandline with [-p|--pass].\n"
185 | //<< " When this is set, the " << csync(color::yellow) << "sDefaultPass" << csync() << " key in the INI will be ignored.\n"
186 | ;
187 | return 0;
188 | }
189 |
190 | /// determine the target server info & operate on the hosts file
191 | const auto hostsfile_path{ locator.from_extension(".hosts") };
192 | std::optional hostsfile;
193 |
194 | // --remove|--rm|--rm-host|--remove-host
195 | if (const auto& arg_removeHost{ args.getv_any("rm", "remove", "rm-host", "remove-host") }; arg_removeHost.has_value()) {
196 | if (!std::filesystem::exists(hostsfile_path))
197 | throw make_exception("The hosts file hasn't been created yet. (Use \"--save\" to create one)");
198 |
199 | // load the hosts file directly
200 | ini::INI ini(hostsfile_path);
201 |
202 | // remove the specified entry
203 | if (const auto it{ ini.find(arg_removeHost.value()) }; it != ini.end())
204 | ini.erase(it);
205 | else throw make_exception("The specified saved host \"", arg_removeHost.value(), "\" doesn't exist! (Use \"--list\" to see a list of saved hosts)");
206 |
207 | // save the hosts file
208 | if (ini.write(hostsfile_path)) {
209 | std::cout
210 | << "Successfully removed \"" << csync(color::yellow) << arg_removeHost.value() << csync() << "\" from the hosts list.\n"
211 | << "Saved hosts file to " << hostsfile_path << '\n'
212 | ;
213 | return 0;
214 | }
215 | else throw make_exception("Failed to save hosts file to ", hostsfile_path, '!');
216 | }
217 | // --list|--list-hosts
218 | else if (args.check_any('l', "list", "list-hosts", "list-host")) {
219 | if (!std::filesystem::exists(hostsfile_path))
220 | throw make_exception("The hosts file hasn't been created yet. (Use \"--save-host\" to create one)");
221 |
222 | // load the hosts file
223 | if (!hostsfile.has_value())
224 | hostsfile = config::SavedHosts(hostsfile_path);
225 |
226 | if (hostsfile->empty())
227 | throw make_exception("The hosts file doesn't have any entries yet. (Use \"--save-host\" to create one)");
228 |
229 | // if quiet was specified, get the length of the longest saved host name
230 | size_t longestNameLength{};
231 | if (quiet) {
232 | for (const auto& [name, _] : *hostsfile) {
233 | if (name.size() > longestNameLength)
234 | longestNameLength = name.size();
235 | }
236 | }
237 |
238 | // print out the hosts list
239 | for (const auto& [name, info] : *hostsfile) {
240 | if (!quiet) {
241 | std::cout
242 | << csync(color::yellow) << name << csync() << '\n'
243 | << " Hostname: \"" << info.host << "\"\n"
244 | << " Port: \"" << info.port << "\"\n"
245 | ;
246 | }
247 | else {
248 | std::cout
249 | << csync(color::yellow) << name << csync()
250 | << indent(longestNameLength + 2, name.size())
251 | << "( " << info.host << ':' << info.port << " )\n"
252 | ;
253 | }
254 | }
255 |
256 | return 0;
257 | }
258 |
259 | net::rcon::target_info target{
260 | env::getvar(programNameStr + "_HOST").value_or(DEFAULT_TARGET_HOST),
261 | env::getvar(programNameStr + "_PORT").value_or(DEFAULT_TARGET_PORT),
262 | env::getvar(programNameStr + "_PASS").value_or("")
263 | };
264 |
265 | // -S|-R|--saved|--recall
266 | if (const auto& arg_saved{ args.getv_any('S', 'R', "saved", "recall") }; arg_saved.has_value()) {
267 | if (!std::filesystem::exists(hostsfile_path))
268 | throw make_exception("The hosts file hasn't been created yet. (Use \"--save\" to create one)");
269 |
270 | // load the hosts file
271 | if (!hostsfile.has_value())
272 | hostsfile = config::SavedHosts(hostsfile_path);
273 |
274 | // try getting the specified saved target's info
275 | if (const auto savedTarget{ hostsfile->get_host(arg_saved.value()) }; savedTarget.has_value()) {
276 | target = savedTarget.value();
277 | }
278 | else throw make_exception("The specified saved host \"", arg_saved.value(), "\" doesn't exist! (Use \"--list\" to see a list of saved hosts)");
279 |
280 | std::clog << MessageHeader(LogLevel::Debug) << "Recalled saved host information for \"" << arg_saved.value() << "\": " << target << std::endl;
281 | }
282 | // -H|--host|--hostname
283 | if (const auto& arg_hostname{ args.getv_any('H', "host", "hostname") }; arg_hostname.has_value())
284 | target.host = arg_hostname.value();
285 | // -P|--port
286 | if (const auto& arg_port{ args.getv_any('P', "port") }; arg_port.has_value())
287 | target.port = arg_port.value();
288 | // -p|--pass|--password
289 | if (const auto& arg_password{ args.getv_any('p', "pass", "password") }; arg_password.has_value())
290 | target.pass = arg_password.value();
291 |
292 | // --save|--save-host
293 | if (const auto& arg_saveHost{ args.getv_any("save", "save-host") }; arg_saveHost.has_value()) {
294 | // load the hosts file
295 | if (!hostsfile.has_value()) {
296 | hostsfile = std::filesystem::exists(hostsfile_path)
297 | ? config::SavedHosts(hostsfile_path)
298 | : config::SavedHosts();
299 | }
300 |
301 | const bool exists{ hostsfile->contains(arg_saveHost.value()) };
302 | auto& entry{ (*hostsfile)[arg_saveHost.value()] };
303 |
304 | // break early if no changes will be made
305 | if (exists && entry == target) {
306 | std::cout << "Host \"" << csync(color::yellow) << arg_saveHost.value() << csync() << "\" was already saved with the specified server info.\n";
307 | return 0;
308 | }
309 |
310 | // set the target
311 | entry = target;
312 |
313 | // create directory structure
314 | if (!std::filesystem::exists(hostsfile_path))
315 | std::filesystem::create_directories(hostsfile_path.parent_path());
316 |
317 | // write to disk
318 | ini::INI ini;
319 | hostsfile->export_to(ini);
320 | if (ini.write(hostsfile_path)) {
321 | std::cout
322 | << "Host \"" << csync(color::yellow) << arg_saveHost.value() << csync() << "\" was " << (exists ? "updated" : "created") << " with the specified server info.\n"
323 | << "Saved hosts file to " << hostsfile_path << '\n'
324 | ;
325 | return 0;
326 | }
327 | else throw make_exception("Failed to save hosts file to ", hostsfile_path, '!');
328 | }
329 |
330 | // initialize the client
331 | net::rcon::RconClient client;
332 |
333 | // connect to the server
334 | client.connect(target.host, target.port);
335 |
336 | // -t|--timeout
337 | client.set_timeout(args.castgetv_any([](auto&& arg) { return str::stoi(std::forward(arg)); }, 't', "timeout").value_or(3000));
338 | // ^ this needs to be set AFTER connecting
339 |
340 | // authenticate with the server
341 | if (!client.authenticate(target.pass)) {
342 | throw ExceptionBuilder()
343 | .line("Authentication Error: Incorrect Password!")
344 | .line("Target Hostname/IP: ", target.host)
345 | .line("Target Port: ", target.port)
346 | .line("Suggested Solutions:")
347 | .line("1. Verify the password you entered is correct.")
348 | .line("2. Make sure this is the correct target.")
349 | .build();
350 | }
351 |
352 | /// get commands from STDIN & the commandline
353 | std::vector commands;
354 | if (hasPendingDataSTDIN()) {
355 | // get commands from STDIN
356 | for (std::string buf; std::getline(std::cin, buf);) {
357 | commands.emplace_back(buf);
358 | }
359 | }
360 | if (const auto parameters{ args.getv_all() };
361 | !parameters.empty()) {
362 | commands.insert(commands.end(), parameters.begin(), parameters.end());
363 | }
364 |
365 | const bool noPrompt{ args.check_any('Q', "no-prompt") };
366 | const bool echoCommands{ args.check_any('e', "echo") };
367 |
368 | // Oneshot Mode
369 | if (!commands.empty()) {
370 | // get the command delay, if one was specified
371 | std::chrono::milliseconds commandDelay;
372 | bool useCommandDelay{ false };
373 | if (const auto waitArg{ args.getv_any('w', "wait") }; waitArg.has_value()) {
374 | commandDelay = std::chrono::milliseconds{ str::tonumber(waitArg.value()) };
375 | useCommandDelay = true;
376 | }
377 |
378 | // oneshot mode
379 | bool fst{ true };
380 | for (const auto& command : commands) {
381 | // wait for the specified number of milliseconds
382 | if (useCommandDelay) {
383 | if (fst) fst = false;
384 | else std::this_thread::sleep_for(commandDelay);
385 | }
386 |
387 | if (echoCommands) {
388 | if (!noPrompt) // print the shell prompt
389 | print_input_prompt(std::cout, target.host, csync);
390 | // echo the command
391 | std::cout << command << '\n';
392 | }
393 |
394 | // execute the command and print the result
395 | std::cout << str::trim(client.command(command)) << std::endl;
396 | }
397 | }
398 |
399 | const bool disableExitKeyword{ args.check_any("no-exit") };
400 | const bool allowEmptyCommands{ args.check_any("allow-empty") };
401 |
402 | // Interactive mode
403 | if (commands.empty() || args.check_any('i', "interactive")) {
404 | if (!noPrompt) {
405 | std::cout << "Authentication Successful.\nUse ";
406 | if (!disableExitKeyword) std::cout << " or type \"exit\"";
407 | std::cout << " to quit.\n";
408 | }
409 |
410 | // interactive mode input loop
411 | while (true) {
412 | if (!quiet && !noPrompt) // print the shell prompt
413 | print_input_prompt(std::cout, target.host, csync);
414 |
415 | // get user input
416 | std::string str;
417 | std::getline(std::cin, str);
418 |
419 | // check for data remaining in the socket's buffer from previous commands
420 | if (const auto& buffer_size{ client.buffer_size() }; buffer_size > 0) {
421 | std::clog << MessageHeader(LogLevel::Warning) << "The buffer contains " << buffer_size << " unexpected bytes! Dumping the buffer to STDOUT." << std::endl;
422 |
423 | // print the buffered data before continuing
424 | std::cout << str::trim(net::rcon::bytes_to_string(client.flush())) << std::endl;
425 | }
426 |
427 | // validate the input
428 | if (!allowEmptyCommands && str::trim(str).empty()) {
429 | std::cerr << csync(color::cyan) << "[not sent: empty]" << csync() << '\n';
430 | continue;
431 | }
432 | // check for the exit keyword
433 | else if (!disableExitKeyword && str == "exit")
434 | break; //< exit on keyword input
435 |
436 | // send the command and get the response
437 | str = str::trim(client.command(str));
438 |
439 | if (str.empty()) {
440 | // response is empty
441 | std::cerr << csync(color::orange) << "[empty response]" << csync() << '\n';
442 | }
443 | else {
444 | // replace minecraft bukkit color codes with ANSI sequences
445 | str = mc_color::replace_color_codes(str);
446 |
447 | // print the response
448 | std::cout << str << std::endl;
449 | }
450 | }
451 | }
452 |
453 | return 0;
454 | } catch (std::exception const& ex) {
455 | // catch & log exceptions
456 | std::clog << MessageHeader(LogLevel::Fatal) << ex.what() << std::endl;
457 | throw; //< rethrow
458 | }
459 | }
460 |
--------------------------------------------------------------------------------
/ARRCON/ARRCON.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/radj307/ARRCON/0859b1b6e21d31573407001ed88d29b86de46fec/ARRCON/ARRCON.ico
--------------------------------------------------------------------------------
/ARRCON/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | # ARRCON/ARRCON
2 | file(GLOB_RECURSE HEADERS
3 | RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}"
4 | CONFIGURE_DEPENDS
5 | "*.h*"
6 | )
7 | file(GLOB_RECURSE SRCS
8 | RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}"
9 | CONFIGURE_DEPENDS
10 | "*.c*"
11 | )
12 |
13 | string(TIMESTAMP _current_year "%Y")
14 |
15 | file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/rc")
16 | if (WIN32)
17 | include(ResourceMaker)
18 |
19 | MAKE_STRINGRC_VERSIONINFO(
20 | _arrcon_stringrc_versioninfo
21 | "${ARRCON_VERSION}"
22 | "Copyright © ${_current_year} by radj307"
23 | "radj307"
24 | "ARRCON"
25 | "Commandline client for communicating with servers using the Source RCON Protocol."
26 | )
27 | MAKE_STRINGRC_ICON(
28 | _arrcon_stringrc_icon
29 | "${CMAKE_CURRENT_SOURCE_DIR}/ARRCON.ico"
30 | )
31 |
32 | MAKE_RESOURCE("${CMAKE_CURRENT_BINARY_DIR}/rc/ARRCON.rc" "${_arrcon_stringrc_versioninfo}" "${_arrcon_stringrc_icon}")
33 | endif()
34 |
35 | MAKE_VERSION_HEADER("${CMAKE_CURRENT_BINARY_DIR}/rc/version.h" ARRCON "${ARRCON_VERSION_EXTENDED}")
36 | include(CopyrightMaker)
37 | MAKE_COPYRIGHT_HEADER("${CMAKE_CURRENT_BINARY_DIR}/rc/copyright.h" ARRCON ${_current_year} radj307)
38 |
39 | file(GLOB RESOURCES
40 | CONFIGURE_DEPENDS
41 | "${CMAKE_CURRENT_BINARY_DIR}/rc/*"
42 | )
43 |
44 | include_directories("/opt/local/include")
45 |
46 | add_executable(ARRCON "${SRCS}" "${RESOURCES}")
47 |
48 | set_property(TARGET ARRCON PROPERTY CXX_STANDARD 20)
49 | set_property(TARGET ARRCON PROPERTY CXX_STANDARD_REQUIRED ON)
50 |
51 | if (MSVC)
52 | target_compile_options(ARRCON PRIVATE "${307lib_compiler_commandline}")
53 | endif()
54 |
55 | target_include_directories(ARRCON PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/rc")
56 |
57 | target_sources(ARRCON PRIVATE "${HEADERS}")
58 |
59 | ## Setup Boost:
60 | # Try to find an existing Boost 1.84.0 package
61 | find_package(Boost 1.84.0 COMPONENTS asio)
62 | # Fallback to FetchContent if not found
63 | if (NOT Boost_FOUND)
64 | message(STATUS "Downloading Boost 1.84.0 via FetchContent")
65 |
66 | include(FetchContent)
67 | FetchContent_Declare(
68 | Boost
69 | GIT_REPOSITORY https://github.com/boostorg/boost.git
70 | GIT_TAG boost-1.84.0
71 | )
72 | FetchContent_MakeAvailable(Boost)
73 | endif()
74 |
75 | target_link_libraries(ARRCON PRIVATE
76 | TermAPI
77 | filelib
78 | Boost::asio
79 | )
80 |
--------------------------------------------------------------------------------
/ARRCON/ExceptionBuilder.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 | // 307lib::shared
3 | #include //< for ex::except
4 | #include //< for shared::indent()
5 |
6 | // 307lib::TermAPI
7 | #include //< for term::MessageMarginSize
8 |
9 | class ExceptionBuilder {
10 | using this_t = ExceptionBuilder;
11 |
12 | std::stringstream ss;
13 | bool isFirstLine{ true };
14 |
15 | public:
16 | ExceptionBuilder() {}
17 |
18 | /**
19 | * @brief Builds an exception and returns it.
20 | * @returns ex::except with the previously-specified message.
21 | */
22 | ex::except build() const
23 | {
24 | return{ ss.str() };
25 | }
26 |
27 | /**
28 | * @brief Adds a line to the exception message.
29 | * @param ...content - The content of the line.
30 | * @returns *this
31 | */
32 | this_t& line(auto&&... content)
33 | {
34 | if (!isFirstLine)
35 | ss << std::endl << indent(term::MessageMarginSize);
36 | else isFirstLine = false;
37 |
38 | (ss << ... << std::forward(content));
39 |
40 | return *this;
41 | }
42 | };
43 |
--------------------------------------------------------------------------------
/ARRCON/config.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 | #include "logging.hpp"
3 | #include "net/target_info.hpp"
4 |
5 | // 307lib::filelib
6 | #include //< for ini::INI
7 |
8 | // STL
9 | #include //< for std::filesystem::path
10 | #include