Writing a plugin
How do I create a plugin?
The documentation for our plugin system can be found here.
To start, we recommend you install our hello_world.tar.gz sample, and see how it works, and go from there.
Plugins are quite simple, they're basically just a list of files, extracted to a directory, some of which are scripts to execute code you want. Included in the files are also "hooks", data that is inserted into the DA skins allowing you to add links to your plugin from DA itself.
Another example.
Other related features with the plugins: Ability to completely control the socket , which lets you send files. It allows you to remove the DirectAdmin html "bubble" (header and footer) and also gives you control of the headers. Without this option, your output is inserted into the middle of the DA window.
More related items can be found in the versions system.
How to call php with your php.ini
If you're running a plugin, and you need to use your own php.ini file, then use the following shebang line with your php.ini file's location:
#!/usr/local/bin/php -nc /usr/local/directadmin/plugins/NAME/yourphp.ini
It's important that you use the -n option, especially with CustomBuild 2.0, or else you may have modules loaded twice due the hard-coded nature of the with-config-file-scan-dir, which could load extra ini files, resulting in the loading of modules twice (like zend/ioncube), which would cause errors.
You'll also notice that all php command line options and values are added as one shebang option. Shebang only supports 1 extra option after the interpreter, so they must all be put together into 1 option. (Limitation of the shebang line design).
If you want to run php and not use any php.ini (internal defaults), then use:
#!/usr/local/bin/php -n
to force your php instance to ignore all php.ini settings you may have set in your global php.ini.
Debugging
It's been noted that if you've saved your files in "dos" mode, the shebang line (#!/usr/..) can often have issues with options after the space, like the -nc/usr.. flag. Ensure you save your files is unix format. To do this, you can use the dos2unix command, install dos2unix if it's missing:
yum install dos2unix
dos2unix index.html
How to use $_GET and $_POST in a plugin
The DirectAdmin plugin system can use php, however the php used is /usr/local/bin/php, and not the same php used by Apache. The differences are that many of the default/loaded variables made available to php coders are not there by default with the command line version.
This is a basic php code to convert the environment variables that DA does pass, into the $_GET and $_POST arrays that you're used to working with through Apache:
//make code look like CLI for $_GET and $_POST
$_GET = Array();
$QUERY_STRING=getenv('QUERY_STRING');
if ($QUERY_STRING != "")
{
parse_str(html_entity_decode($QUERY_STRING), $get_array);
foreach ($get_array as $key => $value)
{
$_GET[urldecode($key)] = urldecode($value);
}
}
$_POST = Array();
$POST_STRING=getenv('POST');
if ($POST_STRING != "")
{
parse_str(html_entity_decode($POST_STRING), $post_array);
foreach ($post_array as $key => $value)
{
$_POST[urldecode($key)] = urldecode($value);
}
}
How to create a SUID wrapper to use in plugin
SUID wrappers can be very dangerous if used incorrectly.
Only create a wrapper as a last resort, if no other options are available.
- To create a binary wrapper, create a .c file called wrapper.c, and add this code:
/* This is an SUID wrapper to run a command as some other User.
It allows the calling User to gain the permissions
as defined by the User below.
Written by DirectAdmin
WARNING: ensure you do excessive security checking.
If you use an SUID binary, you're giving anyone who
calls it access to that User.. so ensure the code
you use doesn't allow the caller to run arbitrary commands.
For example, if you only need access to this User to read data from a file,
then do not provide any variables, and grab the data directly from that file.
Don't run arbitrary commands through this binary via command line variables.
Also, if you're going to display sensitive info from this binary to the
calling script, expect that it will be possible for the client to do
the same, without the script. Extra checks to verify the calling script
would be required.
*** ONLY USE AN SUID WRAPPER AS A LAST RESORT ***
************************************/
//User we want to run as.
//To run as root, comment out the next line, else set the desired User.
#define RUN_AS_USER "admin"
//only allows accounts in the admin.list
//comment out the next line to allow anyone to call it.
#define CALLED_BY_ADMIN_ACCOUNT_ONLY
/************************************/
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <pwd.h>
#include <string.h>
#include <errno.h>
extern int errno;
#define BUFF_LEN 128
#define ADMIN_LIST "/usr/local/directadmin/data/admin/admin.list"
int am_directadmin_child(void)
{
FILE *fp = NULL;
char path_buff[BUFF_LEN];
char link_buff[BUFF_LEN];
int ppid = getppid();
while(ppid > 1)
{
snprintf(path_buff, BUFF_LEN, "/proc/%d/stat", (int)ppid);
fp = fopen(path_buff, "r");
if(fp == NULL)
{
printf("Error reading %s: %s\n", path_buff, strerror(errno));
return 0;
}
fscanf(fp, "%*d %*s %*s %d", &ppid);
fclose(fp);
sprintf(path_buff, "/proc/%d/exe", (int)ppid);
memset(link_buff, 0, BUFF_LEN);
if (readlink(path_buff, link_buff, BUFF_LEN-1) == -1)
{
printf("Error reading exe link: %s: %s\n", path_buff, strerror(errno));
return 0;
}
if (strncmp(link_buff, "/usr/local/directadmin/directadmin", 128) == 0)
return 1;
}
return 0;
}
#ifdef CALLED_BY_ADMIN_ACCOUNT_ONLY
int called_by_admin_account(const char *a)
{
if (!a || !*a) return 0;
FILE *fp = NULL;
char account_buff[BUFF_LEN];
fp = fopen(ADMIN_LIST, "r");
if (fp == NULL)
{
printf("Error reading %s: %s\n", ADMIN_LIST, strerror(errno));
return 0;
}
int i=0; //buffer index
int ch=0; //read character
int last=0; //lookback character
while (((ch = fgetc(fp)) != EOF) && (i<BUFF_LEN-1))
{
if (ch != '\n')
{
account_buff[i++] = ch;
last = ch;
}
else
{
account_buff[i] = '\0';
i=0;
if (!strncmp(account_buff, a, BUFF_LEN)) //match!
{
fclose(fp);
return 1;
}
}
}
fclose(fp);
return 0;
}
#endif
#define aTOz(ch) ('a'<=ch && ch<='z')
#define ATOZ(ch) ('A'<=ch && ch<='Z')
#define ZTON(ch) ('0'<=ch && ch<='9')
int safe_word_check(const char *word)
{
if (!word || !*word) return 0;
int len = strlen(word);
if (len > 100)
{
printf("Option '%s' is too long\n", word);
exit(10);
}
for (int i=0; i<len; i++)
{
if (aTOz(word[i])) continue;
if (ATOZ(word[i])) continue;
if (ZTON(word[i])) continue;
switch(word[i])
{
case '_' :
case '-' :
case '.' :
continue;
break;
}
printf("Option '%s' contains a disallowed character: '%c'\n", word, word[i]);
exit(11);
}
return 1;
}
int main(int argc, char **argv)
{
uid_t original_uid = getuid();
struct passwd *pwd_caller = getpwuid(original_uid);
if (pwd_caller == NULL) { printf("getpwuid error: %s\n", strerror(errno)); return 0; }
if (!pwd_caller->pw_name || strlen(pwd_caller->pw_name) > 16)
{
printf("Couldn't get username from uid=%d\n", original_uid);
exit(8);
}
char original_username[BUFF_LEN];
strncpy(original_username, pwd_caller->pw_name, BUFF_LEN-1);
if (*original_username == '\0')
{
printf("Caller username seems to be blank\n");
exit(9);
}
if (setuid(0) == -1)
{
printf("Error setting to uid 0. Ensure %s is chmod to 4755\n", argv[0]);
printf("setuid(0) error: %s\n", strerror(errno));
exit(4);
}
if (setgid(0) == -1)
{
printf("Error setting to gid 0. Ensure %s is chmod to 4755\n", argv[0]);
exit(5);
}
//We are now running as full root.
if (!am_directadmin_child())
{
printf("Not a directadmin child\n");
exit(6);
}
#ifdef CALLED_BY_ADMIN_ACCOUNT_ONLY
if (!called_by_admin_account(original_username))
{
printf("Not called by an Admin account (%s)\n", original_username);
exit(7);
}
#endif
#ifdef RUN_AS_USER
struct passwd *pwd_info = getpwnam(RUN_AS_USER);
if (pwd_info == NULL)
{
printf("Unable to get system information on %s: %s\n", RUN_AS_USER, strerror(errno));
exit(1);
}
if (setgid(pwd_info->pw_gid) == -1)
{
printf("setgid(%d) error: %s\n", pwd_info->pw_gid, strerror(errno));
exit(2);
}
if (setuid(pwd_info->pw_uid) == -1)
{
printf("setuid(%d) error: %s\n", pwd_info->pw_gid, strerror(errno));
exit(3);
}
#endif
//max 3 command options. Can add more if needed.
//first entry is 0, and will be set to the command.
//last entry must be 0, leaving 3 command options
//If you add more, ensure to change the i<=3 below.
char *cmd_argv[] = { 0, 0, 0, 0, 0 };
char cmd[] = "/usr/bin/id";
struct stat filestat;
if (stat(cmd, &filestat))
{
printf("Error with %s: %s\n", cmd, strerror(errno));
exit(14);
}
cmd_argv[0] = cmd;
for (int i=1; i<argc && i<=3; i++)
{
if (argv[i])
safe_word_check(argv[i]); //this will exit if unsafe
cmd_argv[i] = argv[i];
}
clearenv();
execv(cmd, cmd_argv);
//will never get here because execv becomes the process.
printf("Never going to see this\n");
exit(0);
}
- Then compile the wrapper, and test it out:
g++ -o wrapper wrapper.c && chown root:root wrapper && chmod 4755 wrapper
- Lastly, test it out to ensure it works. Log in to ssh as some other User, then test it out. For example:
[test@server ~]$ id
uid=503(test) gid=504(test) groups=504(test)
[test@server ~]$ /home/admin/wrapper
uid=502(admin) gid=502(admin) groups=502(admin)
[test@server ~]$
As long as you see the proper uid/gid values along with "admin", in the output of the wrapper, then you have confirmed that the wrapper is running as "admin", from user "test".
Notes If the system is running CloudLinux, to bypass CageFS and gain root, the SUID wrapper line needs to be added to /etc/cagefs/proxy.commands.
See this related document:
http://docs.cloudlinux.com/index.html?executing_by_proxy.html
Optional template: plugin_iframe.html
Not present by default, but this is used with the following plugin feature:
iframe=yes
which current hard-codes the iframe html output around the |OUTPUT| token.
This new optional template, if created, would be used in place of the hardcoded html. Main purpose is to make debugging easier and should not be needed for most cases.
/usr/local/directadmin/data/templates/custom/plugin_iframe.html
Sample:
<html><head><base target="_parent" />
<meta charset="utf-8" />
</head><body><div id="iframe-container">
|OUTPUT|
</div></body></html>