smugfs(4): new program

This commit is contained in:
Russ Cox 2008-08-03 07:42:27 -07:00
parent 3d36f44373
commit 18824b5868
17 changed files with 4299 additions and 0 deletions

17
src/cmd/smugfs/COPYRIGHT Normal file
View file

@ -0,0 +1,17 @@
The files in this directory are subject to the following license.
The author of this software is Russ Cox.
Copyright (c) 2008 Russ Cox
Permission to use, copy, modify, and distribute this software for any
purpose without fee is hereby granted, provided that this entire notice
is included in all copies of any software which is or includes a copy
or modification of this software and in all copies of the supporting
documentation for such software.
THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR IMPLIED
WARRANTY. IN PARTICULAR, THE AUTHOR MAKES NO REPRESENTATION OR WARRANTY
OF ANY KIND CONCERNING THE MERCHANTABILITY OF THIS SOFTWARE OR ITS
FITNESS FOR ANY PARTICULAR PURPOSE.

22
src/cmd/smugfs/NOTES Normal file
View file

@ -0,0 +1,22 @@
* Threading:
Uploads run in parallel with main fs operation.
Otherwise, main fs operation is single-threaded.
Could multi-thread the rest but would have to lock the
cache properly first.
Right now, only one upload at a time.
Could have more by kicking off multiple
uploader procs.
* Implement subcategories.
* Implement renames of categories.
* Implement renames of albums.
* Implement album settings file.
* Implement image settings file.

190
src/cmd/smugfs/a.h Normal file
View file

@ -0,0 +1,190 @@
#include <u.h>
#include <errno.h>
#include <libc.h>
#include <fcall.h>
#include <thread.h>
#include <auth.h>
#include <9p.h>
#include <libsec.h>
#define APIKEY "G9ANE2zvCozKEoLQ5qaR1AUtcE5YpuDj"
#define HOST "api.smugmug.com"
#define UPLOAD_HOST "upload.smugmug.com"
#define API_VERSION "1.2.1"
#define PATH "/services/api/json/" API_VERSION "/"
#define USER_AGENT "smugfs (part of Plan 9 from User Space)"
void* emalloc(int);
void* erealloc(void*, int);
char* estrdup(char*);
int urlencodefmt(Fmt*);
int timefmt(Fmt*);
int writen(int, void*, int);
// Generic cache
typedef struct Cache Cache;
typedef struct CEntry CEntry;
struct CEntry
{
char *name;
struct {
CEntry *next;
CEntry *prev;
} list;
struct {
CEntry *next;
} hash;
};
Cache *newcache(int sizeofentry, int maxentry, void (*cefree)(CEntry*));
CEntry *cachelookup(Cache*, char*, int);
void cacheflush(Cache*, char*);
// JSON parser
typedef struct Json Json;
enum
{
Jstring,
Jnumber,
Jobject,
Jarray,
Jtrue,
Jfalse,
Jnull
};
struct Json
{
int ref;
int type;
char *string;
double number;
char **name;
Json **value;
int len;
};
void jclose(Json*);
Json* jincref(Json*);
vlong jint(Json*);
Json* jlookup(Json*, char*);
double jnumber(Json*);
int jsonfmt(Fmt*);
int jstrcmp(Json*, char*);
char* jstring(Json*);
Json* jwalk(Json*, char*);
Json* parsejson(char*);
// Wrapper to hide whether we're using OpenSSL for HTTPS.
typedef struct Protocol Protocol;
typedef struct Pfd Pfd;
struct Protocol
{
Pfd *(*connect)(char *host);
int (*read)(Pfd*, void*, int);
int (*write)(Pfd*, void*, int);
void (*close)(Pfd*);
};
Protocol http;
Protocol https;
// HTTP library
typedef struct HTTPHeader HTTPHeader;
struct HTTPHeader
{
int code;
char proto[100];
char codedesc[100];
vlong contentlength;
char contenttype[100];
};
char *httpreq(Protocol *proto, char *host, char *request, HTTPHeader *hdr, int rfd, vlong rlength);
int httptofile(Protocol *proto, char *host, char *req, HTTPHeader *hdr, int wfd);
// URL downloader - caches in files on disk
int download(char *url, HTTPHeader *hdr);
void downloadflush(char*);
// JSON RPC
enum
{
MaxResponse = 1<<29,
};
Json* jsonrpc(Protocol *proto, char *host, char *path, char *method, char *name1, va_list arg, int usecache);
Json* jsonupload(Protocol *proto, char *host, char *req, int rfd, vlong rlength);
void jcacheflush(char*);
extern int chattyhttp;
// SmugMug RPC
#ifdef __GNUC__
#define check_nil __attribute__((sentinel))
#else
#define check_nil
#endif
Json* smug(char *method, char *name1, ...) check_nil; // cached, http
Json* ncsmug(char *method, char *name1, ...) check_nil; // not cached, https
// Session information
extern Json *userinfo;
extern char *sessid;
// File system
extern Srv xsrv;
void xinit(void);
extern int nickindex(char*);
// Logging
typedef struct Logbuf Logbuf;
struct Logbuf
{
Req *wait;
Req **waitlast;
int rp;
int wp;
char *msg[128];
};
extern void lbkick(Logbuf*);
extern void lbappend(Logbuf*, char*, ...);
extern void lbvappend(Logbuf*, char*, va_list);
/* #pragma varargck argpos lbappend 2 */
extern void lbread(Logbuf*, Req*);
extern void lbflush(Logbuf*, Req*);
/* #pragma varargck argpos flog 1 */
extern void rpclog(char*, ...);
extern void rpclogflush(Req*);
extern void rpclogread(Req*);
extern void rpclogwrite(Req*);
enum
{
STACKSIZE = 32768
};
extern int printerrors;

149
src/cmd/smugfs/cache.c Normal file
View file

@ -0,0 +1,149 @@
#include "a.h"
struct Cache
{
CEntry **hash;
int nhash;
CEntry *head;
CEntry *tail;
int nentry;
int maxentry;
int sizeofentry;
void (*cefree)(CEntry*);
};
static void
nop(CEntry *ce)
{
}
static uint
hash(const char *s)
{
uint h;
uchar *p;
h = 0;
for(p=(uchar*)s; *p; p++)
h = h*37 + *p;
return h;
}
Cache*
newcache(int sizeofentry, int maxentry, void (*cefree)(CEntry*))
{
Cache *c;
int i;
assert(sizeofentry >= sizeof(CEntry));
c = emalloc(sizeof *c);
c->sizeofentry = sizeofentry;
c->maxentry = maxentry;
c->nentry = 0;
for(i=1; i<maxentry; i<<=1)
;
c->nhash = i;
c->hash = emalloc(c->nhash * sizeof c->hash[0]);
if(cefree == nil)
cefree = nop;
c->cefree = cefree;
return c;
}
static void
popout(Cache *c, CEntry *e)
{
if(e->list.prev)
e->list.prev->list.next = e->list.next;
else
c->head = e->list.next;
if(e->list.next)
e->list.next->list.prev = e->list.prev;
else
c->tail = e->list.prev;
}
static void
insertfront(Cache *c, CEntry *e)
{
e->list.next = c->head;
c->head = e;
if(e->list.next)
e->list.next->list.prev = e;
else
c->tail = e;
}
static void
movetofront(Cache *c, CEntry *e)
{
popout(c, e);
insertfront(c, e);
}
static CEntry*
evict(Cache *c)
{
CEntry *e;
e = c->tail;
popout(c, e);
c->cefree(e);
free(e->name);
e->name = nil;
memset(e, 0, c->sizeofentry);
insertfront(c, e);
return e;
}
CEntry*
cachelookup(Cache *c, char *name, int create)
{
int h;
CEntry *e;
h = hash(name) % c->nhash;
for(e=c->hash[h]; e; e=e->hash.next){
if(strcmp(name, e->name) == 0){
movetofront(c, e);
return e;
}
}
if(!create)
return nil;
if(c->nentry >= c->maxentry)
e = evict(c);
else{
e = emalloc(c->sizeofentry);
insertfront(c, e);
c->nentry++;
}
e->name = estrdup(name);
h = hash(name) % c->nhash;
e->hash.next = c->hash[h];
c->hash[h] = e;
return e;
}
void
cacheflush(Cache *c, char *substr)
{
CEntry **l, *e;
int i;
for(i=0; i<c->nhash; i++){
for(l=&c->hash[i]; (e=*l); ){
if(substr == nil || strstr(e->name, substr)){
*l = e->hash.next;
c->nentry--;
popout(c, e);
c->cefree(e);
free(e->name);
free(e);
}else
l = &e->hash.next;
}
}
}

105
src/cmd/smugfs/download.c Normal file
View file

@ -0,0 +1,105 @@
#include "a.h"
typedef struct DEntry DEntry;
struct DEntry
{
CEntry ce;
HTTPHeader hdr;
char *tmpfile;
int fd;
};
static void
dfree(CEntry *ce)
{
DEntry *d;
d = (DEntry*)ce;
if(d->tmpfile){
remove(d->tmpfile);
free(d->tmpfile);
close(d->fd);
}
}
static Cache *downloadcache;
static char*
parseurl(char *url, char **path)
{
char *host, *p;
int len;
if(memcmp(url, "http://", 7) != 0)
return nil;
p = strchr(url+7, '/');
if(p == nil)
p = url+strlen(url);
len = p - (url+7);
host = emalloc(len+1);
memmove(host, url+7, len);
host[len] = 0;
if(*p == 0)
p = "/";
*path = p;
return host;
}
int
download(char *url, HTTPHeader *hdr)
{
DEntry *d;
char *host, *path;
char buf[] = "/var/tmp/smugfs.XXXXXX";
char *req;
int fd;
Fmt fmt;
if(downloadcache == nil)
downloadcache = newcache(sizeof(DEntry), 128, dfree);
host = parseurl(url, &path);
if(host == nil)
return -1;
d = (DEntry*)cachelookup(downloadcache, url, 1);
if(d->tmpfile){
free(host);
*hdr = d->hdr;
return dup(d->fd, -1);
}
d->fd = -1; // paranoia
if((fd = opentemp(buf, ORDWR|ORCLOSE)) < 0){
free(host);
return -1;
}
fmtstrinit(&fmt);
fmtprint(&fmt, "GET %s HTTP/1.0\r\n", path);
fmtprint(&fmt, "Host: %s\r\n", host);
fmtprint(&fmt, "User-Agent: " USER_AGENT "\r\n");
fmtprint(&fmt, "\r\n");
req = fmtstrflush(&fmt);
fprint(2, "Get %s\n", url);
if(httptofile(&http, host, req, hdr, fd) < 0){
free(host);
free(req);
return -1;
}
free(host);
free(req);
d->tmpfile = estrdup(buf);
d->fd = dup(fd, -1);
d->hdr = *hdr;
return fd;
}
void
downloadflush(char *substr)
{
if(downloadcache)
cacheflush(downloadcache, substr);
}

1853
src/cmd/smugfs/fs.c Normal file

File diff suppressed because it is too large Load diff

237
src/cmd/smugfs/http.c Normal file
View file

@ -0,0 +1,237 @@
#include "a.h"
static char*
haveheader(char *buf, int n)
{
int i;
for(i=0; i<n; i++){
if(buf[i] == '\n'){
if(i+2 < n && buf[i+1] == '\r' && buf[i+2] == '\n')
return buf+i+3;
if(i+1 < n && buf[i+1] == '\n')
return buf+i+2;
}
}
return 0;
}
static int
parseheader(char *buf, int n, HTTPHeader *hdr)
{
int nline;
char *data, *ebuf, *p, *q, *next;
memset(hdr, 0, sizeof *hdr);
ebuf = buf+n;
data = haveheader(buf, n);
if(data == nil)
return -1;
data[-1] = 0;
if(data[-2] == '\r')
data[-2] = 0;
if(chattyhttp > 1){
fprint(2, "--HTTP Response Header:\n");
fprint(2, "%s\n", buf);
fprint(2, "--\n");
}
nline = 0;
for(p=buf; *p; p=next, nline++){
q = strchr(p, '\n');
if(q){
next = q+1;
*q = 0;
if(q > p && q[-1] == '\r')
q[-1] = 0;
}else
next = p+strlen(p);
if(nline == 0){
if(memcmp(p, "HTTP/", 5) != 0){
werrstr("invalid HTTP version: %.10s", p);
return -1;
}
q = strchr(p, ' ');
if(q == nil){
werrstr("invalid HTTP version");
return -1;
}
*q++ = 0;
strncpy(hdr->proto, p, sizeof hdr->proto);
hdr->proto[sizeof hdr->proto-1] = 0;
while(*q == ' ')
q++;
if(*q < '0' || '9' < *q){
werrstr("invalid HTTP response code");
return -1;
}
p = q;
q = strchr(p, ' ');
if(q == nil)
q = p+strlen(p);
else
*q++ = 0;
hdr->code = strtol(p, &p, 10);
if(*p != 0)
return -1;
while(*q == ' ')
q++;
strncpy(hdr->codedesc, q, sizeof hdr->codedesc);
hdr->codedesc[sizeof hdr->codedesc-1] = 0;
continue;
}
q = strchr(p, ':');
if(q == nil)
continue;
*q++ = 0;
while(*q != 0 && (*q == ' ' || *q == '\t'))
q++;
if(cistrcmp(p, "Content-Type") == 0){
strncpy(hdr->contenttype, q, sizeof hdr->contenttype);
hdr->contenttype[sizeof hdr->contenttype-1] = 0;
continue;
}
if(cistrcmp(p, "Content-Length") == 0 && '0' <= *q && *q <= '9'){
hdr->contentlength = strtoll(q, 0, 10);
continue;
}
}
if(nline < 1){
werrstr("no header");
return -1;
}
memmove(buf, data, ebuf - data);
return ebuf - data;
}
static char*
genhttp(Protocol *proto, char *host, char *req, HTTPHeader *hdr, int wfd, int rfd, vlong rtotal)
{
int n, m, total, want;
char buf[8192], *data;
Pfd *fd;
if(chattyhttp > 1){
fprint(2, "--HTTP Request:\n");
fprint(2, "%s", req);
fprint(2, "--\n");
}
fd = proto->connect(host);
if(fd == nil){
if(chattyhttp > 0)
fprint(2, "connect %s: %r\n", host);
return nil;
}
n = strlen(req);
if(proto->write(fd, req, n) != n){
if(chattyhttp > 0)
fprint(2, "write %s: %r\n", host);
proto->close(fd);
return nil;
}
if(rfd >= 0){
while(rtotal > 0){
m = sizeof buf;
if(m > rtotal)
m = rtotal;
if((n = read(rfd, buf, m)) <= 0){
fprint(2, "read: missing data\n");
proto->close(fd);
return nil;
}
if(proto->write(fd, buf, n) != n){
fprint(2, "write data: %r\n");
proto->close(fd);
return nil;
}
rtotal -= n;
}
}
total = 0;
while(!haveheader(buf, total)){
n = proto->read(fd, buf+total, sizeof buf-total);
if(n <= 0){
if(chattyhttp > 0)
fprint(2, "read missing header\n");
proto->close(fd);
return nil;
}
total += n;
}
n = parseheader(buf, total, hdr);
if(n < 0){
fprint(2, "failed response parse: %r\n");
proto->close(fd);
return nil;
}
if(hdr->contentlength >= MaxResponse){
werrstr("response too long");
proto->close(fd);
return nil;
}
if(hdr->contentlength >= 0 && n > hdr->contentlength)
n = hdr->contentlength;
want = sizeof buf;
data = nil;
total = 0;
goto didread;
while(want > 0 && (n = proto->read(fd, buf, want)) > 0){
didread:
if(wfd >= 0){
if(writen(wfd, buf, n) < 0){
proto->close(fd);
werrstr("write error");
return nil;
}
}else{
data = erealloc(data, total+n);
memmove(data+total, buf, n);
}
total += n;
if(total > MaxResponse){
proto->close(fd);
werrstr("response too long");
return nil;
}
if(hdr->contentlength >= 0 && total + want > hdr->contentlength)
want = hdr->contentlength - total;
}
proto->close(fd);
if(hdr->contentlength >= 0 && total != hdr->contentlength){
werrstr("got wrong content size %d %d", total, hdr->contentlength);
return nil;
}
hdr->contentlength = total;
if(wfd >= 0)
return (void*)1;
else{
data = erealloc(data, total+1);
data[total] = 0;
}
return data;
}
char*
httpreq(Protocol *proto, char *host, char *req, HTTPHeader *hdr, int rfd, vlong rlength)
{
return genhttp(proto, host, req, hdr, -1, rfd, rlength);
}
int
httptofile(Protocol *proto, char *host, char *req, HTTPHeader *hdr, int fd)
{
if(fd < 0){
werrstr("bad fd");
return -1;
}
if(genhttp(proto, host, req, hdr, fd, -1, 0) == nil)
return -1;
return 0;
}

171
src/cmd/smugfs/icache.c Normal file
View file

@ -0,0 +1,171 @@
#include "a.h"
// This code is almost certainly wrong.
typedef struct Icache Icache;
struct Icache
{
char *url;
HTTPHeader hdr;
char *tmpfile;
int fd;
Icache *next;
Icache *prev;
Icache *hash;
};
enum {
NHASH = 128,
MAXCACHE = 128,
};
static struct {
Icache *hash[NHASH];
Icache *head;
Icache *tail;
int n;
} icache;
static Icache*
icachefind(char *url)
{
int h;
Icache *ic;
h = hash(url) % NHASH;
for(ic=icache.hash[h]; ic; ic=ic->hash){
if(strcmp(ic->url, url) == 0){
/* move to front */
if(ic->prev) {
ic->prev->next = ic->next;
if(ic->next)
ic->next->prev = ic->prev;
else
icache.tail = ic->prev;
ic->prev = nil;
ic->next = icache.head;
icache.head->prev = ic;
icache.head = ic;
}
return ic;
}
}
return nil;
}
static Icache*
icacheinsert(char *url, HTTPHeader *hdr, char *file, int fd)
{
int h;
Icache *ic, **l;
if(icache.n == MAXCACHE){
ic = icache.tail;
icache.tail = ic->prev;
if(ic->prev)
ic->prev->next = nil;
else
icache.head = ic->prev;
h = hash(ic->url) % NHASH;
for(l=&icache.hash[h]; *l; l=&(*l)->hash){
if(*l == ic){
*l = ic->hash;
goto removed;
}
}
sysfatal("cannot find ic in cache");
removed:
free(ic->url);
close(ic->fd);
remove(ic->file);
free(ic->file);
}else{
ic = emalloc(sizeof *ic);
icache.n++;
}
ic->url = estrdup(url);
ic->fd = dup(fd, -1);
ic->file = estrdup(file);
ic->hdr = *hdr;
h = hash(url) % NHASH;
ic->hash = icache.hash[h];
icache.hash[h] = ic;
ic->prev = nil;
ic->next = icache.head;
if(ic->next)
ic->next->prev = ic;
else
icache.tail = ic;
return ic;
}
void
icacheflush(char *substr)
{
Icache **l, *ic;
for(l=&icache.head; (ic=*l); ) {
if(substr == nil || strstr(ic->url, substr)) {
icache.n--;
*l = ic->next;
free(ic->url);
close(ic->fd);
remove(ic->file);
free(ic->file);
free(ic);
}else
l = &ic->next;
}
if(icache.head) {
icache.head->prev = nil;
for(ic=icache.head; ic; ic=ic->next){
if(ic->next)
ic->next->prev = ic;
else
icache.tail = ic;
}
}else
icache.tail = nil;
}
int
urlfetch(char *url, HTTPHeader hdr)
{
Icache *ic;
char buf[50], *host, *path, *p;
int fd, len;
ic = icachefind(url);
if(ic != nil){
*hdr = ic->hdr;
return dup(ic->fd, -1);
}
if(memcmp(url, "http://", 7) != 0){
werrstr("non-http url");
return -1;
}
p = strchr(url+7, '/');
if(p == nil)
p = url+strlen(url);
len = p - (url+7);
host = emalloc(len+1);
memmove(host, url+7, len);
host[len] = 0;
if(*p == 0)
p = "/";
strcpy(buf, "/var/tmp/smugfs.XXXXXX");
fd = opentemp(buf, ORDWR|ORCLOSE);
if(fd < 0)
return -1;
if(httptofile(http, host, req, &hdr, fd) < 0){
free(host);
return -1;
}
free(host);
icacheinsert(url, &hdr, buf, fd);
return fd;
}

555
src/cmd/smugfs/json.c Normal file
View file

@ -0,0 +1,555 @@
#include "a.h"
static Json *parsevalue(char**);
static char*
wskip(char *p)
{
while(*p == ' ' || *p == '\t' || *p == '\n' || *p == '\v')
p++;
return p;
}
static int
ishex(int c)
{
return '0' <= c && c <= '9' ||
'a' <= c && c <= 'f' ||
'A' <= c && c <= 'F';
}
static Json*
newjval(int type)
{
Json *v;
v = emalloc(sizeof *v);
v->ref = 1;
v->type = type;
return v;
}
static Json*
badjval(char **pp, char *fmt, ...)
{
char buf[ERRMAX];
va_list arg;
if(fmt){
va_start(arg, fmt);
vsnprint(buf, sizeof buf, fmt, arg);
va_end(arg);
errstr(buf, sizeof buf);
}
*pp = nil;
return nil;
}
static char*
_parsestring(char **pp, int *len)
{
char *p, *q, *w, *s, *r;
char buf[5];
Rune rune;
p = wskip(*pp);
if(*p != '"'){
badjval(pp, "missing opening quote for string");
return nil;
}
for(q=p+1; *q && *q != '\"'; q++){
if(*q == '\\' && *(q+1) != 0)
q++;
if((*q & 0xFF) < 0x20){ // no control chars
badjval(pp, "control char in string");
return nil;
}
}
if(*q == 0){
badjval(pp, "no closing quote in string");
return nil;
}
s = emalloc(q - p);
w = s;
for(r=p+1; r<q; ){
if(*r != '\\'){
*w++ = *r++;
continue;
}
r++;
switch(*r){
default:
free(s);
badjval(pp, "bad escape \\%c in string", *r&0xFF);
return nil;
case '\\':
case '\"':
case '/':
*w++ = *r++;
break;
case 'b':
*w++ = '\b';
r++;
break;
case 'f':
*w++ = '\f';
r++;
break;
case 'n':
*w++ = '\n';
r++;
break;
case 'r':
*w++ = '\r';
r++;
break;
case 't':
*w++ = '\t';
r++;
break;
case 'u':
r++;
if(!ishex(r[0]) || !ishex(r[1]) || !ishex(r[2]) || !ishex(r[3])){
free(s);
badjval(pp, "bad hex \\u%.4s", r);
return nil;
}
memmove(buf, r, 4);
buf[4] = 0;
rune = strtol(buf, 0, 16);
if(rune == 0){
free(s);
badjval(pp, "\\u0000 in string");
return nil;
}
r += 4;
w += runetochar(w, &rune);
break;
}
}
*w = 0;
if(len)
*len = w - s;
*pp = q+1;
return s;
}
static Json*
parsenumber(char **pp)
{
char *p, *q;
char *t;
double d;
Json *v;
/* -?(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))?([Ee][-+]?[0-9]+) */
p = wskip(*pp);
q = p;
if(*q == '-')
q++;
if(*q == '0')
q++;
else{
if(*q < '1' || *q > '9')
return badjval(pp, "invalid number");
while('0' <= *q && *q <= '9')
q++;
}
if(*q == '.'){
q++;
if(*q < '0' || *q > '9')
return badjval(pp, "invalid number");
while('0' <= *q && *q <= '9')
q++;
}
if(*q == 'e' || *q == 'E'){
q++;
if(*q == '-' || *q == '+')
q++;
if(*q < '0' || *q > '9')
return badjval(pp, "invalid number");
while('0' <= *q && *q <= '9')
q++;
}
t = emalloc(q-p+1);
memmove(t, p, q-p);
t[q-p] = 0;
errno = 0;
d = strtod(t, nil);
if(errno != 0){
free(t);
return badjval(pp, nil);
}
free(t);
v = newjval(Jnumber);
v->number = d;
*pp = q;
return v;
}
static Json*
parsestring(char **pp)
{
char *s;
Json *v;
int len;
s = _parsestring(pp, &len);
if(s == nil)
return nil;
v = newjval(Jstring);
v->string = s;
v->len = len;
return v;
}
static Json*
parsename(char **pp)
{
if(strncmp(*pp, "true", 4) == 0){
*pp += 4;
return newjval(Jtrue);
}
if(strncmp(*pp, "false", 5) == 0){
*pp += 5;
return newjval(Jfalse);
}
if(strncmp(*pp, "null", 4) == 0){
*pp += 4;
return newjval(Jtrue);
}
return badjval(pp, "invalid name");
}
static Json*
parsearray(char **pp)
{
char *p;
Json *v;
p = *pp;
if(*p++ != '[')
return badjval(pp, "missing bracket for array");
v = newjval(Jarray);
p = wskip(p);
if(*p != ']'){
for(;;){
if(v->len%32 == 0)
v->value = erealloc(v->value, (v->len+32)*sizeof v->value[0]);
if((v->value[v->len++] = parsevalue(&p)) == nil){
jclose(v);
return badjval(pp, nil);
}
p = wskip(p);
if(*p == ']')
break;
if(*p++ != ','){
jclose(v);
return badjval(pp, "missing comma in array");
}
}
}
p++;
*pp = p;
return v;
}
static Json*
parseobject(char **pp)
{
char *p;
Json *v;
p = *pp;
if(*p++ != '{')
return badjval(pp, "missing brace for object");
v = newjval(Jobject);
p = wskip(p);
if(*p != '}'){
for(;;){
if(v->len%32 == 0){
v->name = erealloc(v->name, (v->len+32)*sizeof v->name[0]);
v->value = erealloc(v->value, (v->len+32)*sizeof v->value[0]);
}
if((v->name[v->len++] = _parsestring(&p, nil)) == nil){
jclose(v);
return badjval(pp, nil);
}
p = wskip(p);
if(*p++ != ':'){
jclose(v);
return badjval(pp, "missing colon in object");
}
if((v->value[v->len-1] = parsevalue(&p)) == nil){
jclose(v);
return badjval(pp, nil);
}
p = wskip(p);
if(*p == '}')
break;
if(*p++ != ','){
jclose(v);
return badjval(pp, "missing comma in object");
}
}
}
p++;
*pp = p;
return v;
}
static Json*
parsevalue(char **pp)
{
*pp = wskip(*pp);
switch(**pp){
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
case '-':
return parsenumber(pp);
case 't':
case 'f':
case 'n':
return parsename(pp);
case '\"':
return parsestring(pp);
case '[':
return parsearray(pp);
case '{':
return parseobject(pp);
default:
return badjval(pp, "unexpected char <%02x>", **pp & 0xFF);
}
}
Json*
parsejson(char *text)
{
Json *v;
v = parsevalue(&text);
if(v && text && *wskip(text) != 0){
jclose(v);
werrstr("extra data in json");
return nil;
}
return v;
}
void
_printjval(Fmt *fmt, Json *v, int n)
{
int i;
if(v == nil){
fmtprint(fmt, "nil");
return;
}
switch(v->type){
case Jstring:
fmtprint(fmt, "\"%s\"", v->string);
break;
case Jnumber:
if(floor(v->number) == v->number)
fmtprint(fmt, "%.0f", v->number);
else
fmtprint(fmt, "%g", v->number);
break;
case Jobject:
fmtprint(fmt, "{");
if(n >= 0)
n++;
for(i=0; i<v->len; i++){
if(n > 0)
fmtprint(fmt, "\n%*s", n*4, "");
fmtprint(fmt, "\"%s\" : ", v->name[i]);
_printjval(fmt, v->value[i], n);
fmtprint(fmt, ",");
}
if(n > 0){
n--;
if(v->len > 0)
fmtprint(fmt, "\n%*s", n*4);
}
fmtprint(fmt, "}");
break;
case Jarray:
fmtprint(fmt, "[");
if(n >= 0)
n++;
for(i=0; i<v->len; i++){
if(n > 0)
fmtprint(fmt, "\n%*s", n*4, "");
_printjval(fmt, v->value[i], n);
fmtprint(fmt, ",");
}
if(n > 0){
n--;
if(v->len > 0)
fmtprint(fmt, "\n%*s", n*4);
}
fmtprint(fmt, "]");
break;
case Jtrue:
fmtprint(fmt, "true");
break;
case Jfalse:
fmtprint(fmt, "false");
break;
case Jnull:
fmtprint(fmt, "null");
break;
}
}
/*
void
printjval(Json *v)
{
Fmt fmt;
char buf[256];
fmtfdinit(&fmt, 1, buf, sizeof buf);
_printjval(&fmt, v, 0);
fmtprint(&fmt, "\n");
fmtfdflush(&fmt);
}
*/
int
jsonfmt(Fmt *fmt)
{
Json *v;
v = va_arg(fmt->args, Json*);
if(fmt->flags&FmtSharp)
_printjval(fmt, v, 0);
else
_printjval(fmt, v, -1);
return 0;
}
Json*
jincref(Json *v)
{
if(v == nil)
return nil;
++v->ref;
return v;
}
void
jclose(Json *v)
{
int i;
if(v == nil)
return;
if(--v->ref > 0)
return;
if(v->ref < 0)
sysfatal("jclose: ref %d", v->ref);
switch(v->type){
case Jstring:
free(v->string);
break;
case Jarray:
for(i=0; i<v->len; i++)
jclose(v->value[i]);
free(v->value);
break;
case Jobject:
for(i=0; i<v->len; i++){
free(v->name[i]);
jclose(v->value[i]);
}
free(v->value);
free(v->name);
break;
}
free(v);
}
Json*
jlookup(Json *v, char *name)
{
int i;
if(v->type != Jobject)
return nil;
for(i=0; i<v->len; i++)
if(strcmp(v->name[i], name) == 0)
return v->value[i];
return nil;
}
Json*
jwalk(Json *v, char *path)
{
char elem[128], *p, *next;
int n;
for(p=path; *p && v; p=next){
next = strchr(p, '/');
if(next == nil)
next = p+strlen(p);
if(next-p >= sizeof elem)
sysfatal("jwalk path elem too long - %s", path);
memmove(elem, p, next-p);
elem[next-p] = 0;
if(*next == '/')
next++;
if(v->type == Jarray && *elem && (n=strtol(elem, &p, 10)) >= 0 && *p == 0){
if(n >= v->len)
return nil;
v = v->value[n];
}else
v = jlookup(v, elem);
}
return v;
}
char*
jstring(Json *jv)
{
if(jv == nil || jv->type != Jstring)
return nil;
return jv->string;
}
vlong
jint(Json *jv)
{
if(jv == nil || jv->type != Jnumber)
return -1;
return jv->number;
}
double
jnumber(Json *jv)
{
if(jv == nil || jv->type != Jnumber)
return 0;
return jv->number;
}
int
jstrcmp(Json *jv, char *s)
{
char *t;
t = jstring(jv);
if(t == nil)
return -2;
return strcmp(t, s);
}

244
src/cmd/smugfs/jsonrpc.c Normal file
View file

@ -0,0 +1,244 @@
#include "a.h"
// JSON request/reply cache.
int chattyhttp;
typedef struct JEntry JEntry;
struct JEntry
{
CEntry ce;
Json *reply;
};
static Cache *jsoncache;
static void
jfree(CEntry *ce)
{
JEntry *j;
j = (JEntry*)ce;
jclose(j->reply);
}
static JEntry*
jcachelookup(char *request)
{
if(jsoncache == nil)
jsoncache = newcache(sizeof(JEntry), 1000, jfree);
return (JEntry*)cachelookup(jsoncache, request, 1);
}
void
jcacheflush(char *substr)
{
if(jsoncache == nil)
return;
cacheflush(jsoncache, substr);
}
// JSON RPC over HTTP
static char*
makehttprequest(char *host, char *path, char *postdata)
{
Fmt fmt;
fmtstrinit(&fmt);
fmtprint(&fmt, "POST %s HTTP/1.0\r\n", path);
fmtprint(&fmt, "Host: %s\r\n", host);
fmtprint(&fmt, "User-Agent: " USER_AGENT "\r\n");
fmtprint(&fmt, "Content-Type: application/x-www-form-urlencoded\r\n");
fmtprint(&fmt, "Content-Length: %d\r\n", strlen(postdata));
fmtprint(&fmt, "\r\n");
fmtprint(&fmt, "%s", postdata);
return fmtstrflush(&fmt);
}
static char*
makerequest(char *method, char *name1, va_list arg)
{
char *p, *key, *val;
Fmt fmt;
fmtstrinit(&fmt);
fmtprint(&fmt, "&");
p = name1;
while(p != nil){
key = p;
val = va_arg(arg, char*);
if(val == nil)
sysfatal("jsonrpc: nil value");
fmtprint(&fmt, "%U=%U&", key, val);
p = va_arg(arg, char*);
}
// TODO: These are SmugMug-specific, probably.
fmtprint(&fmt, "method=%s&", method);
if(sessid)
fmtprint(&fmt, "SessionID=%s&", sessid);
fmtprint(&fmt, "APIKey=%s", APIKEY);
return fmtstrflush(&fmt);
}
static char*
dojsonhttp(Protocol *proto, char *host, char *request, int rfd, vlong rlength)
{
char *data;
HTTPHeader hdr;
data = httpreq(proto, host, request, &hdr, rfd, rlength);
if(data == nil){
fprint(2, "httpreq: %r\n");
return nil;
}
if(strcmp(hdr.contenttype, "application/json") != 0 &&
(strcmp(hdr.contenttype, "text/html; charset=utf-8") != 0 || data[0] != '{')){ // upload.smugmug.com, sigh
werrstr("bad content type: %s", hdr.contenttype);
fprint(2, "Content-Type: %s\n", hdr.contenttype);
write(2, data, hdr.contentlength);
return nil;
}
if(hdr.contentlength == 0){
werrstr("no content");
return nil;
}
return data;
}
Json*
jsonrpc(Protocol *proto, char *host, char *path, char *method, char *name1, va_list arg, int usecache)
{
char *httpreq, *request, *reply;
JEntry *je;
Json *jv, *jstat, *jmsg;
request = makerequest(method, name1, arg);
je = nil;
if(usecache){
je = jcachelookup(request);
if(je->reply){
free(request);
return jincref(je->reply);
}
}
rpclog("%T %s", request);
httpreq = makehttprequest(host, path, request);
free(request);
if((reply = dojsonhttp(proto, host, httpreq, -1, 0)) == nil){
free(httpreq);
return nil;
}
free(httpreq);
jv = parsejson(reply);
free(reply);
if(jv == nil){
rpclog("%s: error parsing JSON reply: %r", method);
return nil;
}
if(jstrcmp((jstat = jlookup(jv, "stat")), "ok") == 0){
if(je)
je->reply = jincref(jv);
return jv;
}
if(jstrcmp(jstat, "fail") == 0){
jmsg = jlookup(jv, "message");
if(jmsg){
// If there are no images, that's not an error!
// (But SmugMug says it is.)
if(strcmp(method, "smugmug.images.get") == 0 &&
jstrcmp(jmsg, "empty set - no images found") == 0){
jclose(jv);
jv = parsejson("{\"stat\":\"ok\", \"Images\":[]}");
if(jv == nil)
sysfatal("parsejson: %r");
je->reply = jincref(jv);
return jv;
}
if(printerrors)
fprint(2, "%s: %J\n", method, jv);
rpclog("%s: %J", method, jmsg);
werrstr("%J", jmsg);
jclose(jv);
return nil;
}
rpclog("%s: json status: %J", method, jstat);
jclose(jv);
return nil;
}
rpclog("%s: json stat=%J", method, jstat);
jclose(jv);
return nil;
}
Json*
ncsmug(char *method, char *name1, ...)
{
Json *jv;
va_list arg;
va_start(arg, name1);
// TODO: Could use https only for login.
jv = jsonrpc(&https, HOST, PATH, method, name1, arg, 0);
va_end(arg);
rpclog("reply: %J", jv);
return jv;
}
Json*
smug(char *method, char *name1, ...)
{
Json *jv;
va_list arg;
va_start(arg, name1);
jv = jsonrpc(&http, HOST, PATH, method, name1, arg, 1);
va_end(arg);
return jv;
}
Json*
jsonupload(Protocol *proto, char *host, char *req, int rfd, vlong rlength)
{
Json *jv, *jstat, *jmsg;
char *reply;
if((reply = dojsonhttp(proto, host, req, rfd, rlength)) == nil)
return nil;
jv = parsejson(reply);
free(reply);
if(jv == nil){
fprint(2, "upload: error parsing JSON reply\n");
return nil;
}
if(jstrcmp((jstat = jlookup(jv, "stat")), "ok") == 0)
return jv;
if(jstrcmp(jstat, "fail") == 0){
jmsg = jlookup(jv, "message");
if(jmsg){
fprint(2, "upload: %J\n", jmsg);
werrstr("%J", jmsg);
jclose(jv);
return nil;
}
fprint(2, "upload: json status: %J\n", jstat);
jclose(jv);
return nil;
}
fprint(2, "upload: %J\n", jv);
jclose(jv);
return nil;
}

120
src/cmd/smugfs/log.c Normal file
View file

@ -0,0 +1,120 @@
#include "a.h"
void
lbkick(Logbuf *lb)
{
char *s;
int n;
Req *r;
while(lb->wait && lb->rp != lb->wp){
r = lb->wait;
lb->wait = r->aux;
if(lb->wait == nil)
lb->waitlast = &lb->wait;
r->aux = nil;
if(r->ifcall.count < 5){
respond(r, "log read request count too short");
continue;
}
s = lb->msg[lb->rp];
lb->msg[lb->rp] = nil;
if(++lb->rp == nelem(lb->msg))
lb->rp = 0;
n = r->ifcall.count;
if(n < strlen(s)+1+1){
memmove(r->ofcall.data, s, n-5);
n -= 5;
r->ofcall.data[n] = '\0';
/* look for first byte of UTF-8 sequence by skipping continuation bytes */
while(n>0 && (r->ofcall.data[--n]&0xC0)==0x80)
;
strcpy(r->ofcall.data+n, "...\n");
}else{
strcpy(r->ofcall.data, s);
strcat(r->ofcall.data, "\n");
}
r->ofcall.count = strlen(r->ofcall.data);
free(s);
respond(r, nil);
}
}
void
lbread(Logbuf *lb, Req *r)
{
if(lb->waitlast == nil)
lb->waitlast = &lb->wait;
*lb->waitlast = r;
lb->waitlast = (Req**)(void*)&r->aux;
r->aux = nil;
lbkick(lb);
}
void
lbflush(Logbuf *lb, Req *r)
{
Req **l;
for(l=&lb->wait; *l; l=(Req**)(void*)&(*l)->aux){
if(*l == r){
*l = r->aux;
r->aux = nil;
if(*l == nil)
lb->waitlast = l;
respond(r, "interrupted");
break;
}
}
}
void
lbappend(Logbuf *lb, char *fmt, ...)
{
va_list arg;
va_start(arg, fmt);
lbvappend(lb, fmt, arg);
va_end(arg);
}
void
lbvappend(Logbuf *lb, char *fmt, va_list arg)
{
char *s;
s = vsmprint(fmt, arg);
if(s == nil)
sysfatal("out of memory");
if(lb->msg[lb->wp])
free(lb->msg[lb->wp]);
lb->msg[lb->wp] = s;
if(++lb->wp == nelem(lb->msg))
lb->wp = 0;
lbkick(lb);
}
Logbuf rpclogbuf;
void
rpclogread(Req *r)
{
lbread(&rpclogbuf, r);
}
void
rpclogflush(Req *r)
{
lbflush(&rpclogbuf, r);
}
void
rpclog(char *fmt, ...)
{
va_list arg;
va_start(arg, fmt);
lbvappend(&rpclogbuf, fmt, arg);
va_end(arg);
}

108
src/cmd/smugfs/main.c Normal file
View file

@ -0,0 +1,108 @@
#include "a.h"
char *keypattern = "";
char *sessid;
Json *userinfo;
int printerrors;
void
usage(void)
{
fprint(2, "usage: smugfs [-k keypattern] [-m mtpt] [-s srv]\n");
threadexitsall("usage");
}
void
smuglogin(void)
{
Json *v;
char *s;
UserPasswd *up;
printerrors = 1;
up = auth_getuserpasswd(auth_getkey,
"proto=pass role=client server=smugmug.com "
"user? !password? %s", keypattern);
if(up == nil)
sysfatal("cannot get username/password: %r");
v = ncsmug("smugmug.login.withPassword",
"EmailAddress", up->user,
"Password", up->passwd,
nil);
if(v == nil)
sysfatal("login failed: %r");
memset(up->user, 'X', strlen(up->user));
memset(up->passwd, 'X', strlen(up->passwd));
free(up);
sessid = jstring(jwalk(v, "Login/Session/id"));
if(sessid == nil)
sysfatal("no session id");
sessid = estrdup(sessid);
s = jstring(jwalk(v, "Login/User/NickName"));
if(s == nil)
sysfatal("no nick name");
if(nickindex(s) != 0)
sysfatal("bad nick name");
userinfo = jincref(jwalk(v, "Login"));
jclose(v);
printerrors = 0;
}
void
threadmain(int argc, char **argv)
{
char *mtpt, *name;
mtpt = nil;
name = nil;
ARGBEGIN{
case 'D':
chatty9p++;
break;
case 'F':
chattyfuse++;
break;
case 'H':
chattyhttp++;
break;
case 'm':
mtpt = EARGF(usage());
break;
case 's':
name = EARGF(usage());
break;
case 'k':
keypattern = EARGF(usage());
break;
default:
usage();
}ARGEND
if(argc != 0)
usage();
if(name == nil && mtpt == nil)
mtpt = "/n/smug";
/*
* Check twice -- if there is an exited smugfs instance
* mounted there, the first access will fail but unmount it.
*/
if(mtpt && access(mtpt, AEXIST) < 0 && access(mtpt, AEXIST) < 0)
sysfatal("mountpoint %s does not exist", mtpt);
fmtinstall('H', encodefmt);
fmtinstall('[', encodefmt); // base-64
fmtinstall('J', jsonfmt);
fmtinstall('M', dirmodefmt);
fmtinstall('T', timefmt);
fmtinstall('U', urlencodefmt);
xinit();
smuglogin();
threadpostmountsrv(&xsrv, name, mtpt, 0);
threadexits(nil);
}

21
src/cmd/smugfs/mkfile Normal file
View file

@ -0,0 +1,21 @@
<$PLAN9/src/mkhdr
TARG=smugfs
HFILES=a.h
OFILES=\
cache.$O\
download.$O\
fs.$O\
http.$O\
json.$O\
jsonrpc.$O\
log.$O\
main.$O\
openssl.$O\
tcp.$O\
util.$O\
<$PLAN9/src/mkone

98
src/cmd/smugfs/openssl.c Normal file
View file

@ -0,0 +1,98 @@
#include <u.h>
#include <openssl/bio.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#include "a.h"
AUTOLIB(ssl)
static void
httpsinit(void)
{
ERR_load_crypto_strings();
ERR_load_SSL_strings();
SSL_load_error_strings();
SSL_library_init();
}
struct Pfd
{
BIO *sbio;
};
static Pfd*
opensslconnect(char *host)
{
Pfd *pfd;
BIO *sbio;
SSL_CTX *ctx;
SSL *ssl;
static int didinit;
char buf[1024];
if(!didinit){
httpsinit();
didinit = 1;
}
ctx = SSL_CTX_new(SSLv23_client_method());
sbio = BIO_new_ssl_connect(ctx);
BIO_get_ssl(sbio, &ssl);
SSL_set_mode(ssl, SSL_MODE_AUTO_RETRY);
snprint(buf, sizeof buf, "%s:https", host);
BIO_set_conn_hostname(sbio, buf);
if(BIO_do_connect(sbio) <= 0 || BIO_do_handshake(sbio) <= 0){
ERR_error_string_n(ERR_get_error(), buf, sizeof buf);
BIO_free_all(sbio);
werrstr("openssl: %s", buf);
return nil;
}
pfd = emalloc(sizeof *pfd);
pfd->sbio = sbio;
return pfd;
}
static void
opensslclose(Pfd *pfd)
{
if(pfd == nil)
return;
BIO_free_all(pfd->sbio);
free(pfd);
}
static int
opensslwrite(Pfd *pfd, void *v, int n)
{
int m, total;
char *p;
p = v;
total = 0;
while(total < n){
if((m = BIO_write(pfd->sbio, p+total, n-total)) <= 0){
if(total == 0)
return m;
return total;
}
total += m;
}
return total;
}
static int
opensslread(Pfd *pfd, void *v, int n)
{
return BIO_read(pfd->sbio, v, n);
}
Protocol https =
{
opensslconnect,
opensslread,
opensslwrite,
opensslclose
};

50
src/cmd/smugfs/tcp.c Normal file
View file

@ -0,0 +1,50 @@
#include "a.h"
struct Pfd
{
int fd;
};
static Pfd*
httpconnect(char *host)
{
char buf[1024];
Pfd *pfd;
int fd;
snprint(buf, sizeof buf, "tcp!%s!http", host);
if((fd = dial(buf, nil, nil, nil)) < 0)
return nil;
pfd = emalloc(sizeof *pfd);
pfd->fd = fd;
return pfd;
}
static void
httpclose(Pfd *pfd)
{
if(pfd == nil)
return;
close(pfd->fd);
free(pfd);
}
static int
httpwrite(Pfd *pfd, void *v, int n)
{
return writen(pfd->fd, v, n);
}
static int
httpread(Pfd *pfd, void *v, int n)
{
return read(pfd->fd, v, n);
}
Protocol http = {
httpconnect,
httpread,
httpwrite,
httpclose,
};

81
src/cmd/smugfs/util.c Normal file
View file

@ -0,0 +1,81 @@
#include "a.h"
void*
emalloc(int n)
{
void *v;
v = mallocz(n, 1);
if(v == nil)
sysfatal("out of memory");
return v;
}
void*
erealloc(void *v, int n)
{
v = realloc(v, n);
if(v == nil)
sysfatal("out of memory");
return v;
}
char*
estrdup(char *s)
{
s = strdup(s);
if(s == nil)
sysfatal("out of memory");
return s;
}
int
timefmt(Fmt *f)
{
Tm tm;
vlong ms;
ms = nsec()/1000000;
tm = *localtime(ms/1000);
fmtprint(f, "%02d:%02d:%02d.%03d",
tm.hour, tm.min, tm.sec,
(int)(ms%1000));
return 0;
}
int
writen(int fd, void *buf, int n)
{
long m, tot;
for(tot=0; tot<n; tot+=m){
m = n - tot;
if(m > 8192)
m = 8192;
if(write(fd, (uchar*)buf+tot, m) != m)
break;
}
return tot;
}
int
urlencodefmt(Fmt *fmt)
{
int x;
char *s;
s = va_arg(fmt->args, char*);
for(; *s; s++){
x = (uchar)*s;
if(x == ' ')
fmtrune(fmt, '+');
else if(('a' <= x && x <= 'z') || ('A' <= x && x <= 'Z') || ('0' <= x && x <= '9')
|| strchr("$-_.+!*'()", x)){
fmtrune(fmt, x);
}else
fmtprint(fmt, "%%%02ux", x);
}
return 0;
}