From 78426afe18d9ce730a4d92033ca261f9b2f173a0 Mon Sep 17 00:00:00 2001 From: Nakidai Date: Sun, 4 Jan 2026 17:41:32 +0300 Subject: Add code --- .gitignore | 4 ++ LICENSE | 13 +++++ Makefile | 16 +++++++ README | 9 ++++ handle.c | 160 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ircd.h | 74 ++++++++++++++++++++++++++++ loop.c | 134 +++++++++++++++++++++++++++++++++++++++++++++++++++ main.c | 52 ++++++++++++++++++++ message.c | 81 +++++++++++++++++++++++++++++++ peer.c | 73 ++++++++++++++++++++++++++++ user.c | 50 +++++++++++++++++++ writef.c | 40 ++++++++++++++++ 12 files changed, 706 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README create mode 100644 handle.c create mode 100644 ircd.h create mode 100644 loop.c create mode 100644 main.c create mode 100644 message.c create mode 100644 peer.c create mode 100644 user.c create mode 100644 writef.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..30d2722 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.core +*.o +*.out +ircd diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7802b18 --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright (c) 2026 Naidai Perumenei + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5953be1 --- /dev/null +++ b/Makefile @@ -0,0 +1,16 @@ +NAME ?= ircd + +CFLAGS += -Wall -Wextra + +OBJS += handle.o +OBJS += loop.o +OBJS += main.o +OBJS += message.o +OBJS += peer.o +OBJS += user.o +OBJS += writef.o + +${NAME}: ${OBJS} + ${CC} -o ${NAME} ${CFLAGS} ${LDFLAGS} ${OBJS} +clean: + rm -f ${NAME} ${OBJS} diff --git a/README b/README new file mode 100644 index 0000000..473e983 --- /dev/null +++ b/README @@ -0,0 +1,9 @@ +LibreIRCd +========= +Simple IRC server in C + +usage: ircd hostname bindaddr port + +--------------- +This project is incomplete: channels, modes, a lot of validation is not present +yet. diff --git a/handle.c b/handle.c new file mode 100644 index 0000000..79a3b9a --- /dev/null +++ b/handle.c @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2026 Nakidai Perumenei + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include "ircd.h" + +#include +#include +#include + + +#define ensure(cond, iffalse, doexit) if (!(cond)) { iffalse; if (doexit >= 0) return doexit; } + +static int +nick(struct Message *msg, struct Peer *peer) +{ + ensure(msg->params[0] && msg->params[0], writef( + peer->fd, + ":%s 431 %s :No nickname given", + hostname, + getnick(peer) + ), 0); + + user_reg(peer, msg->params[0], NULL, NULL); + + return 0; +} + +static int +privmsg(struct Message *msg, struct Peer *peer) +{ + size_t i; + + ensure(peer->type, writef( + peer->fd, + ":%s 451 %s :You have not registered", + hostname, + getnick(peer) + ), 0); + ensure(msg->params[0] && *msg->params[0], writef( + peer->fd, + ":%s 411 %s :No recipient given (PRIVMSG)", + hostname, + getnick(peer) + ), 0); + ensure(msg->params[1] && *msg->params[1], writef( + peer->fd, + ":%s 412 %s :No text to send", + hostname, + getnick(peer) + ), 0); + + /* TODO: implement channels */ + ensure(!strchr("!+#&", *msg->params[0]), writef( + peer->fd, + ":%s 421 %s :Channels are not supported yet", + hostname, + getnick(peer) + ), 0); + + /* TODO: implement server-to-server communication */ + for (i = 0; i < PEERS_MAX; ++i) + if (peers[i].type == CLIENT && !strcmp(peers[i].nick, msg->params[0])) + writef( + peers[i].fd, + ":%s!%s@%s PRIVMSG %s :%s", + peer->nick, + peer->user, + peer->host, + peer->nick, + msg->params[1] + ); + return 0; +} + +static int +quit(struct Message *msg, struct Peer *peer) +{ + return 1; +} + +static int +user(struct Message *msg, struct Peer *peer) +{ + size_t i; + + ensure(!peer->type, writef( + peer->fd, + ":%s 462 %s :Unauthorized command (already registered)", + hostname, + getnick(peer) + ), 0); + for (i = 0; i < 4; ++i) + ensure(msg->params[i] && *msg->params[i], writef( + peer->fd, + ":%s 461 %s %s :Not enough parameters", + hostname, + getnick(peer), + msg->command + ), 0); + + user_reg(peer, NULL, msg->params[0], msg->params[3]); + + return 0; +} + +static int +default_handler(struct Message *msg, struct Peer *peer) +{ + ensure(peer->type, 0, 0); + + writef(peer->fd, ":%s 421 %s %s :Unknown command", hostname, getnick(peer), msg->command); + return 0; +} + +static struct Handler { + const char *name; + Handler *handler; +} handlers[] = +{ + { "nick", nick }, + { "privmsg", privmsg }, + { "quit", quit }, + { "user", user }, +}; + +Handler * +find(const char *command) +{ + struct Handler *l, *m, *r; + int res; + + l = handlers; + r = handlers + lengthof(handlers) - 1; + while (l <= r) + { + m = l + (r - l) / 2; + res = strcmp(command, m->name); + if (res > 0) + l = ++m; + else if (res < 0) + r = --m; + else + return m->handler; + } + + return default_handler; +} diff --git a/ircd.h b/ircd.h new file mode 100644 index 0000000..5e787da --- /dev/null +++ b/ircd.h @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2026 Nakidai Perumenei + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef __IRCD_H__ +#define __IRCD_H__ + +#include + + +#define PARAM_MAX 15 +#define PEERS_MAX 512 +#define MESSAGE_MAX 512 + +#define BIT(x) x##_BIT, x = 1 << x##_BIT, x##_BIT_ = x##_BIT +#define lengthof(X) (sizeof(X) / sizeof(*(X))) + +struct Message +{ + char *nick, *user, *host, *command, *params[PARAM_MAX]; +}; + +struct Peer +{ + int fd; + enum ClientType { UNREGED, CLIENT, SERVER, SERVICE } type; + char nick[16], user[16], real[32], host[64]; + char **channels; + size_t channels_count; + char modes[52]; + char buf[MESSAGE_MAX]; + size_t recvd; +}; + +struct Channel +{ + enum { GLOBAL, LOCAL, MODELESS, SAFE } type; + char name[64]; + struct { + char mode; + char param[MESSAGE_MAX - 1]; + } *modes; + size_t modes_count; +}; + +typedef int Handler(struct Message *msg, struct Peer *peer); + +extern struct Peer peers[PEERS_MAX]; +extern const char *hostname; +extern const char *host; +extern int port; + +const char *getnick(const struct Peer *peer); +void user_reg(struct Peer *peer, const char *nick, const char *user, const char *real); + +int parse_message(char *buf, struct Message *msg); +int handle(struct Peer *peer); +Handler *find(const char *command); +int writef(int fd, const char *fmt, ...); +void ircd(void); + +#endif /* __IRCD_H__ */ diff --git a/loop.c b/loop.c new file mode 100644 index 0000000..ba14a4d --- /dev/null +++ b/loop.c @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2026 Nakidai Perumenei + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include "ircd.h" + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + + +struct Peer peers[PEERS_MAX]; +static struct pollfd pfd[PEERS_MAX + 1]; +static size_t remove_schedule[PEERS_MAX]; + +const char *host; +int port; + +void +ircd(void) +{ + size_t i, schedulei, connected, pid; + struct sockaddr_in addr, clientaddr; + int res, sfd, client; + nfds_t passed; + ssize_t recvd; + + addr.sin_family = PF_INET; + addr.sin_port = htons(port); + + res = inet_pton(AF_INET, host, &addr.sin_addr); + if (res == -1) + err(1, "inet_pton()"); + + sfd = socket(AF_INET, SOCK_STREAM, 0); + if (sfd == -1) + err(1, "socket()"); + + res = setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &(int){1}, sizeof(int)); + if (res == -1) + err(1, "setsockopt(SO_REUSEADDR)"); + + res = bind(sfd, (void *)&addr, sizeof(addr)); + if (res == -1) + err(1, "bind()"); + + res = listen(sfd, 128); + if (res == -1) + err(1, "listen()"); + + signal(SIGPIPE, SIG_IGN); + for (connected = 0;;schedulei = 0) + { + pfd[connected] = (struct pollfd){ .fd = sfd, .events = POLLIN }; + for (i = 0; i < connected; ++i) + pfd[i] = (struct pollfd) + { + .fd = peers[i].fd, + .events = POLLIN, + }; + res = poll(pfd, (passed = connected) + 1, -1); + if (res == -1) + err(1, "poll()"); + + if (pfd[connected].revents & POLLIN) + { + client = accept(sfd, (void *)&clientaddr, &(int){sizeof(clientaddr)}); + if (client == -1) + { + warn("accept(sfd)"); + } + else if (connected == PEERS_MAX) + { + /* + * TODO: maybe send 005 or smth? + */ + close(client); + } + else + { + peers[connected++] = (struct Peer){ .fd = client }; + strlcpy( + peers[connected - 1].host, + inet_ntoa(clientaddr.sin_addr), + sizeof(peers->host) + ); + } + } + + for (i = 0; i < passed; ++i) + { + if (!(pfd[i].revents & POLLIN)) + continue; + + recvd = read( + peers[i].fd, + peers[i].buf + peers[i].recvd, + sizeof(peers[i].buf) - peers[i].recvd + ); + peers[i].recvd += recvd; + if (recvd == -1 || !recvd || handle(&peers[i])) + remove_schedule[schedulei++] = i; + } + for (i = 0; i < schedulei; ++i) + { + pid = remove_schedule[i]; + /* TODO: announce_quit(&peers[pid]); */ + writef(peers[pid].fd, "ERROR :Closing Link: %s", peers[pid].host); + close(peers[pid].fd); + peers[pid] = peers[--connected]; + } + schedulei = 0; + } +} diff --git a/main.c b/main.c new file mode 100644 index 0000000..8be182a --- /dev/null +++ b/main.c @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2026 Nakidai Perumenei + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include "ircd.h" + +#include +#include +#include + +#include +#include + + +const char *hostname; + +int +main(int argc, char **argv) +{ + size_t i; + char *p; + + for (i = 1; i < 4; ++i) + if (!argv[i] || !*argv[i]) + errx(1, "usage: %s hostname bindaddr port", argv[0]); + hostname = argv[1]; + host = argv[2]; + port = strtod(argv[3], &p); + if (*p || port <= 0 || port > 65535) + errx(1, "invalid port"); + +#ifdef __OpenBSD__ + if (unveil(NULL, NULL)) + err(1, "unveil()"); + if (pledge("stdio rpath inet", "")) + err(1, "pledge()"); +#endif /* __OpenBSD__ */ + + ircd(); +} diff --git a/message.c b/message.c new file mode 100644 index 0000000..f394ff7 --- /dev/null +++ b/message.c @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2026 Nakidai Perumenei + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include "ircd.h" + +#include +#include + + +int +parse_message(char *buf, struct Message *msg) +{ + char *t, *next; + size_t i; + + memset(msg, 0, sizeof(*msg)); + + if (*buf == ':') + { + t = ++buf; + next = strchr(buf, ' '); + if (!next) + return 1; + *next++ = 0; + buf = next; + + next = strchr(t, '@'); + if (next) + { + *next++ = 0; + if (!*t) + return 1; + + msg->nick = t; + t = next; + + next = strchr(msg->nick, '!'); + if (next) + { + *next++ = 0; + if (!*msg->nick || !*next) + return 1; + msg->user = next; + } + } + if (!*t) + return 1; + msg->host = t; + } + + msg->command = buf; + t = strstr(buf, " :"); + if (t) + *((t += 2) - 2) = 0; + for (i = 0; i < PARAM_MAX - !!t; ++i, buf = next) + { + next = strchr(buf, ' '); + if (!next) + break; + *next++ = 0; + msg->params[i] = next; + } + if (strchr(buf, ' ')) + return 1; + if (t) + msg->params[i] = t; + return 0; +} diff --git a/peer.c b/peer.c new file mode 100644 index 0000000..21dba70 --- /dev/null +++ b/peer.c @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2026 Nakidai Perumenei + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include "ircd.h" + +#include +#include +#include +#include + +#include + + +static void +move(struct Peer *peer, size_t n) +{ + memmove(peer->buf, peer->buf + n, sizeof(peer->buf) - n); + memset(peer->buf + sizeof(peer->buf) - n, 0, n); + peer->recvd -= n; +} + +int +handle(struct Peer *peer) +{ + struct Message msg; + char *p, *c; + + while ((p = memmem(peer->buf, sizeof(peer->buf), "\r\n", 2))) + { + if (memchr(peer->buf, '\0', p - peer->buf)) + { + warnx("got 0 from %d", peer->fd); + goto next; + } + + *p = 0; + if (parse_message(peer->buf, &msg)) + { + warnx("malformed input from %d", peer->fd); + goto next; + } + + for (c = msg.command; *c; ++c) + *c = tolower(*c); + if (find(msg.command)(&msg, peer)) + return 1; +next: + move(peer, p - peer->buf + 2); + } + if (peer->recvd == sizeof(peer->buf)) + { + /* + * TODO: maybe somehow be more careful with peer data? + * or drop them? + */ + memset(peer->buf, 0, sizeof(peer->buf)); + peer->recvd = 0; + } + return 0; +} diff --git a/user.c b/user.c new file mode 100644 index 0000000..4411144 --- /dev/null +++ b/user.c @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2026 Nakidai Perumenei + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include "ircd.h" + +#include + + +const char * +getnick(const struct Peer *peer) +{ + return *peer->nick ? peer->nick : "*"; +} + +void +user_reg(struct Peer *peer, const char *nick, const char *user, const char *real) +{ + if (nick) + strlcpy(peer->nick, nick, sizeof(peer->nick)); + if (user) + strlcpy(peer->user, user, sizeof(peer->user)); + if (real) + strlcpy(peer->real, real, sizeof(peer->real)); + if (*peer->nick && *peer->user && *peer->real) + { + peer->type = CLIENT; + writef( + peer->fd, + ":%s 001 %s :Welcome to the Internet Relay Network %s!%s@%s", + hostname, + getnick(peer), + getnick(peer), + peer->user, + peer->host + ); + } +} diff --git a/writef.c b/writef.c new file mode 100644 index 0000000..7d57e3b --- /dev/null +++ b/writef.c @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2026 Nakidai Perumenei + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include "ircd.h" + +#include +#include +#include + +#include + + +int +writef(int fd, const char *fmt, ...) +{ + static char buf[MESSAGE_MAX + 1]; + va_list args; + int written; + + va_start(args, fmt); + written = vsnprintf(buf, sizeof(buf) - 2, fmt, args); + va_end(args); + written = written < sizeof(buf) - 2 ? written : sizeof(buf) - 2; + memcpy(buf + written, "\r\n", 3); + + return write(fd, buf, written + 3); +} -- cgit 1.4.1