Plan 9 from Bell Labs’s /usr/web/sources/contrib/bedo/flickrfs.c

Copyright © 2021 Plan 9 Foundation.
Distributed under the MIT License.
Download the Plan 9 distribution.


#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);
}

Bell Labs OSI certified Powered by Plan 9

(Return to Plan 9 Home Page)

Copyright © 2021 Plan 9 Foundation. All Rights Reserved.
Comments to webmaster@9p.io.