Skip to content

Commit

Permalink
Upload and delete certificates using HTTP requests (#109)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Mattias Axelsson <[email protected]>
  • Loading branch information
github-actions[bot] and killenheladagen authored Apr 15, 2024
1 parent 9c18239 commit 6eb12d6
Show file tree
Hide file tree
Showing 11 changed files with 484 additions and 25 deletions.
53 changes: 31 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ Enable TLS:

```sh
curl -s --anyauth -u "root:$DEVICE_PASSWORD" \
"http://$DEVICE_IP/axis-cgi/param.cgi?action=update&root.dockerdwrapper.UseTLS=yes"
"http://$DEVICE_IP/axis-cgi/param.cgi?action=update&root.dockerdwrapperwithcompose.UseTLS=yes"
```

Enable TCP Socket:
Expand All @@ -116,34 +116,43 @@ Running the ACAP without TLS requires no further setup.

### TLS Setup

TLS requires a few keys and certificates to work, which are listed in the
subsections below. For more information on how to generate these files, please
consult the official [Docker documentation](https://docs.docker.com/engine/security/protect-access/).
Most of these keys and certificates need to be moved to the Axis device. There are multiple ways to
achieve this, for example by using `scp` to copy the files from a remote machine onto the device.
This can be done by running the following command on the remote machine:
TLS requires the following keys and certificates on the device:

```sh
scp ca.pem server-cert.pem server-key.pem root@<device ip>:/usr/local/packages/dockerdwrapperwithcompose/localdata/
```
* Certificate Authority certificate `ca.pem`
* Server certificate `server-cert.pem`
* Private server key `server-key.pem`

#### The Certificate Authority (CA) certificate
For more information on how to generate these files, please consult the official
[Docker documentation](https://docs.docker.com/engine/security/protect-access/).

This certificate needs to be present in the dockerdwrapperwithcompose package folder on the
Axis device and be named `ca.pem`. The full path of the file should be
`/usr/local/packages/dockerdwrapperwithcompose/localdata/ca.pem`.
The files can be uploaded to the device using HTTP.

#### The server certificate
```sh
curl --anyauth -u "root:$DEVICE_PASSWORD" -F [email protected] -X POST \
http://$DEVICE_IP/local/dockerdwrapperwithcompose/ca.pem
curl --anyauth -u "root:$DEVICE_PASSWORD" -F [email protected] -X POST \
http://$DEVICE_IP/local/dockerdwrapperwithcompose/server-cert.pem
curl --anyauth -u "root:$DEVICE_PASSWORD" -F [email protected] -X POST \
http://$DEVICE_IP/local/dockerdwrapperwithcompose/server-key.pem
```

This certificate needs to be present in the dockerdwrapperwithcompose package folder on the
Axis device and be named `server-cert.pem`. The full path of the file should be
`/usr/local/packages/dockerdwrapperwithcompose/localdata/server-cert.pem`.
If desired, they can be deleted from the device using:

#### The private server key
```sh
curl --anyauth -u "root:$DEVICE_PASSWORD" -X DELETE \
http://$DEVICE_IP/local/dockerdwrapperwithcompose/ca.pem
curl --anyauth -u "root:$DEVICE_PASSWORD" -X DELETE \
http://$DEVICE_IP/local/dockerdwrapperwithcompose/server-cert.pem
curl --anyauth -u "root:$DEVICE_PASSWORD" -X DELETE \
http://$DEVICE_IP/local/dockerdwrapperwithcompose/server-key.pem
```

They can also be copied to the `/usr/local/packages/dockerdwrapperwithcompose/localdata`
directory of the device using `scp`.

This key needs to be present in the dockerdwrapperwithcompose package folder on the Axis device
and be named `server-key.pem`. The full path of the file should be
`/usr/local/packages/dockerdwrapperwithcompose/localdata/server-key.pem`.
```sh
scp ca.pem server-cert.pem server-key.pem root@<device ip>:/usr/local/packages/dockerdwrapperwithcompose/localdata/
```

#### Client key and certificate

Expand Down
9 changes: 6 additions & 3 deletions app/Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PROG1 = dockerdwrapperwithcompose
OBJS1 = $(PROG1).o log.o sd_disk_storage.o tls.o
OBJS1 = $(PROG1).o fcgi_server.o fcgi_write_file_from_stream.o http_request.o log.o sd_disk_storage.o tls.o

PKGS = gio-2.0 glib-2.0 axparameter axstorage
PKGS = gio-2.0 glib-2.0 axparameter axstorage fcgi
CFLAGS += $(shell PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) pkg-config --cflags $(PKGS))
LDLIBS += $(shell PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) pkg-config --libs $(PKGS))

Expand All @@ -16,7 +16,10 @@ $(PROG1): $(OBJS1)
$(CC) $(CFLAGS) $(LDFLAGS) $^ $(LIBS) $(LDLIBS) -o $@

$(PROG1).o tls.o: app_paths.h
$(PROG1).o log.o sd_disk_storage.o tls.o: log.h
$(PROG1).o fcgi_server.o: fcgi_server.h
fcgi_server.o fcgi_write_file_from_stream.o: fcgi_write_file_from_stream.h
$(PROG1).o fcgi_server.o http_request.o log.o sd_disk_storage.o tls.o: log.h
$(PROG1).o http_request.o: http_request.h
$(PROG1).o sd_disk_storage.o: sd_disk_storage.h
$(PROG1).o tls.o: tls.h

Expand Down
8 changes: 8 additions & 0 deletions app/dockerdwrapperwithcompose.c
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

#define _GNU_SOURCE // For sigabbrev_np()
#include "app_paths.h"
#include "fcgi_server.h"
#include "http_request.h"
#include "log.h"
#include "sd_disk_storage.h"
#include "tls.h"
Expand Down Expand Up @@ -793,6 +795,10 @@ int main(int argc, char** argv) {

init_signals();

int fcgi_error = fcgi_start(http_request_callback);
if (fcgi_error)
return fcgi_error;

struct sd_disk_storage* sd_disk_storage = sd_disk_storage_init(sd_card_callback, &app_state);

while (application_exit_code == EX_KEEP_RUNNING) {
Expand All @@ -807,6 +813,8 @@ int main(int argc, char** argv) {
}
main_loop_unref();

fcgi_stop();

set_status_parameter(app_state.param_handle, STATUS_NOT_STARTED);

if (app_state.param_handle != NULL) {
Expand Down
84 changes: 84 additions & 0 deletions app/fcgi_server.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#include "fcgi_server.h"
#include "log.h"
#include <fcgi_config.h>
#include <fcgi_stdio.h>
#include <glib.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sysexits.h>
#include <unistd.h>

#define FCGI_SOCKET_NAME "FCGI_SOCKET_NAME"

static const char* g_socket_path = NULL;
static int g_socket = -1;
static GThread* g_thread = NULL;

static void* handle_fcgi(void* request_callback) {
GThreadPool* workers = g_thread_pool_new((GFunc)request_callback, NULL, -1, FALSE, NULL);
while (workers) {
FCGX_Request* request = g_malloc0(sizeof(FCGX_Request));
FCGX_InitRequest(request, g_socket, FCGI_FAIL_ACCEPT_ON_INTR);
if (FCGX_Accept_r(request) < 0) {
log_info("FCGX_Accept_r: %s", strerror(errno));
g_free(request);
break;
}
g_thread_pool_push(workers, request, NULL);
}
log_info("Stopping FCGI server");
g_thread_pool_free(workers, true, false);
return NULL;
}

int fcgi_start(fcgi_request_callback request_callback) {
log_debug("Starting FCGI server");

g_socket_path = getenv(FCGI_SOCKET_NAME);
if (!g_socket_path) {
log_error("Failed to get environment variable FCGI_SOCKET_NAME");
return EX_SOFTWARE;
}

if (FCGX_Init() != 0) {
log_error("FCGX_Init failed: %s", strerror(errno));
return EX_SOFTWARE;
}

if ((g_socket = FCGX_OpenSocket(g_socket_path, 5)) < 0) {
log_error("FCGX_OpenSocket failed: %s", strerror(errno));
return EX_SOFTWARE;
}
chmod(g_socket_path, S_IRWXU | S_IRWXG | S_IRWXO);

/* Create a thread for request handling */
if ((g_thread = g_thread_new("fcgi_server", &handle_fcgi, request_callback)) == NULL) {
log_error("Failed to launch FCGI server thread");
return EX_SOFTWARE;
}

log_debug("Launched FCGI server thread.");
return EX_OK;
}

void fcgi_stop(void) {
log_debug("Stopping FCGI server.");
FCGX_ShutdownPending();

if (g_socket != -1) {
log_debug("Closing and removing FCGI socket.");
if (shutdown(g_socket, SHUT_RD) != 0) {
log_warning("Could not shutdown socket, err: %s", strerror(errno));
}
if (unlink(g_socket_path) != 0) {
log_warning("Could not unlink socket, err: %s", strerror(errno));
}
}
log_debug("Joining FCGI server thread.");
g_thread_join(g_thread);

g_socket_path = NULL;
g_socket = -1;
g_thread = NULL;
log_debug("FCGI server has stopped.");
}
6 changes: 6 additions & 0 deletions app/fcgi_server.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#pragma once

typedef void (*fcgi_request_callback)(void* request_void_ptr, void* userdata);

int fcgi_start(fcgi_request_callback request_callback);
void fcgi_stop(void);
167 changes: 167 additions & 0 deletions app/fcgi_write_file_from_stream.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
#include "fcgi_write_file_from_stream.h"
#include "fcgi_server.h"
#include "log.h"
#include <unistd.h>

static int request_content_length(const FCGX_Request* request) {
const char* content_length_str = FCGX_GetParam("CONTENT_LENGTH", request->envp);
if (!content_length_str)
return 0;
return strtol(content_length_str, NULL, 10);
}

char* fcgi_write_file_from_stream(FCGX_Request request) {
char* temp_file = NULL;
const int content_length = request_content_length(&request);
const char* content_type = FCGX_GetParam("CONTENT_TYPE", request.envp);

log_debug("Content-Type: %s", content_type);

const char* MULTIPART_FORM_DATA = "multipart/form-data";
if (strncmp(content_type, MULTIPART_FORM_DATA, sizeof(MULTIPART_FORM_DATA) - 1) != 0) {
log_error("Content type \"%s\" is not supported. Use \"%s\" instead.",
content_type,
MULTIPART_FORM_DATA);
return NULL;
}

const char* BOUNDARY_KEY = "boundary=";
const char* boundary_text = strstr(content_type, BOUNDARY_KEY);
if (!boundary_text) {
log_error("No multipart boundary found in content-type \"%s\".", content_type);
return NULL;
}
boundary_text += strlen(BOUNDARY_KEY);
const int boundary_len = strlen(boundary_text);

temp_file = g_strdup_printf("/tmp/fcgi_upload.XXXXXX");
int file_des = mkstemp(temp_file);
if (file_des == -1) {
log_error("Failed to create %s, err %s.", temp_file, strerror(errno));
return NULL;
}
log_debug("Opened %s for writing.", temp_file);

bool remove_temp_file = true; // Clear this to return the filename to the caller.

const int bufferLen = 2048;
char buffer[bufferLen + 1 /* Allow for NULL termination */];

const char* data_start = "\r\n\r\n";
const char* data_end = "\r\n--";

int total_bytes_processed = 0;
bool pre_boundary_found = false;
bool post_boundary_found = false;

int loop_counter = 0; // First iteration is special.
char* p_payload = buffer;
char* p_payload_end = NULL;

while (total_bytes_processed < content_length) {
loop_counter++;
const int available_len = bufferLen - (p_payload - buffer);

const int bytes_read = FCGX_GetStr(p_payload, available_len, request.in);
log_debug("FCGX_GetStr: bytes_read %d, p_payload %p(%d), available_len %d(%d)",
bytes_read,
p_payload,
(int)(p_payload - buffer),
available_len,
available_len - bufferLen);
if (bytes_read < 0) {
log_error("Failed to read from FCGI stream: %s", strerror(errno));
break;
}

/* Look for pre boundary */
if (!pre_boundary_found) {
buffer[bytes_read] = 0; /* NULL terminate */
p_payload = strstr(buffer + boundary_len + 1, data_start);
if (p_payload == NULL) {
log_error("Failed to find boundary in uploaded data.");
}
pre_boundary_found = true;
p_payload += strlen(data_start);
} else {
log_debug("Pre boundary already found");
p_payload = buffer;
}

/* Look for post boundary */
if (!post_boundary_found) {
char* pchar;
for (pchar = (loop_counter == 1) ? p_payload : buffer;
pchar < buffer + bytes_read - ((loop_counter == 1) ? boundary_len : 0);
pchar++) {
if (memcmp(pchar, boundary_text, boundary_len) == 0) {
log_debug("Post boundary found for %.*s", boundary_len, pchar);
pchar -= strlen(data_end);
if (memcmp(pchar, data_end, strlen(data_end)) != 0) {
log_error("Post boundary data end not found");
log_debug("Found %02x%02x%02x%02x at post boundary",
pchar[0],
pchar[1],
pchar[2],
pchar[3]);
goto end;
}
post_boundary_found = true;
break;
}
}
p_payload_end = pchar;
}

int to_write = p_payload_end - p_payload;
int written = 0;
while (to_write > 0) {
if ((written = write(file_des, p_payload + written, to_write)) < 0) {
log_error("Failed to write %d bytes to %s: %s",
to_write,
temp_file,
strerror(errno));
goto end;
}
total_bytes_processed += written;
to_write -= written;
}
log_debug("write: p_payload %p, %d bytes", p_payload, (int)(p_payload_end - p_payload));
log_debug("loop %d, bytes_read %d, done %d",
loop_counter,
bytes_read,
total_bytes_processed);

if (post_boundary_found) {
total_bytes_processed = content_length;
remove_temp_file = false; // File has been successfully received.
} else {
if (!pre_boundary_found) {
log_error("No pre boundary found");
goto end;
}
if (bytes_read != available_len) {
log_error("No post boundary found");
goto end;
}

/* Post boundary may have been partial at payload end. Ensure possible rematch */
p_payload = buffer + boundary_len;
memcpy(buffer, p_payload_end, boundary_len);
}
}

end:
if (file_des != -1) {
log_debug("Closing %s after writing %ld bytes.", temp_file, lseek(file_des, 0, SEEK_CUR));
if (close(file_des) == -1)
log_warning("Failed to close %s: %s", temp_file, strerror(errno));
}
if (remove_temp_file && temp_file) {
if (unlink(temp_file) != 0)
log_error("Failed to remove %s: %s", temp_file, strerror(errno));
g_free(temp_file);
temp_file = NULL;
}
return temp_file;
}
7 changes: 7 additions & 0 deletions app/fcgi_write_file_from_stream.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#pragma once
#include <fcgiapp.h>

// Given a request with multipart/form-data, store incoming data in a file on /tmp. On success,
// return the filename and let the caller do all cleanup. On failure, log the error, clean up the
// file and return NULL.
char* fcgi_write_file_from_stream(FCGX_Request request);
Loading

0 comments on commit 6eb12d6

Please sign in to comment.