blob: 028140939b0cf25ef80c7b5c2a0e8d5443248689 [file] [log] [blame] [raw]
/*
* Copyright 2015-2018 Rivoreo
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*/
#include "common.h"
#include "syncrw.h"
#include <unistd.h>
#include <fcntl.h>
#include <pwd.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <sys/stat.h>
static void print_usage(const char *);
static int fgetline(FILE *f, char *line, size_t len) {
size_t i = 0;
int c;
while((c = fgetc(f)) != '\n') {
if(c == EOF) {
if(!i) return -1;
break;
}
if(i >= len - 1) return -2;
line[i++] = c;
}
line[i] = 0;
return i;
}
static int read_user_info(FILE *f, char **name, char **public_key) {
int i = 0;
char line[4096];
while(1) {
i++;
int len = fgetline(f, line, sizeof line);
if(len == -2) {
int c;
fprintf(stderr, "Warning: line %u in file " USER_LIST_FILE " is too long, skipping\n", i);
while((c = fgetc(f)) != EOF && c != '\n');
continue;
}
if(len < 0) return -1; // EOF
if(len == 0 || *line == '#') continue;
char *p = line;
while(*p && (*p == ' ' || *p == ' ')) p++;
if(!*p) continue;
char *q1 = strchr(p, '"');
if(!q1) {
fprintf(stderr, "Warning: cannot find '\"' at line %u in file " USER_LIST_FILE "\n", i);
continue;
}
*q1 = 0;
if(q1 - p < 8 || strcmp(q1 - 8, "command=") || strchr(p, ' ')) {
fprintf(stderr, "Warning: syntax error in file " USER_LIST_FILE " line %u\n", i);
continue;
}
q1++;
char *q2 = strchr(q1, '"');
if(!q2) {
fprintf(stderr, "Warning: unmatched '\"' in file " USER_LIST_FILE " line %u\n", i);
continue;
}
char *space = strchr(q2 + 1, ' ');
if(!space) {
fprintf(stderr, "Warning: syntax error in file " USER_LIST_FILE " line %u\n", i);
continue;
}
size_t user_name_len = q2 - q1;
if(!user_name_len) {
fprintf(stderr, "Warning: empty user name in file " USER_LIST_FILE " line %u\n", i);
continue;
}
*name = malloc(user_name_len + 1);
if(!*name) {
fprintf(stderr, "Error: allocate %zu bytes failed when processing file " USER_LIST_FILE " line %u\n", user_name_len + 1, i);
return -1;
}
memcpy(*name, q1, user_name_len);
(*name)[user_name_len] = 0;
*public_key = strdup(space + 1);
if(!*public_key) {
fprintf(stderr, "Error: out of memory when processing file " USER_LIST_FILE " line %u\n", i);
return -1;
}
return 0;
}
}
static int remove_ssh_rc_file() {
struct stat st;
if(lstat(".ssh/rc", &st) < 0) {
if(errno == ENOENT) return 0;
perror(".ssh/rc");
return -1;
}
if(S_ISDIR(st.st_mode)) {
fputs("'.ssh/rc' exists, and it is a directory!\n", stderr);
return -1;
}
fputs("sshout shouldn't have a SSH RC file '.ssh/rc'; removing\n", stderr);
if(unlink(".ssh/rc") < 0) {
perror("unlink: .ssh/rc");
return -1;
}
return 0;
}
static int adduser_command(int argc, char **argv) {
char *key = NULL;
int force = 0;
while(1) {
int c = getopt(argc, argv, "a:fh");
if(c == -1) break;
switch(c) {
case 'a':
key = strdup(optarg);
if(!key) {
perror("strdup");
return 1;
}
break;
case 'f':
force = 1;
break;
case 'h':
print_usage(argv[0]);
return 0;
case '?':
return -1;
}
}
//fprintf(stderr, "optind = %d, argc = %d\n", optind, argc);
if(argc - optind != 1) {
print_usage(argv[0]);
return -1;
}
const char *user = argv[optind];
if(!key) {
key = malloc(4096);
if(!key) {
perror("malloc");
return 1;
}
fprintf(stderr, "Input public key for %s: ", user);
if(fgetline(stdin, key, 4096) == -2) {
free(key);
fputs("Public key too long\n", stderr);
return 1;
}
}
// TODO: verify the key format
struct stat st;
if(stat(".ssh", &st) == 0) {
if(!S_ISDIR(st.st_mode)) {
free(key);
fputs("'.ssh' is not a directory\n", stderr);
return 1;
}
uid_t myuid = getuid();
if(st.st_uid != myuid) {
free(key);
fprintf(stderr, "'.ssh' is not owned by sshout (%u != %u)\n", st.st_uid, myuid);
return 1;
}
if(st.st_mode & S_IWOTH) {
fputs("'.ssh' is global writable\n", stderr);
if(chmod(".ssh", st.st_mode & ~(S_IWOTH)) < 0) {
perror("chmod: .ssh");
free(key);
return 1;
}
fputs("fixed\n", stderr);
}
if(remove_ssh_rc_file() < 0) {
free(key);
fputs("Cannot continue\n", stderr);
return 1;
}
} else if(mkdir(".ssh", 0755) < 0) {
perror("mkdir: .ssh");
free(key);
return 1;
}
FILE *f = fopen(USER_LIST_FILE, "a+");
if(!f) {
perror(USER_LIST_FILE);
free(key);
return 1;
}
int existing_count = 0;
{
char *user_name, *public_key;
while(read_user_info(f, &user_name, &public_key) == 0) {
if(strcmp(key, public_key) == 0) {
free(key);
fprintf(stderr, "This public key is already used by user %s.\n"
"Are you pasted wrong key?\n", user_name);
free(user_name);
free(public_key);
return 1;
}
if(strcmp(user, user_name) == 0) existing_count++;
free(user_name);
free(public_key);
}
}
if(existing_count) {
fprintf(stderr, "%d key%s already exist for user %s\n", existing_count, existing_count > 1 ? "s" : "", user);
if(!force) {
char answer[16];
fprintf(stderr, "Are you sure you want to add this key for user %s? ", user);
do {
int len = fgetline(stdin, answer, sizeof answer);
// Ignore line too long error
if(len == -1 || strncasecmp(answer, "no", 2) == 0 || strncmp(answer, "不", 3) == 0 || strcmp(answer, "否") == 0) {
fputs("Operation canceled\n", stderr);
free(key);
return 1;
}
} while(strncasecmp(answer, "yes", 3) && strncmp(answer, "是", 3) && strncmp(answer, "好", 3) && strcmp(answer, "可以"));
}
}
if(fprintf(f, "command=\"%s\",no-agent-forwarding,no-port-forwarding %s\n", user, key) < 0) {
perror("fprintf");
free(key);
return 1;
}
free(key);
return -1;
}
static int listuser_command(int argc, char **argv) {
if(remove_ssh_rc_file() < 0) {
fputs("Warning: configuration error left unresolved\n", stderr);
}
FILE *f = fopen(USER_LIST_FILE, "r");
if(!f) {
perror(USER_LIST_FILE);
return 1;
}
/*
char line[4096];
while(fgetline(f, line, sizeof line) > 0) {
fprintf(stderr, "line = \"%s\"\n", line);
}
*/
char *user_name, *public_key;
while(read_user_info(f, &user_name, &public_key) == 0) {
fprintf(stderr, "User \"%s\", Public key \"%s\"\n", user_name, public_key);
free(user_name);
free(public_key);
}
return 0;
}
static int getmotd_command(int argc, char **argv) {
char buffer[4096];
int fd = open(SSHOUT_MOTD_FILE, O_RDONLY);
if(fd == -1) {
perror(SSHOUT_MOTD_FILE);
return 1;
}
while(1) {
int s = sync_read(fd, buffer, sizeof buffer);
if(s < 0) {
perror("read");
return 1;
}
if(!s) return 0;
s = sync_write(STDOUT_FILENO, buffer, s);
if(s < 0) {
perror("write");
return 1;
}
}
}
static int setmotd_command(int argc, char **argv) {
const char *message = NULL;
int need_del = 0;
while(1) {
int c = getopt(argc, argv, "m:dh");
if(c == -1) break;
switch(c) {
case 'm':
message = optarg;
break;
case 'd':
need_del = 1;
break;
case 'h':
print_usage(argv[0]);
return 0;
case '?':
return -1;
}
}
if(need_del) {
if(message) {
fputs("Option '-d' cannot be used together with '-m'\n", stderr);
return -1;
}
if(unlink(SSHOUT_MOTD_FILE) < 0) {
perror(SSHOUT_MOTD_FILE);
return 1;
}
return 0;
}
int fd = creat(SSHOUT_MOTD_FILE, 0600);
if(fd == -1) {
perror(SSHOUT_MOTD_FILE);
return 1;
}
if(message) {
size_t len = strlen(message);
int s = sync_write(fd, message, len);
if(s < 0) {
perror("write");
return 1;
}
if(len && message[len - 1] != '\n') {
char new_line = '\n';
if(sync_write(fd, &new_line, 1) < 0) {
perror("write");
return 1;
}
}
return 0;
} else {
char buffer[4096];
if(isatty(STDIN_FILENO)) fputs("Type message below:\n", stderr);
while(1) {
int s = read(STDIN_FILENO, buffer, sizeof buffer);
if(s < 0) {
if(errno == EINTR) continue;
perror("read");
return 1;
}
if(!s) return 0;
s = sync_write(fd, buffer, s);
if(s < 0) {
perror("write");
return 1;
}
}
}
}
static struct subcommand {
const char *name;
const char *usage;
int (*func)(int, char **);
} commands[] = {
#define SUBCOMMAND(N,U) { #N, U, N##_command }
SUBCOMMAND(adduser, "[-a <public-key-in-base64>] [-f] <user-name>"),
SUBCOMMAND(listuser, ""),
SUBCOMMAND(getmotd, ""),
SUBCOMMAND(setmotd, "[-m <message> | -d]"),
#undef SUBCOMMAND
{ NULL, NULL }
};
static void print_commands() {
struct subcommand *c = commands;
fputs("Following subcommands are available:\n", stderr);
while(c->name) {
fprintf(stderr, " %s %s\n", c->name, c->usage);
c++;
}
}
static void print_usage(const char *name) {
struct subcommand *c = commands;
while(c->name) {
if(strcmp(c->name, name) == 0) {
fprintf(stderr, "Usage: %s %s\n", name, c->usage);
return;
}
c++;
}
fprintf(stderr, "Error: cannot find usage for command '%s'", name);
}
int main(int argc, char **argv) {
struct passwd *pw = getpwnam("sshout");
if(!pw) {
fputs("sshout user account not exist\n", stderr);
return 1;
}
if(pw->pw_uid == 0) {
fputs("sshout user account have UID 0\n", stderr);
return 1;
}
if(argc < 2) {
print_commands();
return -1;
}
uid_t myeuid = geteuid();
if(myeuid == 0) {
if(setreuid(pw->pw_uid, pw->pw_uid) < 0) {
perror("setreuid");
return 1;
}
} else if(myeuid != pw->pw_uid) {
fprintf(stderr, "Current effective UID %u doesn't equal to sshout user account\n", myeuid);
return 1;
}
const char *home = pw->pw_dir;
struct stat st;
if(stat(home, &st) < 0) {
perror(home);
return 1;
}
if(st.st_uid != pw->pw_uid) {
fprintf(stderr, "Home directory '%s' is not owned by sshout (expecting UID=%u, got %u)\n", home, pw->pw_uid, st.st_uid);
return 1;
}
setenv("HOME", home, 1);
if(chdir(home) < 0) {
perror(home);
return 1;
}
struct subcommand *c = commands;
while(c->name) {
if(strcmp(argv[1], c->name) == 0) return c->func(argc - 1, argv + 1);
c++;
}
fprintf(stderr, "Unknown command '%s'\n", argv[1]);
print_commands();
return -1;
}