1 /* Generic DDNS plugin
2  *
3  * Copyright (C) 2003-2004  Narcis Ilisei <inarcis2002@hotpop.com>
4  * Copyright (C) 2006       Steve Horbachuk
5  * Copyright (C) 2010-2021  Joachim Wiberg <troglobit@gmail.com>
6  *
7  * This program is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License
9  * as published by the Free Software Foundation; either version 2
10  * of the License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program; if not, visit the Free Software Foundation
19  * website at http://www.gnu.org/licenses/gpl-2.0.html or write to the
20  * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
21  * Boston, MA  02110-1301, USA.
22  */
23 
24 #include <ctype.h>
25 #include "plugin.h"
26 
27 /*
28  * Generic update format for sites that perform the update using:
29  *
30  *	http://some.address.domain/somesubdir?some_param_name=ALIAS
31  *
32  * With the standard http stuff and basic base64 encoded auth.
33  * The parameter here is the entire request, except the the alias.
34  */
35 #define GENERIC_BASIC_AUTH_UPDATE_IP_REQUEST				\
36 	"GET %s%s "							\
37 	"HTTP/1.0\r\n"							\
38 	"Host: %s\r\n"							\
39 	"Authorization: Basic %s\r\n"					\
40 	"User-Agent: %s\r\n\r\n"
41 
42 const char * const generic_responses[] =
43     { "OK", "good", "true", "updated", "nochg", NULL };
44 
45 static int request  (ddns_t       *ctx,   ddns_info_t *info, ddns_alias_t *alias);
46 static int response (http_trans_t *trans, ddns_info_t *info, ddns_alias_t *alias);
47 
48 static ddns_system_t generic = {
49 	.name         = "custom",
50 
51 	.request      = (req_fn_t)request,
52 	.response     = (rsp_fn_t)response,
53 
54 	.checkip_name = DDNS_MY_IP_SERVER,
55 	.checkip_url  = DDNS_MY_CHECKIP_URL,
56 	.checkip_ssl  = DDNS_MY_IP_SSL,
57 
58 	.server_name  = "",
59 	.server_url   = ""
60 };
61 
62 /* Replace %? with @str */
replace_fmt(char * fmt,char * str,size_t fmtlen)63 static void replace_fmt(char *fmt, char *str, size_t fmtlen)
64 {
65 	char *src = fmt + fmtlen;
66 	size_t len = strlen(str);
67 
68 	memmove(fmt + len, src, strlen(src) + 1);
69 	memcpy(fmt, str, strlen(str));
70 }
71 
72 /* Skip %? if it has not been specified */
skip_fmt(char * fmt,size_t len)73 static void skip_fmt(char *fmt, size_t len)
74 {
75 	char *src = fmt + len;
76 
77 	memmove(fmt, src, strlen(src) + 1);
78 }
79 
80 
81 /*
82  * Fully custom server URL, with % format specifiers
83  *
84  * %u - username
85  * %p - password, if HTTP basic auth is not used
86  * %h - hostname
87  * %i - IP address
88  */
custom_server_url(ddns_info_t * info,ddns_alias_t * alias)89 static int custom_server_url(ddns_info_t *info, ddns_alias_t *alias)
90 {
91 	char *ptr;
92 
93 	while ((ptr = strchr(info->server_url, '%'))) {
94 		if (!strncmp(ptr, "%u", 2)) {
95 			if (strnlen(info->creds.username, USERNAME_LEN) <= 0) {
96 				logit(LOG_ERR, "Format specifier in ddns-path used: '%%u',"
97 				      " but 'username' configuration option has not been specified!");
98 				skip_fmt(ptr, 2);
99 			} else {
100 				replace_fmt(ptr, info->creds.username, 2);
101 			}
102 
103 			continue;
104 		}
105 
106 		if (!strncmp(ptr, "%p", 2)) {
107 			if (strnlen(info->creds.password, PASSWORD_LEN) <= 0) {
108 				logit(LOG_ERR, "Format specifier in ddns-path used: '%%p',"
109 				      " but 'password' configuration option has not been specified!");
110 				skip_fmt(ptr, 2);
111 			} else {
112 				replace_fmt(ptr, info->creds.password, 2);
113 			}
114 
115 			continue;
116 		}
117 
118 		if (!strncmp(ptr, "%h", 2)) {
119 			replace_fmt(ptr, alias->name, 2);
120 			continue;
121 		}
122 
123 		if (!strncmp(ptr, "%i", 2)) {
124 			replace_fmt(ptr, alias->address, 2);
125 			continue;
126 		}
127 
128 		/* NOTE: This should be the last one */
129 		if (!strncmp(ptr, "%%", 2)) {
130 			replace_fmt(ptr, "%", 2);
131 			continue;
132 		}
133 
134 		logit(LOG_ERR, "Unknown format specifier in ddns-path: '%c'", ptr[1]);
135 		return -1;
136 	}
137 
138 	return strlen(info->server_url);
139 }
140 
tohex(char code)141 static char tohex(char code)
142 {
143 	static const char hex[] = "0123456789abcdef";
144 
145 	return hex[code & 15];
146 }
147 
148 /* Used to check if user already URL encoded */
ishex(char * str)149 static int ishex(char *str)
150 {
151 	if (strlen(str) < 3)
152 		return 0;
153 
154 	if (str[0] == '%' && isxdigit(str[1]) && isxdigit(str[2]))
155 		return 1;
156 
157 	return 0;
158 }
159 
160 /*
161  * Simple URL encoder, with exceptions for /, ?, =, and &, which should
162  * usually be encoded as well, but are here exposed raw to advanced
163  * end-users.
164  */
url_encode(char * str)165 static char *url_encode(char *str)
166 {
167 	char *buf, *ptr;
168 
169 	buf = calloc(strlen(str) * 3 + 1, sizeof(char));
170 	if (!buf)
171 		return NULL;
172 	ptr = buf;
173 
174 	while (str[0]) {
175 		char ch = str[0];
176 
177 		if (ishex(str)) {
178 			*ptr++ = *str++;
179 			*ptr++ = *str++;
180 			*ptr++ = *str++;
181 			continue;
182 		}
183 
184 		if (isalnum(ch) || ch == '-' || ch == '_' || ch == '.' || ch == '~')
185 			*ptr++ = ch;
186 		else if (ch == '/' || ch == '?' || ch == '&' || ch == '=')
187 			*ptr++ = ch;
188 		else if (ch == ' ')
189 			*ptr++ = '+';
190 		else
191 			*ptr++ = '%', *ptr++ = tohex(ch >> 4), *ptr++ = tohex(ch & 15);
192 		str++;
193 	}
194 	*ptr = '\0';
195 
196 	return buf;
197 }
198 
199 /*
200  * This function is called for every listed hostname alias in the
201  * custom{} section.  There is currently no way to only call it
202  * once, in case a DDNS provider supports many hostnames in the
203  * HTTP GET URL.
204  */
request(ddns_t * ctx,ddns_info_t * info,ddns_alias_t * alias)205 static int request(ddns_t *ctx, ddns_info_t *info, ddns_alias_t *alias)
206 {
207 	int ret;
208 	char *url;
209 	char *arg = "";
210 
211 	/*
212 	 * if the user has specified modifiers, then he is probably
213 	 * aware of how to append his hostname or IP, otherwise just
214 	 * append the hostname or ip (depending on the append_myip option)
215 	 */
216 	if (strchr(info->server_url, '%')) {
217 		if (custom_server_url(info, alias) <= 0)
218 			logit(LOG_ERR, "Invalid server URL: %s", info->server_url);
219 	} else {
220 		/* Backwards compat, default to append hostname */
221 		arg = alias->name;
222 
223 		if (info->append_myip)
224 			arg = alias->address;
225 	}
226 
227 	url = url_encode(info->server_url);
228 	if (!url)
229 		return 0;
230 
231 	ret = snprintf(ctx->request_buf, ctx->request_buflen,
232 		       GENERIC_BASIC_AUTH_UPDATE_IP_REQUEST,
233 		       url, arg,
234 		       info->server_name.name,
235 		       info->creds.encoded_password,
236 		       info->user_agent);
237 	free(url);
238 
239 	return ret;
240 }
241 
response(http_trans_t * trans,ddns_info_t * info,ddns_alias_t * alias)242 static int response(http_trans_t *trans, ddns_info_t *info, ddns_alias_t *alias)
243 {
244 	char *resp = trans->rsp_body;
245 	size_t i;
246 
247 	(void)info;
248 	(void)alias;
249 
250 	DO(http_status_valid(trans->status));
251 
252 	for (i = 0; i < info->server_response_num; i++) {
253 		if (strcasestr(resp, info->server_response[i]))
254 			return 0;
255 	}
256 
257 	return RC_DDNS_RSP_NOTOK;
258 }
259 
PLUGIN_INIT(plugin_init)260 PLUGIN_INIT(plugin_init)
261 {
262 	plugin_register(&generic);
263 }
264 
PLUGIN_EXIT(plugin_exit)265 PLUGIN_EXIT(plugin_exit)
266 {
267 	plugin_unregister(&generic);
268 }
269 
270 /**
271  * Local Variables:
272  *  indent-tabs-mode: t
273  *  c-file-style: "linux"
274  * End:
275  */
276