#include #include #include #include #include #include #include #include #include #include<9p.h> const char KEY[] = "a1b693d302635eb916d330aebd0bd5c8"; const char SECRET[] = "36eb40a37bf10381"; const char BOUNDARY[] = "thisismyboundarytherearemanylikeitbutthisoneismine"; char *token; /* Webfs */ char *webmtpt = "/mnt/web"; int ctlfd, conn; int webclone(int *c) { char buf[128]; int n, fd; snprint(buf, sizeof buf, "%s/clone", webmtpt); if((fd = open(buf, ORDWR)) < 0) sysfatal("couldn't open %s: %r", buf); if((n = read(fd, buf, sizeof buf-1)) < 0) sysfatal("reading clone: %r"); if(n == 0) sysfatal("short read on clone"); buf[n] = '\0'; *c = atoi(buf); return fd; } /* Formatters for URL and MD5 digest encoding */ #define ALPHANUM(x) ((x) >= 'a' && (x) <= 'z' || \ (x) >= 'A' && (x) <= 'Z' || \ (x) >= '0' && (x) <= '9' || \ (x) == '_' || (x) == '.' || (x) == '-') #pragma varargck type "U" char* static int urlfmt(Fmt *fmt) { char buf[1024]; char *p, *q; for(p = va_arg(fmt->args, char*), q = buf; *p; p++) if(ALPHANUM(*p)) *q++ = *p; else q += sprint(q, "%%%X", (uchar)*p); *q = '\0'; return fmtstrcpy(fmt, buf); } #pragma varargck type "M" uchar* static int digestfmt(Fmt *fmt) { char buf[MD5dlen*2+1]; uchar *p; int i; p = va_arg(fmt->args, uchar*); for(i=0; iname, b->name); if(c != 0) return c; return strcmp(a->value, b->value); } void sortreq(Request *r) { qsort(r->params, r->nparam, sizeof(Parameter), (int(*)(void *, void *))pcmp); } void add(Request *r, char *name, char *value) { r->params[r->nparam].name = estrdup9p(name); r->params[r->nparam++].value = estrdup9p(value); } void reset(Request *r) { uint i; for(i = 0; i < r->nparam; i++){ free(r->params[i].name); free(r->params[i].value); } r->nparam = 0; strcpy(r->url, "http://flickr.com/services/rest/"); } void sign(Request *r) { uchar digest[MD5dlen]; char buffer[1024]; uint len, i; sortreq(r); len = snprint(buffer, sizeof(buffer), "%s", SECRET); for(i = 0; i < r->nparam; i++) len += snprint(buffer + len, sizeof(buffer) - len, "%s%s", r->params[i].name, r->params[i].value); md5((uchar *)buffer, strlen(buffer), digest, nil); snprint(buffer, sizeof buffer, "%M", digest); add(r, "api_sig", buffer); } void auth(Request *r) { add(r, "auth_token", token); add(r, "api_key", KEY); sign(r); } /* Flickr unique photo ids */ typedef struct{ char *id; /* if nil then it's a unuploaded buffer not a reference */ union{ struct{char *farm, *secret, *server;}; struct{uchar *buf; ulong sz;}; }; } Pid; /* Makes a get via webfs given a request */ Biobuf * get(Request *r) { char buf[2056], *ptr; int i, n; Biobuf *fp; /* Compile url */ ptr = buf + snprint(buf, sizeof buf, "url %s", r->url); for(i = 0; i < r->nparam; i++) ptr += snprint(ptr, sizeof buf + buf - ptr, "%c%U=%U", i == 0 ? '?':'&', r->params[i].name, r->params[i].value); if(write(ctlfd, buf, n = strlen(buf)) != n) sysfatal("get: write: %r"); /* Response */ snprint(buf, sizeof buf, "%s/%d/body", webmtpt, conn); if((fp = Bopen(buf, OREAD)) == nil) sysfatal("get: couldn't open body: %r"); return fp; } /* Posts a photo to flickr */ Biobuf * post(Request *r, Pid *image) { char buf[2056]; int i, n; Biobuf *fp; /* our own webfs connection */ int myconn, myctl; myctl = webclone(&myconn); /* Compile url */ snprint(buf, sizeof buf, "url %s", r->url); if(write(myctl, buf, n = strlen(buf)) != n) sysfatal("post: write: %r"); snprint(buf, sizeof buf, "contenttype multipart/form-data; boundary=%s", BOUNDARY); if(write(myctl, buf, n = strlen(buf)) != n) sysfatal("post: write: %r"); /* Open postbody */ snprint(buf, sizeof buf, "%s/%d/postbody", webmtpt, myconn); if((fp = Bopen(buf, OWRITE)) == nil) sysfatal("post: opening postbody: %r"); /* Post parameters */ for(i = 0; i < r->nparam; i++){ Bprint(fp, "--%s\r\n", BOUNDARY); Bprint(fp, "Content-disposition: form-data; name=\"%s\"\r\n\r\n", r->params[i].name); Bprint(fp, "%s\r\n", r->params[i].value); } /* Now the image itself */ Bprint(fp, "--%s\r\n", BOUNDARY); Bprint(fp, "Content-disposition: form-data; name=\"photo\"; filename=\"photo.jpg\"\r\nContent-Type: image/jpeg\r\n\r\n"); Bwrite(fp, image->buf, image->sz); Bprint(fp, "\r\n--%s\r\n", BOUNDARY); Bterm(fp); /* Response */ snprint(buf, sizeof buf, "%s/%d/body", webmtpt, myconn); if((fp = Bopen(buf, OREAD)) == nil) sysfatal("post: opening body: %r"); close(myctl); return fp; } /* Dumps a request to stdout instead of webfs */ int dump(Request *r) { uint i; print("%s", r->url); for(i = 0; i < r->nparam; i++) print("%c%s=%s", i == 0?'?':'&', r->params[i].name, r->params[i].value); print("\n"); return 0; } /* XML shallow parsing */ struct{ char frob[128]; char token[128]; char pages[16]; char desc[1024]; char id[32]; char title[1024]; char farm[128]; char secret[128]; char server[128]; } Parsed; typedef void (*parser)(char *); void parse(Biobuf *body, uint n, ...) { char *line; uint i; parser p; va_list parsers; memset(&Parsed, 0, sizeof Parsed); while(line = Brdstr(body, '\n', 1)){ /*if(n == 0) fprint(2, "fparse: %s\n", line);*/ va_start(parsers, n); for(i = 0; i < n; i++){ p = va_arg(parsers, parser); p(line); } va_end(parsers); free(line); } Bterm(body); } int parseregex(char *line, Reprog *rx, uint ndest, ...) { Resub *match; va_list dests; char *dest; uint i; ndest++; match = emalloc9p(sizeof(*match) * ndest); match[0].sp = match[0].ep = 0; if(regexec(rx, line, match, ndest) != 1) return -1; va_start(dests, ndest); for(i = 1; i < ndest; i++){ dest = va_arg(dests, char*); strncpy(dest, match[i].sp, match[i].ep - match[i].sp); dest[match[i].ep - match[i].sp] = '\0'; } va_end(dests); free(match); return 0; } void parsephoto(char *line) { static Reprog *rx = nil; static Reprog *trx = nil; if(rx == nil && !(rx = regcomp(""))) sysfatal("parsephoto: couldn't compile rx"); if(trx == nil && !(trx = regcomp("title=\"([^\"]+)\".*/>"))) sysfatal("parsephoto: couldn't compile trx"); if(!parseregex(line, rx, 4, Parsed.id, Parsed.secret, Parsed.server, Parsed.farm)) parseregex(line, trx, 1, Parsed.title); } void parsefrob(char *line) { static Reprog *rx = nil; if(rx == nil && !(rx = regcomp("(.*)"))) sysfatal("getfrob: couldn't compile rx"); parseregex(line, rx, 1, Parsed.frob); } void parsetoken(char *line) { static Reprog *rx = nil; if(rx == nil && !(rx = regcomp("(.*)"))) sysfatal("getfrob: couldn't compile rx"); parseregex(line, rx, 1, Parsed.token); } void parsedesc(char *line) { static Reprog *rx = nil; if(rx == nil && !(rx = regcomp("(.*)"))) sysfatal("getfrob: couldn't compile rx"); parseregex(line, rx, 1, Parsed.desc); } void parseid(char *line) { static Reprog *rx = nil; if(rx == nil && !(rx = regcomp("(.*)"))) sysfatal("getfrob: couldn't compile rx"); parseregex(line, rx, 1, Parsed.id); } void parsepages(char *line) { static Reprog *rx = nil; if(rx == nil && !(rx = regcomp("pages=\"([^\"]+)\""))) sysfatal("parsesearch: couldn't compile rx"); parseregex(line, rx, 1, Parsed.pages); } /* Cache for reading images */ struct{ char url[1024]; ulong sz; ulong n; uchar *data; } Filecache; uchar * cache(char *url, long *n) { Biobuf *fp; long r; /* If already cached */ if(!strncmp(url, Filecache.url, sizeof Filecache.url) && Filecache.n > 0){ *n = Filecache.n; return Filecache.data; } /* Load file from flickr */ Filecache.n = *n = 0; strncpy(Filecache.url, url, sizeof Filecache.url); reset(&fr); strncpy(fr.url, url, sizeof fr.url); if((fp = get(&fr)) == nil) return nil; do{ if(Filecache.sz <= Filecache.n){ Filecache.sz = (Filecache.sz + 1) << 1; Filecache.data = erealloc9p(Filecache.data, sizeof(*Filecache.data) * Filecache.sz); } r = Bread(fp, Filecache.data + Filecache.n, Filecache.sz - Filecache.n); Filecache.n += r; }while(r > 0); Bterm(fp); *n = Filecache.n; return Filecache.data; } /* 9p */ void fsread(Req *r) { Pid *p; char buf[1024]; void *c; long n; p = (Pid*)r->fid->file->aux; if(!p){ respond(r, "empty aux"); return; } if(p->id == nil){ respond(r, "no associated id"); return; } snprint(buf, sizeof buf, "http://farm%s.staticflickr.com/%s/%s_%s_b.jpg", p->farm, p->server, p->id, p->secret); c = cache(buf, &n); if(n == 0){ respond(r, "cache error"); return; } readbuf(r, c, n); respond(r, nil); } void fswstat(Req *r) { char *p, *q; Pid *aux; aux = (Pid*)r->fid->file->aux; /* Name changes */ if(r->d.name && r->d.name[0]){ /* Check extension */ p = strrchr(r->d.name, '.'); q = strrchr(r->fid->file->Dir.name, '.'); if(p == nil || strcmp(p, q)){ respond(r, "cannot change extension"); return; } *p = '\0'; /* Get description */ reset(&fr); add(&fr, "method", "flickr.photos.getInfo"); add(&fr, "photo_id", aux->id); auth(&fr); parse(get(&fr), 1, parsedesc); /* Update flickr */ reset(&fr); add(&fr, "method", "flickr.photos.setMeta"); add(&fr, "photo_id", aux->id); add(&fr, "title", r->d.name); add(&fr, "description", Parsed.desc); auth(&fr); parse(get(&fr), 0); /* Success */ *p = '.'; free(r->fid->file->Dir.name); r->fid->file->Dir.name = estrdup9p(r->d.name); } respond(r, nil); } void fsremove(Req *r) { Pid *aux; if(r->fid->file == nil || r->fid->file->aux == nil){ respond(r, nil); return; } aux = (Pid*)r->fid->file->aux; reset(&fr); add(&fr, "method", "flickr.photos.delete"); add(&fr, "photo_id", aux->id); auth(&fr); parse(get(&fr), 0); respond(r, nil); } void fscreate(Req *r) { Pid *aux; char *p; File *f; p = strrchr(r->ifcall.name, '.'); if(p == nil || strcmp(p, ".jpg")) respond(r, "invalid filename"); if((f = createfile(r->fid->file, r->ifcall.name, nil, 0666, nil)) == nil){ respond(r, "couldn't create file"); return; } aux = emalloc9p(sizeof(*aux)); aux->id = nil; aux->buf = nil; aux->sz = 0; f->aux = aux; r->fid->file = f; r->ofcall.qid = f->qid; respond(r, nil); } void fswrite(Req *r) { Pid *aux; vlong offset; long count; aux = (Pid*) r->fid->file->aux; if(aux->id){ respond(r, "replacing files not supported"); return; } offset = r->ifcall.offset; count = r->ifcall.count; if(offset+count >= aux->sz){ aux->buf = erealloc9p(aux->buf, offset+count+1); aux->sz = offset+count; } memmove(aux->buf+offset, r->ifcall.data, count); r->ofcall.count = count; respond(r, nil); } void fsdestroyfid(Fid *fid) { Pid *aux; char *p; if(fid->file == nil) return; aux = (Pid*)fid->file->aux; if(aux == nil) return; if(aux->id == nil){ /* Upload buffer to flickr */ reset(&fr); strcpy(fr.url, "http://api.flickr.com/services/upload/"); p = strrchr(fid->file->name, '.'); *p = '\0'; add(&fr, "title", fid->file->name); *p = '.'; auth(&fr); /* Parse response */ parse(post(&fr, aux), 1, parseid); if(Parsed.id[0] == '\0') sysfatal("fsdestroyfid: bad response"); //fprint(2, "got id: %s", Parsed.id); free(aux->buf); /* Query image to find farm/server/secret */ reset(&fr); add(&fr, "method", "flickr.photos.getInfo"); add(&fr, "photo_id", Parsed.id); auth(&fr); parse(get(&fr), 1, parsephoto); if(Parsed.id[0] == '\0') sysfatal("fsdestroyfid: getinfo failed"); aux->id = estrdup9p(Parsed.id); aux->farm = estrdup9p(Parsed.farm); aux->server = estrdup9p(Parsed.server); aux->secret = estrdup9p(Parsed.secret); } } Srv fs = { .destroyfid= fsdestroyfid, .read= fsread, .write= fswrite, .wstat= fswstat, .remove= fsremove, .create= fscreate, }; void fsdestroyfile(File *f) { Pid *aux; aux = (Pid*)f->aux; if(aux != nil){ if(aux->id){ free(aux->secret); free(aux->farm); free(aux->id); free(aux->server); free(aux); }else if(aux->sz > 0) free(aux->buf); } } /* Flickr searching to build file tree */ void parsesearch(char *line) { char fn[1024]; File *f; Pid *aux; Parsed.title[0] = '\0'; parsephoto(line); if(Parsed.title[0]){ aux = emalloc9p(sizeof(*aux)); memset(aux, 0, sizeof(*aux)); snprint(fn, sizeof fn, "%s.jpg", Parsed.title); f = createfile(fs.tree->root, fn, nil, 0666, aux); if(f == nil){ fprint(2, "cannot create file: %s\n", fn); free(aux); return; } aux->id = estrdup9p(Parsed.id); aux->farm = estrdup9p(Parsed.farm); aux->secret = estrdup9p(Parsed.secret); aux->server = estrdup9p(Parsed.server); closefile(f); } } void searchflickr(void) { uint page = 1; char buf[16]; do{ reset(&fr); add(&fr, "method", "flickr.photos.search"); add(&fr, "user_id", "me"); add(&fr, "per_page", "500"); snprint(buf, sizeof buf, "%d", page); add(&fr, "page", buf); auth(&fr); parse(get(&fr), 2, parsesearch, parsepages); if(Parsed.pages[0] == '\0'){ fprint(2, "searchflickr: warning couldn't parse pages\n"); break; } page++; }while(page < atoi(Parsed.pages)); } void usage(void) { fprint(2, "%s: [-D] [-w webfs mtpt] [-s srvname] [-m mtpt]\n", argv0); exits("usage"); } void main(int argc, char *argv[]) { UserPasswd *up; char buf[128]; char *mtpt; char *srvname; Qid q; memset(&Filecache, 0, sizeof Filecache); mtpt = "/n/flickr"; srvname = nil; ARGBEGIN{ case 'D': chatty9p++; break; case 'w': webmtpt = EARGF(usage()); break; case 'm': mtpt = EARGF(usage()); break; case 's': srvname = EARGF(usage()); break; case 'h': default: usage(); }ARGEND; if(argc) usage(); fmtinstall('M', digestfmt); fmtinstall('U', urlfmt); ctlfd = webclone(&conn); /* Try finding a token */ up = auth_getuserpasswd(nil, "proto=pass role=client server=flickr.com user=%s", KEY); /* No token */ if(up == nil){ /* Get a frob */ reset(&fr); add(&fr, "method", "flickr.auth.getFrob"); add(&fr, "api_key", KEY); sign(&fr); parse(get(&fr), 1, parsefrob); if(Parsed.frob == nil) sysfatal("couldn't parse frob"); /* Authentication url */ reset(&fr); strcpy(fr.url, "http://flickr.com/services/auth/"); add(&fr, "api_key", KEY); add(&fr, "perms", "delete"); add(&fr, "frob", Parsed.frob); sign(&fr); print("Authenticate then press enter: "); dump(&fr); read(0, buf, 1); /* Fetch token */ reset(&fr); strcpy(fr.url, "http://flickr.com/services/rest/"); add(&fr, "method", "flickr.auth.getToken"); add(&fr, "api_key", KEY); add(&fr, "frob", Parsed.frob); sign(&fr); parse(get(&fr), 1, parsetoken); if(Parsed.token == nil) sysfatal("couldn't parse token"); print("key proto=pass role=client server=flickr.com user=%s !password=%s\n", KEY, Parsed.token); exits(0); } /* We got a token */ token = up->passwd; /* Populate our tree */ fs.tree = alloctree(nil, nil, DMDIR|0777, fsdestroyfile); q = fs.tree->root->qid; searchflickr(); /* Mount ourselves */ postmountsrv(&fs, srvname, mtpt, MREPL|MCREATE); exits(0); }