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
1

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
1

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
1
2

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);
               }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
  1. Then compile the wrapper, and test it out:
g++ -o wrapper wrapper.c && chown root:root wrapper && chmod 4755 wrapper
1
  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 ~]$
1
2
3
4
5
6
7

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
1

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>
1
2
3
4
5
Last Updated: 6/23/2021, 9:36:08 PM