Skip to content

Commit

Permalink
webserver: Add arbitary file upload support (#139)
Browse files Browse the repository at this point in the history
This also supports automatically extracting .zip and .tar.gz files.
  • Loading branch information
PeterJohnson authored Nov 2, 2019
1 parent 642c3ff commit 0d56919
Show file tree
Hide file tree
Showing 8 changed files with 314 additions and 60 deletions.
1 change: 1 addition & 0 deletions deps/tools/configServer/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ OBJS= \
src/MyHttpConnection.o \
src/NetworkSettings.o \
src/SystemStatus.o \
src/UploadHelper.o \
src/VisionSettings.o \
src/VisionStatus.o \
src/WebSocketHandlers.o \
Expand Down
44 changes: 6 additions & 38 deletions deps/tools/configServer/src/Application.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include <wpi/raw_istream.h>
#include <wpi/raw_ostream.h>

#include "UploadHelper.h"
#include "VisionStatus.h"

#define TYPE_TAG "### TYPE:"
Expand Down Expand Up @@ -87,41 +88,7 @@ void Application::Set(wpi::StringRef appType,
UpdateStatus();
}

int Application::StartUpload(wpi::StringRef appType, char* filename,
std::function<void(wpi::StringRef)> onFail) {
int fd = mkstemp(filename);
if (fd < 0) {
wpi::SmallString<64> msg;
msg = "could not open temporary file: ";
msg += std::strerror(errno);
onFail(msg);
}
return fd;
}

void Application::Upload(int fd, bool text, wpi::ArrayRef<uint8_t> contents) {
// write contents
wpi::raw_fd_ostream out(fd, false);
if (text) {
wpi::StringRef str(reinterpret_cast<const char*>(contents.data()),
contents.size());
// convert any Windows EOL to Unix
for (;;) {
size_t idx = str.find("\r\n");
if (idx == wpi::StringRef::npos) break;
out << str.slice(0, idx) << '\n';
str = str.slice(idx + 2, wpi::StringRef::npos);
}
out << str;
// ensure file ends with EOL
if (!str.empty() && str.back() != '\n') out << '\n';
} else {
out << contents;
}
}

void Application::FinishUpload(wpi::StringRef appType, int fd,
const char* tmpFilename,
void Application::FinishUpload(wpi::StringRef appType, UploadHelper& helper,
std::function<void(wpi::StringRef)> onFail) {
wpi::StringRef filename;
if (appType == "upload-java") {
Expand All @@ -136,13 +103,14 @@ void Application::FinishUpload(wpi::StringRef appType, int fd,
msg += appType;
msg += "'";
onFail(msg);
::close(fd);
helper.Close();
return;
}

wpi::SmallString<64> pathname;
pathname = EXEC_HOME;
pathname += filename;
int fd = helper.GetFD();

// change ownership
if (fchown(fd, APP_UID, APP_GID) == -1) {
Expand All @@ -157,7 +125,7 @@ void Application::FinishUpload(wpi::StringRef appType, int fd,
}

// close temporary file
::close(fd);
helper.Close();

// remove old file (need to do this as we can't overwrite a running exe)
if (unlink(pathname.c_str()) == -1) {
Expand All @@ -166,7 +134,7 @@ void Application::FinishUpload(wpi::StringRef appType, int fd,
}

// rename temporary file to new file
if (rename(tmpFilename, pathname.c_str()) == -1) {
if (rename(helper.GetFilename(), pathname.c_str()) == -1) {
wpi::errs() << "could not rename to app executable: "
<< std::strerror(errno) << '\n';
}
Expand Down
7 changes: 3 additions & 4 deletions deps/tools/configServer/src/Application.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ namespace wpi {
class json;
} // namespace wpi

class UploadHelper;

class Application {
struct private_init {};

Expand All @@ -30,10 +32,7 @@ class Application {

void Set(wpi::StringRef appType, std::function<void(wpi::StringRef)> onFail);

int StartUpload(wpi::StringRef appType, char* filename,
std::function<void(wpi::StringRef)> onFail);
void Upload(int fd, bool text, wpi::ArrayRef<uint8_t> contents);
void FinishUpload(wpi::StringRef appType, int fd, const char* tmpFilename,
void FinishUpload(wpi::StringRef appType, UploadHelper& helper,
std::function<void(wpi::StringRef)> onFail);

void UpdateStatus();
Expand Down
81 changes: 81 additions & 0 deletions deps/tools/configServer/src/UploadHelper.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*----------------------------------------------------------------------------*/
/* Copyright (c) 2019 FIRST. All Rights Reserved. */
/* Open Source Software - may be modified and shared by FRC teams. The code */
/* must be accompanied by the FIRST BSD license file in the root directory of */
/* the project. */
/*----------------------------------------------------------------------------*/

#include "UploadHelper.h"

#include <fcntl.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>

#include <wpi/raw_ostream.h>

UploadHelper::UploadHelper(UploadHelper&& oth)
: m_filename{std::move(oth.m_filename)},
m_fd{oth.m_fd},
m_text{oth.m_text} {
oth.m_fd = -1;
}

UploadHelper& UploadHelper::operator=(UploadHelper&& oth) {
m_filename = std::move(oth.m_filename);
m_fd = oth.m_fd;
oth.m_fd = -1;
m_text = oth.m_text;
return *this;
}

bool UploadHelper::Open(wpi::StringRef filename, bool text,
std::function<void(wpi::StringRef)> onFail) {
m_text = text;
m_hasEol = true;
m_filename = filename;
// make it a C string
m_filename.push_back(0);
m_filename.pop_back();

m_fd = mkstemp(m_filename.data());
if (m_fd < 0) {
wpi::SmallString<64> msg;
msg = "could not open temporary file: ";
msg += std::strerror(errno);
onFail(msg);
}
return m_fd >= 0;
}

void UploadHelper::Write(wpi::ArrayRef<uint8_t> contents) {
if (m_fd < 0) return;
// write contents
wpi::raw_fd_ostream out(m_fd, false);
if (m_text) {
wpi::StringRef str(reinterpret_cast<const char*>(contents.data()),
contents.size());
// convert any Windows EOL to Unix
for (;;) {
size_t idx = str.find("\r\n");
if (idx == wpi::StringRef::npos) break;
out << str.slice(0, idx) << '\n';
str = str.slice(idx + 2, wpi::StringRef::npos);
}
out << str;
m_hasEol == str.empty() || str.back() == '\n';
} else {
out << contents;
}
}

void UploadHelper::Close() {
if (m_fd < 0) return;
// ensure text file ends with EOL
if (m_text && !m_hasEol) {
wpi::raw_fd_ostream out(m_fd, false);
out << '\n';
}
::close(m_fd);
m_fd = -1;
}
47 changes: 47 additions & 0 deletions deps/tools/configServer/src/UploadHelper.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*----------------------------------------------------------------------------*/
/* Copyright (c) 2019 FIRST. All Rights Reserved. */
/* Open Source Software - may be modified and shared by FRC teams. The code */
/* must be accompanied by the FIRST BSD license file in the root directory of */
/* the project. */
/*----------------------------------------------------------------------------*/

#ifndef RPICONFIGSERVER_UPLOADHELPER_H_
#define RPICONFIGSERVER_UPLOADHELPER_H_

#include <stdint.h>

#include <functional>

#include <wpi/ArrayRef.h>
#include <wpi/SmallString.h>
#include <wpi/StringRef.h>

class UploadHelper {
public:
UploadHelper() = default;
~UploadHelper() { Close(); }

UploadHelper(const UploadHelper&) = delete;
UploadHelper& operator=(const UploadHelper&) = delete;

UploadHelper(UploadHelper&& oth);
UploadHelper& operator=(UploadHelper&& oth);

explicit operator bool() const { return m_fd != -1; }

const char* GetFilename() { return m_filename.c_str(); }
int GetFD() const { return m_fd; }

bool Open(wpi::StringRef filename, bool text,
std::function<void(wpi::StringRef)> onFail);
void Write(wpi::ArrayRef<uint8_t> contents);
void Close();

private:
wpi::SmallString<128> m_filename;
int m_fd = -1;
bool m_text;
bool m_hasEol;
};

#endif // RPICONFIGSERVER_UPLOADHELPER_H_
103 changes: 86 additions & 17 deletions deps/tools/configServer/src/WebSocketHandlers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include "Application.h"
#include "NetworkSettings.h"
#include "SystemStatus.h"
#include "UploadHelper.h"
#include "VisionSettings.h"
#include "VisionStatus.h"

Expand All @@ -32,14 +33,9 @@ namespace uv = wpi::uv;
#define SERVICE "/service/camera"

struct WebSocketData {
~WebSocketData() {
if (uploadFd != -1) ::close(uploadFd);
}

bool visionLogEnabled = false;
int uploadFd = -1;
bool uploadText = false;
char uploadFilename[128];

UploadHelper upload;

wpi::sig::ScopedConnection sysStatusConn;
wpi::sig::ScopedConnection sysWritableConn;
Expand Down Expand Up @@ -294,23 +290,96 @@ void ProcessWsText(wpi::WebSocket& ws, wpi::StringRef msg) {
Application::GetInstance()->Set(appType, statusFunc);
} else if (subType == "StartUpload") {
auto d = ws.GetData<WebSocketData>();
std::strcpy(d->uploadFilename, EXEC_HOME "/appUploadXXXXXX");
d->uploadFd = Application::GetInstance()->StartUpload(
appType, d->uploadFilename, statusFunc);
d->uploadText = wpi::StringRef(appType).endswith("python");
d->upload.Open(EXEC_HOME "/appUploadXXXXXX",
wpi::StringRef(appType).endswith("python"), statusFunc);
} else if (subType == "FinishUpload") {
auto d = ws.GetData<WebSocketData>();
if (d->uploadFd != -1)
Application::GetInstance()->FinishUpload(appType, d->uploadFd,
d->uploadFilename, statusFunc);
d->uploadFd = -1;
if (d->upload)
Application::GetInstance()->FinishUpload(appType, d->upload,
statusFunc);
SendWsText(ws, {{"type", "applicationSaveComplete"}});
}
} else if (t.startswith("file")) {
wpi::StringRef subType = t.substr(4);
if (subType == "StartUpload") {
auto statusFunc = [s = ws.shared_from_this()](wpi::StringRef msg) {
SendWsText(*s, {{"type", "status"}, {"message", msg}});
};
auto d = ws.GetData<WebSocketData>();
d->upload.Open(EXEC_HOME "/fileUploadXXXXXX", false, statusFunc);
} else if (subType == "FinishUpload") {
auto d = ws.GetData<WebSocketData>();

// change ownership
if (fchown(d->upload.GetFD(), APP_UID, APP_GID) == -1) {
wpi::errs() << "could not change file ownership: "
<< std::strerror(errno) << '\n';
}
d->upload.Close();

bool extract;
try {
extract = j.at("extract").get<bool>();
} catch (const wpi::json::exception& e) {
wpi::errs() << "could not read extract value: " << e.what() << '\n';
unlink(d->upload.GetFilename());
return;
}

std::string filename;
try {
filename = j.at("fileName").get<std::string>();
} catch (const wpi::json::exception& e) {
wpi::errs() << "could not read fileName value: " << e.what() << '\n';
unlink(d->upload.GetFilename());
return;
}

auto extractSuccess = [](wpi::WebSocket& s) {
auto d = s.GetData<WebSocketData>();
unlink(d->upload.GetFilename());
SendWsText(s, {{"type", "fileUploadComplete"}, {"success", true}});
};
auto extractFailure = [](wpi::WebSocket& s) {
auto d = s.GetData<WebSocketData>();
unlink(d->upload.GetFilename());
wpi::errs() << "could not extract file\n";
SendWsText(s, {{"type", "fileUploadComplete"}});
};

if (extract && wpi::StringRef{filename}.endswith("tar.gz")) {
RunProcess(ws, extractSuccess, extractFailure, "/bin/tar",
uv::Process::Uid(APP_UID), uv::Process::Gid(APP_GID),
uv::Process::Cwd(EXEC_HOME), "/bin/tar", "xzf",
d->upload.GetFilename());
} else if (extract && wpi::StringRef{filename}.endswith("zip")) {
d->upload.Close();
RunProcess(ws, extractSuccess, extractFailure, "/usr/bin/unzip",
uv::Process::Uid(APP_UID), uv::Process::Gid(APP_GID),
uv::Process::Cwd(EXEC_HOME), "/usr/bin/unzip",
d->upload.GetFilename());
} else {
wpi::SmallString<64> pathname;
pathname = EXEC_HOME;
pathname += '/';
pathname += filename;
if (unlink(pathname.c_str()) == -1) {
wpi::errs() << "could not remove file: " << std::strerror(errno)
<< '\n';
}

// rename temporary file to new file
if (rename(d->upload.GetFilename(), pathname.c_str()) == -1) {
wpi::errs() << "could not rename: " << std::strerror(errno) << '\n';
}

SendWsText(ws, {{"type", "fileUploadComplete"}});
}
}
}
}

void ProcessWsBinary(wpi::WebSocket& ws, wpi::ArrayRef<uint8_t> msg) {
auto d = ws.GetData<WebSocketData>();
if (d->uploadFd != -1)
Application::GetInstance()->Upload(d->uploadFd, d->uploadText, msg);
if (d->upload) d->upload.Write(msg);
}
Loading

0 comments on commit 0d56919

Please sign in to comment.