pubsub_cgi.c - pubsubhubbubblub - pubsubhubbub client implementation
HTML git clone git://git.codemadness.org/pubsubhubbubblub
DIR Log
DIR Files
DIR Refs
DIR README
DIR LICENSE
---
pubsub_cgi.c (11386B)
---
1 #include <sys/stat.h>
2
3 #include <ctype.h>
4 #include <err.h>
5 #include <errno.h>
6 #include <limits.h>
7 #include <stdio.h>
8 #include <stdlib.h>
9 #include <string.h>
10 #include <time.h>
11 #include <unistd.h>
12
13 #ifdef __OpenBSD__
14 #include <unistd.h>
15 #else
16 #define pledge(p1,p2) 0
17 #define unveil(p1,p2) 0
18 #endif
19
20 #include "hmac_sha1.h"
21
22 static const char *relpath = "/pubsub/";
23
24 #define DATADIR "/pubsub-data"
25
26 static const char *configdir = DATADIR "/config";
27 static const char *datadir = DATADIR "/feeds";
28 static const char *tmpdir = DATADIR "/tmp";
29 static const char *logfile = DATADIR "/log";
30 static time_t now;
31
32 char *
33 readfile(const char *path)
34 {
35 static char buf[256];
36 FILE *fp;
37
38 if (!(fp = fopen(path, "rb")))
39 goto err;
40 if (!fgets(buf, sizeof(buf), fp))
41 goto err;
42 fclose(fp);
43 buf[strcspn(buf, "\n")] = '\0';
44 return buf;
45
46 err:
47 if (fp)
48 fclose(fp);
49 return NULL;
50 }
51
52 int
53 hexdigit(int c)
54 {
55 if (c >= '0' && c <= '9')
56 return c - '0';
57 else if (c >= 'A' && c <= 'F')
58 return c - 'A' + 10;
59 else if (c >= 'a' && c <= 'f')
60 return c - 'a' + 10;
61
62 return 0;
63 }
64
65 /* decode until NUL separator or end of "key". */
66 int
67 decodeparamuntilend(char *buf, size_t bufsiz, const char *s, int end)
68 {
69 size_t i;
70
71 if (!bufsiz)
72 return -1;
73
74 for (i = 0; *s && *s != end; s++) {
75 switch (*s) {
76 case '%':
77 if (i + 3 >= bufsiz)
78 return -1;
79 if (!isxdigit((unsigned char)*(s+1)) ||
80 !isxdigit((unsigned char)*(s+2)))
81 return -1;
82 buf[i++] = hexdigit(*(s+1)) * 16 + hexdigit(*(s+2));
83 s += 2;
84 break;
85 case '+':
86 if (i + 1 >= bufsiz)
87 return -1;
88 buf[i++] = ' ';
89 break;
90 default:
91 if (i + 1 >= bufsiz)
92 return -1;
93 buf[i++] = *s;
94 break;
95 }
96 }
97 buf[i] = '\0';
98
99 return i;
100 }
101
102 /* decode until NUL separator or end of "key". */
103 int
104 decodeparam(char *buf, size_t bufsiz, const char *s)
105 {
106 return decodeparamuntilend(buf, bufsiz, s, '&');
107 }
108
109 char *
110 getparam(const char *query, const char *s)
111 {
112 const char *p, *last = NULL;
113 size_t len;
114
115 len = strlen(s);
116 for (p = query; (p = strstr(p, s)); p += len) {
117 if (p[len] == '=' && (p == query || p[-1] == '&' || p[-1] == '?'))
118 last = p + len + 1;
119 }
120
121 return (char *)last;
122 }
123
124 const char *
125 httpstatusmsg(int code)
126 {
127 switch (code) {
128 case 200: return "200 OK";
129 case 202: return "202 Accepted";
130 case 400: return "400 Bad Request";
131 case 403: return "403 Forbidden";
132 case 404: return "404 Not Found";
133 case 500: return "500 Internal Server Error";
134 }
135 return NULL;
136 }
137
138 void
139 httpstatus(int code)
140 {
141 const char *msg;
142
143 if ((msg = httpstatusmsg(code)))
144 printf("Status: %s\r\n", msg);
145 }
146
147 void
148 httperror(int code, const char *s)
149 {
150 httpstatus(code);
151 fputs("Content-Type: text/plain; charset=utf-8\r\n", stdout);
152 fputs("\r\n", stdout);
153 if (s)
154 printf("%s: %s\r\n", httpstatusmsg(code), s);
155 else
156 printf("%s\r\n", httpstatusmsg(code));
157 exit(0);
158 }
159
160 void
161 badrequest(const char *s)
162 {
163 httperror(400, s);
164 }
165
166 void
167 forbidden(const char *s)
168 {
169 httperror(403, s);
170 }
171
172 void
173 notfound(const char *s)
174 {
175 httperror(404, s);
176 }
177
178 void
179 servererror(const char *s)
180 {
181 httperror(500, s);
182 }
183
184 void
185 logrequest(const char *feedname, const char *filename, const char *signature)
186 {
187 FILE *fp;
188
189 /* file format: timestamp TAB feedname TAB data-filename */
190 if (!(fp = fopen(logfile, "a")))
191 servererror("cannot write data");
192 fprintf(fp, "%lld\t", (long long)now);
193 fputs(feedname, fp);
194 fputs("\t", fp);
195 fputs(filename, fp);
196 fputs("\t", fp);
197 fputs(signature, fp);
198 fputs("\n", fp);
199 fclose(fp);
200 }
201
202 char *
203 contenttypetoext(const char *s)
204 {
205 return "xml"; /* for now just support XML, for RSS and Atom */
206 }
207
208 int
209 main(void)
210 {
211 FILE *fpdata;
212 char challenge[256], mode[32] = "", signature[128] = "";
213 char requesturi[4096], requesturidecoded[4096];
214 char feedname[256], token[256] = "";
215 char filename[PATH_MAX], tmpfilename[PATH_MAX];
216 char configpath[PATH_MAX], feedpath[PATH_MAX], secretpath[PATH_MAX];
217 char tokenpath[PATH_MAX];
218 char *contentlength = "", *contenttype = "", *method = "GET", *query = "";
219 char *p, *fileext, *tmp;
220 char buf[4096];
221 size_t n, total;
222 long long ll;
223 int i, j, fd, r;
224 /* HMAC */
225 SHA_CTX ctx;
226 unsigned char key_opad[65]; /* outer padding - key XORd with opad */
227 unsigned char *key;
228 size_t key_len;
229 unsigned char digest[SHA_DIGEST_LENGTH];
230 unsigned char inputdigest[SHA_DIGEST_LENGTH];
231
232 if (unveil(DATADIR, "rwc") == -1)
233 err(1, "unveil");
234 if (pledge("stdio rpath wpath cpath fattr", NULL) == -1)
235 err(1, "pledge");
236
237 if ((tmp = getenv("CONTENT_TYPE")))
238 contenttype = tmp;
239 if ((tmp = getenv("CONTENT_LENGTH")))
240 contentlength = tmp;
241 if ((tmp = getenv("REQUEST_METHOD")))
242 method = tmp;
243 if ((tmp = getenv("QUERY_STRING")))
244 query = tmp;
245
246 /* "8. Authenticated Content Distribution" */
247 if ((p = getenv("HTTP_X_HUB_SIGNATURE"))) {
248 r = snprintf(signature, sizeof(signature), "%s", p);
249 if (r < 0 || (size_t)r >= sizeof(signature))
250 badrequest("invalid signature (truncated)");
251
252 /* accept sha1=digest or sha=digest */
253 if ((tmp = strstr(signature, "sha1=")))
254 tmp += sizeof("sha1=") - 1;
255 else if ((tmp = strstr(signature, "sha=")))
256 tmp += sizeof("sha=") - 1;
257 if (tmp) {
258 for (p = tmp, i = 0; *p; p++, i++) {
259 if (!isxdigit((unsigned char)*p))
260 break;
261 }
262 }
263 if (tmp && !*p && i == (SHA_DIGEST_LENGTH * 2)) {
264 for (i = 0, j = 0, p = tmp; i < SHA_DIGEST_LENGTH; i++, j += 2) {
265 inputdigest[i] = (hexdigit(p[j]) << 4) |
266 hexdigit(p[j + 1]);
267 }
268 } else {
269 badrequest("invalid hash format");
270 }
271 }
272
273 if (!(p = getenv("REQUEST_URI")))
274 p = "";
275 snprintf(requesturi, sizeof(requesturi), "%s", p);
276 if ((p = strchr(requesturi, '?')))
277 *p = '\0'; /* remove query string */
278
279 if (decodeparamuntilend(requesturidecoded, sizeof(requesturidecoded), requesturi, '\0') == -1)
280 badrequest("request URI");
281
282 p = requesturidecoded;
283 if (strncmp(p, relpath, strlen(relpath)))
284 forbidden("invalid relative path");
285 p += strlen(relpath);
286
287 /* first part of path of request URI is the feedname, last part is the (optional) token */
288 if ((tmp = strchr(p, '/'))) {
289 *tmp = '\0'; /* temporary NUL terminate */
290
291 r = snprintf(feedname, sizeof(feedname), "%s", p);
292 if (r < 0 || (size_t)r >= sizeof(feedname))
293 servererror("path truncated");
294
295 r = snprintf(token, sizeof(token), "%s", tmp + 1);
296 if (r < 0 || (size_t)r >= sizeof(token))
297 servererror("path truncated");
298
299 *tmp = '/'; /* restore NUL byte to '/' */
300 } else {
301 r = snprintf(feedname, sizeof(feedname), "%s", p);
302 if (r < 0 || (size_t)r >= sizeof(feedname))
303 servererror("path truncated");
304 }
305 if (strstr(feedname, ".."))
306 badrequest("invalid feed name");
307
308 /* check if configdir of feedname exists, else skip request and return 404 */
309 r = snprintf(configpath, sizeof(configpath), "%s/%s", configdir, feedname);
310 if (r < 0 || (size_t)r >= sizeof(configpath))
311 servererror("path truncated");
312 if (access(configpath, X_OK) == -1)
313 notfound("feed entrypoint does not exist");
314
315 r = snprintf(tokenpath, sizeof(tokenpath), "%s/%s/token", configdir, feedname);
316 if (r < 0 || (size_t)r >= sizeof(tokenpath))
317 servererror("path truncated");
318 if ((tmp = readfile(tokenpath))) {
319 if (strcmp(tmp, token))
320 forbidden("missing or incorrect token in path");
321 }
322
323 if (!strcasecmp(method, "POST")) {
324 if (!feedname[0])
325 badrequest("feed name part of path is missing");
326
327 /* read secret, initialize for HMAC and data signature verification */
328 r = snprintf(secretpath, sizeof(secretpath), "%s/%s/secret", configdir, feedname);
329 if (r < 0 || (size_t)r >= sizeof(secretpath))
330 servererror("path truncated");
331 key = readfile(secretpath);
332 if (key && !signature[0])
333 forbidden("requires signature header X-Hub-Signature");
334
335 if (key) {
336 key_len = strlen(key);
337 hmac_sha1_init(&ctx, key, key_len, key_opad, sizeof(key_opad));
338 }
339
340 /* temporary file with random characters */
341 if ((now = time(NULL)) == (time_t)-1)
342 servererror("cannot get current time");
343 r = snprintf(tmpfilename, sizeof(tmpfilename), "%s/%s/%lld.XXXXXX", tmpdir, feedname, (long long)now);
344 if (r < 0 || (size_t)r >= sizeof(tmpfilename))
345 servererror("path truncated");
346
347 if ((fd = mkstemp(tmpfilename)) == -1)
348 servererror("cannot create tmpfilename");
349 if (!(fpdata = fdopen(fd, "wb")))
350 servererror(tmpfilename);
351
352 total = 0;
353 while ((n = fread(buf, 1, sizeof(buf), stdin)) == sizeof(buf)) {
354 if (fwrite(buf, 1, n, fpdata) != n)
355 break;
356 if (key)
357 SHA1_Update(&ctx, buf, n); /* hash data for signature */
358 total += n;
359 }
360 if (n) {
361 fwrite(buf, 1, n, fpdata);
362 if (key)
363 SHA1_Update(&ctx, buf, n);
364 total += n;
365 }
366 if (ferror(stdin)) {
367 fclose(fpdata);
368 unlink(tmpfilename);
369 servererror("cannot process POST message: read error");
370 }
371 if (fflush(fpdata) || ferror(fpdata)) {
372 fclose(fpdata);
373 unlink(tmpfilename);
374 servererror("cannot process POST message: write error");
375 }
376 fclose(fpdata);
377 chmod(tmpfilename, 0644);
378
379 /* if Content-Length is set then check if it matches */
380 if (contentlength[0]) {
381 ll = strtoll(contentlength, NULL, 10);
382 if (ll < 0 || (size_t)ll != total) {
383 unlink(tmpfilename);
384 badrequest("Content-Length does not match");
385 }
386 }
387
388 if (key) {
389 /* finalize signature digest */
390 hmac_sha1_final(&ctx, key_opad, digest);
391
392 /* compare digest */
393 if (memcmp(inputdigest, digest, sizeof(digest))) {
394 unlink(tmpfilename);
395 forbidden("invalid digest for data");
396 }
397 }
398
399 /* use part of basename of the random temp file as the filename */
400 if (!(tmp = strrchr(tmpfilename, '/')))
401 servererror("invalid path"); /* cannot happen */
402 r = snprintf(feedpath, sizeof(feedpath), "%s/%s", datadir, feedname);
403 if (r < 0 || (size_t)r >= sizeof(feedpath))
404 servererror("path truncated");
405 fileext = contenttypetoext(contenttype);
406 r = snprintf(filename, sizeof(filename), "%s/%s%s%s", feedpath, tmp + 1,
407 fileext[0] ? "." : "", fileext);
408 if (r < 0 || (size_t)r >= sizeof(filename))
409 servererror("path truncated");
410
411 if ((r = rename(tmpfilename, filename)) != 0) {
412 unlink(filename);
413 unlink(tmpfilename);
414 servererror("cannot process POST message: failed to rename file");
415 }
416 chmod(filename, 0644);
417
418 httpstatus(200);
419 fputs("Content-Type: text/plain; charset=utf-8\r\n", stdout);
420 fputs("\r\n", stdout);
421
422 /* output stored file: feedname, basename of the file */
423 if ((tmp = strrchr(filename, '/')))
424 tmp++;
425 else
426 tmp = "";
427 printf("%s/%s\n", feedname, tmp);
428
429 /* write to a log file, this could be a pipe or used with tail -f to monitor */
430 logrequest(feedname, tmp, signature);
431
432 return 0;
433 }
434
435 if ((p = getparam(query, "hub.mode"))) {
436 if (decodeparam(mode, sizeof(mode), p) == -1)
437 badrequest("hub.mode");
438 }
439
440 if (!strcmp(mode, "subscribe") || !strcmp(mode, "unsubscribe")) {
441 if ((p = getparam(query, "hub.challenge"))) {
442 if (decodeparam(challenge, sizeof(challenge), p) == -1)
443 badrequest("hub.challenge");
444 }
445 if (!challenge[0])
446 badrequest("hub.challenge is required, but is missing");
447
448 httpstatus(202);
449 fputs("Content-Type: text/plain; charset=utf-8\r\n", stdout);
450 fputs("\r\n", stdout);
451 printf("%s\r\n", challenge);
452 return 0;
453 } else if (mode[0]) {
454 badrequest("hub.mode: only subscribe or unsubscribe is supported");
455 }
456
457 httpstatus(200);
458 fputs("Content-Type: text/plain; charset=utf-8\r\n", stdout);
459 fputs("\r\n", stdout);
460 printf("pubsubhubbubblub running perfectly and flapping graciously in the wind.\r\n");
461
462 return 0;
463 }