source: roaraudio/plugins/roard/protocol-irc.c @ 5307:4ac0597993b3

Last change on this file since 5307:4ac0597993b3 was 5307:4ac0597993b3, checked in by phi, 12 years ago

corrected get_node(), returned freed buffer

File size: 18.2 KB
Line 
1//irc.c:
2
3/*
4 *      Copyright (C) Philipp 'ph3-der-loewe' Schafft - 2011
5 *
6 *  This file is part of roard a part of RoarAudio,
7 *  a cross-platform sound system for both, home and professional use.
8 *  See README for details.
9 *
10 *  This file is free software; you can redistribute it and/or modify
11 *  it under the terms of the GNU General Public License version 3
12 *  as published by the Free Software Foundation.
13 *
14 *  RoarAudio is distributed in the hope that it will be useful,
15 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
16 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 *  GNU General Public License for more details.
18 *
19 *  You should have received a copy of the GNU General Public License
20 *  along with this software; see the file COPYING.  If not, write to
21 *  the Free Software Foundation, 51 Franklin Street, Fifth Floor,
22 *  Boston, MA 02110-1301, USA.
23 *
24 */
25
26#include <roard/include/roard.h>
27#include <ctype.h>
28
29#define MAX_CHANNELS 8
30
31static int on_nick(int client, const char * cmd, char * args, char * text);
32static int on_user(int client, const char * cmd, char * args, char * text);
33static int on_quit(int client, const char * cmd, char * args, char * text);
34static int on_ping(int client, const char * cmd, char * args, char * text);
35static int on_whois(int client, const char * cmd, char * args, char * text);
36static int on_privmsg(int client, const char * cmd, char * args, char * text);
37
38static int on_join(int client, const char * cmd, char * args, char * text);
39static int on_part(int client, const char * cmd, char * args, char * text);
40static int on_names(int client, const char * cmd, char * args, char * text);
41
42static int on_topic(int client, const char * cmd, char * args, char * text);
43
44static int do_join(int client, const char * channel);
45static int do_part(int client, const char * channel);
46static int do_names(int client, const char * channel);
47
48static void cb_client_delete(struct roar_notify_core * core, struct roar_event * event, void * userdata);
49
50static struct roar_subscriber * subscription_client_delete = NULL;
51
52static char * server_name     = "irc.roard";
53static char * server_fullname = "RoarAudio roard IRC Server plugin";
54
55static char * quit_msg = NULL;
56
57static struct channel {
58 char * name;
59 struct {
60  char * text;
61  char * user;
62  time_t ts;
63 } topic;
64 size_t client_count;
65 int clients[ROAR_CLIENTS_MAX];
66} g_channels[MAX_CHANNELS];
67
68static struct command {
69 const char * name;
70 int (*func)(int client, const char * cmd, char * args, char * text);
71} g_commands[] = {
72 {"NICK", on_nick},
73 {"USER", on_user},
74 {"QUIT", on_quit},
75 {"PING", on_ping},
76 {"WHOIS", on_whois},
77 {"PRIVMSG", on_privmsg},
78 {"NOTICE", on_privmsg},
79 {"JOIN", on_join},
80 {"PART", on_part},
81 {"NAMES", on_names},
82 {"TOPIC", on_topic},
83 {NULL, NULL}
84};
85
86static void init(void) {
87 struct roar_event event;
88
89 memset(&event, 0, sizeof(event));
90
91 event.event = ROAR_OE_BASICS_DELETE;
92
93 event.emitter = -1;
94 event.target = -1;
95 event.target_type = ROAR_OT_CLIENT;
96
97 memset(g_channels, 0, sizeof(g_channels));
98
99 subscription_client_delete = roar_notify_core_subscribe(NULL, &event, cb_client_delete, NULL);
100}
101
102static int is_valid_name (const char * name) {
103 register char c;
104
105 for (; (c = *name) != 0; name++) {
106  if ( !(isalnum(c) || c == '-' || c == '_') )
107   return 0;
108 }
109
110 return 1;
111}
112
113static void strip_nl(char * str) {
114 register char c;
115
116 for (; (c = *str) != 0; str++) {
117  if ( c == '\r' || c == '\n' ) {
118   *str = 0;
119   return;
120  }
121 }
122}
123
124static char * get_nick(int client) {
125 struct roar_client * c;
126 clients_get(client, &c);
127 return c->name;
128}
129
130static const char * get_ident(int client) {
131 static char buf[80];
132 struct roar_client * c;
133 clients_get(client, &c);
134
135 if ( c->uid == -1 ) {
136  buf[0] = '~';
137  buf[1] = 0;
138 } else {
139  snprintf(buf, sizeof(buf)-1, "uid%i~", c->uid);
140  buf[sizeof(buf)-1] = 0;
141 }
142
143 return buf;
144}
145
146static const char * get_node(int client) {
147 struct roar_client * c;
148 static char buf_nnode[80];
149 char * nnode;
150
151 clients_get(client, &c);
152
153 roar_nnode_to_str(&(c->nnode), buf_nnode, sizeof(buf_nnode));
154
155 nnode  = strstr(buf_nnode, ": ");
156 if ( nnode == NULL ) {
157  nnode  = "unknown~";
158 } else {
159  nnode += 2;
160 }
161
162 return nnode;
163}
164
165static const char * get_ufull(int client) {
166 struct roar_client * c;
167 static char buf[80];
168 const char * ident = get_ident(client);
169 const char * nnode = get_node(client);
170
171 clients_get(client, &c);
172
173 snprintf(buf, sizeof(buf)-1, "%s!%s@%s", c->name, ident, nnode);
174
175 buf[sizeof(buf)-1] = 0;
176
177 return buf;
178}
179
180static const char * get_realname(int client) {
181 (void)client;
182 return "(no realname)";
183}
184
185static int get_client_by_nick(const char * nick) {
186 struct roar_client * c;
187 int i;
188
189 for (i = 0; i < ROAR_CLIENTS_MAX; i++) {
190  if ( clients_get(i, &c) != 0 )
191   continue;
192
193  if ( !!strcasecmp(c->name, nick) )
194   continue;
195
196  return i;
197 }
198
199 return -1;
200}
201
202static struct channel * get_channel(const char * name) {
203 struct channel * c;
204 size_t i;
205
206 for (i = 0; i < MAX_CHANNELS; i++) {
207  c = &(g_channels[i]);
208
209  if ( !c->client_count )
210   continue;
211
212  if ( !!strcasecmp(c->name, name) )
213   continue;
214
215  return c;
216 }
217
218 return NULL;
219}
220
221static size_t get_listener_list(int client, const char * channel, int ** listener) {
222 struct channel * c;
223 static int clients[ROAR_CLIENTS_MAX];
224 size_t ret = 0;
225 size_t i, k;
226 int j;
227 int found;
228
229 for (i = 0; i < MAX_CHANNELS; i++) {
230  c = &(g_channels[i]);
231
232  if ( !c->client_count )
233   continue;
234
235  if ( c->clients[client] < 1 )
236   continue;
237
238  if ( channel != NULL && !!strcasecmp(c->name, channel) )
239   continue;
240
241  for (j = 0; j < ROAR_CLIENTS_MAX; j++) {
242   if ( c->clients[j] > 0 ) {
243    found = 0;
244    for (k = 0; k < ret; k++) {
245     if ( clients[k] == j ) {
246      found = 1;
247     }
248    }
249    if ( !found ) {
250     clients[ret] = j;
251     ret++;
252    }
253   }
254  }
255 }
256
257 *listener = clients;
258
259 return ret;
260}
261
262static void put_printf(int client, const char *format, ...) {
263 va_list ap;
264 struct roar_buffer * buf;
265 size_t len = 2048;
266 void * data;
267 int ret;
268
269 if ( roar_buffer_new_data(&buf, len, &data) == -1 )
270  return;
271
272 va_start(ap, format);
273 ret = vsnprintf(data, len-1, format, ap);
274 va_end(ap);
275
276 if ( roar_buffer_set_len(buf, strlen(data)) == -1 ) {
277  roar_buffer_free(buf);
278  return;
279 }
280
281 clients_add_output(client, &buf);
282}
283
284static int do_join(int client, const char * channel) {
285 struct channel * c = NULL;
286 size_t i;
287
288 if ( channel[0] != '#' )
289  return -1;
290
291 if ( !is_valid_name(channel+1) )
292  return -1;
293
294 for (i = 0; i < MAX_CHANNELS; i++) {
295  if ( !g_channels[i].client_count )
296   continue;
297
298  if ( !!strcasecmp(g_channels[i].name, channel) )
299   continue;
300
301  c = &(g_channels[i]);
302  break;
303 }
304
305 if ( c == NULL ) {
306  for (i = 0; i < MAX_CHANNELS; i++) {
307   if ( g_channels[i].client_count )
308    continue;
309
310   c = &(g_channels[i]);
311   break;
312  }
313
314  if ( c == NULL )
315   return -1;
316
317  memset(c, 0, sizeof(*c));
318
319  c->name = roar_mm_strdup(channel);
320 }
321
322 if ( c->clients[client] )
323  return -1;
324
325 c->clients[client] = 1;
326 c->client_count++;
327
328 return 0;
329}
330
331static int do_part(int client, const char * channel) {
332 struct channel * c = NULL;
333 size_t i;
334
335 for (i = 0; i < MAX_CHANNELS; i++) {
336  if ( !g_channels[i].client_count )
337   continue;
338
339  if ( !!strcasecmp(g_channels[i].name, channel) )
340   continue;
341
342  if ( !g_channels[i].clients[client] )
343   return -1;
344
345  c = &(g_channels[i]);
346  break;
347 }
348
349 c->clients[client] = 0;
350 c->client_count--;
351
352 if ( !c->client_count ) {
353  roar_mm_free(c->name);
354
355  if ( c->topic.text != NULL )
356   roar_mm_free(c->topic.text);
357  if ( c->topic.user != NULL )
358   roar_mm_free(c->topic.user);
359 }
360
361 return 0;
362}
363
364static int do_names(int client, const char * channel) {
365 const char * nick = get_nick(client);
366 size_t i;
367 char buf[256];
368 size_t len, offset;
369 char * c_nick;
370 int j;
371
372 for (i = 0; i < MAX_CHANNELS; i++) {
373  if ( !g_channels[i].client_count )
374   continue;
375
376  if ( !!strcasecmp(g_channels[i].name, channel) )
377   continue;
378
379  offset = 0;
380
381  for (j = 0; j < ROAR_CLIENTS_MAX; j++) {
382   if ( g_channels[i].clients[j] == 0 )
383    continue;
384
385   c_nick = get_nick(j);
386   len = strlen(c_nick);
387   if ( (offset + len + 3) > sizeof(buf) ) {
388    buf[offset] = 0;
389    put_printf(client, ":%s 353 %s = %s :%s\n", server_name, nick, channel, buf);
390    offset = 0;
391   }
392
393   memcpy(buf + offset, c_nick, len);
394   offset += len;
395   buf[offset] = ' ';
396   offset++;
397   buf[offset] = 0;
398  }
399
400  if ( offset ) {
401   buf[offset] = 0;
402   put_printf(client, ":%s 353 %s = %s :%s\n", server_name, nick, channel, buf);
403  }
404  put_printf(client, ":%s 366 %s %s :End of /NAMES list.\n", server_name, nick, channel);
405
406  return 0;
407 }
408
409 return -1;
410}
411
412static void cb_client_delete(struct roar_notify_core * core, struct roar_event * event, void * userdata) {
413 int * listener;
414 size_t count;
415 struct channel * c;
416 char * text = quit_msg;
417 int client = event->target;
418 const char * ufull = get_ufull(client);
419 size_t i;
420
421 (void)core, (void)userdata;
422
423 if ( text == NULL ) {
424  text = "Client deleted. Died, kicked or internal error.";
425 }
426
427 put_printf(client, "ERROR :Closing Link: %s (Quit: %s)\n", ufull, text);
428
429  count = get_listener_list(client, NULL, &listener);
430  for (; count; count--, listener++)
431   put_printf(*listener, ":%s QUIT :Quit: %s\n", ufull, text);
432
433 for (i = 0; i < MAX_CHANNELS; i++) {
434  c = &(g_channels[i]);
435
436  if ( !c->client_count )
437   continue;
438
439  if ( !c->clients[client] )
440   continue;
441
442  c->clients[client] = 0;
443  c->client_count--;
444
445  if ( !c->client_count ) {
446   roar_mm_free(c->name);
447
448   if ( c->topic.text != NULL )
449    roar_mm_free(c->topic.text);
450   if ( c->topic.user != NULL )
451    roar_mm_free(c->topic.user);
452  }
453 }
454
455 clients_flush(client);
456}
457
458static int new_client(int client, struct roar_vio_calls * vio, struct roard_listen * lsock) {
459 struct roar_client_server * cs;
460 char * name;
461
462 (void)client, (void)vio, (void)lsock;
463
464 clients_get_server(client, &cs);
465
466 name = ROAR_CLIENT(cs)->name;
467 snprintf(name, sizeof(ROAR_CLIENT(cs)->name), "Client%i~", client);
468
469/*
470:ph7.ph.sft NOTICE AUTH :*** Looking up your hostname...
471:ph7.ph.sft NOTICE AUTH :*** Couldn't resolve your hostname; using your IP address instead
472NICK nick
473USER A B C D
474:ph7.ph.sft NOTICE AUTH :*** Couldn't resolve your hostname; using your IP address instead
475:ph7.ph.sft 002 nick :Your host is ph7.ph.sft, running version Unreal3.2.7
476:ph7.ph.sft 003 nick :This server was created Tue Aug 28 2007 at 10:02:00 CEST
477:ph7.ph.sft 004 nick ph7.ph.sft Unreal3.2.7 iowghraAsORTVSxNCWqBzvdHtGp lvhopsmntikrRcaqOALQbSeIKVfMCuzNTGj
478:ph7.ph.sft 005 nick NAMESX SAFELIST HCN MAXCHANNELS=10 CHANLIMIT=#:10 MAXLIST=b:60,e:60,I:60 NICKLEN=30 CHANNELLEN=32 TOPICLEN=307 KICKLEN=307 AWAYLEN=307 MAXTARGETS=20 WALLCHOPS :are supported by this server
479:ph7.ph.sft 005 nick WATCH=128 SILENCE=15 MODES=12 CHANTYPES=# PREFIX=(ohv)@%+ CHANMODES=beIqa,kfL,lj,psmntirRcOAQKVCuzNSMTG NETWORK=PH2 CASEMAPPING=ascii EXTBAN=~,cqnr ELIST=MNUCT STATUSMSG=@%+ EXCEPTS INVEX :are supported by this server
480:ph7.ph.sft 005 nick CMDS=KNOCK,MAP,DCCALLOW,USERIP :are supported by this server
481:ph7.ph.sft 251 nick :There are 1 users and 0 invisible on 1 servers
482:ph7.ph.sft 253 nick 1 :unknown connection(s)
483:ph7.ph.sft 255 nick :I have 1 clients and 0 servers
484:ph7.ph.sft 265 nick :Current Local Users: 1  Max: 4
485:ph7.ph.sft 266 nick :Current Global Users: 1  Max: 1
486*/
487
488 put_printf(client,
489      ":%s 001 %s :Welcome to the roard based IRC server.\n"
490      ":%s 375 %s :- %s Message of the Day -\n"
491      ":%s 372 %s :- MotD goes here...\n"
492      ":%s 376 %s :End of /MOTD command.\n",
493   server_name, name,
494   server_name, name, server_name,
495   server_name, name,
496   server_name, name
497 );
498
499 return 0;
500}
501
502static int check_client(int client, struct roar_vio_calls * vio) {
503 struct roar_vio_calls     rvio;
504 char cmd[1024*2];
505 char * args;
506 char * text;
507 ssize_t len;
508 size_t i;
509 int found = 0;
510
511 if ( vio == NULL ) {
512  vio = &rvio;
513  roar_vio_open_fh_socket(vio, clients_get_fh(client));
514 }
515
516 len = roar_vio_read(vio, cmd, sizeof(cmd)-1);
517 if ( len < 1 ) {
518  clients_delete(client);
519  return -1;
520 }
521
522 cmd[len] = 0;
523
524 strip_nl(cmd);
525
526 ROAR_DBG("check_client(client=%i, vio=?): cmd=\"%s\"", client, cmd);
527
528 if ( cmd[0] == 0 )
529  return 0;
530
531 args = strstr(cmd, " ");
532
533 if ( args != NULL ) {
534  *args = 0;
535  args++;
536  if ( *args == ':' ) {
537   text = args + 1;
538   args = NULL;
539  } else {
540   text = strstr(args, " :");
541   if ( text != NULL ) {
542    *text = 0;
543    text += 2;
544   }
545  }
546 } else {
547  text = NULL;
548 }
549
550 for (i = 0; g_commands[i].name != NULL; i++) {
551  if ( !strcasecmp(g_commands[i].name, cmd) ) {
552   found = 1;
553   g_commands[i].func(client, cmd, args, text);
554  }
555 }
556
557 if ( !found ) {
558  put_printf(client, ":%s 421 %s %s :Unknown command\n", server_name, get_nick(client), cmd);
559 }
560
561 return 0;
562}
563
564// commands:
565static int on_nick(int client, const char * cmd, char * args, char * text) {
566 char * nick = get_nick(client);
567 int * listener;
568 size_t count;
569 const char * ufull;
570
571 (void)cmd, (void)text;
572
573 if ( args != NULL && args[0] != 0 && is_valid_name(args) && strlen(args) < ROAR_BUFFER_NAME ) {
574  if ( get_client_by_nick(args) != -1 ) {
575   put_printf(client, ":%s 433 %s %s :Nickname is already in use.\n", server_name, nick, args);
576  } else {
577   ufull = get_ufull(client);
578   put_printf(client, ":%s NICK :%s\n", ufull, args);
579   count = get_listener_list(client, NULL, &listener);
580   for (; count; count--, listener++)
581    if ( *listener != client )
582     put_printf(*listener, ":%s NICK :%s\n", ufull, args);
583
584   strcpy(nick, args);
585  }
586 } else {
587  put_printf(client, ":%s 432 %s %s :Erroneous Nickname: Illegal characters\n", server_name, nick, args);
588 }
589
590 return 0;
591}
592
593static int on_user(int client, const char * cmd, char * args, char * text) {
594 (void)client, (void)cmd, (void)args, (void)text;
595 return 0;
596}
597
598static int on_quit(int client, const char * cmd, char * args, char * text) {
599 int ret;
600
601 (void)cmd, (void)args;
602
603 if ( text == NULL )
604  text = "... have a cuddle ...";
605
606 quit_msg = text;
607 ret = clients_delete(client);
608 quit_msg = NULL;
609
610 return ret;
611}
612
613static int on_ping(int client, const char * cmd, char * args, char * text) {
614 (void)cmd, (void)args;
615
616 put_printf(client, "PONG :%s\n", text);
617
618 return 0;
619}
620
621static int on_whois(int client, const char * cmd, char * args, char * text) {
622 const char * clientnick = get_nick(client);
623 const char * tnick = args;
624 int tclient;
625
626 (void)cmd, (void)text;
627
628/*
629:ph7.ph.sft 317 nick phi 131 1322456381 :seconds idle, signon time
630*/
631
632 if ( (tclient = get_client_by_nick(tnick)) == -1 ) {
633  put_printf(client, ":%s 401 %s %s :No such nick/channel\n", server_name, clientnick, tnick);
634 } else {
635  put_printf(client, ":%s 311 %s %s %s %s * :%s\n", server_name, clientnick, tnick,
636             get_ident(tclient), get_node(tclient), get_realname(tclient));
637  put_printf(client, ":%s 312 %s %s %s :%s\n", server_name, clientnick, tnick, server_name, server_fullname);
638 }
639 put_printf(client, ":%s 318 %s %s :End of /WHOIS list.\n", server_name, clientnick, tnick);
640
641 return 0;
642}
643
644static int on_privmsg(int client, const char * cmd, char * args, char * text) {
645 const char * ufull = get_ufull(client);
646 int * listener;
647 size_t count;
648 char * next;
649 int tmp;
650
651 if ( args == NULL || text == NULL )
652  return -1;
653
654 if ( text[0] == 0 )
655  return 0;
656
657 while (args != NULL) {
658  next = strstr(args, ",");
659  if ( next != NULL ) {
660   *next = 0;
661   next++;
662  }
663
664  if ( args[0] == '#' ) {
665   count = get_listener_list(client, args, &listener);
666   for (; count; count--, listener++)
667    if ( *listener != client )
668     put_printf(*listener, ":%s %s %s :%s\n", ufull, cmd, args, text);
669  } else {
670   if ( (tmp = get_client_by_nick(args)) == -1 ) {
671    put_printf(client, ":%s 401 %s %s :No such nick/channel\n", server_name, get_nick(client), args);
672   } else {
673    put_printf(tmp, ":%s %s %s :%s\n", ufull, cmd, args, text);
674   }
675  }
676
677  args = next;
678 }
679
680 return 0;
681}
682
683
684static int on_join(int client, const char * cmd, char * args, char * text) {
685 struct channel * c;
686 const char * ufull = get_ufull(client);
687 int * listener;
688 size_t count;
689 const char * nick;
690
691 (void)cmd, (void)text;
692
693 if ( args == NULL )
694  return -1;
695
696 if ( do_join(client, args) != 0 ) {
697  return -1;
698 }
699
700 count = get_listener_list(client, args, &listener);
701 for (; count; count--, listener++)
702  put_printf(*listener, ":%s JOIN :%s\n", ufull, args);
703
704 c = get_channel(args);
705
706 if ( c->topic.text != NULL ) {
707  nick = get_nick(client);
708  put_printf(client, ":%s 332 %s %s :%s\n"
709                     ":%s 333 %s %s %s %li\n",
710         server_name, nick, c->name, c->topic.text,
711         server_name, nick, c->name, c->topic.user, (long int)c->topic.ts
712  );
713 }
714
715 do_names(client, args);
716
717 return 0;
718}
719
720static int on_part(int client, const char * cmd, char * args, char * text) {
721 const char * ufull = get_ufull(client);
722 int * listener;
723 size_t count;
724
725 (void)cmd;
726
727 if ( args == NULL )
728  return -1;
729
730 if ( text == NULL )
731  text = "Dejoined.";
732
733 count = get_listener_list(client, args, &listener);
734 for (; count; count--, listener++)
735  put_printf(*listener, ":%s PART %s :%s\n", ufull, args, text);
736
737 do_part(client, args);
738
739 return 0;
740}
741
742static int on_names(int client, const char * cmd, char * args, char * text) {
743 (void)cmd, (void)text;
744
745 if ( args == NULL )
746  return -1;
747
748 return do_names(client, args);
749}
750
751static int on_topic(int client, const char * cmd, char * args, char * text) {
752 struct channel * c;
753 const char * ufull = get_ufull(client);
754 int * listener;
755 size_t count;
756 char * nick = get_nick(client);
757
758 (void)cmd;
759
760 if ( args == NULL )
761  return -1;
762
763 c = get_channel(args);
764
765 if ( c == NULL )
766  return -1;
767
768 if ( !c->clients[client] ) {
769  return -1;
770 }
771
772 if ( c->topic.text != NULL )
773  roar_mm_free(c->topic.text);
774 if ( c->topic.user != NULL )
775  roar_mm_free(c->topic.user);
776
777 c->topic.text = NULL;
778 c->topic.user = roar_mm_strdup(nick);
779 c->topic.ts   = time(NULL);
780
781 if ( text != NULL )
782  c->topic.text = roar_mm_strdup(text);
783
784 if ( text == NULL )
785  text = "";
786
787 count = get_listener_list(client, c->name, &listener);
788 for (; count; count--, listener++)
789  put_printf(*listener, ":%s TOPIC %s :%s\n", ufull, c->name, text);
790
791 return 0;
792}
793
794// plugin handling suff:
795
796static int unload(struct roar_dl_librarypara * para, struct roar_dl_libraryinst * lib) {
797 (void)para, (void)lib;
798
799 roar_notify_core_unsubscribe(NULL, subscription_client_delete);
800
801 return 0;
802}
803
804static struct roard_proto proto[1] = {
805 {ROAR_PROTO_IRC, ROAR_SUBSYS_NONE, "Internet Relay Chat", new_client, check_client, NULL, NULL}
806};
807
808ROARD_DL_REG_PROTO(proto)
809
810ROAR_DL_PLUGIN_START(roard_irc_protocol) {
811 ROARD_DL_CHECK_VERSIONS();
812
813 libname.license = "GPL-3.0";
814
815 ROAR_DL_PLUGIN_REG_UNLOAD(unload);
816 ROARD_DL_REGFN_PROTO();
817 init();
818} ROAR_DL_PLUGIN_END
819
820//ll
Note: See TracBrowser for help on using the repository browser.