wplayer

Check-in [a8a14e254d]
Login

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Make "chore" its own repo
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | master | trunk
Files: files | file ages | folders
SHA3-256:a8a14e254d7711acd4511b3eb183284c2a988e852ff42040441dc7dcf8e837c2
User & Date: ajv-899-334-8894@vsta.org 2016-02-10 01:35:50
Context
2016-02-10
01:36
Use chore from its own repo check-in: c1891b637f user: ajv-899-334-8894@vsta.org tags: master, trunk
01:35
Make "chore" its own repo check-in: a8a14e254d user: ajv-899-334-8894@vsta.org tags: master, trunk
2016-02-06
19:02
Logins and cookies check-in: 26103392e4 user: ajv-899-334-8894@vsta.org tags: master, trunk
Changes
Hide Diffs Unified Diffs Ignore Whitespace Patch

Deleted chore/__init__.py.

1
import config, www, server, handlers
<


Deleted chore/config.py.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
#
# config.py
#	Handle loading of config file
#

# Configuration input instance
class Config(object):

    # Set up our fields
    def __init__(self):
	self.fields0 = set()	# Top-level config elems
	self.mults0 = set()	#  ...those which can appear multiple times
	self.subconfig = set()	#  ...those with indented config
	self.fields1 = set()	# Second-level config elems
	self.mults1 = set()	#  ...multiple times
	self.floats = set()	# Elems whose value is a float
	self.ints = set()	#  ...int's
	self.noarg = set()	# Things with no arguments
	self.onearg = set()	#  ...with a single arg

	# Basic config
	#
	# Administrative name of the service we're supplying
	self.fields0.add("service")
	self.onearg.add("service")

    # Accept input line, return line as lexed
    #  into words
    def lex(self, l):
	words = []
	curword = ""
	inquote = False
	while l:
	    # Pull next char from input line
	    c = l[0]
	    l = l[1:]

	    # Looking for beginning of next word
	    if not curword:
		# Whitespace between words
		if c.isspace():
		    continue

		# Beginning of quoted string
		if c == '"':
		    inquote = True
		else:
		    curword += c

	    # Quoted text
	    elif inquote:
		if c == '"':
		    inquote = False
		    words.append(curword)
		    curword = ""
		else:
		    curword += c

	    # End of current word
	    elif c.isspace():
		words.append(curword)
		curword = ""

	    # Another char in current word
	    else:
		curword += c

	# End of line without closing quote
	if inquote:
	    self.err("Unterminated quote")

	# Last word on line
	if curword:
	    words.append(curword)

	return(tuple(words))

    # Tell if this op takes one argument
    def oneargs(self, op):
	return (op in self.onearg) or \
	    (op in self.floats) or (op in self.ints)

    # Report err WRT file/line#
    def err(self, msg):
	raise Exception, "Error in %s at line %d: %s\n" % \
	    (self.cfg_file, self.lnum, msg)

    # Load config file into dict
    def load_cfg(self, cfg_file):

	# Configuration, as a dict
	self.cfg_file = cfg_file
	f = open(cfg_file, "r")
	res = {}
	for t in self.mults0:
	    res[t] = []
	curdict = None

	self.lnum = 0
	for l in f:
	    self.lnum += 1

	    # Note if it's an indented line (and thus annotation
	    #  applying to the current element)
	    indented = l[0].isspace()

	    # Comments & blank lines
	    l = l.strip()
	    if not l:
		continue
	    if l.startswith("#"):
		continue

	    # Basic format is:
	    #  keyword [args...]
	    words = self.lex(l)
	    op = words[0]
	    words = words[1:]

	    # Singleton?
	    if self.oneargs(op) and (len(words) != 1):
		self.err("expect single argument")

	    # Add annotation to this object
	    if indented:
		if curdict is None:
		    self.err("unexpected indented annotation")
		if op not in self.fields1:
		    self.err("unexpected sub-item")

		# Single datum is present directly, otherwise record
		#  the whole list
		if len(words) == 1:
		    val = words[0]
		else:
		    val = tuple(words)

		# Data interp?
		if op in self.ints:
		    val = int(val)
		elif op in self.floats:
		    val = float(val)

		# Multiples possible
		if op in self.mults1:
		    if op in curdict:
			curdict[op].append( val )
		    else:
			curdict[op] = [val]

		# Single config item
		elif op in curdict:
		    self.err("already configured")

		else:
		    curdict[op] = val
		
		continue

	    #
	    # New object, top level
	    #
	    if op not in self.fields0:
		self.err("Unknown configuration type")
	    curdict = None

	    # Each top-level config entry can have a dictionary
	    #  of sub-config
	    if op in self.subconfig:
		curdict = {}

		# Config elements which permit multiples
		if op not in self.mults0:
		    if op in res:
			self.err("already configured")
		    res[op] = curdict
		else:
		    if op in self.noarg:
			# Each config entry is just the sub-config dict
			res[op].append( curdict )

		    # One or more args, ( arg0, ..., curdict{} )
		    elif op in self.onearg:
			if len(words) != 1:
			    self.err("Need one argument")
		    res[op].append( tuple(words) + (curdict,) )

	    # Floating point arguments
	    elif op in self.floats:
		try:
		    res[op] = float(words[0])
		except:
		    self.err("bad floating point value")

	    # Integer arguments
	    elif op in self.ints:
		if not words[0].isdigit():
		    self.err("bad integer")
		res[op] = int(words[0])

	    # Simple strings or lists thereof
	    else:
		res[op] = words[0] if (len(words) == 1) else tuple(words)

	# Here's our finished dict of config stuff
	f.close()
	return res
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<






























































































































































































































































































































































































































Deleted chore/css/README.md.

1
2
These are CSS files which provide a useful baseline of
configuration for typical web page elements.
<
<




Deleted chore/css/main.css.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
/*! HTML5 Boilerplate v5.2.0 | MIT License | https://html5boilerplate.com/ */

/*
 * What follows is the result of much research on cross-browser styling.
 * Credit left inline and big thanks to Nicolas Gallagher, Jonathan Neal,
 * Kroc Camen, and the H5BP dev community and team.
 */

/* ==========================================================================
   Base styles: opinionated defaults
   ========================================================================== */

html {
    color: #222;
    font-size: 1em;
    line-height: 1.4;
}

/*
 * Remove text-shadow in selection highlight:
 * https://twitter.com/miketaylr/status/12228805301
 *
 * These selection rule sets have to be separate.
 * Customize the background color to match your design.
 */

::-moz-selection {
    background: #b3d4fc;
    text-shadow: none;
}

::selection {
    background: #b3d4fc;
    text-shadow: none;
}

/*
 * A better looking default horizontal rule
 */

hr {
    display: block;
    height: 1px;
    border: 0;
    border-top: 1px solid #ccc;
    margin: 1em 0;
    padding: 0;
}

/*
 * Remove the gap between audio, canvas, iframes,
 * images, videos and the bottom of their containers:
 * https://github.com/h5bp/html5-boilerplate/issues/440
 */

audio,
canvas,
iframe,
img,
svg,
video {
    vertical-align: middle;
}

/*
 * Remove default fieldset styles.
 */

fieldset {
    border: 0;
    margin: 0;
    padding: 0;
}

/*
 * Allow only vertical resizing of textareas.
 */

textarea {
    resize: vertical;
}

/* ==========================================================================
   Browser Upgrade Prompt
   ========================================================================== */

.browserupgrade {
    margin: 0.2em 0;
    background: #ccc;
    color: #000;
    padding: 0.2em 0;
}

/* ==========================================================================
   Author's custom styles
   ========================================================================== */

















/* ==========================================================================
   Helper classes
   ========================================================================== */

/*
 * Hide visually and from screen readers:
 */

.hidden {
    display: none !important;
}

/*
 * Hide only visually, but have it available for screen readers:
 * http://snook.ca/archives/html_and_css/hiding-content-for-accessibility
 */

.visuallyhidden {
    border: 0;
    clip: rect(0 0 0 0);
    height: 1px;
    margin: -1px;
    overflow: hidden;
    padding: 0;
    position: absolute;
    width: 1px;
}

/*
 * Extends the .visuallyhidden class to allow the element
 * to be focusable when navigated to via the keyboard:
 * https://www.drupal.org/node/897638
 */

.visuallyhidden.focusable:active,
.visuallyhidden.focusable:focus {
    clip: auto;
    height: auto;
    margin: 0;
    overflow: visible;
    position: static;
    width: auto;
}

/*
 * Hide visually and from screen readers, but maintain layout
 */

.invisible {
    visibility: hidden;
}

/*
 * Clearfix: contain floats
 *
 * For modern browsers
 * 1. The space content is one way to avoid an Opera bug when the
 *    `contenteditable` attribute is included anywhere else in the document.
 *    Otherwise it causes space to appear at the top and bottom of elements
 *    that receive the `clearfix` class.
 * 2. The use of `table` rather than `block` is only necessary if using
 *    `:before` to contain the top-margins of child elements.
 */

.clearfix:before,
.clearfix:after {
    content: " "; /* 1 */
    display: table; /* 2 */
}

.clearfix:after {
    clear: both;
}

/* ==========================================================================
   EXAMPLE Media Queries for Responsive Design.
   These examples override the primary ('mobile first') styles.
   Modify as content requires.
   ========================================================================== */

@media only screen and (min-width: 35em) {
    /* Style adjustments for viewports that meet the condition */
}

@media print,
       (-webkit-min-device-pixel-ratio: 1.25),
       (min-resolution: 1.25dppx),
       (min-resolution: 120dpi) {
    /* Style adjustments for high resolution devices */
}

/* ==========================================================================
   Print styles.
   Inlined to avoid the additional HTTP request:
   http://www.phpied.com/delay-loading-your-print-css/
   ========================================================================== */

@media print {
    *,
    *:before,
    *:after {
        background: transparent !important;
        color: #000 !important; /* Black prints faster:
                                   http://www.sanbeiji.com/archives/953 */
        box-shadow: none !important;
        text-shadow: none !important;
    }

    a,
    a:visited {
        text-decoration: underline;
    }

    a[href]:after {
        content: " (" attr(href) ")";
    }

    abbr[title]:after {
        content: " (" attr(title) ")";
    }

    /*
     * Don't show links that are fragment identifiers,
     * or use the `javascript:` pseudo protocol
     */

    a[href^="#"]:after,
    a[href^="javascript:"]:after {
        content: "";
    }

    pre,
    blockquote {
        border: 1px solid #999;
        page-break-inside: avoid;
    }

    /*
     * Printing Tables:
     * http://css-discuss.incutio.com/wiki/Printing_Tables
     */

    thead {
        display: table-header-group;
    }

    tr,
    img {
        page-break-inside: avoid;
    }

    img {
        max-width: 100% !important;
    }

    p,
    h2,
    h3 {
        orphans: 3;
        widows: 3;
    }

    h2,
    h3 {
        page-break-after: avoid;
    }
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
















































































































































































































































































































































































































































































































































































Deleted chore/css/normalize.css.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */

/**
 * 1. Set default font family to sans-serif.
 * 2. Prevent iOS and IE text size adjust after device orientation change,
 *    without disabling user zoom.
 */

html {
  font-family: sans-serif; /* 1 */
  -ms-text-size-adjust: 100%; /* 2 */
  -webkit-text-size-adjust: 100%; /* 2 */
}

/**
 * Remove default margin.
 */

body {
  margin: 0;
}

/* HTML5 display definitions
   ========================================================================== */

/**
 * Correct `block` display not defined for any HTML5 element in IE 8/9.
 * Correct `block` display not defined for `details` or `summary` in IE 10/11
 * and Firefox.
 * Correct `block` display not defined for `main` in IE 11.
 */

article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
main,
menu,
nav,
section,
summary {
  display: block;
}

/**
 * 1. Correct `inline-block` display not defined in IE 8/9.
 * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
 */

audio,
canvas,
progress,
video {
  display: inline-block; /* 1 */
  vertical-align: baseline; /* 2 */
}

/**
 * Prevent modern browsers from displaying `audio` without controls.
 * Remove excess height in iOS 5 devices.
 */

audio:not([controls]) {
  display: none;
  height: 0;
}

/**
 * Address `[hidden]` styling not present in IE 8/9/10.
 * Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22.
 */

[hidden],
template {
  display: none;
}

/* Links
   ========================================================================== */

/**
 * Remove the gray background color from active links in IE 10.
 */

a {
  background-color: transparent;
}

/**
 * Improve readability of focused elements when they are also in an
 * active/hover state.
 */

a:active,
a:hover {
  outline: 0;
}

/* Text-level semantics
   ========================================================================== */

/**
 * Address styling not present in IE 8/9/10/11, Safari, and Chrome.
 */

abbr[title] {
  border-bottom: 1px dotted;
}

/**
 * Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
 */

b,
strong {
  font-weight: bold;
}

/**
 * Address styling not present in Safari and Chrome.
 */

dfn {
  font-style: italic;
}

/**
 * Address variable `h1` font-size and margin within `section` and `article`
 * contexts in Firefox 4+, Safari, and Chrome.
 */

h1 {
  font-size: 2em;
  margin: 0.67em 0;
}

/**
 * Address styling not present in IE 8/9.
 */

mark {
  background: #ff0;
  color: #000;
}

/**
 * Address inconsistent and variable font size in all browsers.
 */

small {
  font-size: 80%;
}

/**
 * Prevent `sub` and `sup` affecting `line-height` in all browsers.
 */

sub,
sup {
  font-size: 75%;
  line-height: 0;
  position: relative;
  vertical-align: baseline;
}

sup {
  top: -0.5em;
}

sub {
  bottom: -0.25em;
}

/* Embedded content
   ========================================================================== */

/**
 * Remove border when inside `a` element in IE 8/9/10.
 */

img {
  border: 0;
}

/**
 * Correct overflow not hidden in IE 9/10/11.
 */

svg:not(:root) {
  overflow: hidden;
}

/* Grouping content
   ========================================================================== */

/**
 * Address margin not present in IE 8/9 and Safari.
 */

figure {
  margin: 1em 40px;
}

/**
 * Address differences between Firefox and other browsers.
 */

hr {
  box-sizing: content-box;
  height: 0;
}

/**
 * Contain overflow in all browsers.
 */

pre {
  overflow: auto;
}

/**
 * Address odd `em`-unit font size rendering in all browsers.
 */

code,
kbd,
pre,
samp {
  font-family: monospace, monospace;
  font-size: 1em;
}

/* Forms
   ========================================================================== */

/**
 * Known limitation: by default, Chrome and Safari on OS X allow very limited
 * styling of `select`, unless a `border` property is set.
 */

/**
 * 1. Correct color not being inherited.
 *    Known issue: affects color of disabled elements.
 * 2. Correct font properties not being inherited.
 * 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
 */

button,
input,
optgroup,
select,
textarea {
  color: inherit; /* 1 */
  font: inherit; /* 2 */
  margin: 0; /* 3 */
}

/**
 * Address `overflow` set to `hidden` in IE 8/9/10/11.
 */

button {
  overflow: visible;
}

/**
 * Address inconsistent `text-transform` inheritance for `button` and `select`.
 * All other form control elements do not inherit `text-transform` values.
 * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
 * Correct `select` style inheritance in Firefox.
 */

button,
select {
  text-transform: none;
}

/**
 * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
 *    and `video` controls.
 * 2. Correct inability to style clickable `input` types in iOS.
 * 3. Improve usability and consistency of cursor style between image-type
 *    `input` and others.
 */

button,
html input[type="button"], /* 1 */
input[type="reset"],
input[type="submit"] {
  -webkit-appearance: button; /* 2 */
  cursor: pointer; /* 3 */
}

/**
 * Re-set default cursor for disabled elements.
 */

button[disabled],
html input[disabled] {
  cursor: default;
}

/**
 * Remove inner padding and border in Firefox 4+.
 */

button::-moz-focus-inner,
input::-moz-focus-inner {
  border: 0;
  padding: 0;
}

/**
 * Address Firefox 4+ setting `line-height` on `input` using `!important` in
 * the UA stylesheet.
 */

input {
  line-height: normal;
}

/**
 * It's recommended that you don't attempt to style these elements.
 * Firefox's implementation doesn't respect box-sizing, padding, or width.
 *
 * 1. Address box sizing set to `content-box` in IE 8/9/10.
 * 2. Remove excess padding in IE 8/9/10.
 */

input[type="checkbox"],
input[type="radio"] {
  box-sizing: border-box; /* 1 */
  padding: 0; /* 2 */
}

/**
 * Fix the cursor style for Chrome's increment/decrement buttons. For certain
 * `font-size` values of the `input`, it causes the cursor style of the
 * decrement button to change from `default` to `text`.
 */

input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
  height: auto;
}

/**
 * 1. Address `appearance` set to `searchfield` in Safari and Chrome.
 * 2. Address `box-sizing` set to `border-box` in Safari and Chrome.
 */

input[type="search"] {
  -webkit-appearance: textfield; /* 1 */
  box-sizing: content-box; /* 2 */
}

/**
 * Remove inner padding and search cancel button in Safari and Chrome on OS X.
 * Safari (but not Chrome) clips the cancel button when the search input has
 * padding (and `textfield` appearance).
 */

input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration {
  -webkit-appearance: none;
}

/**
 * Define consistent border, margin, and padding.
 */

fieldset {
  border: 1px solid #c0c0c0;
  margin: 0 2px;
  padding: 0.35em 0.625em 0.75em;
}

/**
 * 1. Correct `color` not being inherited in IE 8/9/10/11.
 * 2. Remove padding so people aren't caught out if they zero out fieldsets.
 */

legend {
  border: 0; /* 1 */
  padding: 0; /* 2 */
}

/**
 * Remove default vertical scrollbar in IE 8/9/10/11.
 */

textarea {
  overflow: auto;
}

/**
 * Don't inherit the `font-weight` (applied by a rule above).
 * NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
 */

optgroup {
  font-weight: bold;
}

/* Tables
   ========================================================================== */

/**
 * Remove most spacing between table cells.
 */

table {
  border-collapse: collapse;
  border-spacing: 0;
}

td,
th {
  padding: 0;
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
















































































































































































































































































































































































































































































































































































































































































































































































































































































Deleted chore/handlers.py.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
#
# www.py
#	Handling for actual web server bits
#
import pdb
import base64, socket, sys, json, os, time
from BaseHTTPServer import BaseHTTPRequestHandler
from sendfile import sendfile

# Re-login every 30 days
LoginInterval = 60*60*24*30

# Return value for a digit (hex)
def digval(c):
    if c.isdigit():
	return ord(c) - ord('0')
    c = c.lower()
    if (c < 'a') or (c > 'f'):
	return None
    return (ord(c) - ord('a')) + 10

# Remove %XX hex char values from form input string
# I just cannot effin believe this isn't somewhere already...
def unescape(s):
    res = ""
    c1 = c2 = idx = None
    for c in s:
	if idx is None:
	    if c != '%':
		res += c
	    else:
		idx = 1
	    continue
	if idx == 1:
	    c1 = digval(c)
	    idx = 2
	    continue
	assert idx == 2
	idx = None
	if c1 is None:
	    res += "?"
	else:
	    c2 = digval(c)
	    if c2 is None:
		res += "?"
	    else:
		res += chr((c1 << 4) + c2)
	c1 = c2 = None
    return res

# An instance of this runs to dispatch a given HTTP request
#
# An actual app will inherit this and add app-specific dispatchers.
#
# self.server gets set to our web server's state
class Chore_Handler(BaseHTTPRequestHandler):

    # Default config
    def __init__(self, conn, addr, webserver, inits):

	# /js/..., /imgs/..., and such just serve files
	self.lits = ("js", "imgs", "css", "html")

	# Code which, in turn, tries to dispatch ops
	# (Each is (op, fn), e.g., ("GET", self.do_get1))
	self.dispatchers = [ ("GET", self.base_get), ]

	# Add on others (mixins)
	for i in inits:
	    i(self)

	# Default title
	self.title = "web server"

	# These get dicts if there are options
	self.vals = self.rvals = None

	# Hook for custom headers
	self.extra_headers = []

	# This both init's, and runs the web service
	# (BTW, this sucks.  Break out instance creation and
	#  service start--always.)
	BaseHTTPRequestHandler.__init__(self, conn, addr, webserver)

    # Hook to set up SSL
    def setup(self):
	self.connection = self.request
	self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
        self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)

    # Find & dispatch handler
    def dispatch(self, op, *args):

	# Always decode <path>?<opts>
	self.options()

	# Find handler
	for tup in self.dispatchers:
	    if tup[0] == op:
		dispatched,res = tup[1](*args)
		if dispatched:
		    return res

	# Nobody could make heads or tails of it
	self.send_error(404)
	return None

    # HTTP GET operation
    def do_GET(self):
	sys.stderr.write("GET: %s\n" % (self.path,))
	self.base_op = "GET"
	buf = self.dispatch("GET")
	if buf:
	    self.wfile.write(buf)

    # HTTP HEAD; header of a GET result, no body
    def do_HEAD(self):
	sys.stderr.write("HEAD: %s\n" % (self.path,))
	self.base_op = "HEAD"
	buf = self.dispatch("GET")

    # A POST; dispatch our various form inputs
    def do_POST(self):
	sys.stderr.write("POST: %s\n" % (self.path,))
	self.base_op = "POST"
	content_len = int(self.headers.getheader('content-length', 0))
	dbuf = self.rfile.read(content_len)
	buf = self.dispatch("POST", dbuf)
	if buf:
	    self.wfile.write(buf)

    # Standard header for web pages
    def build_header(self):
        buf = '<html><head><title>%s</title>' % (self.title,)
        buf += ' <meta name="viewport"' \
            ' content="width=device-width, initial-scale=1">\n'
        buf += ' <link rel="stylesheet"' \
            ' href="/css/normalize.css">\n'
        buf += ' <link rel="stylesheet"' \
            ' href="/css/main.css">\n'
        buf += '</head><body>\n'
	return buf

    # Tack on end of HTML
    def build_tailer(self, buf):
	buf += "</body></html>\n"
	return buf

    # Map file extension to MIME type
    # Returns (isbin?, mime-type) or None
    def get_mtype(self, fn):
	if fn.endswith(".js"):
	    return False, "application/javascript"
	if fn.endswith(".jpg"):
	    return True,"image/jpeg"
	if fn.endswith(".png"):
	    return True,"image/png"
	if fn.endswith(".svg"):
	    return True,"image/svg+xml"
	if fn.endswith(".gif"):
	    return True,"image/gif"
	if fn.endswith(".ico"):
	    return True,"image/vnd.microsoft.icon"
	if fn.endswith(".wav"):
	    return True,"audio/x-wav"
	if fn.endswith(".mp3"):
	    return True,"audio/mpeg"
	if fn.endswith(".flac"):
	    return True,"audio/flac"
	if fn.endswith(".ogg"):
	    return True,"audio/ogg"
	if fn.endswith(".html"):
	    return False,"text/html"
	if fn.endswith(".htm"):
	    return False,"text/html"
	if fn.endswith(".txt"):
	    return False,"text/plain"
	if fn.endswith(".json"):
	    return False,"text/plain"
	if fn.endswith(".css"):
	    return False,"text/css"

	# Unknown file type
	return None

    # Decode a "range:" header option, return
    #  (offset,length) or None if we don't like
    #  the region (TBD, multiple ranges and
    #  multipart)
    # We're passed the file's os.stat as well as
    #  the range: field value.
    def decode_range(self, st, range):
	# Byte units, please
	if not range.startswith("bytes="):
	    return None
	range = range[6:]

	# Single range
	if ',' in range:
	    return None

	# Start to offset
	if range[0] == '-':
	    range = range[1:]
	    if not range.isdigit():
		return None
	    val1 = int(range)
	    if val1 >= st.st_size:
		return None
	    return (0, val1)

	# Offset to end...
	elif range[-1] == '-':
	    range = range[:-1]
	    if not range.isdigit():
		return None
	    val2 = int(range)
	    if val2 >= st.st_size:
		return None
	    return (val2, st.st_size - val2)

	# Offset1 to offset2
	else:
	    parts = range.split('-')
	    if len(parts) != 2:
		return None
	    if not all(p.isdigit() for p in parts):
		return None
	    val1 = int(parts[0])
	    val2 = int(parts[1])
	    if val1 >= val2:
		return None
	    return (val1, val2-val1)

    # Try to serve the named file
    def send_files(self, fn):
	# Hanky-panky?
	if ".." in fn:
	    self.send_error(404)
	    return None

	# Make sure we know its MIME type
	tup = self.get_mtype(fn)
	if tup is None:
	    # Unknown file type
	    self.send_error(404)
	    return None
	isbin,mtyp = tup

	try:
	    f = open(fn, "r")
	except:
	    self.send_error(404)
	    return None

	# Get dope on file overall
	st = os.fstat(f.fileno())
	startoff = 0
	nbyte = st.st_size
	ranged = False

	# Sub-ranged output
	if 'range' in self.headers:
	    tup = self.decode_range(st, self.headers['range'])
	    if tup is None:
		# Bad range
		self.send_error(416)
		return None
	    ranged = True
	    startoff,nbyte = tup
	    print "range:", startoff, "for", nbyte
	else:
	    startoff = 0
	    nbyte = st.st_size

	# For media files, use sendfile() rather than passing
	#  it all through this process.
	# We also support ranges here.
	if isbin:

	    # Ranged or normal response
	    if ranged:
		self.send_response(206)
	    else:
		buf = f.read()
		self.send_response(200)
	    self.send_header("Content-type", mtyp)
	    self.send_header("Content-Length", nbyte)
	    if ranged:
		self.send_header("Content-Range",
		"bytes %d-%d/%d" % (startoff, startoff+nbyte-1, nbyte))
	    self.send_header("Last-Modified",
		time.asctime(time.localtime(st.st_mtime)))
	    self.end_headers()

	    # Don't push out body if they're just asking us about
	    #  the file's size via HEAD
	    if self.base_op == "GET":
		sendfile(self.wfile.fileno(), f.fileno(), startoff, nbyte)

	    # We've pushed it, tell the upper layers
	    buf = None

	# Text, just shuffle bytes around as a whole
	# TBD are gigabyte text files... an encyclopedia, anybody?
	# But don't forget the DOS-style line endings; can't just
	#  use sendfile() if you honor that.
	else:
	    buf = f.read()
	    buf = self.send_result(buf, mtyp)

	# Done with the file
	f.close()

	# Return contents (or None if we already pushed it out)
	return buf

    # Common code to strip and decode options
    # Also bursts the path to self.paths[]
    def options(self):
	p = self.path

	# Options?
	if "?" in p:
	    idx = p.rindex("?")
	    if self.parseKV(p[idx+1:]) is None:
		return None
	    self.path = p = p[:idx]

	# Burst path
	self.paths = pp = p.strip("/").split("/")


    # Basic GET functions
    def base_get(self):

	# Top level
	pp = self.paths
	if (not pp) or ((len(pp) == 1) and not pp[0]):
	    return True,self.send_top()

	# Service file content
	if pp[0] in self.lits:
	    fname = os.path.join(*pp)
	    return True,self.send_files(fname)

	# Couldn't help
	return False,None

    # Send a 401 response, indicating authentication issues
    def send401(self, msg):
	approot = self.server.approot

	appname = approot.config.get("service",
	    "Web Application Interface")
	self.send_response(401)
	self.send_header('WWW-Authenticate',
	    'Basic realm="%s"' % (appname,))
	self.send_header('Content-type', 'text/html')
	self.end_headers()
	self.wfile.write(msg)
	return False

    # Decode Cookie: header option
    def get_cookie(self, target):
	# Header present?
	pdb.set_trace()
	cookies = self.headers.get("cookie")
	if not cookies:
	    return None

	# Parse into cookies; each is "key=value", with
	#  semicolon separating them (if more than one)
	cookies = [c.strip().split('=') for c in cookies.split(';')]
	for tup in cookies:
	    if len(tup) != 2:
		continue

	    # Here's a match
	    if tup[0] == target:
		return tup[1]

	# Don't have that cookie
	return None

    # Each token is saved in var/cookies as a filename
    def valid_token(self, token):
	if ".." in token:
	    # No effin way
	    return False
	if not os.access("var/cookies/" + token, os.R_OK):
	    return False
	return True

    # Look up the username, see if it's the right password
    # Return True if they are OK, else False
    def authenticate(self, user, pw):
	webserver = self.server
	accts = webserver.config["accounts"]

	f = open(accts, "r")
	for l in f:
	    l = l.strip()
	    if (not l) or l.startswith("#"):
		continue

	    # <user> <pass>
	    tup = l.split()
	    if len(tup) != 2:
		continue

	    # For this user, do you know the password?
	    if tup[0] == user:
		f.close()
		return tup[1] == pw

	f.close()
	return False

    # We need a new cookie
    # Fills in to server.cookies[]
    def new_cookie(self):
	webserver = self.server

	# Generate a random cookie, store as an active cookie
	cookie = base64.b64encode(os.urandom(36))
	webserver.cookies.add(cookie)

	# Expires some day
	expires = time.time() + LoginInterval
	tm = time.strftime("%a, %d-%b-%Y %T GMT", time.gmtime(expires))

	# Set it on the user's browser
	self.extra_headers.append(
	 ("Set-Cookie", "loginToken=%s; Expires=%s" % (cookie, tm)) )

	# And save it across server restarts
	try:
	    # Just touch it
	    f = open("var/cookies/" + cookie, "w")
	    f.close()
	except:
	    # Oh, well.  It's good for the session.
	    pass

    # Intercept after request is parsed; apply HTTP authentication
    def parse_request(self):

	# Make sure it's well-formed
	res = BaseHTTPRequestHandler.parse_request(self)
	if not res:
	    return False

	# No configured authentication, let them proceed
	webserver = self.server
	if webserver.authentication is None:
	    return True

	# See if they can satisfy us with an authentication cookie
	cookie = self.get_cookie("loginToken")
	if cookie is not None:
	    # Already verified
	    if cookie in webserver.cookies:
		return True

	    # Look up in filesystem
	    if self.valid_token(cookie):
		self.cookie = cookie
		return True

	#
	# It's either absent, or invalid, or expired.
	#

	# We're going to need a user/pass
	auth = self.headers.getheader('Authorization')
	if auth is None:
	    return self.send401("Authentication required\r\n")

	# Do we accept it?
	auth = base64.decodestring(auth[6:])
	tup = auth.split(":")
	if len(tup) != 2:
	    return self.send401("Malformed authentication response\r\n")
	if not self.authenticate(tup[0], tup[1]):
	    return self.send401("Incorrect username or password\r\n")

	# Generate new cookie, record
	self.new_cookie()

	# Welcome on
	return True

    # Send header
    # Also canonicalize to DOS-style line endings (ew)
    # This code only handles textual responses; binary/large
    #  media is handled inline.
    def send_result(self, buf, mtyp, cacheable=False):

	# Rewrite to DOS line endings
	buf = buf.replace("\n", "\r\n")

	# Send response
	self.send_response(200)
        self.send_header("Content-type", mtyp)
        self.send_header("Content-Length", len(buf))
	if not cacheable:
	    self.send_header("Cache-Control", "NoCache")
	else:
	    self.send_header("Last-Modified",
		time.asctime(time.localtime(self.changed)))

	# Add on any extras we've calculated
	if self.extra_headers:
	    for tag,val in self.extra_headers:
		self.send_header(tag, val)
	    del self.extra_headers[:]

        self.end_headers()

	return buf

    # Generate a meta REFRESH body, send it out
    #  to the client
    def send_redir(self, url, msg=None):
	if msg is None:
	    timeout = 0
	    msg = "Refreshing..."
	else:
	    # If we have something to say, give'em 5 seconds
	    #  to admire its wisdom.
	    timeout = 5
        buf = \
"""
<html>
<head>
<title>%s</title>
<meta http-equiv="REFRESH"
content="%d;url=%s" />
</head>
<body>
%s
</body>
</html>
""" % (self.title, timeout, url, msg)

	buf = self.send_result(buf, "text/html")
	return buf

    # In <url>[?key[=val][&key[=val...]]], parse key[/val] and
    #  put into self.vals{} and self.rvals{}
    # Returns the input @buf, or None if there was a problem
    def parseKV(self, buf):
	# Walk each key/val, in the format "k[=v]"
	# vals{} assembles vals[k] = v, and
	#  rvals{} assembles rvals[v] = k
	self.vals = vals = {}
	self.rvals = rvals = {}
	for kv in buf.split("&"):
	    # Split to k and v
	    tup2 = kv.split("=")
	    if len(tup2) == 1:
		k = tup2[0]
		v = True
	    elif len(tup2) == 2:
		k,v = tup2
	    else:
		# x=y=z, something like that?
		self.send_error(404)
		return None

	    # Field name should be folded to lower case, as case
	    #  sensitivity varies by browser.
	    k = k.lower()
	    if isinstance(v, bool):
		vals[k] = v
	    else:
		# The show/artist/track can have spaces, which have
		#  turned into plus signs.
		v = v.replace("+", " ")
		vals[k] = unescape(v)
		rvals[v] = k

	# Success; @buf untouched
	return buf

    # Take the JSON-ish dict @d, and send its JSON
    #  encoding back to our client
    def send_json(self, d):
	buf = json.dumps(d)
	buf = self.send_result(buf, "application/json")
	return buf
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<


































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































Deleted chore/server.py.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#
# server.py
#	Code for firing up a server
#
import threading
import www

class Server(object):
    def __init__(self, config, apphandler):
	# Our HTTP server(s)
	self.servers = []
	self.apphandler = apphandler

	# Our timeout service thread
	self.timer = None

	# Callbacks when time expires
	# List of tuples; (expires-tm, call-fn, call-arg)
	self.timeouts = None

	# Global dict of config
	self.config = config

    # Time based service
    def run_timer(self):
	while True:
	    time.sleep(TIME_GRANULARITY)
	    tm = time.time()

	    # Sweep pending user transactions, release
	    #  any which have timed out.
	    for name,user in self.users.iteritems():
		pend = user.pending
		for tup in tuple(pend):
		    if tup[0] <= tm:
			# Expired; release semaphore and
			#  thus wake them up
			pend.remove(tup)
			tup[1].release()

    # Invoke this if you use run_timer() and related timeout
    #  services.
    def start_timers(self):
	# Here's where you add them
	self.timeouts = []

	# Our timeout thread.
	t = self.timer = threading.Thread(target=self.run_timer)
	t.start()

    # Invoke this if you want web server interface(s)
    def start_http(self):

	# Run HTTP.  All activity is rooted from HTTP transactions
	#  coming in.
	for (proto, cfg) in self.config["serve"]:
	    print "start", proto

	    # Build a web server
	    web = www.HTTP(proto, cfg, self.apphandler, self)

	    # Give it a thread
	    t = threading.Thread(target=web.run)

	    # Keep track (mostly for debugging)
	    self.servers.append( (t, web) )

	    # Spin up the thread
	    t.start()

	print "http server(s) started"

<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
















































































































































Deleted chore/www.py.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
#
# www.py
#	Code to run a web server on a specific port and protocol
#
import sys, socket, fcntl, struct, threading

# Get listen IP address given interface
def get_ip_address(ifname):
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    try:
	res = socket.inet_ntoa(fcntl.ioctl(
	    s.fileno(),
	    0x8915,  # SIOCGIFADDR
	    struct.pack('256s', ifname[:15])
	)[20:24])
	return res
    except:
	raise Exception, "Invalid interface name: %s" % (ifname,)

# Enable config for our HTTP listener(s)
def add_config(c):
    # A web server here; we can serve multiple ports, protocols,
    #  and interfaces
    c.fields0.add("serve")
    c.subconfig.add("serve")
    c.onearg.add("serve")
    c.mults0.add("serve")

    # Config port, which network interface, and SSL stuff
    c.fields1.update( ("port", "iface", "publicCert", "privateKey",
	"accounts") )

    # Port is numeric
    c.ints.add("port")

    # These take only one argument
    c.onearg.update( ("serve", "iface", "publicCert", "privateKey",
	"accounts") )

# HTTP server, listens and spins up threads for each request
class HTTP(object):

    # Our connection back to the root app state
    #
    # @proto indicates http[s]
    # The rest of the config params are in the @config dict
    #
    # @apphandler is an HTTP request handler, rooted
    #  from chore.handlers.Chore_Handler
    # @approot is rooted from chore.server.Server
    def __init__(self, proto, config, apphandler, approot):
	self.proto = proto

	# This is the sub-config for a given HTTP server
	#  entry.
	self.config = config

	self.apphandler = apphandler
	self.approot = approot

	# The socket we'll open
	self.socket = None

	# Authentication?  Or None.
	self.authentication = config.get("accounts")
	if self.authentication:
	    self.cookies = set()
	else:
	    self.cookies = None

    # We're on our own thread, handle request
    def launch(self, conn, tup):
	try:
	    try:
		handler = self.apphandler(conn, tup, self)
	    except:
		# Probably a closed socket on us
		pass
	    # The __init__ runs the service; when it returns
	    #  we're already done.
	finally:
	    try:
		conn.close()
	    except:
		pass
	sys.exit(0)

    # Process web clients; each gets their own thread
    def run(self):
	approot = self.approot
	c = self.config

	# Base socket
	s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

	# Wrap in SSL?
	if self.proto == "https":
	    # This is slow/heavy to import
	    from OpenSSL import SSL

	    # Sanity--SSL configured
	    if not c.get("privateKey"):
		raise Exception, "Private SSL key not configured"
	    if not c.get("publicCert"):
		raise Exception, "Public SSL certificate not configured"

	    # Create socket
	    ctx = SSL.Context(SSL.SSLv23_METHOD)
	    ctx.use_privatekey_file(c["privateKey"])
	    ctx.use_certificate_file(c["publicCert"])
	    s = self.socket = SSL.Connection(ctx, s)

	    # Default port
	    port = c.get("port", 443)

	elif self.proto == "http":

	    # Default port
	    port = c.get("port", 80)

	else:
	    raise Exception, "Unknown protocol %s" % (self.proto,)

	# Standard options for socket
	s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
	ifname = c.get("iface")
	if ifname:
	    addr = get_ip_address(ifname)
	else:
	    addr = ""
	s.bind( (addr, port) )
	s.listen(20)

	# Endless service loop
	print "Handler for", self.proto, "started on", addr, port
	while True:
	    conn,tup = s.accept()
	    t = threading.Thread(target=self.launch, args=(conn,tup))
	    t.start()

<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<