Writing a plugin

How do I create a plugin?

The documentation for our plugin system can be found hereopen in new window.

To start, we recommend you install our hello_world.tar.gzopen in new window 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.

For native integration into Evolution, Vue.JS could be used. You may use http://www.custombuild.eu/plugin/custombuild.tar.gz as the example of real-world plugin as it has native Vue integration with Evolution skin. DirectAdmin also supports "widgets" and "widget-only" plugins. "Vue widget only" example https://forum.directadmin.com/showthread.php?t=57994. Another example without Vue (displayed as iframe): https://forum.directadmin.com/showthread.php?t=43292.

Other related features with the plugins: Ability to completely control the socketopen in new window , 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 itemsopen in new window 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.

  1. 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);
}
  1. Then compile the wrapper, and test it out:
g++ -o wrapper wrapper.c && chown root:root wrapper && chmod 4755 wrapper
  1. 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.htmlopen in new window

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>
Last Updated: