Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 33 additions & 107 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ Video is deliberately independent of the MAVLink session: it survives a
telemetry dropout, and with a publish password it needs no MAVLink at
all.

**Ports.** Up to three per entry, allocated by an admin from the web UI
**Ports.** Up to five per entry, allocated by an admin from the web UI
(suggested from 40001). Each carries one stream, on TCP and UDP.
**These are public listening ports and must be open in the firewall.**

Expand All @@ -65,6 +65,23 @@ side, so a scanner between flights can become the authorised address and
the aircraft's video is then refused until the grace expires. **Entries
used for video should set `bidi_sign` or a publish password.**

A publish password normally *replaces* the MAVLink-session check rather
than adding to it — an operator sets one precisely so address matching
is not the gate. Some publishers cannot present one at all, though: a
camera speaking RTMP from its own firmware has nowhere to put it unless
its stream-key field tolerates a query, and plain UDP never does. The
per-slot **MAVLink publish** option lets one slot fall back to the
session check while the rest of the entry stays password-only:

```bash
./keydb.py videoflag 11024 0 session_ok
```

It is opt-in and per slot, so enabling it on the camera's slot does not
weaken the others. A password that *is* supplied and is wrong is still
refused — the fallback applies only when none was offered, so a typo
cannot quietly succeed on the strength of the address.

**Watching.** In the browser from the web UI, or outside it with the
`ffplay`/`vlc` command the page offers. The browser player needs H.264:
Chrome and Firefox will not decode HEVC in Media Source Extensions on
Expand Down Expand Up @@ -122,59 +139,6 @@ source venv/bin/activate
pip install pymavlink
```

### Video

Optional, off unless an entry has it enabled. A user points a camera at
one of their entry's video ports and any number of ground stations can
watch, with the same NAT traversal and per-entry credentials the MAVLink
side already provides. Recordings land beside the tlogs under
`logs/<port2>/<date>/` and are covered by the same retention.

Video is deliberately independent of the MAVLink session: it survives a
telemetry dropout, and with a publish password it needs no MAVLink at
all.

**Ports.** Up to three per entry, allocated by an admin from the web UI
(suggested from 40001). Each carries one stream, on TCP and UDP.
**These are public listening ports and must be open in the firewall.**

**Publishing.**

| Transport | Credential |
|---|---|
| MPEG-TS over UDP | none possible — see below |
| RTSP | `?pw=` on the request URI |
| RTMP | `?pw=` on the stream key, e.g. `FPV?pw=secret` |

Plain MPEG-TS over UDP has nowhere to carry a password, so it is
admitted on the MAVLink-session path only: a publisher is accepted when
a MAVLink session for the entry was seen from the same address within
the grace window. On a non-bidi entry *any* datagram latches the user
side, so a scanner between flights can become the authorised address and
the aircraft's video is then refused until the grace expires. **Entries
used for video should set `bidi_sign` or a publish password.**

**Watching.** In the browser from the web UI, or outside it with the
`ffplay`/`vlc` command the page offers. The browser player needs H.264:
Chrome and Firefox will not decode HEVC in Media Source Extensions on
desktop Linux, and nothing here transcodes.

**Disk.** Video has its own budget, separate from telemetry, so a busy
camera can never evict a user's tlogs. Set it per entry in the web UI;
a free-space floor stops recording before the disk fills.

**Log rotation.** The daemon's own log is not rotated by default.
Install the supplied config once, as root:

```bash
sudo install -m 644 scripts/supportproxy.logrotate \
/etc/logrotate.d/supportproxy
```

It uses `copytruncate`, which is required rather than preferred when the
daemon's stdout is a file systemd holds open — see the comments in that
file.

## Building SupportProxy

```bash
Expand Down Expand Up @@ -317,59 +281,6 @@ netstat -ln | grep ":1000[0-9]"

SupportProxy can also be run using Docker for easier deployment and management.

### Video

Optional, off unless an entry has it enabled. A user points a camera at
one of their entry's video ports and any number of ground stations can
watch, with the same NAT traversal and per-entry credentials the MAVLink
side already provides. Recordings land beside the tlogs under
`logs/<port2>/<date>/` and are covered by the same retention.

Video is deliberately independent of the MAVLink session: it survives a
telemetry dropout, and with a publish password it needs no MAVLink at
all.

**Ports.** Up to three per entry, allocated by an admin from the web UI
(suggested from 40001). Each carries one stream, on TCP and UDP.
**These are public listening ports and must be open in the firewall.**

**Publishing.**

| Transport | Credential |
|---|---|
| MPEG-TS over UDP | none possible — see below |
| RTSP | `?pw=` on the request URI |
| RTMP | `?pw=` on the stream key, e.g. `FPV?pw=secret` |

Plain MPEG-TS over UDP has nowhere to carry a password, so it is
admitted on the MAVLink-session path only: a publisher is accepted when
a MAVLink session for the entry was seen from the same address within
the grace window. On a non-bidi entry *any* datagram latches the user
side, so a scanner between flights can become the authorised address and
the aircraft's video is then refused until the grace expires. **Entries
used for video should set `bidi_sign` or a publish password.**

**Watching.** In the browser from the web UI, or outside it with the
`ffplay`/`vlc` command the page offers. The browser player needs H.264:
Chrome and Firefox will not decode HEVC in Media Source Extensions on
desktop Linux, and nothing here transcodes.

**Disk.** Video has its own budget, separate from telemetry, so a busy
camera can never evict a user's tlogs. Set it per entry in the web UI;
a free-space floor stops recording before the disk fills.

**Log rotation.** The daemon's own log is not rotated by default.
Install the supplied config once, as root:

```bash
sudo install -m 644 scripts/supportproxy.logrotate \
/etc/logrotate.d/supportproxy
```

It uses `copytruncate`, which is required rather than preferred when the
daemon's stdout is a file systemd holds open — see the comments in that
file.

## Building the Docker Image

```bash
Expand Down Expand Up @@ -503,6 +414,21 @@ proxy restart.
- Everyone else gets the self-service UI (rename their own entry, rotate
their own passphrase, reset their signing timestamp).

### Log access

Each entry's edit page has a **Log access** setting:

- **Private** (the default): only the entry owner and server admins can read
its logs.
- **Login Required**: any user with a valid SupportProxy login can browse and
download them.
- **Public**: anyone with the `/admin/logs/<port2>/...` URL can browse and
download them without logging in.

Shared access is read-only. Deleting individual recordings or a whole day
continues to require either the entry owner's `/me/logs/` view or a server
admin. Log responses remain non-cacheable even when the entry is Public.

### Install dependencies

```bash
Expand Down
72 changes: 58 additions & 14 deletions httpreq.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -110,26 +110,41 @@ std::string HttpRequest::header(const char *name) const
return "";
}

std::string HttpRequest::query(const char *name) const
size_t HttpRequest::header_count(const char *name) const
{
const std::string want = name;
const std::string want = lower(name);
size_t count = 0;
size_t pos = 0;
while (pos <= query_.size()) {
size_t amp = query_.find('&', pos);
if (amp == std::string::npos) {
amp = query_.size();
while (pos < headers_.size()) {
size_t eol = headers_.find('\n', pos);
if (eol == std::string::npos) {
eol = headers_.size();
}
const std::string kv = query_.substr(pos, amp - pos);
const size_t eq = kv.find('=');
if (eq != std::string::npos && kv.compare(0, eq, want) == 0) {
return http_url_decode(kv.substr(eq + 1));
std::string line = headers_.substr(pos, eol - pos);
if (!line.empty() && line.back() == '\r') {
line.pop_back();
}
if (amp == query_.size()) {
break;
const size_t colon = line.find(':');
if (colon != std::string::npos) {
std::string field = line.substr(0, colon);
const size_t end = field.find_last_not_of(" \t");
if (end != std::string::npos) {
field.resize(end + 1);
}
if (lower(field) == want) {
count++;
}
}
pos = amp + 1;
pos = eol + 1;
}
return "";
return count;
}

std::string HttpRequest::query(const char *name) const
{
std::string value;
(void)http_query_value(target_, name, value);
return value;
}

std::string http_url_decode(const std::string &s)
Expand All @@ -152,6 +167,35 @@ std::string http_url_decode(const std::string &s)
return o;
}

bool http_query_value(const std::string &target, const char *name,
std::string &value)
{
value.clear();
const size_t q = target.find('?');
if (q == std::string::npos) {
return false;
}
const std::string want = name;
size_t pos = q + 1;
while (pos <= target.size()) {
size_t amp = target.find('&', pos);
if (amp == std::string::npos) {
amp = target.size();
}
const std::string kv = target.substr(pos, amp - pos);
const size_t eq = kv.find('=');
if (eq != std::string::npos && kv.compare(0, eq, want) == 0) {
value = http_url_decode(kv.substr(eq + 1));
return true; // first value wins; duplicates cannot erase it
}
if (amp == target.size()) {
break;
}
pos = amp + 1;
}
return false;
}

std::string http_basic_password(const std::string &authorization)
{
const std::string prefix = "Basic ";
Expand Down
13 changes: 13 additions & 0 deletions httpreq.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ class HttpRequest {
// Header lookup, case-insensitive. Empty string when absent.
std::string header(const char *name) const;

// Number of occurrences of a header, case-insensitive. Security
// parsers use this to reject ambiguous framing fields.
size_t header_count(const char *name) const;

// Query parameter from the request target. Empty when absent.
std::string query(const char *name) const;

Expand All @@ -55,6 +59,15 @@ class HttpRequest {
// escapes are left as-is rather than silently dropped.
std::string http_url_decode(const std::string &s);

/*
Fetch the first named query parameter from a request target. The boolean
distinguishes an absent parameter from one explicitly supplied with an
empty value; callers making access-control decisions must not collapse the
two. `value` is percent-decoded when present.
*/
bool http_query_value(const std::string &target, const char *name,
std::string &value);

/*
A request target with credential query values replaced.

Expand Down
Loading
Loading