Compare commits

..

29 Commits
0.0.0 ... main

Author SHA1 Message Date
milo 096cd62b2b fix JOURNAL_STREAM detection to make sure its actually a sd service 2024-02-03 00:04:09 -05:00
tali d936d36a6a bump version 2024-02-02 17:58:40 -05:00
tali cf11a13945 create postinst script 2024-02-02 17:58:40 -05:00
tali 454d27780c move config files to usr/share/talircd/default/x 2024-02-02 17:58:40 -05:00
tali 00eae82eb3 add LIST command 2024-02-02 17:35:46 -05:00
tali a34651b283 fix send_to_chan: non mebers cant send to moderated channels 2024-02-02 17:10:46 -05:00
tali 81918b9813 bump version 2024-02-02 13:34:51 -05:00
tali 8556fafd3d fixup deploy script 2024-02-02 13:33:47 -05:00
tali 54291669a5 remove no longer relevant TODO comment 2024-02-02 13:24:56 -05:00
tali d407222608 actually obey LOG_LEVEL env instead of always picking TRACE 2024-02-02 13:23:19 -05:00
tali 59b79167a4 create deb package deployment infrastructure 2024-02-02 13:22:58 -05:00
tali 7d8669867c wip deploy 2024-02-02 12:35:23 -05:00
tali 50d27b7cd9 remove redundant setsocketopt 2024-02-02 12:24:11 -05:00
tali cac7a091fe yeet the mutex 2024-02-02 11:50:34 -05:00
tali 59511905c9 slightly modify is_trailing so we don't use String.starts_with 2024-02-02 11:46:58 -05:00
tali 6e4b1af984 remove Set.of_string so we dont use String.fold_left 2024-02-02 11:46:58 -05:00
tali ebdf476a25 relax some dep versions 2024-02-02 11:20:50 -05:00
tali aed3089995 send QUIT to all active connections 2024-02-01 19:04:54 -05:00
tali 04478ffa0e add Wheel.iter for arbitrary order iteration 2024-02-01 19:04:54 -05:00
tali 69f182d0c2 add Dllist.iter_l, reset 2024-02-01 19:04:54 -05:00
tali 10fbd898c1 add KillSignal=SIGTERM to example systemd service 2024-02-01 19:04:54 -05:00
tali e160156b78 quit a little more gracefully when interrupted (SIGINT, SIGTERM) 2024-02-01 18:47:16 -05:00
tali e355bac41c use some sneaky codegen to get the 'version' variable 2024-02-01 14:34:43 -05:00
tali b92d84f843 add metadata to dune-project 2024-02-01 14:24:20 -05:00
tali 3631cd8075 check in example motd 2024-02-01 14:08:40 -05:00
tali 4cbd4421ce improve error messages in main somewhat 2024-02-01 14:08:27 -05:00
tali e2967fabd9 implement sd-notify protocol 2024-02-01 14:08:25 -05:00
tali 6790c22c4a load MOTD from file 2024-02-01 13:32:01 -05:00
tali 4cc0e8a6e1 fix logging issues
- clear dgram buffer before sending to journald
- only change pretty alignment to be bigger
2024-02-01 13:27:19 -05:00
30 changed files with 498 additions and 170 deletions

5
.gitignore vendored
View File

@ -1 +1,4 @@
_build/ _build/
_opam/
_dist/
*.install

View File

@ -1,10 +1,12 @@
include (val Logging.sublogs Server.logger "Main")
let min_level = let min_level =
match Sys.getenv_opt "LOG_LEVEL" |> Option.map String.uppercase_ascii with match Sys.getenv_opt "LOG_LEVEL" |> Option.map String.uppercase_ascii with
| Some "TRACE" -> Logging.TRACE | Some "TRACE" -> Logging.TRACE
| Some "DEBUG" -> Logging.TRACE | Some "DEBUG" -> Logging.DEBUG
| Some "INFO" -> Logging.TRACE | Some "INFO" -> Logging.INFO
| Some ("WARN" | "WARNING") -> Logging.TRACE | Some ("WARN" | "WARNING") -> Logging.WARN
| Some ("ERR" | "ERROR") -> Logging.TRACE | Some ("ERR" | "ERROR") -> Logging.ERROR
| _ -> Logging.INFO | _ -> Logging.INFO
let no_color = Option.is_some (Sys.getenv_opt "LOG_NO_COLOR") let no_color = Option.is_some (Sys.getenv_opt "LOG_NO_COLOR")
@ -12,17 +14,34 @@ let no_timestamp = Option.is_some (Sys.getenv_opt "LOG_NO_TIMESTAMP")
let no_namespace = Option.is_some (Sys.getenv_opt "LOG_NO_NAMESPACE") let no_namespace = Option.is_some (Sys.getenv_opt "LOG_NO_NAMESPACE")
let () = let () =
match Sys.getenv_opt "JOURNAL_STREAM" with if Logging.should_upgrade_to_journald () then
| Some _ ->
Logging.init_journald_writer () Logging.init_journald_writer ()
~min_level ~min_level
| None -> else
Logging.init_pretty_writer stderr Logging.init_pretty_writer stdout
~min_level ~min_level
~color:(not no_color) ~color:(not no_color)
~timestamp:(not no_timestamp) ~timestamp:(not no_timestamp)
~namespace:(not no_namespace) ~namespace:(not no_namespace)
let sd_notify_sock =
Option.map
(fun path ->
let sock_fd = Unix.socket PF_UNIX SOCK_DGRAM 0 ~cloexec:true in
let dest = Unix.ADDR_UNIX path in
sock_fd, dest)
(Sys.getenv_opt "NOTIFY_SOCKET")
let sd_notify msg =
Option.iter
(fun (sock_fd, dest) ->
let dgram = Bytes.of_string msg in
Unix.sendto sock_fd dgram 0 (Bytes.length dgram) [] dest |> ignore)
sd_notify_sock
(* TODO: s-exp/json/toml config format *)
let port = let port =
try try
let port = int_of_string (Sys.getenv "IRC_PORT") in let port = int_of_string (Sys.getenv "IRC_PORT") in
@ -36,15 +55,32 @@ let hostname =
| Some x -> x | Some x -> x
| None -> "irc.tali.software" | None -> "irc.tali.software"
let motd_file =
match Sys.getenv_opt "IRC_MOTD" with
| Some x -> x
| None -> "./motd.txt"
let config : Server.config = { let config : Server.config = {
port; port;
hostname; hostname;
listen_backlog = 8; listen_backlog = 8;
ping_interval = 60; ping_interval = 60;
whowas_history_len = 1000; whowas_history_len = 1000;
motd_file;
(* TODO: motd *) notify = function
| `ready -> sd_notify "READY=1"
| `stopping -> sd_notify "STOPPING=1"
} }
let () = let () =
Lwt_main.run @@ Server.run config Printexc.register_printer
(function
| Unix.Unix_error (eno, who, _) -> Some (Fmt.str "%s: %s" who (Unix.error_message eno))
| Failure msg -> Some ("internal error: " ^ msg)
| Invalid_argument who -> Some ("internal error: invalid argumnet: " ^ who)
| _ -> None);
try
Lwt_main.run @@ Server.run config
with exn ->
error (fun m -> m "%a" Fmt.exn exn);
exit 1

3
conf.txt Normal file
View File

@ -0,0 +1,3 @@
IRC_MOTD=/etc/talircd.motd
IRC_HOSTNAME=irc.tali.software
IRC_PORT=6667

43
deploy/_build.sh Executable file
View File

@ -0,0 +1,43 @@
#!/usr/bin/env bash
opam init --disable-sandboxing --no-setup
eval $(opam env)
root=dist/root
rm -rf $root
mkdir -p $root/usr/lib/systemd/system $root/DEBIAN
# build ocaml program
opam pin add ./talircd --kind=path --no-action --yes
opam install talircd --destdir=$root/usr --yes
# install service file
install -m 644 talircd/deploy/talircd.service $root/usr/lib/systemd/system
# generate package control file
pkg=$(opam info talircd -fname)
ver="$(opam info talircd -fversion)"
rev=0
dsc="$(opam info talircd -fdescription)"
mtr="iitalics"
arch="$(dpkg-architecture -q DEB_TARGET_ARCH)"
control=$root/DEBIAN/control
set -x
echo "Package: ${pkg}" > $control
echo "Version: ${ver}" >> $control
echo "Description: ${dsc}" >> $control
echo "Maintainer: ${mtr}" >> $control
echo "Architecture: ${arch}" >> $control
install -m 755 talircd/deploy/postinst.sh $root/DEBIAN/postinst
# generate .deb
dpkg-deb --root-owner-group -b $root "dist/${pkg}_${ver}-${rev}_${arch}.deb"

58
deploy/deploy.sh Executable file
View File

@ -0,0 +1,58 @@
#!/usr/bin/env bash
MACHINE=${MACHINE:-talircd-build}
MACHINE_PATH=/var/lib/machines/${MACHINE}
PROJECT_ROOT=${PROJECT_ROOT:-$(git rev-parse --show-toplevel)}
DIST_DIR=${DIST_DIR:-${PROJECT_ROOT}/_dist}
CODENAME=bullseye
MIRROR=${MIRROR:-http://debian.csail.mit.edu/debian/}
function init() {
# TODO: fakeroot ?
set -x
sudo debootstrap \
--include=ocaml-base,ocaml,opam,ca-certificates,git,rsync \
--components=main,contrib \
${CODENAME} ${MACHINE_PATH} ${MIRROR}
}
function cleanall() {
set -x
sudo rm -rf ${MACHINE_PATH}
}
function in_container() {
set -x
mkdir -p ${DIST_DIR}
sudo systemd-nspawn -q \
--machine ${MACHINE} \
--bind-ro ${PROJECT_ROOT}:/root/talircd \
--bind ${DIST_DIR}:/root/dist:rootidmap \
-E OPAMROOTISOK=1 \
"$@"
}
function clean() {
rm -rf ${DIST_DIR}/root
in_container rm -rf /root/.opam
}
function build() {
in_container --chdir /root /root/talircd/deploy/_build.sh
}
function shell() {
in_container
}
case ${1:-build} in
init) init ;;
build) build ;;
clean) clean ;;
cleanall) cleanall ;;
shell) shell ;;
*)
echo "invalid command $1"
esac

4
deploy/postinst.sh Normal file
View File

@ -0,0 +1,4 @@
#!/usr/bin/sh
[ -f /etc/talircd.conf ] || install -m 644 /usr/share/talircd/default/conf /etc/talircd.conf
[ -f /etc/talircd.motd ] || install -m 644 /usr/share/talircd/default/motd /etc/talircd.motd
systemctl daemon-reload

9
deploy/talircd.service Normal file
View File

@ -0,0 +1,9 @@
[Unit]
Description=tali IRCd
ConditionPathExists=/etc/talircd.conf
[Service]
Type=notify
ExecStart=/usr/bin/talircd
KillSignal=SIGTERM
EnvironmentFile=/etc/talircd.conf

6
dune Normal file
View File

@ -0,0 +1,6 @@
(install
(package talircd)
(files
(motd.txt as default/motd)
(conf.txt as default/conf))
(section share))

View File

@ -1,13 +1,21 @@
(lang dune 3.8) (lang dune 3.8)
(name talircd) (name talircd)
(version 0.0.3)
(generate_opam_files true) (generate_opam_files true)
(package (package
(name talircd) (name talircd)
(depends (synopsis "IRC server")
ocaml (description "IRC server for cats written in ocaml")
dune (license "LGPL-2.0-or-later")
(lwt (= 5.7.0)) (homepage "https://git.lain.faith/iitalics/talircd/")
(ppx_expect (= v0.16.0)) (bug_reports "https://git.lain.faith/iitalics/talircd/issues")
(ppx_deriving (= 5.2.1)) (authors "iitalics <git.lain.faith/iitalics>")
(fmt (= 0.9.0)) (maintainers "iitalics <git.lain.faith/iitalics>")
(ptime (= 1.1.0)))) (depends ocaml dune
(lwt (>= 5.7))
(ppx_expect (and (>= v0.15) (< v0.17)))
(ppx_deriving (>= 5.2))
(fmt (>= 0.9))
(ptime (>= 1.1))))

View File

@ -25,6 +25,12 @@ let create () =
let is_empty seq = seq.next == seq let is_empty seq = seq.next == seq
let reset seq =
begin
seq.next <- seq;
seq.prev <- seq;
end
let remove node = let remove node =
if node.node_active then begin if node.node_active then begin
node.node_active <- false; node.node_active <- false;
@ -74,6 +80,15 @@ let fold_r f seq acc =
in in
loop seq.prev acc loop seq.prev acc
let iter_l f seq =
let rec loop curr =
if curr != seq then
let node = node_of_seq curr in
if node.node_active then f node.node_data;
loop node.node_next
in
loop seq.next
let find f seq = let find f seq =
let rec loop curr = let rec loop curr =
if curr == seq then if curr == seq then

View File

@ -21,6 +21,9 @@ val create : unit -> 'a t
val is_empty : 'a t -> bool val is_empty : 'a t -> bool
(** Returns [true] iff the given sequence is empty *) (** Returns [true] iff the given sequence is empty *)
val reset : 'a t -> unit
(** [reset ()] is a lazy way to remove all the elements from the sequence *)
val add_l : 'a -> 'a t -> 'a node val add_l : 'a -> 'a t -> 'a node
(** [add_l x s] adds [x] to the left of the sequence [s] *) (** [add_l x s] adds [x] to the left of the sequence [s] *)
@ -57,6 +60,10 @@ val fold_r : ('a -> 'b -> 'b) -> 'a t -> 'b -> 'b
where [e1], [e2], ..., [en] are the elements of [s] where [e1], [e2], ..., [en] are the elements of [s]
*) *)
val iter_l : ('a -> unit) -> 'a t -> unit
(** [iter_l f s] applies [f] on all elements of [s] starting from
the left *)
val find : ('a -> bool) -> 'a t -> 'a val find : ('a -> bool) -> 'a t -> 'a
(** [find_node_l f s] returns the first element of [s] starting from the left (** [find_node_l f s] returns the first element of [s] starting from the left
that satisfies [f] or raises [Not_found] if none exists. *) that satisfies [f] or raises [Not_found] if none exists. *)

View File

@ -25,6 +25,11 @@ let%expect_test _ =
Wheel.add wh 6 |> ignore; Wheel.add wh 6 |> ignore;
(* t=3 *) (* t=3 *)
print_ints_nl (Wheel.tick wh); [%expect {| [] |}]; print_ints_nl (Wheel.tick wh); [%expect {| [] |}];
let every = ref [] in
Wheel.iter (fun x -> every := x :: !every) wh;
print_ints_nl (List.sort compare !every); [%expect {| [1;2;3;4;5;6] |}];
(* t=0 *) (* t=0 *)
print_ints_nl (Wheel.tick wh); [%expect {| [1;2;3] |}]; print_ints_nl (Wheel.tick wh); [%expect {| [1;2;3] |}];
(* t=1 *) (* t=1 *)

View File

@ -22,3 +22,8 @@ let[@tail_mod_cons] rec empty t =
let tick t = let tick t =
t.index <- (t.index + 1) mod Array.length t.entries; t.index <- (t.index + 1) mod Array.length t.entries;
empty t empty t
let iter f t =
for i = 0 to Array.length t.entries - 1 do
Dllist.iter_l f t.entries.(i)
done

View File

@ -73,32 +73,18 @@ module Set = struct
let add elt s = union s (singleton elt) let add elt s = union s (singleton elt)
let remove elt s = diff s (singleton elt) let remove elt s = diff s (singleton elt)
let of_list l =
List.fold_left (fun s m -> add m s) empty l
let to_string s = let to_string s =
let bs = Bytes.create 7 in Seq.filter_map
let bit ch elt i = (fun m -> if mem m s then Some (to_char m) else None)
if mem elt s then (Bytes.set bs i ch; i + 1) (List.to_seq [`i; `m; `n; `o; `s; `t; `w]) |>
else i String.of_seq
in
let n =
0 |> bit 'i' `i |> bit 'm' `m |> bit 'n' `n |> bit 'o' `o |>
bit 's' `s |> bit 't' `t |> bit 'w' `w
in
Bytes.sub_string bs 0 n
let pp ppf s = let pp ppf s =
Format.pp_print_string ppf (to_string s) Format.pp_print_string ppf (to_string s)
let of_string s =
let chr = function
| 'i' -> `i | 'm' -> `m | 'n' -> `n | 'o' -> `o
| 's' -> `s | 't' -> `t | 'w' -> `w
| _ -> invalid_arg "Irc.Mode.Set.of_string"
in
String.fold_left (fun s c -> add (chr c) s) empty s
let of_list l =
List.fold_left (fun s m -> add m s) empty l
type change = { type change = {
add : t; add : t;
rem : t; rem : t;
@ -281,14 +267,14 @@ let%expect_test _ =
let print_change_nl c = Format.kasprintf print_string "%a\n" Set.pp_change c in let print_change_nl c = Format.kasprintf print_string "%a\n" Set.pp_change c in
print_set_nl Set.empty; [%expect {| [] |}]; print_set_nl Set.empty; [%expect {| [] |}];
print_set_nl Set.(of_string "i"); [%expect {| [i] |}]; print_set_nl Set.(of_list [`i]); [%expect {| [i] |}];
print_set_nl Set.(of_string "no"); [%expect {| [no] |}]; print_set_nl Set.(of_list [`n;`o]); [%expect {| [no] |}];
print_set_nl Set.(of_string "sm"); [%expect {| [ms] |}]; print_set_nl Set.(of_list [`s;`m]); [%expect {| [ms] |}];
print_set_nl Set.(of_string "wi"); [%expect {| [iw] |}]; print_set_nl Set.(of_list [`w;`i]); [%expect {| [iw] |}];
print_bool_nl Set.(mem `i (of_string "ins")); [%expect "true"]; print_bool_nl Set.(mem `i (of_list [`i;`n;`s])); [%expect "true"];
print_bool_nl Set.(mem `w (of_string "ins")); [%expect "false"]; print_bool_nl Set.(mem `w (of_list [`i;`n;`s])); [%expect "false"];
print_bool_nl Set.(mem `w (of_string "wwww")); [%expect "true"]; print_bool_nl Set.(mem `w (of_list [`w;`w;`w;`w])); [%expect "true"];
print_bool_nl Set.(mem `t (of_string "imnosw")); [%expect "false"]; print_bool_nl Set.(mem `t (of_list [`i;`m;`n;`o;`s;`w])); [%expect "false"];
let print_parse_error f = let print_parse_error f =
try f () |> ignore; print_endline "()" try f () |> ignore; print_endline "()"
@ -305,7 +291,8 @@ let%expect_test _ =
print_change_nl (Parse.user_modes "-o+o"); [%expect {| +o |}]; print_change_nl (Parse.user_modes "-o+o"); [%expect {| +o |}];
print_parse_error (fun () -> Parse.user_modes "+I"); [%expect {| unknown mode I |}]; print_parse_error (fun () -> Parse.user_modes "+I"); [%expect {| unknown mode I |}];
let m, c = Set.normalize (Set.of_string "iw") (Parse.user_modes "-w+io") in let m = Set.of_list [`i;`w] in
let m, c = Set.normalize m (Parse.user_modes "-w+io") in
Format.printf "%a -> [%a]\n@." Set.pp_change c Set.pp m; Format.printf "%a -> [%a]\n@." Set.pp_change c Set.pp m;
[%expect {| +o-w -> [io] |}]; [%expect {| +o-w -> [io] |}];

View File

@ -45,6 +45,7 @@ module Set : sig
val empty : t val empty : t
val singleton : [< elt] -> t val singleton : [< elt] -> t
val of_list : [< elt] list -> t
val mem : [< elt] -> t -> bool val mem : [< elt] -> t -> bool
val add : [< elt] -> t -> t val add : [< elt] -> t -> t
val remove : [< elt] -> t -> t val remove : [< elt] -> t -> t
@ -55,9 +56,6 @@ module Set : sig
val pp : Format.formatter -> t -> unit val pp : Format.formatter -> t -> unit
val to_string : t -> string val to_string : t -> string
val of_string : string -> t
val of_list : [< elt] list -> t
(* val to_list : t -> elt list *)
type change = { type change = {
add : t; add : t;

View File

@ -21,16 +21,21 @@ type t = {
trailing : bool; trailing : bool;
} [@@deriving show { with_path = false }] } [@@deriving show { with_path = false }]
let is_param_trailing p = let rec is_trailing arg i =
String.starts_with p ~prefix:":" || String.contains p ' ' if i >= String.length arg then
arg = ""
else match arg.[i] with
| ' ' | '\t' -> true
| ':' when i = 0 -> true
| _ -> is_trailing arg (i + 1)
let rec is_params_trailing = function let rec ends_with_trailing = function
| [] -> false | [] -> false
| [p] -> is_param_trailing p | [p] -> is_trailing p 0
| _ :: tl -> is_params_trailing tl | _ :: tl -> ends_with_trailing tl
let make ?(prefix = No_prefix) ?(always_trailing = false) command params = let make ?(prefix = No_prefix) ?(always_trailing = false) command params =
let trailing = always_trailing || is_params_trailing params in let trailing = always_trailing || ends_with_trailing params in
{ prefix; command; params; trailing } { prefix; command; params; trailing }
let write buf t = let write buf t =
@ -166,6 +171,12 @@ let%expect_test _ =
make "NICK" [":)"] |> print_msg_nl; make "NICK" [":)"] |> print_msg_nl;
[%expect {| "NICK ::)\r\n" |}]; [%expect {| "NICK ::)\r\n" |}];
make "NICK" ["(:"] |> print_msg_nl;
[%expect {| "NICK (:\r\n" |}];
make "NICK" [""] |> print_msg_nl;
[%expect {| "NICK :\r\n" |}];
make "USER" ["milo"; "0"; "*"; "milo"] ~always_trailing:true |> print_msg_nl; make "USER" ["milo"; "0"; "*"; "milo"] ~always_trailing:true |> print_msg_nl;
[%expect {| "USER milo 0 * :milo\r\n" |}]; [%expect {| "USER milo 0 * :milo\r\n" |}];

View File

@ -6,14 +6,19 @@ type t = {
mutex : Mutex.t; mutex : Mutex.t;
sock_fd : Unix.file_descr; sock_fd : Unix.file_descr;
dest : Unix.sockaddr; dest : Unix.sockaddr;
dgram : Buffer.t; buf : Buffer.t;
} }
let should_upgrade () =
let stderr = Unix.fstat Unix.stderr in
let dev_ino = Printf.sprintf "%d:%d" stderr.st_dev stderr.st_ino in
Sys.getenv_opt "JOURNAL_STREAM" = Some dev_ino
let make ?(path = default_socket_path) () = { let make ?(path = default_socket_path) () = {
mutex = Mutex.create (); mutex = Mutex.create ();
sock_fd = Unix.socket PF_UNIX SOCK_DGRAM 0 ~cloexec:true; sock_fd = Unix.socket PF_UNIX SOCK_DGRAM 0 ~cloexec:true;
dest = ADDR_UNIX path; dest = ADDR_UNIX path;
dgram = Buffer.create 256; buf = Buffer.create 256;
} }
let add_field dgram key value = let add_field dgram key value =
@ -36,11 +41,13 @@ let syslog_priority = function
| ERROR -> "3" (* LOG_ERR *) | ERROR -> "3" (* LOG_ERR *)
let writer t ~ts ~ns ~lvl msg = let writer t ~ts ~ns ~lvl msg =
Mutex.lock t.mutex;
let dgram = let dgram =
Mutex.protect t.mutex @@ fun () -> Buffer.clear t.buf;
ignore ts; ignore ts;
add_field t.dgram "MESSAGE" (Printf.sprintf "%s: %s" ns msg); add_field t.buf "MESSAGE" (Printf.sprintf "%s: %s" ns msg);
add_field t.dgram "PRIORITY" (syslog_priority lvl); add_field t.buf "PRIORITY" (syslog_priority lvl);
Buffer.to_bytes t.dgram Buffer.to_bytes t.buf
in in
Mutex.unlock t.mutex;
Unix.sendto t.sock_fd dgram 0 (Bytes.length dgram) [] t.dest |> ignore Unix.sendto t.sock_fd dgram 0 (Bytes.length dgram) [] t.dest |> ignore

View File

@ -107,3 +107,5 @@ let init_journald_writer
Journald.make () ?path |> Journald.make () ?path |>
Journald.writer |> Journald.writer |>
add_writer ?min_level add_writer ?min_level
let should_upgrade_to_journald = Journald.should_upgrade

View File

@ -36,3 +36,5 @@ val init_journald_writer :
?min_level:level -> ?min_level:level ->
?path:string -> ?path:string ->
unit -> unit unit -> unit
val should_upgrade_to_journald : unit -> bool

5
lib/logging/mutex.ml Normal file
View File

@ -0,0 +1,5 @@
(* stub implementation since we actually aren't multithreaded *)
type t = unit
external create : unit -> t = "%identity"
external lock : t -> unit = "%identity"
external unlock : t -> unit = "%identity"

View File

@ -95,7 +95,7 @@ let writer t ~ts ~ns ~lvl msg =
let f mask = t.flags land mask = mask in let f mask = t.flags land mask = mask in
let align bp = let align bp =
let n = t.align_to - Buffer.length bp in let n = t.align_to - Buffer.length bp in
t.align_to <- Buffer.length bp; t.align_to <- max t.align_to (Buffer.length bp);
n n
in in
let indent bp = let indent bp =

View File

@ -24,6 +24,7 @@ let creation_time t = t.creation_time
let topic t = fst t.topic let topic t = fst t.topic
let topic_who_time t = snd t.topic let topic_who_time t = snd t.topic
let set_topic ~who ?time t text = t.topic <- text, Some (who, value_or_now time) let set_topic ~who ?time t text = t.topic <- text, Some (who, value_or_now time)
let member_count t = t.member_count
let mode t = t.chan_mode let mode t = t.chan_mode
let set_mode t new_mode = t.chan_mode <- new_mode let set_mode t new_mode = t.chan_mode <- new_mode
let limit t = t.chan_limit let limit t = t.chan_limit

View File

@ -280,22 +280,22 @@ let on_msg_mode t name args =
Ok () Ok ()
(* messages and channels *) (* messages *)
let get_priv_opt chan user =
try
let mem = Router.membership chan user in
Some mem.mem_priv
with Not_found ->
None
let send_to_chan ~from chan = let send_to_chan ~from chan =
let cannot_send = let priv_required =
try if Mode.Set.mem `m (Chan.mode chan) then Some Voice
let mem = Router.membership chan from in else if Mode.Set.mem `n (Chan.mode chan) then Some Normal
(* check if moderated (+m) *) else None
if Mode.Set.mem `m (Chan.mode chan) then
mem.mem_priv < Voice
else
false
with Not_found ->
(* check if no external messages (+n) *)
Mode.Set.mem `n (Chan.mode chan)
in in
if cannot_send then if get_priv_opt chan from < priv_required then
Error (cannotsendtochan (Chan.name chan)) Error (cannotsendtochan (Chan.name chan))
else else
Ok (Chan.name chan, [`to_chan chan]) Ok (Chan.name chan, [`to_chan chan])
@ -336,6 +336,9 @@ let on_msg_away t status =
set_away t me status; set_away t me status;
Ok () Ok ()
(* channels *)
let membership_prefix = function let membership_prefix = function
| Normal -> "" | Normal -> ""
| Voice -> "+" | Voice -> "+"
@ -344,30 +347,29 @@ let membership_prefix = function
let is_invisible user = let is_invisible user =
Mode.Set.mem `i (User.mode user) Mode.Set.mem `i (User.mode user)
let list_names t me chan = let is_secret chan =
let is_secret = Mode.Set.mem `s (Chan.mode chan) in Mode.Set.mem `s (Chan.mode chan)
let list_names t me chan =
let members = let members =
match Router.membership chan me with match Router.membership chan me with
| _is_member -> Chan.membership chan | _is_member -> Chan.membership chan
| exception Not_found -> | exception Not_found ->
if is_secret then if is_secret chan then
[] []
else else
Chan.membership_when Chan.membership_when
(fun mem -> not (is_invisible mem.mem_user)) (fun mem -> not (is_invisible mem.mem_user))
chan chan
in in
let nicks = let nicks =
List.map List.map
(fun mem -> (fun mem ->
membership_prefix mem.mem_priv ^ User.nick mem.mem_user) membership_prefix mem.mem_priv ^ User.nick mem.mem_user)
members members
in in
let chan_name = Chan.name chan in let chan_name = Chan.name chan in
let chan_sym = if is_secret then "@" else "=" in let chan_sym = if is_secret chan then "@" else "=" in
begin begin
(* TODO: concat member names until message becomes too long *) (* TODO: concat member names until message becomes too long *)
List.iter (fun nick -> reply t ("353", [chan_sym; chan_name; nick])) nicks; List.iter (fun nick -> reply t ("353", [chan_sym; chan_name; nick])) nicks;
@ -617,8 +619,8 @@ let list_whois t user =
reply t ("320", [nick; "is a cat, meow :3"]); reply t ("320", [nick; "is a cat, meow :3"]);
reply t ("379", [nick; Fmt.str "is using modes +%s" let mode = Mode.Set.{ add = User.mode user; rem = empty }in
(Mode.Set.to_string (User.mode user))]); reply t ("379", [nick; Fmt.str "is using modes %a" Mode.Set.pp_change mode]);
Option.iter Option.iter
(fun text -> (fun text ->
@ -683,6 +685,43 @@ let on_msg_userhost t nicks =
reply t ("302", [String.concat " " results]); reply t ("302", [String.concat " " results]);
Ok () Ok ()
let list_channels t me channels =
begin
reply t ("321", ["Channel"; "Users Name"]);
Seq.iter
(function
| Error err -> reply t err
| Ok chan ->
try
if is_secret chan then Router.membership chan me |> ignore;
let count = Chan.member_count chan in
let topic = Option.value (Chan.topic chan) ~default:"" in
reply t ("322", [Chan.name chan; string_of_int count; topic])
with Not_found ->
())
channels;
reply t ("323", ["End of /LIST"]);
end
let on_msg_list t names =
let* me = require_registered t in
let channels = match names with
| [] ->
Seq.map Result.ok
(Router.all_channels_seq t.router)
| _ ->
Seq.map
(fun name ->
try
match name_type name with
| `chan -> Ok (Router.find_chan t.router name)
| `nick | `invalid -> raise Not_found
with Not_found ->
Error (nosuchnick name))
(List.to_seq names)
in
list_channels t me channels;
Ok ()
(* welcome and quit *) (* welcome and quit *)
@ -935,6 +974,8 @@ let dispatch t = function
| "WHOWAS", ([] | "" :: _) -> Error nonicknamegiven | "WHOWAS", ([] | "" :: _) -> Error nonicknamegiven
| "WHOWAS", [nick] -> on_msg_whowas t nick "" | "WHOWAS", [nick] -> on_msg_whowas t nick ""
| "WHOWAS", nick :: count :: _ -> on_msg_whowas t nick count | "WHOWAS", nick :: count :: _ -> on_msg_whowas t nick count
| "LIST", chans :: _ -> on_msg_list t (String.split_on_char ',' chans)
| "LIST", [] -> on_msg_list t []
| "USERHOST", nicks -> on_msg_userhost t nicks | "USERHOST", nicks -> on_msg_userhost t nicks
| ("USER" | "JOIN" | "NAMES" | "PART" | "KICK" | "MODE" | "WHO") as cmd, _ -> | ("USER" | "JOIN" | "NAMES" | "PART" | "KICK" | "MODE" | "WHO") as cmd, _ ->
Error (needmoreparams cmd) Error (needmoreparams cmd)
@ -948,7 +989,6 @@ let dispatch t = function
| tgt :: msg :: _ -> on_msg_privmsg t tgt msg ~cmd | tgt :: msg :: _ -> on_msg_privmsg t tgt msg ~cmd
end end
(* TODO: "LIST" *)
| cmd, _ -> | cmd, _ ->
Error (unknowncommand cmd) Error (unknowncommand cmd)
@ -965,7 +1005,6 @@ let split_command_params cmd params =
| ("PRIVMSG" | "NOTICE" | "NAMES" | "PART"), tgts :: rest | ("PRIVMSG" | "NOTICE" | "NAMES" | "PART"), tgts :: rest
when String.contains tgts ',' -> when String.contains tgts ',' ->
(* TODO: "JOIN" should be handled specially *)
String.split_on_char ',' tgts |> String.split_on_char ',' tgts |>
List.map (fun tgt -> cmd, tgt :: rest) List.map (fun tgt -> cmd, tgt :: rest)

View File

@ -6,3 +6,10 @@
logging irc data) logging irc data)
(inline_tests) (inline_tests)
(preprocess (pps ppx_expect))) (preprocess (pps ppx_expect)))
(rule
(package talircd)
(target "meta_server_info.ml")
(action
(with-stdout-to %{target}
(echo "let version = \"%{version:talircd}\";;"))))

View File

@ -28,6 +28,19 @@ let find_chan t name =
let whowas t nick = let whowas t nick =
Cache.find_all t.whowas (string_ci nick) Cache.find_all t.whowas (string_ci nick)
let all_channels_seq t =
Hashtbl.to_seq_values t.channels
let nuke t =
begin
Hashtbl.iter (fun _ u -> Dllist.reset u.membership) t.users;
Hashtbl.iter (fun _ c -> Dllist.reset c.members) t.channels;
Hashtbl.clear t.users;
Hashtbl.clear t.channels;
t.lusers <- 0;
t.luserchannels <- 0;
end
let relay ~(from : user) (msg : Msg.t) tgts = let relay ~(from : user) (msg : Msg.t) tgts =
let msg = let msg =
if msg.prefix = No_prefix then if msg.prefix = No_prefix then

View File

@ -4,20 +4,29 @@ open Lwt.Infix
include (val Logging.sublogs logger "Server") include (val Logging.sublogs logger "Server")
type ping_wheel = Connection.t Wheel.t type config = {
port : int;
listen_backlog : int;
ping_interval : int;
whowas_history_len : int;
hostname : string;
motd_file : string;
notify : [`ready | `stopping] -> unit;
}
let listener ~(port : int) ~(listen_backlog : int) : (fd * sockaddr) Lwt_stream.t = let bind_server
let sock : fd Lwt.t = ~(port : int)
let fd = Lwt_unix.socket PF_INET SOCK_STREAM 0 in ~(listen_backlog : int)
Lwt_unix.setsockopt fd SO_KEEPALIVE false; : fd Lwt.t =
Lwt_unix.setsockopt fd SO_REUSEPORT true; let fd = Lwt_unix.socket PF_INET SOCK_STREAM 0 in
let srv_adr = Unix.ADDR_INET (Unix.inet_addr_any, port) in let srv_adr = Unix.ADDR_INET (Unix.inet_addr_any, port) in
let* () = Lwt_unix.bind fd srv_adr in let* () = Lwt_unix.bind fd srv_adr in
Lwt_unix.listen fd listen_backlog; Lwt_unix.listen fd listen_backlog;
info (fun m -> m "listening on %a" pp_sockaddr srv_adr); info (fun m -> m "listening on %a" pp_sockaddr srv_adr);
Lwt.return fd Lwt.return fd
in
let accept () = sock >>= Lwt_unix.accept >|= Option.some in let accepts (fd : fd) : (fd * sockaddr) Lwt_stream.t =
let accept () = Lwt_unix.accept fd >>= Lwt.return_some in
Lwt_stream.from accept Lwt_stream.from accept
let reader (fd : fd) : Msg.t Lwt_stream.t = let reader (fd : fd) : Msg.t Lwt_stream.t =
@ -66,7 +75,7 @@ let handle_client
(conn_addr : sockaddr) (conn_addr : sockaddr)
~(server_info : Server_info.t) ~(server_info : Server_info.t)
~(router : Router.t) ~(router : Router.t)
~(ping_wheel : ping_wheel) ~(ping_wheel : Connection.t Wheel.t)
= =
info (fun m -> m "new connection %a" pp_sockaddr conn_addr); info (fun m -> m "new connection %a" pp_sockaddr conn_addr);
let conn : Connection.t = let conn : Connection.t =
@ -92,23 +101,60 @@ let handle_client
(fun e -> error (fun m -> m "%a:@ %a" pp_sockaddr conn_addr Fmt.exn e)); (fun e -> error (fun m -> m "%a:@ %a" pp_sockaddr conn_addr Fmt.exn e));
end end
type config = { let interval dt =
port : int; let tick () =
listen_backlog : int; let* () = Lwt_unix.sleep dt in
ping_interval : int; Lwt.return_some ()
whowas_history_len : int;
hostname : string;
(* TODO: motd *)
}
let run { port; listen_backlog; ping_interval;
whowas_history_len; hostname } : unit Lwt.t
=
let server_info =
Server_info.make
~hostname
(* ~motd *)
in in
Lwt_stream.from tick
let interrupt () =
let signal, signal_waiter = Lwt.wait () in
let on_signal num =
trace (fun m -> m "caught signal %d" num);
try Lwt.wakeup signal_waiter () with
Invalid_argument _ -> failwith "unceremoniously exiting"
in
Lwt_unix.on_signal (2 (* SIGINT *)) on_signal |> ignore;
Lwt_unix.on_signal (15 (* SIGTERM *)) on_signal |> ignore;
signal
let run {
port;
listen_backlog;
ping_interval;
whowas_history_len;
hostname;
motd_file;
notify;
}
: unit Lwt.t =
debug (fun m -> m "ping interval:@ %ds" ping_interval);
debug (fun m -> m "whowas history:@ %d" whowas_history_len);
let* motd =
let* file = Lwt_io.open_file motd_file ~mode:Input in
let* lines = Lwt_io.read_lines file |> Lwt_stream.to_list in
let+ () = Lwt_io.close file in
debug (fun m -> m "motd file:@ %d lines" (List.length lines));
lines
in
let server_info =
Server_info.make ()
~hostname
~motd
in
info (fun m -> m "hostname:@ %s" server_info.hostname);
info (fun m -> m "version:@ %s" server_info.version);
info (fun m -> m "created:@ %s" server_info.created);
let* server : fd =
bind_server
~port
~listen_backlog
in
notify `ready;
let router : Router.t = let router : Router.t =
Router.make Router.make
@ -120,37 +166,45 @@ let run { port; listen_backlog; ping_interval;
ping_interval ping_interval
in in
let on_tick () = let ping conn =
(* trace (fun m -> m "tick"); *) match Connection.on_ping conn with
List.iter | Ok () -> Wheel.add ping_wheel conn
(fun conn -> | Error _ -> Connection.close conn ~reason:"Connection timeout"
match Connection.on_ping conn with
| Ok () -> Wheel.add ping_wheel conn
| Error () -> Connection.close conn ~reason:"Connection timed out")
(Wheel.tick ping_wheel)
in in
let pinger_promise = let pinger_promise =
Lwt_stream.iter Lwt_stream.iter
on_tick (fun () ->
(Lwt_stream.from @@ fun () -> List.iter ping
let* () = Lwt_unix.sleep 1.0 in (Wheel.tick ping_wheel))
Lwt.return_some ()) (interval 1.0)
in
let on_con (fd, adr) =
handle_client fd adr
~server_info
~router
~ping_wheel
in in
let listener_promise = let listener_promise =
Lwt_stream.iter Lwt_stream.iter
on_con (fun (fd, addr) ->
(listener handle_client fd addr
~port ~server_info
~listen_backlog) ~router
~ping_wheel)
(accepts server)
in in
listener_promise <&> pinger_promise let* () =
Lwt.pick [
listener_promise <?> pinger_promise;
interrupt ()
]
in
notify `stopping;
info (fun m -> m "shutting down");
let* () = Lwt_unix.close server in
Router.nuke router;
Wheel.iter (fun conn -> Connection.close conn ~reason:"Server shutting down")
(* ping wheel should contain every active connection *)
ping_wheel;
(* give some time for the messages to send *)
Lwt_unix.sleep 0.5

View File

@ -1,4 +1,7 @@
open! Import open! Import
include (Meta_server_info : sig
val version : string
end)
let tz_offset_s = Ptime_clock.current_tz_offset_s () let tz_offset_s = Ptime_clock.current_tz_offset_s ()
let pp_time = Ptime.pp_human () ?tz_offset_s let pp_time = Ptime.pp_human () ?tz_offset_s
@ -37,22 +40,13 @@ let default_conf = {
init_cmode = Mode.Set.of_list [`n; `s; `t]; init_cmode = Mode.Set.of_list [`n; `s; `t];
} }
let make ~hostname = { let make ?(conf = default_conf) ~hostname ~motd () = {
version = version;
(* TODO: generate version string at build time? *)
"0.0.0";
created = Fmt.str "%a" pp_time (Ptime_clock.now ()); created = Fmt.str "%a" pp_time (Ptime_clock.now ());
admin_info = "the admin of this server is @iitalics@octodon.social";
hostname; hostname;
admin_info = motd;
(* TODO: make configurable *) conf;
"the admin of this server is @iitalics@octodon.social";
motd = [
(* TODO: load from file *)
"MEOW MEOW MEOW MEOW MEOW";
"meow meow meow meow meow";
"meowmeowmeowmeowmeowmeow";
];
conf = default_conf;
} }
let prefix t = Msg.Server_prefix t.hostname let prefix t = Msg.Server_prefix t.hostname

6
motd.txt Normal file
View File

@ -0,0 +1,6 @@
--------------------------------------------
MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW
Meow Meow Meow Meow Meow Meow Meow Meow Meow
meow meow meow meow meow meow meow meow meow
meow meow meow meow meow meow meow meow meow
--------------------------------------------

View File

@ -1,13 +1,21 @@
# This file is generated by dune, edit dune-project instead # This file is generated by dune, edit dune-project instead
opam-version: "2.0" opam-version: "2.0"
version: "0.0.3"
synopsis: "IRC server"
description: "IRC server for cats written in ocaml"
maintainer: ["iitalics <git.lain.faith/iitalics>"]
authors: ["iitalics <git.lain.faith/iitalics>"]
license: "LGPL-2.0-or-later"
homepage: "https://git.lain.faith/iitalics/talircd/"
bug-reports: "https://git.lain.faith/iitalics/talircd/issues"
depends: [ depends: [
"ocaml" "ocaml"
"dune" {>= "3.8"} "dune" {>= "3.8"}
"lwt" {= "5.7.0"} "lwt" {>= "5.7"}
"ppx_expect" {= "v0.16.0"} "ppx_expect" {>= "v0.15" & < "v0.17"}
"ppx_deriving" {= "5.2.1"} "ppx_deriving" {>= "5.2"}
"fmt" {= "0.9.0"} "fmt" {>= "0.9"}
"ptime" {= "1.1.0"} "ptime" {>= "1.1"}
"odoc" {with-doc} "odoc" {with-doc}
] ]
build: [ build: [

View File

@ -1,8 +0,0 @@
[Unit]
Description=tali IRCd
[Service]
Environment=IRC_HOSTNAME=irc.tali.software
#Environment=IRC_PORT=6667
#Environment=LOG_LEVEL=DEBUG
ExecStart=/usr/local/bin/talircd