diff --git a/CMakeLists.txt b/CMakeLists.txt index 670a0b8..29b25e9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.10) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(CMAKE_BUILD_TYPE DEBUG) +# set(CMAKE_BUILD_TYPE DEBUG) # set(CMAKE_BUILD_TYPE RELEASE) project(cgproxy VERSION 4.0) diff --git a/cgattach.cpp b/cgattach.cpp index fe8b146..d89ab07 100644 --- a/cgattach.cpp +++ b/cgattach.cpp @@ -4,8 +4,8 @@ using namespace std; void print_usage() { fprintf(stdout, "usage: cgattach \n"); } int main(int argc, char *argv[]) { - int flag=setuid(0); - if (flag!=0) { + int flag = setuid(0); + if (flag != 0) { perror("cgattach need root"); exit(EXIT_FAILURE); } @@ -19,5 +19,5 @@ int main(int argc, char *argv[]) { string pid = string(argv[1]); string cgroup_target = string(argv[2]); - CGPROXY::CGROUP::attach(pid,cgroup_target); + CGPROXY::CGROUP::attach(pid, cgroup_target); } diff --git a/cgnoproxy.cpp b/cgnoproxy.cpp index 8177306..0dcf7ba 100644 --- a/cgnoproxy.cpp +++ b/cgnoproxy.cpp @@ -1,31 +1,32 @@ -#include #include "socket_client.hpp" +#include using json = nlohmann::json; using namespace CGPROXY; -bool attach2cgproxy(){ - pid_t pid=getpid(); - json j; - j["type"] = MSG_TYPE_NOPROXY_PID; - j["data"] = pid; - int status; - SOCKET::send(j.dump(), status); - return status==0; +bool attach2cgproxy() { + pid_t pid = getpid(); + json j; + j["type"] = MSG_TYPE_NOPROXY_PID; + j["data"] = pid; + int status; + SOCKET::send(j.dump(), status); + return status == 0; } -int main(int argc, char *argv[]){ - int shift=1; - if (argc==1){ - error("usage: cgnoproxy [--debug] \nexample: cgnoproxy curl -I https://www.google.com"); - exit(EXIT_FAILURE); - } - processArgs(argc,argv,shift); +int main(int argc, char *argv[]) { + int shift = 1; + if (argc == 1) { + error("usage: cgnoproxy [--debug] \nexample: cgnoproxy curl -I " + "https://www.google.com"); + exit(EXIT_FAILURE); + } + processArgs(argc, argv, shift); - if (!attach2cgproxy()){ - error("attach process failed"); - exit(EXIT_FAILURE); - } + if (!attach2cgproxy()) { + error("attach process failed"); + exit(EXIT_FAILURE); + } - string s=join2str(argc-shift,argv+shift,' '); - return system(s.c_str()); + string s = join2str(argc - shift, argv + shift, ' '); + return system(s.c_str()); } \ No newline at end of file diff --git a/cgproxy.cpp b/cgproxy.cpp index 867eee3..9d72972 100644 --- a/cgproxy.cpp +++ b/cgproxy.cpp @@ -1,31 +1,32 @@ -#include #include "socket_client.hpp" +#include using json = nlohmann::json; using namespace CGPROXY; -bool attach2cgproxy(){ - pid_t pid=getpid(); - json j; - j["type"] = MSG_TYPE_PROXY_PID; - j["data"] = pid; - int status; - SOCKET::send(j.dump(), status); - return status==0; +bool attach2cgproxy() { + pid_t pid = getpid(); + json j; + j["type"] = MSG_TYPE_PROXY_PID; + j["data"] = pid; + int status; + SOCKET::send(j.dump(), status); + return status == 0; } -int main(int argc, char *argv[]){ - int shift=1; - if (argc==1){ - error("usage: cgproxy [--debug] \nexample: cgroxy curl -I https://www.google.com"); - exit(EXIT_FAILURE); - } - processArgs(argc,argv,shift); - - if (!attach2cgproxy()){ - error("attach process failed"); - exit(EXIT_FAILURE); - } +int main(int argc, char *argv[]) { + int shift = 1; + if (argc == 1) { + error( + "usage: cgproxy [--debug] \nexample: cgroxy curl -I https://www.google.com"); + exit(EXIT_FAILURE); + } + processArgs(argc, argv, shift); - string s=join2str(argc-shift,argv+shift,' '); - return system(s.c_str()); + if (!attach2cgproxy()) { + error("attach process failed"); + exit(EXIT_FAILURE); + } + + string s = join2str(argc - shift, argv + shift, ' '); + return system(s.c_str()); } \ No newline at end of file diff --git a/cgproxyd.cpp b/cgproxyd.cpp index 65effaf..06aeade 100644 --- a/cgproxyd.cpp +++ b/cgproxyd.cpp @@ -1,17 +1,16 @@ +#include "cgroup_attach.hpp" #include "common.hpp" +#include "config.hpp" #include "socket_server.hpp" +#include #include #include -#include #include #include #include #include #include #include -#include -#include "config.hpp" -#include "cgroup_attach.hpp" using namespace std; using json = nlohmann::json; @@ -19,66 +18,69 @@ using namespace CGPROXY::SOCKET; using namespace CGPROXY::CONFIG; using namespace CGPROXY::CGROUP; -namespace CGPROXY{ +namespace CGPROXY { -class cgproxyd{ +class cgproxyd { thread_arg arg_t; Config config; pthread_t socket_thread_id = -1; - static cgproxyd* instance; - static int handle_msg_static(char* msg){ + static cgproxyd *instance; + static int handle_msg_static(char *msg) { if (!instance) { error("no cgproxyd instance assigned"); return ERROR; } return instance->handle_msg(msg); } - static void signalHandler( int signum ){ - debug("Signal %d received.", &signum); - if (!instance){ error("no cgproxyd instance assigned");} - else { instance->stop(); } + static void signalHandler(int signum) { + debug("Signal %d received.", signum); + if (!instance) { + error("no cgproxyd instance assigned"); + } else { + instance->stop(); + } exit(signum); } int handle_msg(char *msg) { debug("received msg: %s", msg); json j; - try{ j = json::parse(msg); }catch(exception& e){debug("msg paser error");return MSG_ERROR;} + try { + j = json::parse(msg); + } catch (exception &e) { + debug("msg paser error"); + return MSG_ERROR; + } int type, status; int pid, cgroup_target; try { type = j.at("type").get(); - switch (type) - { + switch (type) { case MSG_TYPE_JSON: - status=config.loadFromJson(j.at("data")); - if (status==SUCCESS) status=applyConfig(&config); + status = config.loadFromJson(j.at("data")); + if (status == SUCCESS) status = applyConfig(&config); return status; break; case MSG_TYPE_CONFIG_PATH: - status=config.loadFromFile(j.at("data").get()); - if (status==SUCCESS) status=applyConfig(&config); + status = config.loadFromFile(j.at("data").get()); + if (status == SUCCESS) status = applyConfig(&config); return status; break; case MSG_TYPE_PROXY_PID: - pid=j.at("data").get(); - status=attach(pid, config.cgroup_proxy_preserved); + pid = j.at("data").get(); + status = attach(pid, config.cgroup_proxy_preserved); return status; break; case MSG_TYPE_NOPROXY_PID: - pid=j.at("data").get(); - status=attach(pid, config.cgroup_noproxy_preserved); + pid = j.at("data").get(); + status = attach(pid, config.cgroup_noproxy_preserved); return status; break; - default: - return MSG_ERROR; - break; + default: return MSG_ERROR; break; }; - } catch (out_of_range &e) { - return MSG_ERROR; - } catch (exception &e){ + } catch (out_of_range &e) { return MSG_ERROR; } catch (exception &e) { return ERROR; } } @@ -86,25 +88,21 @@ class cgproxyd{ pthread_t startSocketListeningThread() { arg_t.handle_msg = &handle_msg_static; pthread_t thread_id; - int status = - pthread_create(&thread_id, NULL, &SocketServer::startThread, &arg_t); - if (status != 0) - error("socket thread create failed"); + int status = pthread_create(&thread_id, NULL, &SocketServer::startThread, &arg_t); + if (status != 0) error("socket thread create failed"); return thread_id; } - void assignStaticInstance(){ - instance=this; - } + void assignStaticInstance() { instance = this; } - public: - int start(int argc, char* argv[]) { +public: + int start(int argc, char *argv[]) { signal(SIGINT, &signalHandler); - signal(SIGTERM,&signalHandler); - signal(SIGHUP,&signalHandler); + signal(SIGTERM, &signalHandler); + signal(SIGHUP, &signalHandler); - int shift=1; - processArgs(argc,argv,shift); + int shift = 1; + processArgs(argc, argv, shift); config.loadFromFile(DEFAULT_CONFIG_FILE); applyConfig(&config); @@ -121,20 +119,18 @@ class cgproxyd{ // no need to track running status return 0; } - void stop(){ + void stop() { debug("stopping"); system(TPROXY_IPTABLS_CLEAN); } - ~cgproxyd(){ - stop(); - } + ~cgproxyd() { stop(); } }; -cgproxyd* cgproxyd::instance=NULL; +cgproxyd *cgproxyd::instance = NULL; -} +} // namespace CGPROXY -int main(int argc, char* argv[]) { +int main(int argc, char *argv[]) { CGPROXY::cgproxyd d; - return d.start(argc,argv); + return d.start(argc, argv); } \ No newline at end of file diff --git a/cgroup_attach.hpp b/cgroup_attach.hpp index 06fdeb5..7b52bb5 100644 --- a/cgroup_attach.hpp +++ b/cgroup_attach.hpp @@ -1,66 +1,63 @@ #ifndef CGPROUP_ATTACH_H #define CGPROUP_ATTACH_H +#include "common.hpp" #include #include #include #include +#include #include #include #include #include #include -#include -#include "common.hpp" using namespace std; -namespace CGPROXY::CGROUP{ +namespace CGPROXY::CGROUP { bool exist(string path) { struct stat st; - if (stat(path.c_str(), &st) != -1) { - return S_ISDIR(st.st_mode); - } + if (stat(path.c_str(), &st) != -1) { return S_ISDIR(st.st_mode); } return false; } bool validate(string pid, string cgroup) { bool pid_v = validPid(pid); bool cg_v = validCgroup(cgroup); - if (pid_v && cg_v) - return true; - + if (pid_v && cg_v) return true; + error("attach paramater validate error"); return_error } -string get_cgroup2_mount_point(int &status){ - char cgroup2_mount_point[100]=""; - FILE* fp = popen("findmnt -t cgroup2 -n -o TARGET", "r"); - int count=fscanf(fp,"%s",&cgroup2_mount_point); +string get_cgroup2_mount_point(int &status) { + char cgroup2_mount_point[100] = ""; + FILE *fp = popen("findmnt -t cgroup2 -n -o TARGET", "r"); + int count = fscanf(fp, "%s", &cgroup2_mount_point); fclose(fp); - if (count=0){ + if (count = 0) { error("cgroup2 not supported"); - status=-1; + status = -1; return NULL; } - status=0; + status = 0; return cgroup2_mount_point; } int attach(const string pid, const string cgroup_target) { - if (getuid()!=0) { + if (getuid() != 0) { error("need root to attach cgroup"); return_error } - debug("attaching %s to %s",pid.c_str(),cgroup_target.c_str()); - + debug("attaching %s to %s", pid.c_str(), cgroup_target.c_str()); + int status; - if (!validate(pid, cgroup_target)) return_error - string cgroup_mount_point = get_cgroup2_mount_point(status); - if (status!=0) return_error - string cgroup_target_path = cgroup_mount_point + cgroup_target; + if (!validate(pid, cgroup_target)) + return_error string cgroup_mount_point = get_cgroup2_mount_point(status); + if (status != 0) + return_error string cgroup_target_path = cgroup_mount_point + cgroup_target; string cgroup_target_procs = cgroup_target_path + "/cgroup.procs"; // check if exist, we will create it if not exist @@ -87,17 +84,17 @@ int attach(const string pid, const string cgroup_target) { // maybe there some write error, for example process pid may not exist if (!procs) { - error("write %s to %s failed, maybe process %s not exist", - pid.c_str(), cgroup_target_procs.c_str(), pid.c_str()); + error("write %s to %s failed, maybe process %s not exist", pid.c_str(), + cgroup_target_procs.c_str(), pid.c_str()); return_error } return_success } -int attach(const int pid, const string cgroup_target){ +int attach(const int pid, const string cgroup_target) { return attach(to_str(pid), cgroup_target); } -} +} // namespace CGPROXY::CGROUP #endif \ No newline at end of file diff --git a/common.hpp b/common.hpp index 07fe998..f891b88 100644 --- a/common.hpp +++ b/common.hpp @@ -27,39 +27,38 @@ #define CGROUP_ERROR 6 #define FILE_ERROR 7 - #include +#include #include #include -#include using namespace std; -static bool enable_debug=false; -static bool print_help=false; +static bool enable_debug = false; +static bool print_help = false; -#define error(...) {fprintf(stderr, __VA_ARGS__);fprintf(stderr, "\n");} -#define debug(...) if (enable_debug) {fprintf(stdout, __VA_ARGS__);fprintf(stdout, "\n");} +#define error(...) \ + { \ + fprintf(stderr, __VA_ARGS__); \ + fprintf(stderr, "\n"); \ + } +#define debug(...) \ + if (enable_debug) { \ + fprintf(stdout, __VA_ARGS__); \ + fprintf(stdout, "\n"); \ + } #define return_error return -1; #define return_success return 0; - -void processArgs(const int argc, char *argv[], int &shift){ - for (int i=1;i -string to_str(T... args) { +template string to_str(T... args) { stringstream ss; ss.clear(); ss << std::boolalpha; @@ -67,41 +66,34 @@ string to_str(T... args) { return ss.str(); } -string join2str(const vector t, const char delm=' '){ - string s; - for (const auto &e : t) - e!=*(t.end()-1)?s+=e+delm:s+=e; - return s; +string join2str(const vector t, const char delm = ' ') { + string s; + for (const auto &e : t) e != *(t.end() - 1) ? s += e + delm : s += e; + return s; } -string join2str(const int argc,char** argv, const char delm=' '){ - string s; - for (int i=0;i cgroup){ - for (auto &e:cgroup){ - if (!regex_match(e, regex("^/[a-zA-Z0-9\\-_./@]*$"))){ - return false; - } +bool validCgroup(const vector cgroup) { + for (auto &e : cgroup) { + if (!regex_match(e, regex("^/[a-zA-Z0-9\\-_./@]*$"))) { return false; } } return true; } -bool validPid(const string pid){ - return regex_match(pid, regex("^[0-9]+$")); -} +bool validPid(const string pid) { return regex_match(pid, regex("^[0-9]+$")); } -bool validPort(const int port){ - return port>0; -} +bool validPort(const int port) { return port > 0; } #endif \ No newline at end of file diff --git a/config.hpp b/config.hpp index 2e8fd48..42c22d2 100644 --- a/config.hpp +++ b/config.hpp @@ -1,28 +1,27 @@ #ifndef CONFIG_H #define CONFIG_H #include "common.hpp" -#include "socket_server.hpp" #include +#include #include #include -#include +#include #include #include #include #include #include -#include -#include using namespace std; using json = nlohmann::json; -namespace CGPROXY::CONFIG{ +namespace CGPROXY::CONFIG { struct Config { - public: - const string cgroup_proxy_preserved=CGROUP_PROXY_PRESVERED; - const string cgroup_noproxy_preserved=CGROUP_NOPROXY_PRESVERED; - private: +public: + const string cgroup_proxy_preserved = CGROUP_PROXY_PRESVERED; + const string cgroup_noproxy_preserved = CGROUP_NOPROXY_PRESVERED; + +private: vector cgroup_proxy; vector cgroup_noproxy; bool enable_gateway = false; @@ -36,7 +35,7 @@ struct Config { public: void toEnv() { mergeReserved(); - setenv("cgroup_proxy", join2str(cgroup_proxy,':').c_str(), 1); + setenv("cgroup_proxy", join2str(cgroup_proxy, ':').c_str(), 1); setenv("cgroup_noproxy", join2str(cgroup_noproxy, ':').c_str(), 1); setenv("enable_gateway", to_str(enable_gateway).c_str(), 1); setenv("port", to_str(port).c_str(), 1); @@ -47,48 +46,60 @@ public: setenv("enable_ipv6", to_str(enable_ipv6).c_str(), 1); } - int saveToFile(const string f){ - ofstream o(f); - if (!o.is_open()) return FILE_ERROR; - json j=toJson(); - o << setw(4) << j << endl; - o.close(); - return 0; + int saveToFile(const string f) { + ofstream o(f); + if (!o.is_open()) return FILE_ERROR; + json j = toJson(); + o << setw(4) << j << endl; + o.close(); + return 0; } - json toJson(){ - json j; - #define add2json(v) j[#v]=v; - add2json(cgroup_proxy); - add2json(cgroup_noproxy); - add2json(enable_gateway); - add2json(port); - add2json(enable_dns); - add2json(enable_tcp); - add2json(enable_udp); - add2json(enable_ipv4); - add2json(enable_ipv6); - #undef add2json - return j; +#define add2json(v) j[#v] = v; + json toJson() { + json j; + add2json(cgroup_proxy); + add2json(cgroup_noproxy); + add2json(enable_gateway); + add2json(port); + add2json(enable_dns); + add2json(enable_tcp); + add2json(enable_udp); + add2json(enable_ipv4); + add2json(enable_ipv6); + return j; } +#undef add2json int loadFromFile(const string f) { debug("loading config: %s", f.c_str()); ifstream ifs(f); - if (ifs.is_open()){ + if (ifs.is_open()) { json j; - try { ifs >> j; }catch (exception& e){error("parse error: %s", f.c_str());ifs.close();return PARSE_ERROR;} + try { + ifs >> j; + } catch (exception &e) { + error("parse error: %s", f.c_str()); + ifs.close(); + return PARSE_ERROR; + } ifs.close(); return loadFromJson(j); - }else{ - error("open failed: %s",f.c_str()); + } else { + error("open failed: %s", f.c_str()); return FILE_ERROR; } } +#define tryassign(v) \ + try { \ + j.at(#v).get_to(v); \ + } catch (exception & e) {} int loadFromJson(const json &j) { - if (!validateJson(j)) {error("json validate fail"); return PARAM_ERROR;} - #define tryassign(v) try{j.at(#v).get_to(v);}catch(exception &e){} + if (!validateJson(j)) { + error("json validate fail"); + return PARAM_ERROR; + } tryassign(cgroup_proxy); tryassign(cgroup_noproxy); tryassign(enable_gateway); @@ -98,35 +109,36 @@ public: tryassign(enable_udp); tryassign(enable_ipv4); tryassign(enable_ipv6); - #undef assign return 0; } +#undef assign - void mergeReserved(){ - #define merge(v) { \ - v.erase(std::remove(v.begin(), v.end(), v ## _preserved), v.end()); \ - v.insert(v.begin(), v ## _preserved); \ - } +#define merge(v) \ + { \ + v.erase(std::remove(v.begin(), v.end(), v##_preserved), v.end()); \ + v.insert(v.begin(), v##_preserved); \ + } + void mergeReserved() { merge(cgroup_proxy); merge(cgroup_noproxy); - #undef merge - } +#undef merge - bool validateJson(const json &j){ - bool status=true; - const set boolset={"enable_gateway","enable_dns","enable_tcp","enable_udp","enable_ipv4","enable_ipv6"}; - for (auto& [key, value] : j.items()) { - if (key=="cgroup_proxy"||key=="cgroup_noproxy"){ - if (value.is_string()&&!validCgroup((string)value)) status=false; + bool validateJson(const json &j) { + bool status = true; + const set boolset = {"enable_gateway", "enable_dns", "enable_tcp", + "enable_udp", "enable_ipv4", "enable_ipv6"}; + for (auto &[key, value] : j.items()) { + if (key == "cgroup_proxy" || key == "cgroup_noproxy") { + if (value.is_string() && !validCgroup((string)value)) status = false; // TODO what if vector etc. - if (value.is_array()&&!validCgroup((vector)value)) status=false; - if (!value.is_string()&&!value.is_array()) status=false; - }else if (key=="port"){ - if (!validPort(value)) status=false; - }else if (boolset.find(key)!=boolset.end()){ - if (!value.is_boolean()) status=false; - }else{ + if (value.is_array() && !validCgroup((vector)value)) status = false; + if (!value.is_string() && !value.is_array()) status = false; + } else if (key == "port") { + if (!validPort(value)) status = false; + } else if (boolset.find(key) != boolset.end()) { + if (!value.is_boolean()) status = false; + } else { error("unknown key: %s", key.c_str()); return false; } @@ -139,5 +151,5 @@ public: } }; -} +} // namespace CGPROXY::CONFIG #endif \ No newline at end of file diff --git a/socket_client.hpp b/socket_client.hpp index ce92128..bedbc7f 100644 --- a/socket_client.hpp +++ b/socket_client.hpp @@ -1,6 +1,7 @@ #ifndef SOCKET_CLIENT_H #define SOCKET_CLIENT_H +#include "common.hpp" #include #include #include @@ -9,18 +10,17 @@ #include #include #include -#include "common.hpp" using namespace std; -namespace CGPROXY::SOCKET{ +namespace CGPROXY::SOCKET { -#define return_if_error(flag, msg) \ - if (flag == -1) { \ - perror(msg); \ - status = CONN_ERROR; \ - close(sfd); \ - return; \ +#define return_if_error(flag, msg) \ + if (flag == -1) { \ + perror(msg); \ + status = CONN_ERROR; \ + close(sfd); \ + return; \ } void send(const char *msg, int &status) { @@ -35,8 +35,7 @@ void send(const char *msg, int &status) { unix_socket.sun_family = AF_UNIX; strncpy(unix_socket.sun_path, SOCKET_PATH, sizeof(unix_socket.sun_path) - 1); - flag = - connect(sfd, (struct sockaddr *)&unix_socket, sizeof(struct sockaddr_un)); + flag = connect(sfd, (struct sockaddr *)&unix_socket, sizeof(struct sockaddr_un)); return_if_error(flag, "connect"); int msg_len = strlen(msg); @@ -60,5 +59,5 @@ void send(const string msg, int &status) { debug("return status: %d", status); } -} +} // namespace CGPROXY::SOCKET #endif \ No newline at end of file diff --git a/socket_server.hpp b/socket_server.hpp index a37a6f3..534b689 100644 --- a/socket_server.hpp +++ b/socket_server.hpp @@ -1,6 +1,8 @@ #ifndef SOCKET_SERVER_H #define SOCKET_SERVER_H +#include "common.hpp" +#include #include #include #include @@ -8,21 +10,19 @@ #include #include #include +#include #include #include #include -#include -#include -#include "common.hpp" using namespace std; namespace fs = std::filesystem; -namespace CGPROXY::SOCKET{ +namespace CGPROXY::SOCKET { -#define continue_if_error(flag, msg) \ - if (flag == -1) { \ - perror(msg); \ - continue; \ +#define continue_if_error(flag, msg) \ + if (flag == -1) { \ + perror(msg); \ + continue; \ } struct thread_arg { @@ -38,19 +38,18 @@ public: debug("starting socket listening"); sfd = socket(AF_UNIX, SOCK_STREAM, 0); - if (fs::exists(SOCKET_PATH)&&unlink(SOCKET_PATH)==-1){ - error("%s exist, and can't unlink",SOCKET_PATH); + if (fs::exists(SOCKET_PATH) && unlink(SOCKET_PATH) == -1) { + error("%s exist, and can't unlink", SOCKET_PATH); return; } memset(&unix_socket, '\0', sizeof(struct sockaddr_un)); unix_socket.sun_family = AF_UNIX; - strncpy(unix_socket.sun_path, SOCKET_PATH, - sizeof(unix_socket.sun_path) - 1); + strncpy(unix_socket.sun_path, SOCKET_PATH, sizeof(unix_socket.sun_path) - 1); bind(sfd, (struct sockaddr *)&unix_socket, sizeof(struct sockaddr_un)); listen(sfd, LISTEN_BACKLOG); - chmod(SOCKET_PATH,S_IRWXU|S_IRWXG|S_IRWXO); + chmod(SOCKET_PATH, S_IRWXU | S_IRWXG | S_IRWXO); while (true) { close(cfd); @@ -66,7 +65,7 @@ public: char msg[msg_len]; flag = read(cfd, msg, msg_len * sizeof(char)); continue_if_error(flag, "read msg"); - msg[msg_len]='\0'; + msg[msg_len] = '\0'; // handle msg int status = callback(msg); // send back flag @@ -89,6 +88,6 @@ public: } }; -} +} // namespace CGPROXY::SOCKET #endif \ No newline at end of file