pubsub_cgi.c - pubsubhubbubblub - pubsubhubbub client implementation | |
git clone git://git.codemadness.org/pubsubhubbubblub | |
Log | |
Files | |
Refs | |
README | |
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+… | |
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 *signa… | |
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_M… | |
217 char tokenpath[PATH_MAX]; | |
218 char *contentlength = "", *contenttype = "", *method = "GET", *q… | |
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 opa… | |
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_LENGT… | |
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(requesturideco… | |
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 … | |
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 … | |
309 r = snprintf(configpath, sizeof(configpath), "%s/%s", configdir,… | |
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", config… | |
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 v… | |
328 r = snprintf(secretpath, sizeof(secretpath), "%s/%s/secr… | |
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-Signa… | |
334 | |
335 if (key) { | |
336 key_len = strlen(key); | |
337 hmac_sha1_init(&ctx, key, key_len, key_opad, siz… | |
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/%l… | |
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… | |
354 if (fwrite(buf, 1, n, fpdata) != n) | |
355 break; | |
356 if (key) | |
357 SHA1_Update(&ctx, buf, n); /* hash data … | |
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 e… | |
370 } | |
371 if (fflush(fpdata) || ferror(fpdata)) { | |
372 fclose(fpdata); | |
373 unlink(tmpfilename); | |
374 servererror("cannot process POST message: write … | |
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 matc… | |
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 f… | |
400 if (!(tmp = strrchr(tmpfilename, '/'))) | |
401 servererror("invalid path"); /* cannot happen */ | |
402 r = snprintf(feedpath, sizeof(feedpath), "%s/%s", datadi… | |
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", fe… | |
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… | |
415 } | |
416 chmod(filename, 0644); | |
417 | |
418 httpstatus(200); | |
419 fputs("Content-Type: text/plain; charset=utf-8\r\n", std… | |
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 wit… | |
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)… | |
443 badrequest("hub.challenge"); | |
444 } | |
445 if (!challenge[0]) | |
446 badrequest("hub.challenge is required, but is mi… | |
447 | |
448 httpstatus(202); | |
449 fputs("Content-Type: text/plain; charset=utf-8\r\n", std… | |
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 s… | |
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 gracious… | |
461 | |
462 return 0; | |
463 } |