mirror of
https://github.com/ps5-payload-dev/shsrv.git
synced 2026-10-22 19:52:31 -05:00
644 lines
12 KiB
C
644 lines
12 KiB
C
/* Copyright (C) 2024 John Törnblom
|
|
|
|
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, 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.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program; see the file COPYING. If not, see
|
|
<http://www.gnu.org/licenses/>. */
|
|
|
|
#include <errno.h>
|
|
#include <poll.h>
|
|
#include <pthread.h>
|
|
#include <unistd.h>
|
|
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <limits.h>
|
|
#include <signal.h>
|
|
|
|
#include <sys/wait.h>
|
|
|
|
#include <ps5/klog.h>
|
|
|
|
#include "builtin.h"
|
|
#include "elfldr.h"
|
|
|
|
#include "libtelnet.h"
|
|
|
|
|
|
#define SHELL_LINE_BUFSIZE 1024
|
|
#define SHELL_TOK_BUFSIZE 128
|
|
#define SHELL_ARG_DELIM " \t\r\n\a"
|
|
#define SHELL_CMD_DELIM "|;&"
|
|
|
|
|
|
typedef struct sce_version {
|
|
unsigned long unknown1;
|
|
char str_version[0x1c];
|
|
unsigned int bin_version;
|
|
unsigned long unknown2;
|
|
} sce_version_t;
|
|
|
|
|
|
typedef struct telnet_client_state {
|
|
telnet_t *telnet;
|
|
int remote_fd;
|
|
int stdin_write_fd;
|
|
int stdout_read_fd;
|
|
pid_t pid;
|
|
} telnet_client_state_t;
|
|
|
|
|
|
static const telnet_telopt_t telopts[] = {
|
|
{-1, 0, 0}
|
|
};
|
|
|
|
|
|
int sceKernelSetProcessName(const char*);
|
|
int sceKernelGetSystemSwVersion(sce_version_t *);
|
|
int sceKernelGetProsperoSystemSwVersion(sce_version_t *);
|
|
int sceKernelGetHwModelName(char *);
|
|
int sceKernelGetHwSerialNumber(char *);
|
|
long sceKernelGetCpuFrequency(void);
|
|
int sceKernelGetCpuTemperature(int *);
|
|
int sceKernelGetSocSensorTemperature(int, int *);
|
|
|
|
|
|
/**
|
|
* Read a line from stdin.
|
|
**/
|
|
static char*
|
|
sh_readline(void) {
|
|
int bufsize = SHELL_LINE_BUFSIZE;
|
|
int position = 0;
|
|
char *buffer_backup;
|
|
char *buffer = calloc(bufsize, sizeof(char));
|
|
char c;
|
|
|
|
if(!buffer) {
|
|
perror("calloc");
|
|
return NULL;
|
|
}
|
|
|
|
while(1) {
|
|
int len = read(STDIN_FILENO, &c, 1);
|
|
if(len == -1 && errno == EINTR) {
|
|
continue;
|
|
}
|
|
|
|
if(len <= 0) {
|
|
free(buffer);
|
|
return NULL;
|
|
}
|
|
|
|
if(c == '\n') {
|
|
buffer[position] = '\0';
|
|
return buffer;
|
|
}
|
|
|
|
buffer[position++] = c;
|
|
|
|
if(position >= bufsize) {
|
|
bufsize += SHELL_LINE_BUFSIZE;
|
|
buffer_backup = buffer;
|
|
buffer = realloc(buffer, bufsize);
|
|
if(!buffer) {
|
|
perror("realloc");
|
|
free(buffer_backup);
|
|
return NULL;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Split a string into an array of substrings seperated by
|
|
* a delimiter.
|
|
**/
|
|
static char**
|
|
sh_splitstring(char *line, char *delim) {
|
|
int bufsize = SHELL_TOK_BUFSIZE;
|
|
int position = 0;
|
|
char **tokens = calloc(bufsize, sizeof(char));
|
|
char *token, **tokens_backup;
|
|
char *state = 0;
|
|
|
|
if(!tokens) {
|
|
perror("calloc");
|
|
return NULL;
|
|
}
|
|
|
|
token = strtok_r(line, delim, &state);
|
|
while(token != NULL) {
|
|
tokens[position] = token;
|
|
position++;
|
|
|
|
if(position >= bufsize) {
|
|
bufsize += SHELL_TOK_BUFSIZE;
|
|
tokens_backup = tokens;
|
|
tokens = realloc(tokens, bufsize * sizeof(char*));
|
|
if(!tokens) {
|
|
perror("realloc");
|
|
free(tokens_backup);
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
token = strtok_r(NULL, delim, &state);
|
|
}
|
|
|
|
tokens[position] = NULL;
|
|
return tokens;
|
|
}
|
|
|
|
|
|
/**
|
|
* Wait for a child process to terminate.
|
|
**/
|
|
static int
|
|
sh_waitpid(pid_t pid) {
|
|
int status;
|
|
pid_t res;
|
|
|
|
if((res=waitpid(pid, &status, WUNTRACED)) < 0) {
|
|
perror("waitpid");
|
|
return -1;
|
|
}
|
|
|
|
if(WIFEXITED(status)) {
|
|
return WEXITSTATUS(status);
|
|
}
|
|
|
|
if(WIFSIGNALED(status)) {
|
|
puts(strsignal(WTERMSIG(status)));
|
|
return -1;
|
|
}
|
|
|
|
if(WIFSTOPPED(status)) {
|
|
puts(strsignal(WSTOPSIG(status)));
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
|
|
/**
|
|
* Search the env varriable PATH for a file with the given name.
|
|
**/
|
|
static int
|
|
sh_which(const char* name, char* path) {
|
|
char **paths = NULL;
|
|
char* PATH;
|
|
|
|
if(name[0] == '/' && !access(name, R_OK | X_OK)) {
|
|
strcpy(path, name);
|
|
return 0;
|
|
}
|
|
|
|
PATH = strdup(getenv("PATH"));
|
|
if(!(paths=sh_splitstring(PATH, ":"))) {
|
|
free(PATH);
|
|
return 0;
|
|
}
|
|
|
|
for(int i=0; paths[i]; i++) {
|
|
sprintf(path, "%s/%s", paths[i], name);
|
|
if(!access(path, R_OK | X_OK)) {
|
|
free(paths);
|
|
free(PATH);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
free(paths);
|
|
free(PATH);
|
|
|
|
return -1;
|
|
}
|
|
|
|
|
|
/**
|
|
* Read a file from disk at the given path.
|
|
**/
|
|
static uint8_t*
|
|
sh_readfile(const char* path) {
|
|
uint8_t* buf;
|
|
ssize_t len;
|
|
FILE* file;
|
|
|
|
if(!(file=fopen(path, "rb"))) {
|
|
perror("fopen");
|
|
return 0;
|
|
}
|
|
|
|
if(fseek(file, 0, SEEK_END)) {
|
|
perror("fseek");
|
|
return 0;
|
|
}
|
|
|
|
if((len=ftell(file)) < 0) {
|
|
perror("ftell");
|
|
return 0;
|
|
}
|
|
|
|
if(fseek(file, 0, SEEK_SET)) {
|
|
perror("fseek");
|
|
return 0;
|
|
}
|
|
|
|
if(!(buf=malloc(len))) {
|
|
return 0;
|
|
}
|
|
|
|
if(fread(buf, 1, len, file) != len) {
|
|
perror("fread");
|
|
free(buf);
|
|
return 0;
|
|
}
|
|
|
|
if(fclose(file)) {
|
|
perror("fclose");
|
|
free(buf);
|
|
return 0;
|
|
}
|
|
|
|
return buf;
|
|
}
|
|
|
|
|
|
/**
|
|
* Execute a shell command.
|
|
**/
|
|
static pid_t
|
|
sh_execute(char **argv) {
|
|
char path[PATH_MAX];
|
|
pid_t pid = 0;
|
|
uint8_t* elf;
|
|
int argc = 0;
|
|
|
|
while(argv[argc]) {
|
|
argc++;
|
|
}
|
|
|
|
if(!argc) {
|
|
return -1;
|
|
}
|
|
|
|
if((pid=builtin_cmd_run(argv[0], argc, argv)) >= 0) {
|
|
return pid;
|
|
}
|
|
|
|
if(!sh_which(argv[0], path) && (elf=sh_readfile(path))) {
|
|
pid = elfldr_spawn(STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO, elf, argv);
|
|
free(elf);
|
|
return pid;
|
|
}
|
|
|
|
fprintf(stderr, "%s: command not found\n", argv[0]);
|
|
|
|
return -1;
|
|
}
|
|
|
|
|
|
/**
|
|
* Print the shell prompt to stdout.
|
|
**/
|
|
static void
|
|
sh_prompt(void) {
|
|
char buf[PATH_MAX];
|
|
char *cwd;
|
|
|
|
if(!(cwd=getenv("PWD"))) {
|
|
cwd = getcwd(buf, sizeof(buf));
|
|
}
|
|
|
|
fprintf(stdout, "%s$ ", cwd ? cwd : "(null)");
|
|
fflush(stdout);
|
|
}
|
|
|
|
|
|
/**
|
|
* Output a greeting to stdout.
|
|
**/
|
|
static void
|
|
sh_greet(void) {
|
|
sce_version_t v;
|
|
char s[1000];
|
|
int temp = 0;
|
|
|
|
printf("\n");
|
|
printf("Welcome to shsrv.elf running on pid %d, ", getppid());
|
|
printf("compiled %s at %s\n\n", __DATE__, __TIME__);
|
|
|
|
s[0] = '\0';
|
|
if(sceKernelGetHwModelName(s)) {
|
|
perror("sceKernelGetHwModelName");
|
|
} else {
|
|
printf("Model: %20s\n", s);
|
|
}
|
|
|
|
if(sceKernelGetHwSerialNumber(s)) {
|
|
perror("sceKernelGetHwSerialNumber");
|
|
} else {
|
|
printf("S/N: %20s\n", s);
|
|
}
|
|
|
|
if(sceKernelGetProsperoSystemSwVersion(&v)) {
|
|
perror("sceKernelGetSystemSwVersion");
|
|
} else {
|
|
printf("S/W: %20s\n", v.str_version);
|
|
}
|
|
|
|
if(sceKernelGetSocSensorTemperature(0, &temp)) {
|
|
perror("sceKernelGetSocSensorTemperature");
|
|
} else {
|
|
printf("SoC temp: %d °C\n", temp);
|
|
}
|
|
|
|
if(sceKernelGetCpuTemperature(&temp)) {
|
|
perror("sceKernelGetCpuTemperature");
|
|
} else {
|
|
printf("CPU temp: %d °C\n", temp);
|
|
}
|
|
|
|
printf("CPU freq: %4ld MHz\n",
|
|
sceKernelGetCpuFrequency() / (1000*1000));
|
|
|
|
printf("\nType 'help' for a list of commands\n");
|
|
printf("\n");
|
|
}
|
|
|
|
|
|
static void*
|
|
sh_thread(void *ctx) {
|
|
telnet_client_state_t *state = (telnet_client_state_t*)ctx;
|
|
int pipefd[2] = {-1, -1};
|
|
char *line = NULL;
|
|
char **cmds = NULL;
|
|
char **args = NULL;
|
|
int infd = 0;
|
|
int outfd = 1;
|
|
pid_t pid;
|
|
|
|
chdir("/");
|
|
setenv("HOME", "/", 0);
|
|
setenv("PWD", "/", 0);
|
|
setenv("PATH", "/mnt/usb0/homebrew/bin:/user/homebrew/bin", 0);
|
|
|
|
sh_greet();
|
|
|
|
while(1) {
|
|
sh_prompt();
|
|
|
|
if(!(line=sh_readline())) {
|
|
exit(0);
|
|
}
|
|
|
|
if(!(cmds=sh_splitstring(line, SHELL_CMD_DELIM))) {
|
|
free(line);
|
|
continue;
|
|
}
|
|
|
|
infd = dup(0);
|
|
outfd = dup(1);
|
|
|
|
for(int i=0; cmds[i]; i++) {
|
|
if(!(args=sh_splitstring(cmds[i], SHELL_ARG_DELIM))) {
|
|
continue;
|
|
}
|
|
|
|
if(cmds[i+1] && !pipe(pipefd)) {
|
|
dup2(pipefd[1], 1);
|
|
close(pipefd[1]);
|
|
} else {
|
|
dup2(outfd, 1);
|
|
}
|
|
|
|
if((pid=sh_execute(args)) > 0) {
|
|
state->pid = pid;
|
|
sh_waitpid(pid);
|
|
state->pid = -1;
|
|
}
|
|
|
|
if(cmds[i+1]) {
|
|
dup2(pipefd[0], 0);
|
|
close(pipefd[0]);
|
|
} else {
|
|
dup2(infd, 0);
|
|
}
|
|
|
|
fflush(NULL);
|
|
free(args);
|
|
}
|
|
free(line);
|
|
free(cmds);
|
|
|
|
close(infd);
|
|
close(outfd);
|
|
}
|
|
|
|
exit(0);
|
|
}
|
|
|
|
|
|
static void
|
|
sh_on_telnet_data(telnet_client_state_t *state, telnet_event_t *ev) {
|
|
if(write(state->stdin_write_fd, ev->data.buffer, ev->data.size)
|
|
!= ev->data.size) {
|
|
klog_perror("TELNET_EV_DATA");
|
|
}
|
|
}
|
|
|
|
static void
|
|
sh_on_telnet_send(telnet_client_state_t *state, telnet_event_t *ev) {
|
|
if(write(state->remote_fd, ev->data.buffer, ev->data.size)
|
|
!= ev->data.size) {
|
|
klog_perror("TELNET_EV_SEND");
|
|
}
|
|
}
|
|
|
|
static void
|
|
sh_on_telnet_iac(telnet_client_state_t *state, telnet_event_t *ev) {
|
|
switch(ev->iac.cmd) {
|
|
case TELNET_SUSP:
|
|
if(state->pid > 0) {
|
|
kill(state->pid, SIGSTOP);
|
|
}
|
|
break;
|
|
|
|
case TELNET_IP:
|
|
if(state->pid > 0) {
|
|
// When SIGINT is sent to a prospero process, it is stopped instead
|
|
// of terminated, casuing zombie procs. For now, send SIGKILL instead.
|
|
kill(state->pid, SIGKILL);
|
|
//kill(state->pid, SIGINT);
|
|
}
|
|
break;
|
|
|
|
case TELNET_ABORT:
|
|
if(state->pid > 0) {
|
|
kill(state->pid, SIGABRT);
|
|
}
|
|
break;
|
|
|
|
case TELNET_AO: // Abort output (but continue running)
|
|
break;
|
|
|
|
case TELNET_EC: // Erase char
|
|
break;
|
|
|
|
case TELNET_EL: // Erase line
|
|
break;
|
|
|
|
case TELNET_AYT: //Are You There
|
|
break;
|
|
|
|
default:
|
|
klog_printf("Unknown telnet command %d received\n", ev->iac.cmd);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
sh_telnet_evt(telnet_t *telnet, telnet_event_t *ev, void *ctx) {
|
|
telnet_client_state_t *state = (telnet_client_state_t*)ctx;
|
|
|
|
switch(ev->type) {
|
|
case TELNET_EV_DATA:
|
|
sh_on_telnet_data(state, ev);
|
|
break;
|
|
|
|
case TELNET_EV_SEND:
|
|
sh_on_telnet_send(state, ev);
|
|
break;
|
|
|
|
case TELNET_EV_IAC:
|
|
sh_on_telnet_iac(state, ev);
|
|
break;
|
|
|
|
case TELNET_EV_WARNING:
|
|
klog_printf("TELNET_EV_WARNING: %s:%s::%d: %s (error %d)",
|
|
ev->error.file, ev->error.func, ev->error.line,
|
|
ev->error.msg, ev->error.errcode);
|
|
break;
|
|
|
|
case TELNET_EV_ERROR:
|
|
klog_printf("TELNET_EV_ERROR: %s:%s::%d: %s (error %d)",
|
|
ev->error.file, ev->error.func, ev->error.line,
|
|
ev->error.msg, ev->error.errcode);
|
|
break;
|
|
|
|
default:
|
|
klog_printf("Unkown telnet event %d\n", ev->type);
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Launch sh.elf.
|
|
**/
|
|
int main(int argc, char** argv) {
|
|
telnet_client_state_t state;
|
|
struct pollfd pollfds[2];
|
|
char buf[0x1000];
|
|
int pipefds[2];
|
|
pthread_t trd;
|
|
ssize_t len;
|
|
|
|
setsid();
|
|
|
|
// remote socket is duplicated on stdin, stdout, and stderr
|
|
state.remote_fd = dup(STDIN_FILENO);
|
|
|
|
if(!(state.telnet=telnet_init(telopts, sh_telnet_evt, TELNET_FLAG_NVT_EOL,
|
|
&state))) {
|
|
klog_perror("telnet_init");
|
|
exit(errno);
|
|
}
|
|
|
|
// create a pipe for stdin so we can intercept its data and signal
|
|
// the telnet state machine
|
|
if(pipe(pipefds) < 0) {
|
|
klog_perror("pipe");
|
|
exit(errno);
|
|
}
|
|
if(dup2(pipefds[0], STDIN_FILENO) < 0) {
|
|
klog_perror("dup2");
|
|
exit(errno);
|
|
}
|
|
state.stdin_write_fd = pipefds[1];
|
|
|
|
// do the same for staout and stderr
|
|
if(pipe(pipefds)) {
|
|
klog_perror("pipe");
|
|
exit(errno);
|
|
}
|
|
state.stdout_read_fd = pipefds[0];
|
|
if(dup2(pipefds[1], STDOUT_FILENO) < 0) {
|
|
klog_perror("dup2");
|
|
exit(errno);
|
|
}
|
|
setvbuf(stdout, NULL, _IONBF, 0); // no buffering
|
|
if(dup2(pipefds[1], STDERR_FILENO) < 0) {
|
|
klog_perror("dup2");
|
|
exit(errno);
|
|
}
|
|
setvbuf(stderr, NULL, _IONBF, 0); // no buffering
|
|
|
|
// run shell in its own thread
|
|
pthread_create(&trd, NULL, sh_thread, &state);
|
|
|
|
// setup poll arguments
|
|
memset(pollfds, 0, sizeof(pollfds));
|
|
pollfds[0].fd = state.remote_fd;
|
|
pollfds[0].events = POLLIN;
|
|
pollfds[1].fd = state.stdout_read_fd;
|
|
pollfds[1].events = POLLIN;
|
|
|
|
// generate telnet events
|
|
while(1) {
|
|
if(poll(pollfds, 2, 1) < 0) {
|
|
klog_perror("poll");
|
|
exit(errno);
|
|
}
|
|
|
|
if(pollfds[0].revents & (POLLERR | POLLHUP) ||
|
|
pollfds[1].revents & (POLLERR | POLLHUP)) {
|
|
klog_perror("poll");
|
|
exit(errno);
|
|
}
|
|
|
|
if(pollfds[0].revents & POLLIN) {
|
|
if((len=read(pollfds[0].fd, buf, sizeof(buf))) <= 0) {
|
|
if(len) {
|
|
klog_perror("read");
|
|
}
|
|
exit(errno);
|
|
}
|
|
telnet_recv(state.telnet, buf, len);
|
|
}
|
|
if(pollfds[1].revents & POLLIN) {
|
|
if((len=read(pollfds[1].fd, buf, sizeof(buf))) <= 0) {
|
|
if(len) {
|
|
klog_perror("read");
|
|
}
|
|
exit(errno);
|
|
}
|
|
telnet_send_text(state.telnet, buf, len);
|
|
}
|
|
}
|
|
|
|
telnet_free(state.telnet);
|
|
exit(0);
|
|
}
|