43 #include <boost/iostreams/filter/gzip.hpp>
44 #include <boost/exception/get_error_info.hpp>
45 #include <boost/random.hpp>
46 #include <boost/generator_iterator.hpp>
50 #if !(defined(_WIN32))
55 #define DBG_CS LOG_STREAM(debug, log_campaignd)
56 #define LOG_CS LOG_STREAM(info, log_campaignd)
57 #define WRN_CS LOG_STREAM(warn, log_campaignd)
58 #define ERR_CS LOG_STREAM(err, log_campaignd)
72 sig_atomic_t need_reload = 0;
74 void flag_sighup(
int signal)
77 LOG_CS <<
"SIGHUP caught, scheduling config reload.\n";
81 void exit_sigint(
int signal)
83 assert(signal == SIGINT);
84 LOG_CS <<
"SIGINT caught, exiting without cleanup immediately.\n";
88 void exit_sigterm(
int signal)
90 assert(signal == SIGTERM);
91 LOG_CS <<
"SIGTERM caught, exiting without cleanup immediately.\n";
95 time_t monotonic_clock()
97 #if defined(_POSIX_MONOTONIC_CLOCK) && !defined(_WIN32)
99 clock_gettime(CLOCK_MONOTONIC, &ts);
102 #warning monotonic_clock() is not truly monotonic!
103 return time(
nullptr);
110 return util::create_hash(passphrase, campaign[
"passsalt"]) == campaign[
"passhash"];
115 static const std::string itoa64 =
"./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
116 boost::mt19937 mt(time(0));
118 boost::uniform_int<> from_str(0, itoa64.length() - 1);
119 boost::variate_generator< boost::mt19937, boost::uniform_int<> > get_char(mt, from_str);
121 for(
size_t i = 0;
i <
len;
i++) {
122 salt[
i] = itoa64[get_char()];
131 campaign[
"passsalt"] = salt;
141 , cfg_file_(cfg_file)
147 , feedback_url_format_()
151 , net_manager_(min_threads, max_threads)
152 , server_manager_(
port_)
155 signal(
SIGHUP, flag_sighup);
157 signal(SIGINT, exit_sigint);
158 signal(SIGTERM, exit_sigterm);
160 LOG_CS <<
"Port: " <<
port_ <<
" Worker threads min/max: " << min_threads
161 <<
'/' << max_threads <<
'\n';
167 if (campaign[
"passphrase"].empty()) {
171 LOG_CS <<
"Campaign '" << campaign[
"title"] <<
"' uses unhashed passphrase. Fixing.\n";
172 set_passphrase(campaign, campaign[
"passphrase"]);
173 campaign[
"passphrase"] =
"";
196 LOG_CS <<
"READ-ONLY MODE ACTIVE\n";
199 const bool use_system_sendfile =
cfg_[
"network_use_system_sendfile"].to_bool();
218 if(!
cfg_[
"control_socket"].empty()) {
233 cfg_[
"network_use_system_sendfile"] = use_system_sendfile;
265 DBG_CS <<
"writing configuration and add-ons list to disk...\n";
273 const std::map<std::string, std::string>::const_iterator
itor =
hooks_.find(hook);
274 if(itor ==
hooks_.end()) {
285 ERR_CS <<
"Tried to execute a script on an unsupported platform\n";
290 if((childpid = fork()) == -1) {
291 ERR_CS <<
"fork failed while updating campaign " << addon <<
'\n';
299 execlp(script.c_str(), script.c_str(), addon.c_str(),
static_cast<char *
>(
nullptr));
302 std::cerr <<
"ERROR: exec failed with errno " << errno <<
" for addon " << addon
331 time_t last_ts = monotonic_clock();
341 LOG_CS <<
"Reloaded configuration\n";
345 bool force_flush =
false;
351 if(ctl ==
"shut_down") {
352 LOG_CS <<
"Shut down requested by admin, shutting down...\n";
354 }
else if(ctl ==
"readonly") {
359 LOG_CS <<
"Read only mode: " << (
read_only_ ?
"enabled" :
"disabled") <<
'\n';
360 }
else if(ctl ==
"flush") {
362 LOG_CS <<
"Flushing config to disk...\n";
363 }
else if(ctl ==
"reload") {
365 if(ctl[1] ==
"blacklist") {
366 LOG_CS <<
"Reloading blacklist...\n";
369 ERR_CS <<
"Unrecognized admin reload argument: " << ctl[1] <<
'\n';
372 LOG_CS <<
"Reloading all configuration...\n";
377 }
else if(ctl ==
"setpass") {
379 ERR_CS <<
"Incorrect number of arguments for 'setpass'\n";
386 ERR_CS <<
"Add-on '" << addon_id <<
"' not found, cannot set passphrase\n";
387 }
else if(newpass.empty()) {
389 ERR_CS <<
"Add-on passphrases may not be empty!\n";
391 set_passphrase(campaign, newpass);
393 LOG_CS <<
"New passphrase set for '" << addon_id <<
"'\n";
397 ERR_CS <<
"Unrecognized admin command: " << ctl.
full() <<
'\n';
401 const time_t cur_ts = monotonic_clock();
403 if(force_flush || labs(cur_ts - last_ts) >= 10*60) {
425 request_handlers_table::const_iterator j
447 network::connection const * err_connection = boost::get_error_info<network::connection_info>(
e);
449 if(err_connection !=
nullptr) {
450 err_sock = *err_connection;
453 if(err_sock == 0 && sock > 0) {
474 #define REGISTER_CAMPAIGND_HANDLER(req_id) \
475 register_handler(#req_id, &server::handle_##req_id)
489 LOG_CS <<
"sending campaign list to " << req.
addr <<
" using gzip";
491 time_t epoch = time(
nullptr);
494 campaign_list[
"timestamp"] = epoch;
495 if(req.
cfg[
"times_relative_to"] !=
"now") {
499 bool before_flag =
false;
500 time_t before = epoch;
506 bool after_flag =
false;
507 time_t after = epoch;
518 if(!name.empty() && name !=
i[
"name"]) {
524 if(before_flag && (tm.empty() || lexical_cast_default<time_t>(tm, 0) >= before)) {
527 if(after_flag && (tm.empty() || lexical_cast_default<time_t>(tm, 0) <= after)) {
534 for(
const config& j :
i.child_range(
"translation"))
536 if(j[
"language"] == lang) {
552 j[
"passphrase"] =
"";
557 j[
"feedback_url"] =
"";
567 j.clear_children(
"feedback");
571 response.
add_child(
"campaigns", campaign_list);
578 LOG_CS <<
"sending campaign '" << req.
cfg[
"name"] <<
"' to " << req.
addr <<
" using gzip";
588 std::cerr <<
" size: <unknown> KiB\n";
589 ERR_CS <<
"File size unknown, aborting send.\n";
590 send_error(
"Add-on '" + req.
cfg[
"name"].str() +
"' could not be read by the server.", req.
sock);
594 std::cerr <<
" size: " << size/1024 <<
"KiB\n";
599 if(req.
cfg[
"increase_downloads"].to_bool(
true)) {
600 const int downloads = campaign[
"downloads"].to_int() + 1;
601 campaign[
"downloads"] = downloads;
611 LOG_CS <<
"in read-only mode, request for upload terms denied\n";
612 send_error(
"The server is currently in read-only mode, add-on uploads are disabled.", req.
sock);
616 LOG_CS <<
"sending terms " << req.
addr <<
"\n";
617 send_message(
"All add-ons uploaded to this server must be licensed under the terms of the GNU General Public License (GPL). By uploading content to this server, you certify that you have the right to place the content under the conditions of the GPL, and choose to do so.", req.
sock);
625 LOG_CS <<
"uploading campaign '" << upload[
"name"] <<
"' from " << req.
addr <<
".\n";
629 config *campaign =
nullptr;
631 bool passed_name_utf8_check =
false;
635 passed_name_utf8_check =
true;
645 if(!passed_name_utf8_check) {
646 LOG_CS <<
"Upload aborted - invalid_utf8_exception caught on handle_upload() check 1, "
647 <<
"the add-on pbl info contains invalid UTF-8\n";
648 send_error(
"Add-on rejected: The add-on name contains an invalid UTF-8 sequence.", req.
sock);
650 LOG_CS <<
"Upload aborted - invalid_utf8_exception caught on handle_upload() check 2, "
651 <<
"the internal add-ons list contains invalid UTF-8\n";
652 send_error(
"Server error: The server add-ons list is damaged.", req.
sock);
659 LOG_CS <<
"Upload aborted - uploads not permitted in read-only mode.\n";
660 send_error(
"Add-on rejected: The server is currently in read-only mode.", req.
sock);
662 LOG_CS <<
"Upload aborted - no add-on data.\n";
663 send_error(
"Add-on rejected: No add-on data was supplied.", req.
sock);
665 LOG_CS <<
"Upload aborted - invalid add-on name.\n";
666 send_error(
"Add-on rejected: The name of the add-on is invalid.", req.
sock);
668 LOG_CS <<
"Upload aborted - add-on name starts with an illegal formatting character.\n";
669 send_error(
"Add-on rejected: The name of the add-on starts with an illegal formatting character.", req.
sock);
670 }
else if(upload[
"title"].empty()) {
671 LOG_CS <<
"Upload aborted - no add-on title specified.\n";
672 send_error(
"Add-on rejected: You did not specify the title of the add-on in the pbl file!", req.
sock);
674 LOG_CS <<
"Upload aborted - add-on title starts with an illegal formatting character.\n";
675 send_error(
"Add-on rejected: The title of the add-on starts with an illegal formatting character.", req.
sock);
677 LOG_CS <<
"Upload aborted - unknown add-on type specified.\n";
678 send_error(
"Add-on rejected: You did not specify a known type for the add-on in the pbl file! (See PblWML: wiki.wesnoth.org/PblWML)", req.
sock);
679 }
else if(upload[
"author"].empty()) {
680 LOG_CS <<
"Upload aborted - no add-on author specified.\n";
681 send_error(
"Add-on rejected: You did not specify the author(s) of the add-on in the pbl file!", req.
sock);
682 }
else if(upload[
"version"].empty()) {
683 LOG_CS <<
"Upload aborted - no add-on version specified.\n";
684 send_error(
"Add-on rejected: You did not specify the version of the add-on in the pbl file!", req.
sock);
685 }
else if(upload[
"description"].empty()) {
686 LOG_CS <<
"Upload aborted - no add-on description specified.\n";
687 send_error(
"Add-on rejected: You did not specify a description of the add-on in the pbl file!", req.
sock);
688 }
else if(upload[
"email"].empty()) {
689 LOG_CS <<
"Upload aborted - no add-on email specified.\n";
690 send_error(
"Add-on rejected: You did not specify your email address in the pbl file!", req.
sock);
692 LOG_CS <<
"Upload aborted - invalid file names in add-on data.\n";
693 send_error(
"Add-on rejected: The add-on contains an illegal file or directory name."
694 " File or directory names may not contain whitespace or any of the following characters: '/ \\ : ~'",
696 }
else if(campaign && !authenticate(*campaign, upload[
"passphrase"])) {
697 LOG_CS <<
"Upload aborted - incorrect passphrase.\n";
698 send_error(
"Add-on rejected: The add-on already exists, and your passphrase was incorrect.", req.
sock);
700 const time_t upload_ts = time(
nullptr);
702 LOG_CS <<
"Upload is owner upload.\n";
706 upload[
"title"].str(),
707 upload[
"description"].str(),
708 upload[
"author"].str(),
710 upload[
"email"].str()))
712 LOG_CS <<
"Upload denied - blacklisted add-on information.\n";
713 send_error(
"Add-on upload denied. Please contact the server administration for assistance.", req.
sock);
717 LOG_CS <<
"Upload aborted - the add-on pbl info contains invalid UTF-8 and cannot be "
718 <<
"checked against the blacklist\n";
719 send_error(
"Add-on rejected: The add-on publish information contains an invalid UTF-8 sequence.", req.
sock);
723 const bool existing_upload = campaign !=
nullptr;
727 if(campaign ==
nullptr) {
729 (*campaign)[
"original_timestamp"] = upload_ts;
732 (*campaign)[
"title"] = upload[
"title"];
733 (*campaign)[
"name"] = upload[
"name"];
734 (*campaign)[
"filename"] =
"data/" + upload[
"name"].str();
735 (*campaign)[
"author"] = upload[
"author"];
736 (*campaign)[
"description"] = upload[
"description"];
737 (*campaign)[
"version"] = upload[
"version"];
738 (*campaign)[
"icon"] = upload[
"icon"];
739 (*campaign)[
"translate"] = upload[
"translate"];
740 (*campaign)[
"dependencies"] = upload[
"dependencies"];
741 (*campaign)[
"upload_ip"] = req.
addr;
742 (*campaign)[
"type"] = upload[
"type"];
743 (*campaign)[
"email"] = upload[
"email"];
745 if(!existing_upload) {
746 set_passphrase(*campaign, upload[
"passphrase"]);
749 if((*campaign)[
"downloads"].empty()) {
750 (*campaign)[
"downloads"] = 0;
752 (*campaign)[
"timestamp"] = upload_ts;
754 int uploads = (*campaign)[
"uploads"].to_int() + 1;
755 (*campaign)[
"uploads"] = uploads;
757 (*campaign).clear_children(
"feedback");
758 if(
const config& url_params = upload.
child(
"feedback")) {
759 (*campaign).add_child(
"feedback", url_params);
762 const std::string& filename = (*campaign)[
"filename"].str();
763 data[
"title"] = (*campaign)[
"title"];
765 data[
"campaign_name"] = (*campaign)[
"name"];
766 data[
"author"] = (*campaign)[
"author"];
767 data[
"description"] = (*campaign)[
"description"];
768 data[
"version"] = (*campaign)[
"version"];
769 data[
"timestamp"] = (*campaign)[
"timestamp"];
770 data[
"original_timestamp"] = (*campaign)[
"original_timestamp"];
771 data[
"icon"] = (*campaign)[
"icon"];
772 data[
"type"] = (*campaign)[
"type"];
790 fire(
"hook_post_upload", upload[
"name"]);
799 LOG_CS <<
"in read-only mode, request to delete '" << erase[
"name"] <<
"' from " << req.
addr <<
" denied\n";
800 send_error(
"Cannot delete add-on: The server is currently in read-only mode.", req.
sock);
804 LOG_CS <<
"deleting campaign '" << erase[
"name"] <<
"' requested from " << req.
addr <<
"\n";
813 if(!authenticate(campaign, erase[
"passphrase"])
814 && (
campaigns()[
"master_password"].empty()
815 ||
campaigns()[
"master_password"] != erase[
"passphrase"]))
823 if(
remove(campaign[
"filename"].str().c_str()) != 0) {
824 ERR_CS <<
"failed to delete archive for campaign '" << erase[
"name"]
825 <<
"' (" << campaign[
"filename"] <<
"): " << strerror(errno)
830 for(
size_t index = 0; itors.first != itors.second; ++
index, ++itors.first)
832 if(&campaign == &*itors.first) {
842 fire(
"hook_post_erase", erase[
"name"]);
851 LOG_CS <<
"in read-only mode, request to change passphrase denied\n";
852 send_error(
"Cannot change passphrase: The server is currently in read-only mode.", req.
sock);
860 }
else if(!authenticate(campaign, cpass[
"passphrase"])) {
862 }
else if(cpass[
"new_passphrase"].empty()) {
865 set_passphrase(campaign, cpass[
"new_passphrase"]);
885 if(argc >= 2 && atoi(argv[1])){
891 std::cerr <<
"Could not parse config file\n";
894 std::cerr <<
"File I/O error\n";
897 std::cerr <<
"Aborted with network error: " << e.
message <<
'\n';
899 }
catch(std::bad_function_call& ) {
900 std::cerr <<
"Bad request handler function call\n";
child_itors child_range(const std::string &key)
std::string feedback_url_format_
static lg::log_domain log_campaignd("campaignd")
void write(const config &cfg)
GLvoid **typedef void(GLAPIENTRY *PFNGLGETVERTEXATTRIBDVPROC)(GLuint
void handle_request_terms(const request &)
all_children_iterator ordered_end() const
const char * what() const
void handle_delete(const request &)
config & child_or_add(const std::string &key)
Returns a reference to the first child with the given key.
Client request information object.
connection receive_data(config &cfg, connection connection_num, unsigned int timeout, bandwidth_in_ptr *bandwidth_in)
const config & server_info() const
Retrieves the contents of the [server_info] WML node.
utf8::string lowercase(const utf8::string &s)
Returns a lowercased version of the string.
void process_send_queue(connection, size_t)
Function to send any data that is in a connection's send_queue, up to a maximum of 'max_size' bytes �...
void fire(const std::string &hook, const std::string &addon)
Fires a hook script.
size_t args_count() const
Returns the total number of arguments, not including the command itself.
request_handlers_table handlers_
const config & child_or_empty(const std::string &key) const
Returns the first child with the given key, or an empty config if there is none.
GLint GLenum GLsizei GLint GLsizei const GLvoid * data
void clear_children(const std::string &key)
To lexical_cast(From value)
Lexical cast converts one type to another.
std::map< std::string, std::string > hooks_
void load_blacklist()
Reads the add-ons upload blacklist from WML.
std::string full() const
Return the full command line string.
void load_config(const config &v)
void register_handlers()
Registers client request handlers.
Variant for storing WML attributes.
void add_license(config &cfg)
Adds a COPYING.txt file with the full text of the GNU GPL to an add-on.
const std::string cfg_file_
bool set_log_domain_severity(std::string const &name, int severity)
void send_file(const std::string &filename, connection connection_num, const std::string &packet_type)
const config & campaigns() const
Retrieves the contents of the [campaigns] WML node.
GLsizei const char ** path
std::string blacklist_file_
std::string create_hash(const std::string &password, const std::string &salt, int iteration_count)
Class for writing a config out to a file in pieces.
std::string normalize_path(const std::string &path)
Returns the absolute path of a file.
void write_file(const std::string &fname, const std::string &data)
Throws io_exception if an error occurs.
const child_map::key_type & key
std::istream * istream_file(const std::string &fname, bool treat_failure_as_error=true)
void remove_child(const std::string &key, unsigned index)
std::ostream * ostream_file(std::string const &fname, bool create_directory=true)
void erase(const std::string &key)
config & add_child(const std::string &key)
const network::connection sock
bool is_blacklisted(const std::string &name, const std::string &title, const std::string &description, const std::string &author, const std::string &ip, const std::string &email) const
Whether an add-on described by these fields is blacklisted.
#define REGISTER_CAMPAIGND_HANDLER(req_id)
void handle_change_passphrase(const request &)
Templates and utility-routines for strings and numbers.
ADDON_TYPE get_addon_type(const std::string &str)
size_t send_data(const config &cfg, connection connection_num, const std::string &packet_type)
Function to send data down a given connection, or broadcast to all peers if connection_num is 0...
bool addon_name_legal(const std::string &name)
Checks whether an add-on id/name is legal or not.
void send_error(const std::string &msg, network::connection sock)
Send a client an error message.
void write_config()
Writes the server configuration WML back to disk.
boost::scoped_ptr< input_stream > input_
Server control socket.
std::map< std::string, tfilter >::iterator itor
Thrown by operations encountering invalid UTF-8 data.
const std::string revision
Represents a server control line written to a communication socket.
void handle_request_campaign(const request &)
An exception object used when an IO error occurs.
void send_message(const std::string &msg, network::connection sock)
Send a client an informational message.
std::pair< child_iterator, child_iterator > child_itors
int load_config()
Reads the server configuration from WML.
connection accept_connection()
Function to accept a connection from a remote host.
void register_handler(const std::string &cmd, const request_handler &func)
Registers a single request handler.
static void msg(const char *act, debug_info &i, const char *to="", const char *result="")
bool string_bool(const std::string &str, bool def)
Convert no, false, off, 0, 0.0 to false, empty to def, and others to true.
Declarations for File-IO.
void read(config &cfg, std::istream &in, abstract_validator *validator)
static int writer(lua_State *L, const void *b, size_t size, void *B)
bool disconnect(connection s)
Function to disconnect from a certain host, or close all connections if connection_num is 0...
int main(int argc, char **argv)
int file_size(const std::string &fname)
Returns the size of a file, or -1 if the file doesn't exist.
GLuint const GLchar * name
void handle_request_campaign_list(const request &)
std::function< void(server *, const request &req)> request_handler
std::string ip_address(connection connection_num)
Function to get the remote ip address of a socket.
const unsigned short default_campaignd_port
Default port number for the addon server.
config & child(const std::string &key, int n=0)
Returns the nth child with the given key, or a reference to an invalid config if there is none...
Standard logging facilities (interface).
void read(const config &cfg)
Initializes the blacklist from WML.
server(const std::string &cfg_file, size_t min_threads=10, size_t max_threads=0)
GLsizei GLenum GLuint GLuint GLsizei char * message
bool is_text_markup_char(char c)
void set_use_system_sendfile(bool use)
void find_translations(const config &base_dir, config &addon)
Scans an add-on archive directory for translations.
bool check_names_legal(const config &dir)
Probes an add-on archive for illegal names.
A config object defines a single node in a WML file, with access to child nodes.
void run()
Runs the server request processing loop.
std::string format_addon_feedback_url(const std::string &format, const config ¶ms)
Format a feedback URL for an add-on.
Interfaces for manipulating version numbers of engine, add-ons, etc.
void write(std::ostream &out, configr_of const &cfg, unsigned int level)
Thrown when a lexical_cast fails.
void handle_upload(const request &)
GLsizei const GLcharARB ** string
connection const null_connection
config & get_campaign(const std::string &id)
Retrieves a campaign by id if found, or a null config otherwise.
all_children_iterator ordered_begin() const
int compress_level_
Used for add-on archives.