URI:
       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 }