commit - 734e5440a8de4d4c26514b2f8405005037c773d9
commit + c86fbcd84d03494d9288500e642ba3a7c93dfac1
blob - 83651f6ccabe27ae4d422ff6b32a7d59531b64a3
blob + c41631176ccef8a0e007566bc6ae0bbd5b4386ab
--- src/milna.h
+++ src/milna.h
#ifndef MILNA_H
#define MILNA_H
-#define MILNA_VERSION "1.7"
+#define MILNA_VERSION "1.8"
#include <stdint.h>
typedef struct {
blob - b844cd5990ba0ca4798943ad8c6e2b2411ce1ba2
blob + 01fbc553eb4f576a0ac8584701a4d1142bdf174b
--- src/net.c
+++ src/net.c
#else
#include <sys/socket.h>
#include <netinet/in.h>
+#include <netinet/tcp.h>
#include <netdb.h>
#include <unistd.h>
#include <fcntl.h>
out[o] = 0;
}
+/* ---- peer table (slot-indexed; roster + crypto keys) ---- */
static struct peer {
int used;
char id[32], name[32];
static net_video_fn v_handler;
static void *v_user;
+/* ---- helpers ---- */
static void tcp_line(const char *line) { /* send one control line */
pthread_mutex_lock(&N.mu);
if (N.tcp >= 0) {
udp_send_media(2, 0, opus, len);
}
+/* screenshare/system audio sink: wire type 4 (music stream, separate from
+ * voice type 2). uses its own sequence so it doesn't disturb voice FEC. */
static void net_share_sink(const uint8_t *opus, int len, void *user) {
(void)user;
udp_send_media(4, 0, opus, len);
int layer) {
if (!N.udp_ok) return -1;
if (layer < 0 || layer > 3) layer = 0;
- /* separate frame sequences per (source,layer): chunks never interleave */
static uint16_t fseq_[2][4];
uint16_t fseq = ++fseq_[!!is_screen][layer];
int cnt = (len + (VCHUNK - 8) - 1) / (VCHUNK - 8);
pthread_mutex_unlock(&N.mu);
send_my_key(id, pk);
-
+ /* tell the newcomer my current mute/deafen state so their roster shows
+ * the right badge immediately (state msgs are otherwise only on change) */
if (my_muted || my_deaf) net_announce_state(my_muted, my_deaf);
LOG("%s in slot %d", p->name, slot);
}
int src = (pt[7] >> 4) & 1; /* 0=camera 1=screen */
int dlen = ptlen - 8;
int idx = pt[2] | pt[3] << 8;
+ /* reassembly buffers (p->va[].buf) are freed by peer_del on the TCP
+ * thread. hold N.mu across the buffer ops and RE-CHECK p->used after
+ * locking, so a peer leaving mid-frame can't free the buffer out
+ * from under this memcpy (was a use-after-free / data race). the
+ * critical section is just a memcpy, so it stays cheap. */
+ uint8_t *done_buf = NULL; int done_len = 0, done_key = 0;
+ pthread_mutex_lock(&N.mu);
+ if (!p->used) { pthread_mutex_unlock(&N.mu); return; }
struct vasm *a = &p->va[src];
if (!a->buf) {
a->buf = malloc(VFRAME_MAX);
- if (!a->buf) return;
+ if (!a->buf) { pthread_mutex_unlock(&N.mu); return; }
a->seq = -1;
}
if (fseq != a->seq) {
a->key = key;
}
long off = (long)idx * (VCHUNK - 8);
- if (off + dlen > VFRAME_MAX) { a->seq = -1; return; }
+ if (off + dlen > VFRAME_MAX) { a->seq = -1; pthread_mutex_unlock(&N.mu); return; }
memcpy(a->buf + off, pt + 8, dlen);
a->got++;
if (off + dlen > a->len) a->len = (int)(off + dlen);
if (a->got == a->cnt && v_handler) {
static int first = 1;
if (first) { LOG("first video frame RECEIVED (slot %d, %d bytes)", slot, a->len); first = 0; }
- v_handler(slot, a->buf, a->len, a->key, src, v_user);
+ /* copy the completed frame into a private buffer UNDER the lock,
+ * so peer_del freeing a->buf can't race the decode. decode the
+ * copy after unlocking (the decoder can be slow and must not
+ * hold the peer lock). */
+ done_buf = malloc((size_t)a->len);
+ if (done_buf) {
+ memcpy(done_buf, a->buf, (size_t)a->len);
+ done_len = a->len; done_key = a->key;
+ }
}
+ pthread_mutex_unlock(&N.mu);
+ if (done_buf) {
+ v_handler(slot, done_buf, done_len, done_key, src, v_user);
+ free(done_buf);
+ }
}
}
+/* ---- threads ---- */
static void *tcp_main(void *arg) {
(void)arg;
char buf[8192]; int fill = 0;
if (sodium_init() < 0) return -1;
pthread_mutex_init(&N.mu, NULL);
- /* parse host[:port]; default port 9292; tolerate old ws:// urls */
char host[128]; int port = 9292;
const char *s = server;
if (!strncmp(s, "ws://", 5)) s += 5;
LOG("control connect failed"); sock_close(N.tcp); N.tcp = -1;
freeaddrinfo(res); return -3;
}
+ { int one = 1;
+ setsockopt(N.tcp, IPPROTO_TCP, TCP_NODELAY,
+ (const char *)&one, sizeof(one)); }
memcpy(&N.raddr, res->ai_addr, sizeof(N.raddr));
N.raddr.sin_port = htons((uint16_t)port);
freeaddrinfo(res);
N.udp = socket(AF_INET, SOCK_DGRAM, 0);
+ if (N.udp >= 0) {
+ int tos = 0xB8;
+ setsockopt(N.udp, IPPROTO_IP, IP_TOS,
+ (const char *)&tos, sizeof(tos));
+ int rcv = 1 << 20; /* 1 MiB */
+ setsockopt(N.udp, SOL_SOCKET, SO_RCVBUF,
+ (const char *)&rcv, sizeof(rcv));
+ }
+ /* identity + sender key for this session */
crypto_box_keypair(N.pk, N.sk);
randombytes_buf(N.skey, sizeof(N.skey));
randombytes_buf(N.snbase, sizeof(N.snbase));
blob - 059854a1b2977e34382b3bc8fb35f4e147b4cfb8
blob + e9cb2be653e83b08633356e88ac51293627f7736
--- src/update.c
+++ src/update.c
#define EXE ""
#endif
-#define VER_URL "https://mi.rsap.sh/milna/version"
-#define BIN_BASE "https://mi.rsap.sh/milna/milna-"
+#define VER_URL "https://mi.rsap.sh/version"
+#define BIN_BASE "https://mi.rsap.sh/milna-"
static char g_self[1024]; /* path to the running executable */
static char g_new[1024]; /* path to the downloaded update */
#ifdef _WIN32
#include <process.h>
static int run_argv(char *const argv[]) {
- /* _spawnvp on windows: no fork, no cocoa, safe. */
intptr_t r = _spawnvp(_P_WAIT, argv[0], (const char *const *)argv);
return r == 0 ? 0 : -1;
}
return buf[0] ? 0 : -1;
}
+static int valid_ver(const char *s) {
+ int digits = 0;
+ for (const char *p = s; *p; p++) {
+ if (*p >= '0' && *p <= '9') digits++;
+ else if (*p != '.') return 0; /* only digits and dots allowed */
+ }
+ return digits > 0 && strlen(s) < 24;
+}
static int newer(const char *remote, const char *local) {
+ if (!valid_ver(remote)) return 0; /* junk remote: never "newer" */
int ra, rb = 0, la, lb = 0;
ra = atoi(remote);
const char *rd = strchr(remote, '.'); if (rd) rb = atoi(rd + 1);
char bp[1024];
int in_bundle = bundle_path(bp, sizeof(bp));
if (in_bundle) {
- /* bundle update: fetch the whole zipped .app, not the bare binary.
- * we stage it as g_new + ".zip" beside the app. */
snprintf(url, sizeof(url), "%smacos-app.zip", BIN_BASE);
char zip[1100]; snprintf(zip, sizeof(zip), "%s.zip", g_new);
char *dz[] = { (char*)"curl", (char*)"-fsSL", (char*)"--max-time",
"milna-macos-app.zip on the vps?\n");
remove(zip); return;
}
- /* strip quarantine on the zip so the unpacked app isn't tainted */
char *xz[] = { (char*)"xattr", (char*)"-d",
(char*)"com.apple.quarantine", zip, NULL };
run_argv(xz);
fclose(z);
fprintf(stderr, "[milna] update: staged bundle zip found, "
"launching swap helper for %s\n", bp);
- /* write a helper script to /tmp */
- char hp[] = "/tmp/milna_update.sh";
- FILE *h = fopen(hp, "w");
+ char hp[] = "/tmp/milna_update.XXXXXX";
+ int hfd = mkstemp(hp);
+ FILE *h = (hfd >= 0) ? fdopen(hfd, "w") : NULL;
if (h) {
- /* $1 = our pid, then it waits for us to die, unzips the new
- * app to a temp dir, swaps it into place, reopens. */
+ fchmod(hfd, 0700);
fprintf(h,
"#!/bin/sh\n"
- "pid=\"$1\"; app=\"%s\"; zip=\"%s\"\n"
+ "pid=\"$1\"; app=\"$2\"; zip=\"$3\"\n"
"for i in $(seq 1 50); do kill -0 \"$pid\" 2>/dev/null || break; sleep 0.1; done\n"
- "tmp=$(mktemp -d /tmp/milna.XXXXXX)\n"
- "if ! /usr/bin/unzip -oq \"$zip\" -d \"$tmp\"; then exit 1; fi\n"
+ "tmp=$(mktemp -d /tmp/milna.XXXXXX) || exit 1\n"
+ "if ! /usr/bin/unzip -oq \"$zip\" -d \"$tmp\"; then rm -rf \"$tmp\"; exit 1; fi\n"
"newapp=$(find \"$tmp\" -maxdepth 2 -name '*.app' -type d | head -1)\n"
- "[ -n \"$newapp\" ] || exit 1\n"
+ "[ -n \"$newapp\" ] || { rm -rf \"$tmp\"; exit 1; }\n"
"/usr/bin/xattr -cr \"$newapp\" 2>/dev/null\n"
"rm -rf \"$app.old\"\n"
"mv \"$app\" \"$app.old\" 2>/dev/null\n"
- "if ! mv \"$newapp\" \"$app\"; then mv \"$app.old\" \"$app\"; exit 1; fi\n"
+ "if ! mv \"$newapp\" \"$app\"; then mv \"$app.old\" \"$app\"; rm -rf \"$tmp\"; exit 1; fi\n"
"/usr/bin/codesign --force --deep --sign - \"$app\" 2>/dev/null\n"
"rm -rf \"$app.old\" \"$tmp\" \"$zip\"\n"
- "/usr/bin/open \"$app\"\n",
- bp, zip);
+ "/usr/bin/open \"$app\"\n");
fclose(h);
- chmod(hp, 0755);
/* spawn detached and exit so the helper can replace us */
char pidbuf[16];
snprintf(pidbuf, sizeof(pidbuf), "%d", (int)getpid());
- char *hv[] = { (char*)"/bin/sh", hp, pidbuf, NULL };
+ char *hv[] = { (char*)"/bin/sh", hp, pidbuf, bp, zip, NULL };
pid_t child = fork();
if (child == 0) {
setsid();
} else {
fprintf(stderr, "[milna] update: promoted, relaunching\n");
#ifdef __APPLE__
-
{
char bundle[1024];
snprintf(bundle, sizeof(bundle), "%s", g_self);
char *p = strstr(bundle, ".app/Contents/MacOS/");
if (p) {
p[4] = 0; /* terminate right after ".app" */
- char cmd[1200];
- snprintf(cmd, sizeof(cmd),
- "codesign --force --sign - '%s' 2>/dev/null", bundle);
- int rc = system(cmd);
+ char *cs[] = { (char*)"codesign", (char*)"--force",
+ (char*)"--sign", (char*)"-", bundle, NULL };
+ int rc = run_argv(cs);
fprintf(stderr, "[milna] update: re-signed bundle "
"(%s, rc=%d)\n", bundle, rc);
}
}
#ifdef __APPLE__
-
void update_dispatch_bg(void);
#endif
}
#ifndef __APPLE__
+/* pthread needs a void*(void*) signature */
static void *update_run_blocking_thunk(void *a) {
(void)a; update_run_blocking(); return NULL;
}
blob - 113c1e75b294a6c5fc5f7a514c9f7e21b0ff0315
blob + a7836ecd0f19d6fe340ed999d6e5ba54801ac466
--- srv/relay.go
+++ srv/relay.go
"net"
"strconv"
"sync"
+ "time"
)
const port = ":9292"
pub string
room string
conn net.Conn
- wmu sync.Mutex
+ wmu sync.Mutex // serializes writes to conn
token [16]byte
- udp *net.UDPAddr
- want int
+ udp *net.UDPAddr // bound media address (nil until hello)
+ want int // simulcast: which video layer this member receives
}
func (m *member) send(v msg) {
h.rooms[room] = map[string]*member{}
}
+ const maxRoom = 16
+ if len(h.rooms[room]) >= maxRoom {
+ m.send(msg{Type: "full"})
+ m.room = ""
+ return
+ }
+ // pick the lowest free slot (u8, room-scoped)
used := map[int]bool{}
for _, o := range h.rooms[room] {
used[o.slot] = true
}
- for s := 0; s < 256; s++ {
+ for s := 0; s < maxRoom; s++ {
if !used[s] {
m.slot = s
break
}
h.mu.Unlock()
h.udp.WriteToUDP(buf[:n], addr) // confirm
- case 2, 3, 4:
+ case 2, 3, 4:
layer := -1
if buf[1] == 3 {
layer = int(buf[3]>>1) & 3
sc := bufio.NewScanner(conn)
sc.Buffer(make([]byte, 64*1024), 64*1024)
+ conn.SetReadDeadline(time.Now().Add(120 * time.Second))
for sc.Scan() {
+ conn.SetReadDeadline(time.Now().Add(120 * time.Second))
var v msg
if json.Unmarshal(sc.Bytes(), &v) != nil {
continue