#include<u.h>
#include<libc.h>
#include<mp.h>
#include<libsec.h>
#include<regexp.h>
#include<bio.h>
#include<auth.h>
#include<fcall.h>
#include<thread.h>
#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; i<MD5dlen; i++)
sprint(buf+2*i, "%.2ux", p[i]);
return fmtstrcpy(fmt, buf);
}
/* Flickr API requests */
typedef struct{
char *name;
char *value;
} Parameter;
typedef struct{
char url[256];
uint nparam;
Parameter params[16];
} Request;
Request fr;
int
pcmp(Parameter *a, Parameter *b)
{
int c = strcmp(a->name, 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("<photo[ \t].*id=\"([^\"]+)\".*secret=\"([^\"]+)\".*server=\"([^\"]+)\".*farm=\"([^\"]+)\".*>")))
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("<frob>(.*)</frob>")))
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("<token>(.*)</token>")))
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("<description>(.*)</description>")))
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("<photoid>(.*)</photoid>")))
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);
}
|