diff --git a/2048/counted_array.py b/2048/counted_array.py new file mode 100644 index 0000000..b25520c --- /dev/null +++ b/2048/counted_array.py @@ -0,0 +1,13 @@ +from numpy import array + +a = array([[None for i in range(4)] for i in range(4)]) + + +counter = 0 + +for i in range(4): + for j in range(4): + counter += 1 + a[i,j] = counter + +print a diff --git a/2048/js/keyboard_input_manager-original.js b/2048/js/keyboard_input_manager-original.js new file mode 100644 index 0000000..e8768ed --- /dev/null +++ b/2048/js/keyboard_input_manager-original.js @@ -0,0 +1,153 @@ +function KeyboardInputManager() { + this.events = {}; + + if (window.navigator.msPointerEnabled) { + //Internet Explorer 10 style + this.eventTouchstart = "MSPointerDown"; + this.eventTouchmove = "MSPointerMove"; + this.eventTouchend = "MSPointerUp"; + } else { + this.eventTouchstart = "touchstart"; + this.eventTouchmove = "touchmove"; + this.eventTouchend = "touchend"; + } + + this.listen(); +} + +KeyboardInputManager.prototype.on = function (event, callback) { + if (!this.events[event]) { + this.events[event] = []; + } + this.events[event].push(callback); +}; + +KeyboardInputManager.prototype.emit = function (event, data) { + var callbacks = this.events[event]; + if (callbacks) { + callbacks.forEach(function (callback) { + callback(data); + }); + } +}; + +KeyboardInputManager.prototype.listen = function () { + var self = this; + + var map = { + 38: 0, // Up + 39: 1, // Right + 40: 2, // Down + 37: 3, // Left + 75: 0, // Vim up + 76: 1, // Vim right + 74: 2, // Vim down + 72: 3, // Vim left + 87: 0, // W + 68: 1, // D + 83: 2, // S + 65: 3 // A + }; + + // Respond to direction keys + document.addEventListener("keydown", function (event) { + var modifiers = event.altKey || event.ctrlKey || event.metaKey || + event.shiftKey; + var mapped = map[event.which]; + + // Ignore the event if it's happening in a text field + if (self.targetIsInput(event)) return; + + if (!modifiers) { + if (mapped !== undefined) { + event.preventDefault(); + self.emit("move", mapped); + } + } + + // R key restarts the game + if (!modifiers && event.which === 82) { + self.restart.call(self, event); + } + }); + + // Respond to button presses + this.bindButtonPress(".retry-button", this.restart); + this.bindButtonPress(".restart-button", this.restart); + this.bindButtonPress(".keep-playing-button", this.keepPlaying); + + // Respond to swipe events + var touchStartClientX, touchStartClientY; + var gameContainer = document.getElementsByClassName("game-container")[0]; + + gameContainer.addEventListener(this.eventTouchstart, function (event) { + if ((!window.navigator.msPointerEnabled && event.touches.length > 1) || + event.targetTouches > 1 || + self.targetIsInput(event)) { + return; // Ignore if touching with more than 1 finger or touching input + } + + if (window.navigator.msPointerEnabled) { + touchStartClientX = event.pageX; + touchStartClientY = event.pageY; + } else { + touchStartClientX = event.touches[0].clientX; + touchStartClientY = event.touches[0].clientY; + } + + event.preventDefault(); + }); + + gameContainer.addEventListener(this.eventTouchmove, function (event) { + event.preventDefault(); + }); + + gameContainer.addEventListener(this.eventTouchend, function (event) { + if ((!window.navigator.msPointerEnabled && event.touches.length > 0) || + event.targetTouches > 0 || + self.targetIsInput(event)) { + return; // Ignore if still touching with one or more fingers or input + } + + var touchEndClientX, touchEndClientY; + + if (window.navigator.msPointerEnabled) { + touchEndClientX = event.pageX; + touchEndClientY = event.pageY; + } else { + touchEndClientX = event.changedTouches[0].clientX; + touchEndClientY = event.changedTouches[0].clientY; + } + + var dx = touchEndClientX - touchStartClientX; + var absDx = Math.abs(dx); + + var dy = touchEndClientY - touchStartClientY; + var absDy = Math.abs(dy); + + if (Math.max(absDx, absDy) > 10) { + // (right : left) : (down : up) + self.emit("move", absDx > absDy ? (dx > 0 ? 1 : 3) : (dy > 0 ? 2 : 0)); + } + }); +}; + +KeyboardInputManager.prototype.restart = function (event) { + event.preventDefault(); + this.emit("restart"); +}; + +KeyboardInputManager.prototype.keepPlaying = function (event) { + event.preventDefault(); + this.emit("keepPlaying"); +}; + +KeyboardInputManager.prototype.bindButtonPress = function (selector, fn) { + var button = document.querySelector(selector); + button.addEventListener("click", fn.bind(this)); + button.addEventListener(this.eventTouchend, fn.bind(this)); +}; + +KeyboardInputManager.prototype.targetIsInput = function (event) { + return event.target.tagName.toLowerCase() === "input"; +}; diff --git a/2048/js/keyboard_input_manager.js b/2048/js/keyboard_input_manager.js new file mode 100644 index 0000000..6785cbe --- /dev/null +++ b/2048/js/keyboard_input_manager.js @@ -0,0 +1,28 @@ +document.onkeydown = checkKey; + +function checkKey(e) { + e = e || window.event; + + var map = { + 38: 0, // Up + 39: 1, // Right + 40: 2, // Down + 37: 3, // Left + 75: 0, // Vim up + 76: 1, // Vim right + 74: 2, // Vim down + 72: 3, // Vim left + 87: 0, // W + 68: 1, // D + 83: 2, // S + 65: 3 // A + }; + + if (e.keyCode == '38') { + alert map[e.keyCode]; + // up arrow + } + else if (e.keyCode == '40') { + // down arrow + } +} diff --git a/2048/main.py b/2048/main.py new file mode 100644 index 0000000..523711f --- /dev/null +++ b/2048/main.py @@ -0,0 +1,56 @@ +from numpy import random, array + +grid = array([[None for i in range(4)] for i in range(4)]) + +def addblock(): + col = random.randint(4) + row = random.randint(4) + if not grid[row, col]: + grid[row, col] = 2 + else: + addblock() + + +def move(way, collapse=0): + print way + change = False + if way in ['down', 'right']: + rows = list(reversed(range(1,4))) + elif way in ['up', 'left']: + rows = range(3) + + for col in range(4): + for row in rows: + if way == 'down': + curr = (row, col) + prev = (row-1, col) + if way == 'up': + curr = (row, col) + prev = (row+1, col) + if way == 'left': + curr = (col, row) + prev = (col, row+1) + if way == 'right': + curr = (col, row) + prev = (col, row-1) + if grid[prev]: + if not grid[curr]: + grid[curr] = grid[prev] + grid[prev] = None + change = True + if grid[prev] == grid[curr]: + grid[curr] *= 2 + grid[prev] = None + collapse += 1 + #if collapse < 2: + # change = True + #else: + change = False + if change: + move(way, collapse) + else: + addblock() + + +addblock() +addblock() diff --git a/2048/main_1.py b/2048/main_1.py new file mode 100644 index 0000000..21d9980 --- /dev/null +++ b/2048/main_1.py @@ -0,0 +1,15 @@ +from numpy import random, array +a = array([[None for i in range(4)] for i in range(4)]) + + +def addblock(): + col = random.randint(4) + row = random.randint(4) + if not a[row,col]: + a[row,col] = 2 + else: + addblock() + +addblock() +addblock() + diff --git a/2048/main_2.py b/2048/main_2.py new file mode 100644 index 0000000..a96b9c7 --- /dev/null +++ b/2048/main_2.py @@ -0,0 +1,36 @@ +from numpy import random +a = [[None for i in range(4)] for i in range(4)] + + +def addblock(): + col = random.randint(4) + row = random.randint(4) + if not a[row][col]: + a[row][col] = 2 + else: + addblock() + +def down(): + for col in range(4): + for row in reversed(range(1,4)): + if a[row-1][col]: + if a[row-1][col] == a[row][col]: + a[row][col] *= 2 + a[row-1][col] = None + if not a[row][col]: + a[row][col] = a[row-1][col] + a[row-1][col] = None + +def up(): + for col in range(4): + for row in range(3): + if a[row+1][col]: + if a[row+1][col] == a[row][col]: + a[row][col] *= 2 + a[row+1][col] = None + if not a[row][col]: + a[row][col] = a[row+1][col] + a[row+1][col] = None +addblock() +addblock() + diff --git a/2048/main_3.py b/2048/main_3.py new file mode 100644 index 0000000..0c2ba36 --- /dev/null +++ b/2048/main_3.py @@ -0,0 +1,55 @@ +from numpy import random +a = [[None for i in range(4)] for i in range(4)] + + +def addblock(): + col = random.randint(4) + row = random.randint(4) + if not a[row][col]: + a[row][col] = 2 + else: + addblock() + +def down(): + for col in range(4): + for row in reversed(range(1,4)): + if a[row-1][col]: + if a[row-1][col] == a[row][col]: + a[row][col] *= 2 + a[row-1][col] = None + if not a[row][col]: + a[row][col] = a[row-1][col] + a[row-1][col] = None + +def up(): + for col in range(4): + for row in range(3): + if a[row+1][col]: + if a[row+1][col] == a[row][col]: + a[row][col] *= 2 + a[row+1][col] = None + if not a[row][col]: + a[row][col] = a[row+1][col] + a[row+1][col] = None + + +def left(): + for row in range(4): + for col in range(3): + if a[row][col+1]: + if a[row][col+1] == a[row][col]: + a[row][col] *= 2 + a[row][col+1] = None + if not a[row][col]: + a[row][col] = a[row][col+1] + a[row][col+1] = None + + +addblock() +addblock() + +a = [[2, 2, None, None], +[None, None, None, None], +[2, None, None, None], +[2, 2, None, None]] + diff --git a/2048/main_4.py b/2048/main_4.py new file mode 100644 index 0000000..9db1b6f --- /dev/null +++ b/2048/main_4.py @@ -0,0 +1,62 @@ +from numpy import random +a = [[None for i in range(4)] for i in range(4)] + + +def addblock(): + col = random.randint(4) + row = random.randint(4) + if not a[row][col]: + a[row][col] = 2 + else: + addblock() + + +def down(): + change = False + for col in range(4): + for row in reversed(range(1, 4)): + if a[row-1][col]: + if a[row-1][col] == a[row][col]: + a[row][col] *= 2 + a[row-1][col] = None + change = True + if not a[row][col]: + a[row][col] = a[row-1][col] + a[row-1][col] = None + change = True + if change: + down() + + +def up(): + for col in range(4): + for row in range(3): + if a[row+1][col]: + if a[row+1][col] == a[row][col]: + a[row][col] *= 2 + a[row+1][col] = None + if not a[row][col]: + a[row][col] = a[row+1][col] + a[row+1][col] = None + + +def left(): + for row in range(4): + for col in range(3): + print a[row][col] + if a[row][col+1]: + if a[row][col+1] == a[row][col]: + a[row][col] *= 2 + a[row][col+1] = None + if not a[row][col]: + a[row][col] = a[row][col+1] + a[row][col+1] = None + + +addblock() +addblock() + +a = [[2, None, None, None], + [None, None, None, None], + [4, None, None, None], + [4, 2, None, None]] diff --git a/2048/main_5.py b/2048/main_5.py new file mode 100644 index 0000000..c70ee97 --- /dev/null +++ b/2048/main_5.py @@ -0,0 +1,55 @@ +from numpy import random, array +a = array([[None for i in range(4)] for i in range(4)]) + +def addblock(): + col = random.randint(4) + row = random.randint(4) + if not a[row, col]: + a[row, col] = 2 + else: + addblock() + +def down(): + change = False + for col in range(4): + for row in reversed(range(1,4)): + if a[row-1,col]: + if a[row-1, col] == a[row, col]: + a[row, col] *= 2 + a[row-1, col] = None + change = True + if not a[row, col]: + a[row, col] = a[row-1, col] + a[row-1, col] = None + change = True + if change: + down() + +def up(): + for col in range(4): + for row in range(3): + if a[row+1, col]: + if a[row+1, col] == a[row, col]: + a[row, col] *= 2 + a[row+1, col] = None + if not a[row, col]: + a[row, col] = a[row+1, col] + a[row+1, col] = None + + +def left(): + for row in range(4): + for col in range(3): + print a[row, col] + if a[row, col+1]: + if a[row, col+1] == a[row, col]: + a[row, col] *= 2 + a[row, col+1] = None + if not a[row, col]: + a[row, col] = a[row, col+1] + a[row, col+1] = None + + +addblock() +addblock() + diff --git a/2048/main_6.py b/2048/main_6.py new file mode 100644 index 0000000..2b92267 --- /dev/null +++ b/2048/main_6.py @@ -0,0 +1,67 @@ +from numpy import random, array +a = array([[None for i in range(4)] for i in range(4)]) + +def addblock(): + col = random.randint(4) + row = random.randint(4) + if not a[row, col]: + a[row, col] = 2 + else: + addblock() + +def up_down(way): + change = False + if way == 'down': + rows = list(reversed(range(1,4))) + if way == 'up': + rows = range(3) + + for col in range(4): + for row in rows: + curr = (row, col) + if way == 'down': + prev = (row-1, col) + if way == 'up': + prev = (row+1, col) + if a[prev]: + if a[prev] == a[curr]: + a[curr] *= 2 + a[prev] = None + change = True + if not a[curr]: + a[curr] = a[prev] + a[prev] = None + change = True + if change: + up_down(way) + +def up(): + for col in range(4): + for row in range(2,-1,-1): + if a[row+1, col]: + if a[row+1, col] == a[row, col]: + a[row, col] *= 2 + a[row+1, col] = None + if not a[row, col]: + a[row, col] = a[row+1, col] + a[row+1, col] = None + + + + +def left(): + for row in range(4): + for col in range(2,-1,-1): + print a[row, col] + if a[row, col+1]: + if a[row, col+1] == a[row, col]: + a[row, col] *= 2 + a[row, col+1] = None + if not a[row, col]: + a[row, col] = a[row, col+1] + a[row, col+1] = None + + +addblock() +addblock() + diff --git a/2048/main_7.py b/2048/main_7.py new file mode 100644 index 0000000..ac4f08a --- /dev/null +++ b/2048/main_7.py @@ -0,0 +1,52 @@ +from numpy import random, array + +a = array([[None for i in range(4)] for i in range(4)]) + + +def addblock(): + col = random.randint(4) + row = random.randint(4) + if not a[row, col]: + a[row, col] = 2 + else: + addblock() + + +def move(way): + change = False + if way in ['down', 'right']: + rows = list(reversed(range(1, 4))) + if way in ['up', 'left']: + rows = range(3) + + for col in range(4): + for row in rows: + if way == 'down': + curr = (row, col) + prev = (row-1, col) + if way == 'up': + curr = (row, col) + prev = (row+1, col) + if way == 'left': + curr = (col, row) + prev = (col, row+1) + if way == 'right': + curr = (col, row) + prev = (col, row-1) + if a[prev]: + if a[prev] == a[curr]: + a[curr] *= 2 + a[prev] = None + change = True + if not a[curr]: + a[curr] = a[prev] + a[prev] = None + change = True + if change: + move(way) + else: + addblock() + + +addblock() +addblock() diff --git a/2048/server.py b/2048/server.py new file mode 100644 index 0000000..920eed7 --- /dev/null +++ b/2048/server.py @@ -0,0 +1,54 @@ +from static import Cling +from wsgiref.simple_server import make_server +from ui import createblock +from main import * + +def format(mylist): + template = ''' + + + + My 2048 + + + + + + + + + +
%s %s %s %s
%s %s %s %s
%s %s %s %s
%s %s %s %s
+ + ''' + content = tuple(map(lambda n: createblock(n), mylist)) + return (template % content) + + +def simple_app(environ, start_response): + if environ['PATH_INFO'] == '/js/keyboard_input_manager.js': + start_response('200 OK', [('content-type','application/javascript')]) + return str(open('js/keyboard_input_manager.js', 'r').read()) + else: + status = '200 OK' + headers = [('Content-type', 'text/html')] + + start_response(status, headers) + return format(a.flatten()) + +httpd = make_server('', 8000, simple_app) +print "Serving on port 8000..." +import threading +threading.Thread(target=httpd.serve_forever).start() + +while True: + nextmove = raw_input('What is your next move? [l,r,u,d] ') + shorthand = {'l': 'left', 'r': 'right', 'u': 'up', 'd': 'down'} + move(shorthand[nextmove]) diff --git a/2048/simpleserver.py b/2048/simpleserver.py new file mode 100644 index 0000000..4755448 --- /dev/null +++ b/2048/simpleserver.py @@ -0,0 +1,70 @@ +from wsgiref.simple_server import make_server +from cgi import parse_qs, escape + + +def application(environ, start_response): + + status = '200 OK' + + # Now content type is text/html + response_headers = [('Content-Type', 'text/html')] + start_response(status, response_headers) + + out = ''' + + 2048 + + + + Welcome to 2048 + + + + + + + + + + + + + + + + + + + + + + + + + + +
2
2
4
2048
+ +
+ + + + +
+ + + +''' + + + get_dict = parse_qs(environ['QUERY_STRING']) + + for key,value in get_dict.items(): + out += "key: %s: value: %s
" % (key,value) + + + return [out] + +httpd = make_server('', 8001, application) +print "Serving on port 8001..." +httpd.serve_forever() diff --git a/2048/ui-server.py b/2048/ui-server.py new file mode 100644 index 0000000..73c857b --- /dev/null +++ b/2048/ui-server.py @@ -0,0 +1,64 @@ +from wsgiref.simple_server import make_server +from cgi import parse_qs, escape +from ui import createblock +from main import * + +def format(grid): + template = ''' + + + + My 2048 + + + + + + + + + +
%s %s %s %s
%s %s %s %s
%s %s %s %s
%s %s %s %s
+ +
+ + + + +
+ + + ''' + #content = tuple(map(lambda cell: createblock(cell), grid)) + content = tuple(map(lambda cell: cell, grid)) + return (template % content) + + +def simple_app(environ, start_response): + if environ['PATH_INFO'] == '/js/keyboard_input_manager.js': + start_response('200 OK', [('content-type','application/javascript')]) + return str(open('js/keyboard_input_manager.js', 'r').read()) + else: + status = '200 OK' + headers = [('Content-type', 'text/html')] + + get_dict = parse_qs(environ['QUERY_STRING']) + + if 'move' in get_dict.keys(): + move(get_dict['move'][0]) + + start_response(status, headers) + return format(grid.flatten()) + +httpd = make_server('', 8004, simple_app) +print "Serving on port 8004..." +import threading +threading.Thread(target=httpd.serve_forever).start() + diff --git a/2048/ui/__init__.py b/2048/ui/__init__.py new file mode 100644 index 0000000..194c897 --- /dev/null +++ b/2048/ui/__init__.py @@ -0,0 +1,31 @@ +from pysvg.builders import Svg, ShapeBuilder, StyleBuilder +from pysvg.text import * + +def createblock(number): + colors = {} + colors[None]=('#eee4da','#776e65') + colors[2]=('#eee4da','#776e65') + colors[4]=('#ede0c8','#776e65') + colors[8]=('#f2b179','#f9f6f2') + colors[16]=('#f59563','#f9f6f2') + colors[32]=('#f67c5f','#f9f6f2') + colors[64]=('#f65e3b','#f9f6f2') + colors[128]=('#edcf72','#f9f6f2') + colors[256]=('#edcc61','#f9f6f2') + colors[512]=('#eee4da','#776e65') + colors[1024]=('#edc53f','#f9f6f2') + colors[2048]=('#edc22e','#f9f6f2') + + canvas = Svg(0,0,100,100) + sb = ShapeBuilder() + canvas.addElement( sb.createRect(5,5,90,90,fill=colors[number][0]) ) + + t = Text(number,50,60) + t.set_style("font-family:FreeSans;font-weight:bold;font-size:36px;text-anchor:middle") + t.set_fill(colors[number][1]) + canvas.addElement(t) + return canvas.getXML() + #canvas.save('/tmp/try7.svg') + + +createblock(None) diff --git a/2048/ui/frame_test.html b/2048/ui/frame_test.html new file mode 100644 index 0000000..a69a6b2 --- /dev/null +++ b/2048/ui/frame_test.html @@ -0,0 +1,18 @@ + + + + + +
+ + + +32 + + + + + +256 + +
diff --git a/2048/ui/mainui_1.py b/2048/ui/mainui_1.py new file mode 100644 index 0000000..2c78424 --- /dev/null +++ b/2048/ui/mainui_1.py @@ -0,0 +1,16 @@ +from pysvg.builders import Svg, ShapeBuilder, StyleBuilder +from pysvg.text import * + +canvas = Svg(0,0,100,100) + +sb = ShapeBuilder() +canvas.addElement( sb.createRect(5,5,90,90,fill="#00FF00") ) + +t = Text('Hello!',50,50) +t.set_style("font-family:FreeSans;font-weight:bold;font-size:24px;text-anchor:middle") +t.set_fill("#FF0000") +canvas.addElement(t) + + +canvas.save('/tmp/try7.svg') + diff --git a/2048/ui/mainui_2.py b/2048/ui/mainui_2.py new file mode 100644 index 0000000..1f96269 --- /dev/null +++ b/2048/ui/mainui_2.py @@ -0,0 +1,29 @@ +from pysvg.builders import Svg, ShapeBuilder, StyleBuilder +from pysvg.text import * + +def createblock(number): + colors = {} + colors[2]=('#eee4da','#776e65') + colors[4]=('#ede0c8','#776e65') + colors[8]=('#f2b179','#f9f6f2') + colors[16]=('#f59563','#f9f6f2') + colors[32]=('#f67c5f','#f9f6f2') + colors[64]=('#f65e3b','#f9f6f2') + colors[128]=('#edcf72','#f9f6f2') + colors[256]=('#edcc61','#f9f6f2') + colors[512]=('#eee4da','#776e65') + colors[1024]=('#edc53f','#f9f6f2') + colors[2048]=('#edc22e','#f9f6f2') + + canvas = Svg(0,0,100,100) + sb = ShapeBuilder() + canvas.addElement( sb.createRect(5,5,90,90,fill=colors[number][0]) ) + + t = Text(number,50,60) + t.set_style("font-family:FreeSans;font-weight:bold;font-size:36px;text-anchor:middle") + t.set_fill(colors[number][1]) + canvas.addElement(t) + canvas.save('/tmp/try7.svg') + + +createblock(128) diff --git a/appengine/2048-js/app.yaml b/appengine/2048-js/app.yaml new file mode 100644 index 0000000..55356f3 --- /dev/null +++ b/appengine/2048-js/app.yaml @@ -0,0 +1,18 @@ +application: your-app-id +version: 1 +runtime: python27 +api_version: 1 +threadsafe: true + +handlers: +- url: /js + static_dir: js + +- url: /.* + script: main.app + +libraries: +- name: webapp2 + version: latest +- name: jinja2 + version: latest diff --git a/appengine/2048-js/grid.py b/appengine/2048-js/grid.py new file mode 100644 index 0000000..4b6e2d7 --- /dev/null +++ b/appengine/2048-js/grid.py @@ -0,0 +1,75 @@ +import random + + +class Grid(object): + def __init__(self): + self.restart() + + def restart(self): + self.lol = [[None for i in range(4)] for i in range(4)] + self.addblock() + self.addblock() + + def get(self, a, b): + return self.lol[a][b] + + def content(self): + return self.lol + + def set(self, cord, value): + a, b = cord + self.lol[a][b] = value + + def addblock(self): + col = random.randint(0, 3) + row = random.randint(0, 3) + if not self.get(row, col): + self.set([row, col], 2) + else: + self.addblock() + + def move(self, way): + change = False + if way in ['down', 'right']: + rows = list(reversed(range(1, 4))) + elif way in ['up', 'left']: + rows = range(3) + else: + return + + for col in range(4): + for row in rows: + if way == 'down': + curr = [row, col] + prev = [row-1, col] + if way == 'up': + curr = [row, col] + prev = [row+1, col] + if way == 'left': + curr = [col, row] + prev = [col, row+1] + if way == 'right': + curr = [col, row] + prev = [col, row-1] + if self.get(*prev): + if not self.get(*curr): + self.set(curr, self.get(*prev)) + self.set(prev, None) + change = True + if self.get(*prev) == self.get(*curr): + self.set(curr, self.get(*curr)*2) + self.set(prev, None) + change = False + if change: + self.move(way) + else: + self.addblock() + + def action(self,what): + if what == 'restart': + self.restart() + else: + self.move(what) + + +grid = Grid() diff --git a/appengine/2048-js/grid.pyc b/appengine/2048-js/grid.pyc new file mode 100644 index 0000000..519c241 Binary files /dev/null and b/appengine/2048-js/grid.pyc differ diff --git a/appengine/2048-js/index.html b/appengine/2048-js/index.html new file mode 100644 index 0000000..ef1db26 --- /dev/null +++ b/appengine/2048-js/index.html @@ -0,0 +1,85 @@ + + + + +My 2048 + + + + +{% for row in range(4) %} + + {% for column in range(4) %} + + {% endfor %} + +{% endfor %} +
{{ blocks[row][column] }}
+ +
+ + + + +
+ + + diff --git a/appengine/2048-js/js/keyboard_input_manager-original.js b/appengine/2048-js/js/keyboard_input_manager-original.js new file mode 100644 index 0000000..e8768ed --- /dev/null +++ b/appengine/2048-js/js/keyboard_input_manager-original.js @@ -0,0 +1,153 @@ +function KeyboardInputManager() { + this.events = {}; + + if (window.navigator.msPointerEnabled) { + //Internet Explorer 10 style + this.eventTouchstart = "MSPointerDown"; + this.eventTouchmove = "MSPointerMove"; + this.eventTouchend = "MSPointerUp"; + } else { + this.eventTouchstart = "touchstart"; + this.eventTouchmove = "touchmove"; + this.eventTouchend = "touchend"; + } + + this.listen(); +} + +KeyboardInputManager.prototype.on = function (event, callback) { + if (!this.events[event]) { + this.events[event] = []; + } + this.events[event].push(callback); +}; + +KeyboardInputManager.prototype.emit = function (event, data) { + var callbacks = this.events[event]; + if (callbacks) { + callbacks.forEach(function (callback) { + callback(data); + }); + } +}; + +KeyboardInputManager.prototype.listen = function () { + var self = this; + + var map = { + 38: 0, // Up + 39: 1, // Right + 40: 2, // Down + 37: 3, // Left + 75: 0, // Vim up + 76: 1, // Vim right + 74: 2, // Vim down + 72: 3, // Vim left + 87: 0, // W + 68: 1, // D + 83: 2, // S + 65: 3 // A + }; + + // Respond to direction keys + document.addEventListener("keydown", function (event) { + var modifiers = event.altKey || event.ctrlKey || event.metaKey || + event.shiftKey; + var mapped = map[event.which]; + + // Ignore the event if it's happening in a text field + if (self.targetIsInput(event)) return; + + if (!modifiers) { + if (mapped !== undefined) { + event.preventDefault(); + self.emit("move", mapped); + } + } + + // R key restarts the game + if (!modifiers && event.which === 82) { + self.restart.call(self, event); + } + }); + + // Respond to button presses + this.bindButtonPress(".retry-button", this.restart); + this.bindButtonPress(".restart-button", this.restart); + this.bindButtonPress(".keep-playing-button", this.keepPlaying); + + // Respond to swipe events + var touchStartClientX, touchStartClientY; + var gameContainer = document.getElementsByClassName("game-container")[0]; + + gameContainer.addEventListener(this.eventTouchstart, function (event) { + if ((!window.navigator.msPointerEnabled && event.touches.length > 1) || + event.targetTouches > 1 || + self.targetIsInput(event)) { + return; // Ignore if touching with more than 1 finger or touching input + } + + if (window.navigator.msPointerEnabled) { + touchStartClientX = event.pageX; + touchStartClientY = event.pageY; + } else { + touchStartClientX = event.touches[0].clientX; + touchStartClientY = event.touches[0].clientY; + } + + event.preventDefault(); + }); + + gameContainer.addEventListener(this.eventTouchmove, function (event) { + event.preventDefault(); + }); + + gameContainer.addEventListener(this.eventTouchend, function (event) { + if ((!window.navigator.msPointerEnabled && event.touches.length > 0) || + event.targetTouches > 0 || + self.targetIsInput(event)) { + return; // Ignore if still touching with one or more fingers or input + } + + var touchEndClientX, touchEndClientY; + + if (window.navigator.msPointerEnabled) { + touchEndClientX = event.pageX; + touchEndClientY = event.pageY; + } else { + touchEndClientX = event.changedTouches[0].clientX; + touchEndClientY = event.changedTouches[0].clientY; + } + + var dx = touchEndClientX - touchStartClientX; + var absDx = Math.abs(dx); + + var dy = touchEndClientY - touchStartClientY; + var absDy = Math.abs(dy); + + if (Math.max(absDx, absDy) > 10) { + // (right : left) : (down : up) + self.emit("move", absDx > absDy ? (dx > 0 ? 1 : 3) : (dy > 0 ? 2 : 0)); + } + }); +}; + +KeyboardInputManager.prototype.restart = function (event) { + event.preventDefault(); + this.emit("restart"); +}; + +KeyboardInputManager.prototype.keepPlaying = function (event) { + event.preventDefault(); + this.emit("keepPlaying"); +}; + +KeyboardInputManager.prototype.bindButtonPress = function (selector, fn) { + var button = document.querySelector(selector); + button.addEventListener("click", fn.bind(this)); + button.addEventListener(this.eventTouchend, fn.bind(this)); +}; + +KeyboardInputManager.prototype.targetIsInput = function (event) { + return event.target.tagName.toLowerCase() === "input"; +}; diff --git a/appengine/2048-js/js/keyboard_input_manager.js b/appengine/2048-js/js/keyboard_input_manager.js new file mode 100644 index 0000000..ca60483 --- /dev/null +++ b/appengine/2048-js/js/keyboard_input_manager.js @@ -0,0 +1,29 @@ +document.onkeydown = checkKey; + +function checkKey(e) { + e = e || window.event; + + var map = { + 38: 0, // Up + 39: 1, // Right + 40: 2, // Down + 37: 3, // Left + 75: 0, // Vim up + 76: 1, // Vim right + 74: 2, // Vim down + 72: 3, // Vim left + 87: 0, // W + 68: 1, // D + 83: 2, // S + 65: 3 // A + }; + alert map[e.keyCode]; + + if (e.keyCode == '38') { + alert map[e.keyCode]; + // up arrow + } + else if (e.keyCode == '40') { + // down arrow + } +} diff --git a/appengine/2048-js/main.py b/appengine/2048-js/main.py new file mode 100644 index 0000000..73aa695 --- /dev/null +++ b/appengine/2048-js/main.py @@ -0,0 +1,43 @@ +import os +import jinja2 +import webapp2 + +from ui import createblock +from grid import * + + +jinja_environment = jinja2.Environment( + loader=jinja2.FileSystemLoader(os.path.dirname(__file__))) + + +def format(grid): + blocks = [[None for i in range(4)] for i in range(4)] + for i in range(4): + for j in range(4): + blocks[i][j] = createblock(grid.get(i,j)).replace('"',"'") + return blocks + +class MainPage(webapp2.RequestHandler): + + def post(self): + nextact = self.request.get('action') + grid.action(nextact) + blocks = format(grid) + self.response.out.write(blocks) + + def get(self): + template = jinja_environment.get_template('index.html') + nextact = None + try: + nextact = self.request.get('action') + grid.action(nextact) + except: + pass + + blocks = format(grid) + template = jinja_environment.get_template('index.html') + self.response.out.write(template.render(blocks=blocks)) + +app = webapp2.WSGIApplication([ + ('/', MainPage), +], debug=True) diff --git a/appengine/2048-js/main.pyc b/appengine/2048-js/main.pyc new file mode 100644 index 0000000..34fcece Binary files /dev/null and b/appengine/2048-js/main.pyc differ diff --git a/appengine/2048-js/pysvg/__init__.py b/appengine/2048-js/pysvg/__init__.py new file mode 100644 index 0000000..b49afc2 --- /dev/null +++ b/appengine/2048-js/pysvg/__init__.py @@ -0,0 +1,14 @@ +#__all__ = [] +#for subpackage in ['core', 'filter', 'gradient', 'linking', 'script','shape','structure','style','text']: +# try: +# exec 'import ' + subpackage +# __all__.append( subpackage ) +# except ImportError: +# pass + + + + + + + \ No newline at end of file diff --git a/appengine/2048-js/pysvg/__init__.pyc b/appengine/2048-js/pysvg/__init__.pyc new file mode 100644 index 0000000..4557f25 Binary files /dev/null and b/appengine/2048-js/pysvg/__init__.pyc differ diff --git a/appengine/2048-js/pysvg/animate.py b/appengine/2048-js/pysvg/animate.py new file mode 100644 index 0000000..4bc8c94 --- /dev/null +++ b/appengine/2048-js/pysvg/animate.py @@ -0,0 +1,237 @@ +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +''' +(C) 2008, 2009 Kerim Mansour +For licensing information please refer to license.txt +''' +from attributes import * +from core import BaseShape, BaseElement + +##################################################### +# Attribute sets for animations +# Animation elements see below +##################################################### +class AnimationAttrib(XLinkAttrib): + """ + The AnimationAttrib class defines the Animation.attrib attribute set. + """ + +class AnimationAttributeAttrib: + """ + The AnimationAttributeAttrib class defines the AnimationAttribute.attrib attribute set. + """ + def set_attributeName(self, attributeName): + self._attributes['attributeName'] = attributeName + def get_attributeName(self): + return self._attributes.get('attributeName') + + def set_attributeType(self, attributeType): + self._attributes['attributeType'] = attributeType + def get_attributeType(self): + return self._attributes.get('attributeType') + +class AnimationTimingAttrib: + """ + The AnimationTimingAttrib class defines the AnimationTiming.attrib attribute set. + """ + def set_begin(self, begin): + self._attributes['begin'] = begin + def get_begin(self): + return self._attributes.get('begin') + + def set_dur(self, dur): + self._attributes['dur'] = dur + def get_dur(self): + return self._attributes.get('dur') + + def set_end(self, end): + self._attributes['end'] = end + def get_end(self): + return self._attributes.get('end') + + def set_min(self, min): + self._attributes['min'] = min + def get_min(self): + return self._attributes.get('min') + + def set_max(self, max): + self._attributes['max'] = max + def get_max(self): + return self._attributes.get('max') + + def set_restart(self, restart): + self._attributes['restart'] = restart + def get_restart(self): + return self._attributes.get('restart') + + def set_repeatCount(self, repeatCount): + self._attributes['repeatCount'] = repeatCount + def get_repeatCount(self): + return self._attributes.get('repeatCount') + + def set_repeatDur(self, repeatDur): + self._attributes['repeatDur'] = repeatDur + def get_repeatDur(self): + return self._attributes.get('repeatDur') + + def set_fill(self, fill): + self._attributes['fill'] = fill + def get_fill(self): + return self._attributes.get('fill') + +class AnimationValueAttrib: + """ + The AnimationValueAttrib class defines the AnimationValue.attrib attribute set. + """ + def set_calcMode(self, calcMode): + self._attributes['calcMode'] = calcMode + def get_calcMode(self): + return self._attributes.get('calcMode') + + def set_values(self, values): + self._attributes['values'] = values + def get_values(self): + return self._attributes.get('values') + + def set_keyTimes(self, keyTimes): + self._attributes['keyTimes'] = keyTimes + def get_keyTimes(self): + return self._attributes.get('keyTimes') + + def set_keySplines(self, keySplines): + self._attributes['keySplines'] = keySplines + def get_keySplines(self): + return self._attributes.get('keySplines') + + def set_from(self, fromField): + self._attributes['from'] = fromField + def get_from(self): + return self._attributes.get('from') + + def set_to(self, toField): + self._attributes['to'] = toField + def get_to(self): + return self._attributes.get('to') + + def set_by(self, by): + self._attributes['by'] = by + def get_by(self): + return self._attributes.get('by') + +class AnimationAdditionAttrib: + """ + The AnimationAdditionAttrib class defines the AnimationAddition.attrib attribute set. + """ + def set_additive(self, additive): + self._attributes['additive'] = additive + def get_additive(self): + return self._attributes.get('additive') + + def set_accumulate(self, accumulate): + self._attributes['accumulate'] = accumulate + def get_accumulate(self): + return self._attributes.get('accumulate') + +class AnimationEventsAttrib: + """ + The AnimationEventsAttrib class defines the AnimationEvents.attrib attribute set. + """ + def set_onbegin(self, onbegin): + self._attributes['onbegin'] = onbegin + def get_onbegin(self): + return self._attributes.get('onbegin') + + def set_onend(self, onend): + self._attributes['onend'] = onend + def get_onend(self): + return self._attributes.get('onend') + + def set_onrepeat(self, onrepeat): + self._attributes['onrepeat'] = onrepeat + def get_onrepeat(self): + return self._attributes.get('onrepeat') + + def set_onload(self, onload): + self._attributes['onload'] = onload + def get_onload(self): + return self._attributes.get('onload') +############################################## +# Animation Elements +############################################## +class Animate(BaseShape, CoreAttrib, ConditionalAttrib, ExternalAttrib, AnimationEventsAttrib, AnimationAttrib, AnimationAttributeAttrib, AnimationTimingAttrib, AnimationValueAttrib, AnimationAdditionAttrib): + """ + Class representing the animate element of an svg doc. + """ + def __init__(self, **kwargs): + BaseElement.__init__(self, 'animate') + self.setKWARGS(**kwargs) + +class Set(BaseShape, CoreAttrib, ConditionalAttrib, ExternalAttrib, AnimationEventsAttrib, AnimationAttrib, AnimationAttributeAttrib, AnimationTimingAttrib): + """ + Class representing the set element of an svg doc. + """ + def __init__(self, **kwargs): + BaseElement.__init__(self, 'set') + self.setKWARGS(**kwargs) + + def set_to(self, toField): + self._attributes['to'] = toField + def get_to(self): + return self._attributes.get('to') + +class AnimateMotion(BaseShape, CoreAttrib, ConditionalAttrib, ExternalAttrib, AnimationEventsAttrib, AnimationAttrib, AnimationTimingAttrib, AnimationValueAttrib, AnimationAdditionAttrib): + """ + Class representing the animateMotion element of an svg doc. + """ + def __init__(self, **kwargs): + BaseElement.__init__(self, 'animateMotion') + self.setKWARGS(**kwargs) + + def set_path(self, path): + self._attributes['path'] = path + def get_path(self): + return self._attributes.get('path') + + def set_keyPoints(self, keyPoints): + self._attributes['keyPoints'] = keyPoints + def get_keyPoints(self): + return self._attributes.get('keyPoints') + + def set_rotate(self, rotate): + self._attributes['rotate'] = rotate + def get_rotate(self): + return self._attributes.get('rotate') + + def set_origin(self, origin): + self._attributes['origin'] = origin + def get_origin(self): + return self._attributes.get('origin') + +class AnimateTransform(BaseShape, CoreAttrib, ConditionalAttrib, ExternalAttrib, AnimationEventsAttrib, AnimationAttrib, AnimationAttributeAttrib, AnimationTimingAttrib, AnimationValueAttrib, AnimationAdditionAttrib): + """ + Class representing the animateTransform element of an svg doc. + """ + def __init__(self, **kwargs): + BaseElement.__init__(self, 'animateTransform') + self.setKWARGS(**kwargs) + + def set_type(self, type): + self._attributes['type'] = type + def get_type(self): + return self._attributes.get('type') + +class AnimateColor(BaseShape, CoreAttrib, ConditionalAttrib, ExternalAttrib, AnimationEventsAttrib, AnimationAttrib, AnimationAttributeAttrib, AnimationTimingAttrib, AnimationValueAttrib, AnimationAdditionAttrib): + """ + Class representing the animateColor element of an svg doc. + """ + def __init__(self, **kwargs): + BaseElement.__init__(self, 'animateColor') + self.setKWARGS(**kwargs) + +class Mpath(BaseShape, CoreAttrib, XLinkAttrib, ExternalAttrib): + """ + Class representing the animateColor element of an svg doc. + """ + def __init__(self, **kwargs): + BaseElement.__init__(self, 'mpath') + self.setKWARGS(**kwargs) diff --git a/appengine/2048-js/pysvg/animate.pyc b/appengine/2048-js/pysvg/animate.pyc new file mode 100644 index 0000000..65d8785 Binary files /dev/null and b/appengine/2048-js/pysvg/animate.pyc differ diff --git a/appengine/2048-js/pysvg/attributes.py b/appengine/2048-js/pysvg/attributes.py new file mode 100644 index 0000000..93caeee --- /dev/null +++ b/appengine/2048-js/pysvg/attributes.py @@ -0,0 +1,840 @@ +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +''' +(C) 2008, 2009 Kerim Mansour +For licensing information please refer to license.txt +''' +class CoreAttrib: + """ + The CoreAttrib class defines the attribute set Core.attrib + that is the core set of attributes that can be present on any element. + """ + def set_id(self, id): + self._attributes['id'] = id + + def get_id(self): + return self._attributes.get('id') + + def set_xml_base(self, xml_base): + self._attributes['xml:base'] = xml_base + + def get_xml_base(self): + return self._attributes.get('xml:base') + + def set_xml_lang(self, language_code): + self._attributes['xml:lang'] = language_code + + def get_xml_lang(self): + return self._attributes.get('xml:lang') + + def set_xml_space(self, xml_space): + self._attributes['xml:space'] = xml_space + + def get_xml_space(self): + return self._attributes.get('xml:space') + +class ConditionalAttrib: + """ + The ConditionalAttrib class defines the Conditional.attrib attribute set. + """ + def set_requiredFeatures(self, requiredFeatures): + self._attributes['requiredFeatures'] = requiredFeatures + + def get_requiredFeatures(self): + return self._attributes.get('requiredFeatures') + + def set_requiredExtensions(self, requiredExtensions): + self._attributes['requiredExtensions'] = requiredExtensions + + def get_requiredExtensions(self): + return self._attributes.get('requiredExtensions') + + def set_systemLanguage(self, language_code): + self._attributes['systemLanguage'] = language_code + + def get_systemLanguage(self): + return self._attributes.get('systemLanguage') + +class StyleAttrib: + """ + The StyleAttrib class defines the Style.attrib attribute set. + """ + def set_style(self, style): + self._attributes['style'] = style + + def get_style(self): + return self._attributes.get('style') + + def set_class(self, aClass): + self._attributes['class'] = aClass + + def get_class(self): + return self._attributes.get('class') + +class GraphicalEventsAttrib: + """ + The GraphicalEventsAttrib class defines the GraphicalEvents.attrib attribute set. + """ + def set_onfocusin(self, onfocusin): + self._attributes['onfocusin'] = onfocusin + + def get_onfocusin(self): + return self._attributes.get('onfocusin') + + def set_onfocusout(self, onfocusout): + self._attributes['onfocusout'] = onfocusout + + def get_onfocusout(self): + return self._attributes.get('onfocusout') + + def set_onactivate(self, onactivate): + self._attributes['onactivate'] = onactivate + + def get_onactivate(self): + return self._attributes.get('onactivate') + + def set_onclick(self, onclick): + self._attributes['onclick'] = onclick + + def get_onclick(self): + return self._attributes.get('onclick') + + def set_onmousedown(self, onmousedown): + self._attributes['onmousedown'] = onmousedown + + def get_onmousedown(self): + return self._attributes.get('onmousedown') + + def set_onmouseup(self, onmouseup): + self._attributes['onmouseup'] = onmouseup + + def get_onmouseup(self): + return self._attributes.get('onmouseup') + + def set_onmouseover(self, onmouseover): + self._attributes['onmouseover'] = onmouseover + + def get_onmouseover(self): + return self._attributes.get('onmouseover') + + def set_onmousemove(self, onmousemove): + self._attributes['onmousemove'] = onmousemove + + def get_onmousemove(self): + return self._attributes.get('onmousemove') + + def set_onmouseout(self, onmouseout): + self._attributes['onmouseout'] = onmouseout + + def get_onmouseout(self): + return self._attributes.get('onmouseout') + + def set_onload(self, onload): + self._attributes['onload'] = onload + + def get_onload(self): + return self._attributes.get('onload') + + + +class CursorAttrib: + """ + The CursorAttrib class defines the Cursor.attrib attribute set. + """ + def set_cursor(self, cursor): + self._attributes['cursor'] = cursor + + def get_cursor(self): + return self._attributes.get('cursor') + +class ExternalAttrib: + """ + The ExternalAttrib class defines the External.attrib attribute set. + """ + def set_externalResourcesRequired(self, externalResourcesRequired): + self._attributes['externalResourcesRequired'] = externalResourcesRequired + + def get_externalResourcesRequired(self): + return self._attributes.get('externalResourcesRequired') + +class DocumentEventsAttrib: + """ + The DocumentEventsAttrib class defines the DocumentEvents.attrib attribute set. + """ + def set_onunload(self, onunload): + self._attributes['onunload'] = onunload + + def get_onunload(self): + return self._attributes.get('onunload') + + def set_onabort(self, onabort): + self._attributes['onabort'] = onabort + + def get_onabort(self): + return self._attributes.get('onabort') + + def set_onerror(self, onerror): + self._attributes['onerror'] = onerror + + def get_onerror(self): + return self._attributes.get('onerror') + + def set_onresize(self, onresize): + self._attributes['onresize'] = onresize + + def get_onresize(self): + return self._attributes.get('onresize') + + def set_onscroll(self, onscroll): + self._attributes['onscroll'] = onscroll + + def get_onscroll(self): + return self._attributes.get('onscroll') + + def set_onzoom(self, onzoom): + self._attributes['onzoom'] = onzoom + + def get_onzoom(self): + return self._attributes.get('onzoom') + +class OpacityAttrib: + """ + The OpacityAttrib class defines the Opacity.attrib attribute set. + """ + def set_opacity(self, opacity): + self._attributes['opacity'] = opacity + + def get_opacity(self): + return self._attributes.get('opacity') + + def set_stroke_opacity(self, stroke_opacity): + self._attributes['stroke-opacity'] = stroke_opacity + + def get_stroke_opacity(self): + return self._attributes.get('stroke-opacity') + + def set_fill_opacity(self, fill_opacity): + self._attributes['fill-opacity'] = fill_opacity + + def get_fill_opacity(self): + return self._attributes.get('fill-opacity') + +class PaintAttrib: + """ + The PaintAttrib class defines the Paint.attrib attribute set. + """ + def set_color(self, color): + self._attributes['color'] = color + + def get_color(self): + return self._attributes.get('color') + + def set_color_interpolation(self, color_interpolation): + self._attributes['color-interpolation'] = color_interpolation + + def get_color_interpolation(self): + return self._attributes.get('color-interpolation') + + def set_color_rendering(self, color_rendering): + self._attributes['color-rendering'] = color_rendering + + def get_color_rendering(self): + return self._attributes.get('color-rendering') + + def set_fill(self, fill): + self._attributes['fill'] = fill + + def get_fill(self): + return self._attributes.get('fill') + + def set_fill_rule(self, fill_rule): + self._attributes['fill-rule'] = fill_rule + + def get_fill_rule(self): + return self._attributes.get('fill-rule') + + def set_stroke(self, stroke): + self._attributes['stroke'] = stroke + + def get_stroke(self): + return self._attributes.get('stroke') + + def set_stroke_dasharray(self, stroke_dasharray): + self._attributes['stroke-dasharray'] = stroke_dasharray + + def get_stroke_dasharray(self): + return self._attributes.get('stroke-dasharray') + + def set_stroke_dashoffset(self, stroke_dashoffset): + self._attributes['stroke-dashoffset'] = stroke_dashoffset + + def get_stroke_dashoffset(self): + return self._attributes.get('stroke-offset') + + def set_stroke_linecap(self, stroke_linecap): + self._attributes['stroke-linecap'] = stroke_linecap + + def get_stroke_linecap(self): + return self._attributes.get('stroke-linecap') + + def set_stroke_linejoin(self, stroke_linejoin): + self._attributes['stroke-linejoin'] = stroke_linejoin + + def get_stroke_linejoin(self): + return self._attributes.get('stroke-linejoin') + + def set_stroke_miterlimit(self, stroke_miterlimit): + self._attributes['stroke-miterlimit'] = stroke_miterlimit + + def get_stroke_miterlimit(self): + return self._attributes.get('stroke-miterlimit') + + def set_stroke_width(self, stroke_width): + self._attributes['stroke-width'] = stroke_width + + def get_stroke_width(self): + return self._attributes.get('stroke-width') + +class GraphicsAttrib: + """ + The GraphicsAttrib class defines the Graphics.attrib attribute set. + """ + def set_display(self, display): + self._attributes['display'] = display + + def get_display(self): + return self._attributes.get('display') + + def set_image_rendering(self, image_rendering): + self._attributes['image-rendering'] = image_rendering + + def get_image_rendering(self): + return self._attributes.get('image-rendering') + + def set_pointer_events(self, pointer_events): + self._attributes['pointer-events'] = pointer_events + + def get_pointer_events(self): + return self._attributes.get('pointer-events') + + def set_shape_rendering(self, shape_rendering): + self._attributes['shape-rendering'] = shape_rendering + + def get_shape_rendering(self): + return self._attributes.get('shape-rendering') + + def set_text_rendering(self, text_rendering): + self._attributes['text-rendering'] = text_rendering + + def get_text_rendering(self): + return self._attributes.get('text-rendering') + + def set_visibility(self, visibility): + self._attributes['visibility'] = visibility + + def get_visibility(self): + return self._attributes.get('visibility') + +class MarkerAttrib: + """ + The MarkerAttrib class defines the Marker.attrib attribute set. + """ + def set_marker_start(self, marker_start): + self._attributes['marker-start'] = marker_start + + def get_marker_start(self): + return self._attributes.get('marker-start') + + def set_marker_mid(self, marker_mid): + self._attributes['marker-mid'] = marker_mid + + def get_marker_mid(self): + return self._attributes.get('marker-mid') + + def set_marker_end(self, marker_end): + self._attributes['marker-end'] = marker_end + + def get_marker_end(self): + return self._attributes.get('marker-end') + +class ViewportAttrib: + """ + The ViewportAttrib class defines the Viewport.attrib attribute set. + """ + def set_clip(self, clip): + self._attributes['clip'] = clip + + def get_clip(self): + return self._attributes.get('clip') + + def set_overflow(self, overflow): + self._attributes['overflow'] = overflow + + def get_overflow(self): + return self._attributes.get('overflow') + +class FilterAttrib: + """ + The FilterAttrib class defines the Filter.attrib attribute sets. + """ + def set_filter(self, filter): + self._attributes['filter'] = filter + + def get_filter(self): + return self._attributes.get('filter') + +class FilterColorAttrib: + """ + The FilterColorAttrib class defines the FilterColor.attrib attribute sets. + """ + def set_color_interpolation_filters(self, color_interpolation_filters): + self._attributes['color-interpolation-filters'] = color_interpolation_filters + + def get_color_interpolation_filters(self): + return self._attributes.get('color-interpolation-filters') + +class FilterPrimitiveAttrib: + """ + The FilterPrimitiveAttrib class defines the FilterPrimitive.attrib attribute sets. + """ + def set_x(self, x): + self._attributes['x'] = x + + def get_x(self): + return self._attributes.get('x') + + def set_y(self, y): + self._attributes['y'] = y + + def get_y(self): + return self._attributes.get('y') + + def set_height(self, height): + self._attributes['height'] = height + + def get_height(self): + return self._attributes.get('height') + + def set_width(self, width): + self._attributes['width'] = width + + def get_width(self): + return self._attributes.get('width') + + def set_result(self, result): + self._attributes['result'] = result + + def get_result(self): + return self._attributes.get('result') + +class FilterPrimitiveWithInAttrib(FilterPrimitiveAttrib): + """ + The FilterPrimitiveWithInAttrib class defines the FilterPrimitiveWithIn.attrib attribute sets. + """ + def set_in(self, inValue): + self._attributes['in'] = inValue + + def get_in(self): + return self._attributes.get('in') + +class XLinkAttrib: + """ + The XLinkAttrib class defines the XLink.attrib, XLinkRequired.attrib, XLinkEmbed.attrib and XLinkReplace.attrib attribute sets. + """ + def set_xlink_type(self, xlink_type): + self._attributes['xlink:type'] = xlink_type + def get_xlink_type(self): + return self._attributes['xlink:type'] + + def set_xlink_href(self, xlink_href): + self._attributes['xlink:href'] = xlink_href + def get_xlink_href(self): + return self._attributes['xlink:href'] + + def set_xlink_role(self, xlink_role): + self._attributes['xlink:role'] = xlink_role + def get_xlink_role(self): + return self._attributes['xlink:role'] + + def set_xlink_arcrole(self, xlink_arcrole): + self._attributes['xlink:arcrole'] = xlink_arcrole + def get_xlink_arcrole(self): + return self._attributes['xlink:arcrole'] + + def set_xlink_title(self, xlink_title): + self._attributes['xlink:title'] = xlink_title + def get_xlink_title(self): + return self._attributes['xlink:title'] + + def set_xlink_show(self, xlink_show): + self._attributes['xlink:show'] = xlink_show + def get_xlink_show(self): + return self._attributes['xlink:show'] + + def set_xlink_actuate(self, xlink_actuate): + self._attributes['xlink:actuate'] = xlink_actuate + def get_xlink_actuate(self): + return self._attributes['xlink:actuate'] + +class TextAttrib: + """ + The TextAttrib class defines the Text.attrib attribute set. + """ + def set_writing_mode(self, writing_mode): + self._attributes['writing-mode'] = writing_mode + def get_writing_mode(self): + return self._attributes['writing-mode'] + +class TextContentAttrib: + """ + The TextContentAttrib class defines the TextContent.attrib attribute set. + """ + def set_alignment_baseline(self, alignment_baseline): + self._attributes['alignment-baseline'] = alignment_baseline + def get_alignment_baseline(self): + return self._attributes['alignment-baseline'] + + def set_baseline_shift(self, baseline_shift): + self._attributes['baseline-shift'] = baseline_shift + def get_baseline_shift(self): + return self._attributes['baseline-shift'] + + def set_direction(self, direction): + self._attributes['direction'] = direction + def get_direction(self): + return self._attributes['direction'] + + def set_dominant_baseline(self, dominant_baseline): + self._attributes['dominant-baseline'] = dominant_baseline + def get_dominant_baseline(self): + return self._attributes['dominant-baseline'] + + def set_glyph_orientation_horizontal(self, glyph_orientation_horizontal): + self._attributes['glyph-orientation-horizontal'] = glyph_orientation_horizontal + def get_glyph_orientation_horizontal(self): + return self._attributes['glyph-orientation-horizontal'] + + def set_glyph_orientation_vertical(self, glyph_orientation_vertical): + self._attributes['glyph-orientation-vertical'] = glyph_orientation_vertical + def get_glyph_orientation_vertical(self): + return self._attributes['glyph-orientation-vertical'] + + def set_kerning(self, kerning): + self._attributes['kerning'] = kerning + def get_kerning(self): + return self._attributes['kerning'] + + def set_letter_spacing(self, letter_spacing): + self._attributes['letter-spacing'] = letter_spacing + def get_letter_spacing(self): + return self._attributes['letter-spacing'] + + def set_text_anchor(self, text_anchor): + self._attributes['text-anchor'] = text_anchor + def get_text_anchor(self): + return self._attributes['text-anchor'] + + def set_text_decoration(self, text_decoration): + self._attributes['text-decoration'] = text_decoration + def get_text_decoration(self): + return self._attributes['text-decoration'] + + def set_unicode_bidi(self, unicode_bidi): + self._attributes['unicode-bidi'] = unicode_bidi + def get_unicode_bidi(self): + return self._attributes['unicode-bidi'] + + def set_word_spacing(self, word_spacing): + self._attributes['word-spacing'] = word_spacing + def get_word_spacing(self): + return self._attributes['word-spacing'] + +class FontAttrib: + """ + The FontAttrib class defines the Font.attrib attribute set. + """ + def set_font_family(self, font_family): + self._attributes['font-family'] = font_family + def get_font_family(self): + return self._attributes['font-family'] + + def set_font_size(self, font_size): + self._attributes['font-size'] = font_size + def get_font_size(self): + return self._attributes['font-size'] + + def set_font_size_adjust(self, font_size_adjust): + self._attributes['font-size-adjust'] = font_size_adjust + def get_font_size_adjust(self): + return self._attributes['font-size-adjust'] + + def set_font_stretch(self, font_stretch): + self._attributes['font-stretch'] = font_stretch + def get_font_stretch(self): + return self._attributes['font-stretch'] + + def set_font_style(self, font_style): + self._attributes['font-style'] = font_style + def get_font_style(self): + return self._attributes['font-style'] + + def set_font_variant(self, font_variant): + self._attributes['font-variant'] = font_variant + def get_font_variant(self): + return self._attributes['font-variant'] + + def set_font_weight(self, font_weight): + self._attributes['font-weight'] = font_weight + def get_font_weight(self): + return self._attributes['font-weight'] + +class MaskAttrib: + """ + The MaskAttrib class defines the Mask.attrib attribute set. + """ + def set_mask(self, mask): + self._attributes['mask'] = mask + + def get_mask(self): + return self._attributes.get('mask') + +class ClipAttrib: + """ + The ClipAttrib class defines the Clip.attrib attribute set. + """ + def set_clip_path(self, clip_path): + self._attributes['clip-path'] = clip_path + + def get_clip_path(self): + return self._attributes.get('clip-path') + + def set_clip_rule(self, clip_rule): + self._attributes['clip-rule'] = clip_rule + + def get_clip_rule(self): + return self._attributes.get('clip-rule') + +class GradientAttrib: + """ + The GradientAttrib class defines the Gradient.attrib attribute set. + """ + def set_stop_color(self, stop_color): + self._attributes['stop-color'] = stop_color + + def get_stop_color(self): + return self._attributes.get('stop-color') + + def set_stop_opacity(self, stop_opacity): + self._attributes['stop-opacity'] = stop_opacity + + def get_stop_opacity(self): + return self._attributes.get('stop-opacity') + +class PresentationAttributes_Color: + """ + The PresentationAttributes_Color class defines the PresentationAttributes_Color.attrib attribute set. + The following presentation attributes have to do with specifying color. + """ + def set_color(self, color): + self._attributes['color'] = color + + def get_color(self): + return self._attributes.get('color') + + def set_color_interpolation(self, color_interpolation): + self._attributes['color-interpolation'] = color_interpolation + + def get_color_interpolation(self): + return self._attributes.get('color-interpolation') + + def set_color_rendering(self, color_rendering): + self._attributes['color-rendering'] = color_rendering + + def get_color_rendering(self): + return self._attributes.get('color-rendering') + +class PresentationAttributes_Containers: + """ + The PresentationAttributes_Containers class defines the PresentationAttributes_Containers.attrib attribute set. + The following presentation attributes apply to container elements. + """ + def set_enable_background(self, enableBackground): + self._attributes['enable-background'] = enableBackground + + def get_enable_background(self): + return self._attributes.get('enable-background') + +class PresentationAttributes_feFlood: + """ + The PresentationAttributes_feFlood class defines the PresentationAttributes_feFlood.attrib attribute set. + The following presentation attributes apply to 'feFlood' elements. + """ + def set_flood_color(self, flood_color): + self._attributes['flood-color'] = flood_color + def get_flood_color(self): + return self._attributes.get('flood-color') + + def set_flood_opacity(self, flood_opacity): + self._attributes['flood-opacity'] = flood_opacity + def get_flood_opacity(self): + return self._attributes.get('flood-opacity') + +class PresentationAttributes_FilterPrimitives: + """ + The PresentationAttributes_FilterPrimitives class defines the PresentationAttributes_FilterPrimitives.attrib attribute set. + The following presentation attributes apply to filter primitives + """ + def set_color_interpolation_filters(self, color_interpolation_filters): + self._attributes['color-interpolation-filters'] = color_interpolation_filters + def get_color_interpolation_filters(self): + return self._attributes.get('color-interpolation-filters') + +class PresentationAttributes_FillStroke: + """ + The PresentationAttributes_FillStroke class defines the PresentationAttributes_FillStroke.attrib attribute set. + The following presentation attributes apply to filling and stroking operations. + """ + def set_fill(self, fill): + self._attributes['fill'] = fill + + def get_fill(self): + return self._attributes.get('fill') + + def set_fill_opacity(self, fill_opacity): + self._attributes['fill-opacity'] = fill_opacity + + def get_fill_opacity(self): + return self._attributes.get('fill-opacity') + + def set_fill_rule(self, fill_rule): + self._attributes['fill-rule'] = fill_rule + + def get_fill_rule(self): + return self._attributes.get('fill-rule') + + def set_stroke(self, stroke): + self._attributes['stroke'] = stroke + + def get_stroke(self): + return self._attributes.get('stroke') + + def set_stroke_opacity(self, stroke_opacity): + self._attributes['stroke-opacity'] = stroke_opacity + + def get_stroke_opacity(self): + return self._attributes.get('stroke-opacity') + + def set_stroke_dasharray(self, stroke_dasharray): + self._attributes['stroke-dasharray'] = stroke_dasharray + + def get_stroke_dasharray(self): + return self._attributes.get('stroke-dasharray') + + def set_stroke_dashoffset(self, stroke_dashoffset): + self._attributes['stroke-dashoffset'] = stroke_dashoffset + + def get_stroke_dashoffset(self): + return self._attributes.get('stroke-offset') + + def set_stroke_linecap(self, stroke_linecap): + self._attributes['stroke-linecap'] = stroke_linecap + + def get_stroke_linecap(self): + return self._attributes.get('stroke-linecap') + + def set_stroke_linejoin(self, stroke_linejoin): + self._attributes['stroke-linejoin'] = stroke_linejoin + + def get_stroke_linejoin(self): + return self._attributes.get('stroke-linejoin') + + def set_stroke_miterlimit(self, stroke_miterlimit): + self._attributes['stroke-miterlimit'] = stroke_miterlimit + + def get_stroke_miterlimit(self): + return self._attributes.get('stroke-miterlimit') + + def set_stroke_width(self, stroke_width): + self._attributes['stroke-width'] = stroke_width + + def get_stroke_width(self): + return self._attributes.get('stroke-width') + +class PresentationAttributes_FontSpecification(FontAttrib): + """ + The PresentationAttributes_FontSpecification class defines the PresentationAttributes_FontSpecification.attrib attribute set. + The following presentation attributes have to do with selecting a font to use. + """ + +class PresentationAttributes_Gradients(GradientAttrib): + """ + The PresentationAttributes_Gradients class defines the PresentationAttributes_Gradients.attrib attribute set. + The following presentation attributes apply to gradient 'stop' elements. + """ + +class PresentationAttributes_Graphics(ClipAttrib, CursorAttrib, GraphicsAttrib, MaskAttrib, FilterAttrib): + """ + The PresentationAttributes_Graphics class defines the PresentationAttributes_Graphics.attrib attribute set. + The following presentation attributes apply to graphics elements + """ + def set_opacity(self, opacity): + self._attributes['opacity'] = opacity + + def get_opacity(self): + return self._attributes.get('opacity') + +class PresentationAttributes_Images: + """ + The PresentationAttributes_Images class defines the PresentationAttributes_Images.attrib attribute set. + The following presentation attributes apply to 'image' elements + """ + def set_color_profile(self, color_profile): + self._attributes['color-profile'] = color_profile + + def get_color_profile(self): + return self._attributes.get('color-profile') + +class PresentationAttributes_LightingEffects: + """ + The PresentationAttributes_LightingEffects class defines the PresentationAttributes_LightingEffects.attrib attribute set. + The following presentation attributes apply to 'feDiffuseLighting' and 'feSpecularLighting' elements + """ + def set_lighting_color(self, lighting_color): + self._attributes['lighting-color'] = lighting_color + def get_lighting_color(self): + return self._attributes.get('lighting-color') + +class PresentationAttributes_Marker(MarkerAttrib): + """ + The PresentationAttributes_Marker class defines the PresentationAttributes_Marker.attrib attribute set. + The following presentation attributes apply to marker operations + """ + +class PresentationAttributes_TextContentElements(TextContentAttrib): + """ + The PresentationAttributes_TextContentElements class defines the PresentationAttributes_TextContentElements.attrib attribute set. + The following presentation attributes apply to text content elements + """ + +class PresentationAttributes_TextElements(TextAttrib): + """ + The following presentation attributes apply to 'text' elements + """ + +class PresentationAttributes_Viewports(ViewportAttrib): + """ + The following presentation attributes apply to elements that establish viewports + """ + +class PresentationAttributes_All(PresentationAttributes_Color, PresentationAttributes_Containers, PresentationAttributes_feFlood, PresentationAttributes_FillStroke, PresentationAttributes_FilterPrimitives, PresentationAttributes_FontSpecification, PresentationAttributes_Gradients, PresentationAttributes_Graphics, PresentationAttributes_Images, PresentationAttributes_LightingEffects, PresentationAttributes_Marker, PresentationAttributes_TextContentElements, PresentationAttributes_TextElements, PresentationAttributes_Viewports): + """ + The PresentationAttributes_All class defines the Presentation.attrib attribute set. + """ + + +class ColorAttrib: + """ + The ColorAttrib class defines the Color.attrib attribute set. + """ + + diff --git a/appengine/2048-js/pysvg/attributes.pyc b/appengine/2048-js/pysvg/attributes.pyc new file mode 100644 index 0000000..329cf2d Binary files /dev/null and b/appengine/2048-js/pysvg/attributes.pyc differ diff --git a/appengine/2048-js/pysvg/builders.py b/appengine/2048-js/pysvg/builders.py new file mode 100644 index 0000000..43ada5a --- /dev/null +++ b/appengine/2048-js/pysvg/builders.py @@ -0,0 +1,359 @@ +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +''' +(C) 2008, 2009 Kerim Mansour +For licensing information please refer to license.txt +''' +from pysvg.animate import * +from pysvg.filter import * +from pysvg.gradient import * +from pysvg.linking import * +from pysvg.script import * +from pysvg.shape import * +from pysvg.structure import * +from pysvg.style import * +from pysvg.text import * + +class ShapeBuilder: + """ + Helper class that creates commonly used objects and shapes with predefined styles and + few but often used parameters. Used to avoid more complex coding for common tasks. + """ + + def createCircle(self, cx, cy, r, strokewidth=1, stroke='black', fill='none'): + """ + Creates a circle + @type cx: string or int + @param cx: starting x-coordinate + @type cy: string or int + @param cy: starting y-coordinate + @type r: string or int + @param r: radius + @type strokewidth: string or int + @param strokewidth: width of the pen used to draw + @type stroke: string (either css constants like "black" or numerical values like "#FFFFFF") + @param stroke: color with which to draw the outer limits + @type fill: string (either css constants like "black" or numerical values like "#FFFFFF") + @param fill: color with which to fill the element (default: no filling) + @return: a circle object + """ + style_dict = {'fill':fill, 'stroke-width':strokewidth, 'stroke':stroke} + myStyle = StyleBuilder(style_dict) + c = Circle(cx, cy, r) + c.set_style(myStyle.getStyle()) + return c + + def createEllipse(self, cx, cy, rx, ry, strokewidth=1, stroke='black', fill='none'): + """ + Creates an ellipse + @type cx: string or int + @param cx: starting x-coordinate + @type cy: string or int + @param cy: starting y-coordinate + @type rx: string or int + @param rx: radius in x direction + @type ry: string or int + @param ry: radius in y direction + @type strokewidth: string or int + @param strokewidth: width of the pen used to draw + @type stroke: string (either css constants like "black" or numerical values like "#FFFFFF") + @param stroke: color with which to draw the outer limits + @type fill: string (either css constants like "black" or numerical values like "#FFFFFF") + @param fill: color with which to fill the element (default: no filling) + @return: an ellipse object + """ + style_dict = {'fill':fill, 'stroke-width':strokewidth, 'stroke':stroke} + myStyle = StyleBuilder(style_dict) + e = Ellipse(cx, cy, rx, ry) + e.set_style(myStyle.getStyle()) + return e + + def createRect(self, x, y, width, height, rx=None, ry=None, strokewidth=1, stroke='black', fill='none'): + """ + Creates a Rectangle + @type x: string or int + @param x: starting x-coordinate + @type y: string or int + @param y: starting y-coordinate + @type width: string or int + @param width: width of the rectangle + @type height: string or int + @param height: height of the rectangle + @type rx: string or int + @param rx: For rounded rectangles, the x-axis radius of the ellipse used to round off the corners of the rectangle. + @type ry: string or int + @param ry: For rounded rectangles, the y-axis radius of the ellipse used to round off the corners of the rectangle. + @type strokewidth: string or int + @param strokewidth: width of the pen used to draw + @type stroke: string (either css constants like "black" or numerical values like "#FFFFFF") + @param stroke: color with which to draw the outer limits + @type fill: string (either css constants like "black" or numerical values like "#FFFFFF") + @param fill: color with which to fill the element (default: no filling) + @return: a rect object + """ + style_dict = {'fill':fill, 'stroke-width':strokewidth, 'stroke':stroke} + myStyle = StyleBuilder(style_dict) + r = Rect(x, y, width, height, rx, ry) + r.set_style(myStyle.getStyle()) + return r + + def createPolygon(self, points, strokewidth=1, stroke='black', fill='none'): + """ + Creates a Polygon + @type points: string in the form "x1,y1 x2,y2 x3,y3" + @param points: all points relevant to the polygon + @type strokewidth: string or int + @param strokewidth: width of the pen used to draw + @type stroke: string (either css constants like "black" or numerical values like "#FFFFFF") + @param stroke: color with which to draw the outer limits + @type fill: string (either css constants like "black" or numerical values like "#FFFFFF") + @param fill: color with which to fill the element (default: no filling) + @return: a polygon object + """ + style_dict = {'fill':fill, 'stroke-width':strokewidth, 'stroke':stroke} + myStyle = StyleBuilder(style_dict) + p = Polygon(points=points) + p.set_style(myStyle.getStyle()) + return p + + def createPolyline(self, points, strokewidth=1, stroke='black'): + """ + Creates a Polyline + @type points: string in the form "x1,y1 x2,y2 x3,y3" + @param points: all points relevant to the polygon + @type strokewidth: string or int + @param strokewidth: width of the pen used to draw + @type stroke: string (either css constants like "black" or numerical values like "#FFFFFF") + @param stroke: color with which to draw the outer limits + @return: a polyline object + """ + style_dict = {'fill':'none', 'stroke-width':strokewidth, 'stroke':stroke} + myStyle = StyleBuilder(style_dict) + p = Polyline(points=points) + p.set_style(myStyle.getStyle()) + return p + + + def createLine(self, x1, y1, x2, y2, strokewidth=1, stroke="black"): + """ + Creates a line + @type x1: string or int + @param x1: starting x-coordinate + @type y1: string or int + @param y1: starting y-coordinate + @type x2: string or int + @param x2: ending x-coordinate + @type y2: string or int + @param y2: ending y-coordinate + @type strokewidth: string or int + @param strokewidth: width of the pen used to draw + @type stroke: string (either css constants like "black" or numerical values like "#FFFFFF") + @param stroke: color with which to draw the outer limits + @return: a line object + """ + style_dict = {'stroke-width':strokewidth, 'stroke':stroke} + myStyle = StyleBuilder(style_dict) + l = Line(x1, y1, x2, y2) + l.set_style(myStyle.getStyle()) + return l + + def convertTupleArrayToPoints(self, arrayOfPointTuples): + """Method used to convert an array of tuples (x,y) into a string + suitable for createPolygon or createPolyline + @type arrayOfPointTuples: An array containing tuples eg.[(x1,y1),(x2,y2] + @param arrayOfPointTuples: All points needed to create the shape + @return a string in the form "x1,y1 x2,y2 x3,y3" + """ + points = "" + for tuple in arrayOfPointTuples: + points += str(tuple[0]) + "," + str(tuple[1]) + " " + return points + + + +###################################################################### +# Style Builder. Utility class to create styles for your shapes etc. +###################################################################### +class StyleBuilder: + """ + Class to create a style string for those not familiar with svg attribute names. + How to use it: + 1) create an instance of StyleBuilder (builder=....) + 2) set the attributes you want to have + 3) create the shape (element) you want + 4) call set_style on the element with "builder.getStyle()" as parameter + """ + def __init__(self, aStyle_dict=None): + if aStyle_dict == None: + self.style_dict = {} + else: + self.style_dict = aStyle_dict + + + # tested below + def setFontFamily(self, fontfamily): + self.style_dict["font-family"] = fontfamily + + def setFontSize(self, fontsize): + self.style_dict["font-size"] = fontsize + + def setFontStyle(self, fontstyle): + self.style_dict["font-style"] = fontstyle + + def setFontWeight(self, fontweight): + self.style_dict["font-weight"] = fontweight + + #tested + def setFilling(self, fill): + self.style_dict["fill"] = fill + + def setFillOpacity(self, fillopacity): + self.style_dict["fill-opacity"] = fillopacity + + def setFillRule(self, fillrule): + self.style_dict["fill-rule"] = fillrule + + def setStrokeWidth(self, strokewidth): + self.style_dict["stroke-width"] = strokewidth + + def setStroke(self, stroke): + self.style_dict["stroke"] = stroke + + #untested below + def setStrokeDashArray(self, strokedasharray): + self.style_dict["stroke-dasharray"] = strokedasharray + def setStrokeDashOffset(self, strokedashoffset): + self.style_dict["stroke-dashoffset"] = strokedashoffset + def setStrokeLineCap(self, strikelinecap): + self.style_dict["stroke-linecap"] = strikelinecap + def setStrokeLineJoin(self, strokelinejoin): + self.style_dict["stroke-linejoin"] = strokelinejoin + def setStrokeMiterLimit(self, strokemiterlimit): + self.style_dict["stroke-miterlimit"] = strokemiterlimit + def setStrokeOpacity(self, strokeopacity): + self.style_dict["stroke-opacity"] = strokeopacity + + + #is used to provide a potential indirect value (currentColor) for the 'fill', 'stroke', 'stop-color' properties. + def setCurrentColor(self, color): + self.style_dict["color"] = color + + # Gradient properties: + def setStopColor(self, stopcolor): + self.style_dict["stop-color"] = stopcolor + + def setStopOpacity(self, stopopacity): + self.style_dict["stop-opacity"] = stopopacity + + #rendering properties + def setColorRendering(self, colorrendering): + self.style_dict["color-rendering"] = colorrendering + + def setImageRendering(self, imagerendering): + self.style_dict["image-rendering"] = imagerendering + + def setShapeRendering(self, shaperendering): + self.style_dict["shape-rendering"] = shaperendering + + def setTextRendering(self, textrendering): + self.style_dict["text-rendering"] = textrendering + + def setSolidColor(self, solidcolor): + self.style_dict["solid-color"] = solidcolor + + def setSolidOpacity(self, solidopacity): + self.style_dict["solid-opacity"] = solidopacity + + #Viewport properties + def setVectorEffect(self, vectoreffect): + self.style_dict["vector-effect"] = vectoreffect + + def setViewPortFill(self, viewportfill): + self.style_dict["viewport-fill"] = viewportfill + + def setViewPortOpacity(self, viewportfillopacity): + self.style_dict["viewport-fill_opacity"] = viewportfillopacity + + # Text properties + def setDisplayAlign(self, displayalign): + self.style_dict["display-align"] = displayalign + + def setLineIncrement(self, lineincrement): + self.style_dict["line-increment"] = lineincrement + + def setTextAnchor(self, textanchor): + self.style_dict["text-anchor"] = textanchor + + #def getStyleDict(self): + # return self.style_dict + + + def getStyle(self): + string = ''#style="' + for key, value in self.style_dict.items(): + if value <> None and value <> '': + string += str(key) + ':' + str(value) + '; ' + return string + +###################################################################### +# Transform Builder. Utility class to create transformations for your shapes etc. +###################################################################### +class TransformBuilder: + """ + Class to create a transform string for those not familiar with svg attribute names. + How to use it: + 1) create an instance of TransformBuilder (builder=....) + 2) set the attributes you want to have + 3) create the shape (element) you want + 4) call set_transform on the element with "builder.getTransform()" as parameter + """ + def __init__(self): + self.transform_dict = {} + + #def setMatrix(self, matrix): + # self.transform_dict["matrix"] = 'matrix(%s)' % matrix + + def setMatrix(self, a, b, c, d, e, f): + self.transform_dict["matrix"] = 'matrix(%s %s %s %s %s %s)' % (a, b, c, d, e, f) + + def setRotation(self, rotate): + self.transform_dict["rotate"] = 'rotate(%s)' % rotate + + #def setRotation(self, rotation, cx=None, cy=None): + # if cx != None and cy != None: + # self.transform_dict["rotate"] = 'rotate(%s %s %s)' % (rotation, cx, cy) + # else: + # self.transform_dict["rotate"] = 'rotate(%s)' % (rotation) + + def setTranslation(self, translate): + self.transform_dict["translate"] = 'translate(%s)' % (translate) + + #def setTranslation(self, x, y=0): + # self.transform_dict["translate"] = 'translate(%s %s)' % (x, y) + + #def setScaling(self, scale): + # self.transform_dict["scale"] = 'scale(%s)' % (scale) + + def setScaling(self, x=None, y=None): + if x == None and y != None: + x = y + elif x != None and y == None: + y = x + self.transform_dict["scale"] = 'scale(%s %s)' % (x, y) + + def setSkewY(self, skewY): + self.transform_dict["skewY"] = 'skewY(%s)' % (skewY) + + def setSkewX(self, skewX): + self.transform_dict["skewX"] = 'skewX(%s)' % (skewX) + + #def getTransformDict(self): + # return self.transform_dict + + def getTransform(self): + string = ''#style="' + for key, value in self.transform_dict.items(): + if value <> None and value <> '': + #string+=str(key)+':'+str(value)+'; ' + string += str(value) + ' ' + return string diff --git a/appengine/2048-js/pysvg/builders.pyc b/appengine/2048-js/pysvg/builders.pyc new file mode 100644 index 0000000..c307a4a Binary files /dev/null and b/appengine/2048-js/pysvg/builders.pyc differ diff --git a/appengine/2048-js/pysvg/core.py b/appengine/2048-js/pysvg/core.py new file mode 100644 index 0000000..b2d2e71 --- /dev/null +++ b/appengine/2048-js/pysvg/core.py @@ -0,0 +1,265 @@ +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +''' +(C) 2008, 2009 Kerim Mansour +For licensing information please refer to license.txt +''' +from attributes import CoreAttrib, ConditionalAttrib, StyleAttrib, GraphicalEventsAttrib, PaintAttrib, OpacityAttrib, GraphicsAttrib, CursorAttrib, FilterAttrib, MaskAttrib, ClipAttrib +import codecs + +class BaseElement: + """ + This is the base class for all svg elements like title etc. It provides common functionality. + It should NOT be directly used by anyone. + """ + def __init__(self, elementName): + """ + initializes the object + @type elementName: string + @param elementName: name of the element (used for the xml tag) + """ + self._elementName=elementName + self._attributes={} #key value + self._textContent="" + self._subElements=[] + + def appendTextContent(self,text): + self.addElement(TextContent(text)) + + def addElement(self,element): + self._subElements.append(element) + + def getElementAt(self,pos): + """ returns the element at a specific position within this svg + """ + return self._subElements[pos] + + def getAllElements(self): + """ returns all elements contained within the top level element list of this element + """ + return self._subElements + + def getAllElementsOfHirarchy(self): + """ returns ALL elements of the complete hirarchy as a flat list + """ + allElements=[] + for element in self.getAllElements(): + allElements.append(element) + if isinstance(element, BaseElement): + allElements.extend(element.getAllElementsOfHirarchy()) + return allElements + + def getElementByID(self, id): + """ returns an element with the specific id and the position of that element within the svg elements array + """ + pos=0 + for element in self._subElements: + if element.get_id()==id: + return (element,pos) + pos+=1 + + def getElementsByType(self, type): + """ + retrieves all Elements that are of type type + @type type: class + @param type: type of the element + """ + foundElements=[] + for element in self.getAllElementsOfHirarchy(): + if isinstance(element, type): + foundElements.append(element) + + return foundElements + + def insertElementAt(self, element, pos): + return self._subElements.insert(pos, element) + + + def getXML(self): + """ + Return a XML representation of the current element. + This function can be used for debugging purposes. It is also used by getXML in SVG + + @return: the representation of the current element as an xml string + """ + xml='<'+self._elementName+' ' + for key,value in self._attributes.items(): + if value != None: + xml+=key+'="'+self.quote_attrib(str(value))+'" ' + if len(self._subElements)==0: #self._textContent==None and + xml+=' />\n' + else: + xml+=' >\n' + #if self._textContent==None: + for subelement in self._subElements: + s = subelement.getXML() + if type(s) != unicode: + s = str(s) + xml+=s + # xml+=str(subelement.getXML()) + #else: + #if self._textContent!=None: + # xml+=self._textContent + xml+='\n' + #print xml + return xml + + #generic methods to set and get atributes (should only be used if something is not supported yet + def setAttribute(self, attribute_name, attribute_value): + self._attributes[attribute_name]=attribute_value + + def getAttribute(self, attribute_name): + return self._attributes.get(attribute_name) + + def getAttributes(self): + """ get all atributes of the element + """ + return self._attributes + + def setKWARGS(self, **kwargs): + """ + Used to set all attributes given in a **kwargs parameter. + Might throw an Exception if attribute was not found. + #TODO: check if we should fix this using "setAttribute" + """ + for key in kwargs.keys(): + #try: + f = getattr(self,'set_' + key) + f(kwargs[key]) + #except: + # print('attribute not found via setter ') + # self.setAttribute(self, key, kwargs[key]) + + def wrap_xml(self, xml, encoding ='ISO-8859-1', standalone='no'): + """ + Method that provides a standard svg header string for a file + """ + header = '''''' %(encoding, standalone) + return header+xml + + def save(self, filename, encoding ='ISO-8859-1', standalone='no'): + """ + Stores any element in a svg file (including header). + Calling this method only makes sense if the root element is an svg elemnt + """ + f = codecs.open(filename, 'w', encoding) + s = self.wrap_xml(self.getXML(), encoding, standalone) + #s = s.replace("&", "&") + f.write(s) + f.close() + #f = open(filename, 'w') + #f.write(self.wrap_xml(self.getXML(), encoding, standalone)) + #f.close() + + def quote_attrib(self, inStr): + """ + Transforms characters between xml notation and python notation. + """ + s1 = (isinstance(inStr, basestring) and inStr or + '%s' % inStr) + s1 = s1.replace('&', '&') + s1 = s1.replace('<', '<') + s1 = s1.replace('>', '>') + if '"' in s1: + # if "'" in s1: + s1 = '%s' % s1.replace('"', """) + # else: + # s1 = "'%s'" % s1 + #else: + # s1 = '"%s"' % s1 + return s1 + +class TextContent: + """ + Class for the text content of an xml element. Can also include PCDATA + """ + def __init__(self,content): + self.content=content + def setContent(self,content): + self.content=content + def getXML(self): + return self.content + def get_id(self): + return None + +#--------------------------------------------------------------------------# +# Below are classes that define attribute sets that pysvg uses for convenience. +# There exist no corresponding attribute sets in svg. +# We simply use these classes as containers for often used attributes. +#--------------------------------------------------------------------------# +class PointAttrib: + """ + The PointAttrib class defines x and y. + """ + def set_x(self, x): + self._attributes['x']=x + def get_x(self): + return self._attributes.get('x') + + def set_y(self, y): + self._attributes['y']=y + def get_y(self): + return self._attributes.get('y') + +class DeltaPointAttrib: + """ + The DeltaPointAttrib class defines dx and dy. + """ + def set_dx(self, dx): + self._attributes['dx']=dx + def get_dx(self): + return self._attributes.get('dx') + + def set_dy(self, dy): + self._attributes['dy']=dy + def get_dy(self): + return self._attributes.get('dy') + +class PointToAttrib: + """ + The PointToAttrib class defines x2 and y2. + """ + def set_x2(self, x2): + self._attributes['x2']=x2 + def get_x2(self): + return self._attributes.get('x2') + + def set_y2(self, y2): + self._attributes['y2']=y2 + def get_y2(self): + return self._attributes.get('y2') + +class DimensionAttrib: + """ + The DimensionAttrib class defines height and width. + """ + def set_height(self, height): + self._attributes['height']=height + + def get_height(self): + return self._attributes.get('height') + + def set_width(self, width): + self._attributes['width']=width + + def get_width(self): + return self._attributes.get('width') + +class RotateAttrib: + """ + The RotateAttrib class defines rotation. + """ + def set_rotate(self, rotate): + self._attributes['rotate']=rotate + + def get_rotate(self): + return self._attributes.get('rotate') + +class BaseShape(BaseElement, CoreAttrib, ConditionalAttrib, StyleAttrib, GraphicalEventsAttrib, PaintAttrib, OpacityAttrib, GraphicsAttrib, CursorAttrib, FilterAttrib, MaskAttrib, ClipAttrib): + """ + Baseclass for all shapes. Do not use this class directly. There is no svg element for it + """ + def set_transform(self, transform): + self._attributes['transform']=transform + def get_transform(self): + return self._attributes.get('transform') diff --git a/appengine/2048-js/pysvg/core.pyc b/appengine/2048-js/pysvg/core.pyc new file mode 100644 index 0000000..a72c887 Binary files /dev/null and b/appengine/2048-js/pysvg/core.pyc differ diff --git a/appengine/2048-js/pysvg/filter.py b/appengine/2048-js/pysvg/filter.py new file mode 100644 index 0000000..e75fbde --- /dev/null +++ b/appengine/2048-js/pysvg/filter.py @@ -0,0 +1,600 @@ +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +''' +(C) 2008, 2009 Kerim Mansour +For licensing information please refer to license.txt +''' +from attributes import * +from core import BaseElement, DeltaPointAttrib, PointAttrib, DimensionAttrib + +class Filter(BaseElement, CoreAttrib, XLinkAttrib, ExternalAttrib, StyleAttrib, PresentationAttributes_All, PointAttrib, DimensionAttrib): + """ + Class representing the filter element of an svg doc. + """ + def __init__(self, x=None, y=None, width=None, height=None, filterRes=None, filterUnits=None, primitiveUnits=None, **kwargs): + BaseElement.__init__(self, 'filter') + self.set_x(x) + self.set_y(y) + self.set_height(height) + self.set_width(width) + self.set_filterRes(filterRes) + self.set_filterUnits(filterUnits) + self.set_primitiveUnits(primitiveUnits) + self.setKWARGS(**kwargs) + + def set_filterUnits(self, filterUnits): + self._attributes['filterUnits'] = filterUnits + def get_filterUnits(self): + return self._attributes.get('filterUnits') + + def set_primitiveUnits(self, primitiveUnits): + self._attributes['primitiveUnits'] = primitiveUnits + def get_primitiveUnits(self): + return self._attributes.get('primitiveUnits') + + def set_filterRes(self, filterRes): + self._attributes['filterRes'] = filterRes + def get_filterRes(self): + return self._attributes.get('filterRes') + +class FeComponentTransfer(BaseElement, CoreAttrib, FilterColorAttrib, FilterPrimitiveWithInAttrib): + """ + Class representing the feComponentTransfer element of an svg doc. + """ + def __init__(self, **kwargs): + BaseElement.__init__(self, 'feComponentTransfer') + self.setKWARGS(**kwargs) + + +class FeBlend(FeComponentTransfer): + """ + Class representing the feBlend element of an svg doc. + """ + def __init__(self, in2=None, mode=None, **kwargs): + BaseElement.__init__(self, 'feBlend') + self.set_in2(in2) + self.set_mode(mode) + self.setKWARGS(**kwargs) + + def set_in2(self, in2): + self._attributes['in2'] = in2 + def get_in2(self): + return self._attributes.get('in2') + + def set_mode(self, mode): + self._attributes['mode'] = mode + def get_mode(self): + return self._attributes.get('mode') + +class FeColorMatrix(FeComponentTransfer): + """ + Class representing the feColorMatrix element of an svg doc. + """ + def __init__(self, type=None, values=None, **kwargs): + BaseElement.__init__(self, 'feColorMatrix') + self.set_type(type) + self.set_values(values) + self.setKWARGS(**kwargs) + + def set_type(self, type): + self._attributes['type'] = type + def get_type(self): + return self._attributes.get('type') + + def set_values(self, values): + self._attributes['values'] = values + def get_values(self): + return self._attributes.get('values') + +class FeComposite(FeComponentTransfer): + """ + Class representing the feComposite element of an svg doc. + """ + def __init__(self, in2=None, operator=None, k1=None, k2=None, k3=None, k4=None, **kwargs): + BaseElement.__init__(self, 'feComposite') + self.set_in2(in2) + self.set_k1(k1) + self.set_k2(k2) + self.set_k3(k3) + self.set_k4(k4) + self.set_operator(operator) + self.setKWARGS(**kwargs) + + def set_in2(self, in2): + self._attributes['in2'] = in2 + def get_in2(self): + return self._attributes.get('in2') + + def set_operator(self, operator): + self._attributes['operator'] = operator + def get_operator(self): + return self._attributes.get('operator') + + def set_k1(self, k1): + self._attributes['k1'] = k1 + def get_k1(self): + return self._attributes.get('k1') + + def set_k2(self, k2): + self._attributes['k2'] = k2 + def get_k2(self): + return self._attributes.get('k2') + + def set_k3(self, k3): + self._attributes['k3'] = k3 + def get_k3(self): + return self._attributes.get('k3') + + def set_k4(self, k4): + self._attributes['k4'] = k4 + def get_k4(self): + return self._attributes.get('k4') + +class FeConvolveMatrix(FeComponentTransfer): + """ + Class representing the feConvolveMatrix element of an svg doc. + """ + def __init__(self, order=None, kernelMatrix=None, divisor=None, bias=None, targetX=None, targetY=None, edgeMode=None, kernelUnitLength=None, preserveAlpha=None, **kwargs): + BaseElement.__init__(self, 'feConvolveMatrix') + self.set_order(order) + self.set_kernelMatrix(kernelMatrix) + self.set_divisor(divisor) + self.set_bias(bias) + self.set_targetX(targetX) + self.set_targetY(targetY) + self.set_edgeMode(edgeMode) + self.set_kernelUnitLength(kernelUnitLength) + self.set_preserveAlpha(preserveAlpha) + self.setKWARGS(**kwargs) + + def set_order(self, order): + self._attributes['order'] = order + def get_order(self): + return self._attributes.get('order') + + def set_kernelMatrix(self, kernelMatrix): + self._attributes['kernelMatrix'] = kernelMatrix + def get_kernelMatrix(self): + return self._attributes.get('kernelMatrix') + + def set_divisor(self, divisor): + self._attributes['divisor'] = divisor + def get_divisor(self): + return self._attributes.get('divisor') + + def set_bias(self, bias): + self._attributes['bias'] = bias + def get_bias(self): + return self._attributes.get('bias') + + def set_targetX(self, targetX): + self._attributes['targetX'] = targetX + def get_targetX(self): + return self._attributes.get('targetX') + + def set_targetY(self, targetY): + self._attributes['targetY'] = targetY + def get_targetY(self): + return self._attributes.get('targetY') + + def set_edgeMode(self, edgeMode): + self._attributes['edgeMode'] = edgeMode + def get_edgeMode(self): + return self._attributes.get('edgeMode') + + def set_kernelUnitLength(self, kernelUnitLength): + self._attributes['kernelUnitLength'] = kernelUnitLength + def get_kernelUnitLength(self): + return self._attributes.get('kernelUnitLength') + + def set_preserveAlpha(self, preserveAlpha): + self._attributes['preserveAlpha'] = preserveAlpha + def get_preserveAlpha(self): + return self._attributes.get('preserveAlpha') + +class FeDiffuseLighting(FeComponentTransfer, StyleAttrib, PaintAttrib, PresentationAttributes_LightingEffects): + """ + Class representing the feDiffuseLighting element of an svg doc. + """ + def __init__(self, surfaceScale=None, diffuseConstant=None, kernelUnitLength=None , **kwargs): + BaseElement.__init__(self, 'feDiffuseLighting') + self.set_surfaceScale(surfaceScale) + self.set_diffuseConstant(diffuseConstant) + self.set_kernelUnitLength(kernelUnitLength) + self.setKWARGS(**kwargs) + + def set_surfaceScale(self, surfaceScale): + self._attributes['surfaceScale'] = surfaceScale + def get_surfaceScale(self): + return self._attributes.get('surfaceScale') + + def set_diffuseConstant(self, diffuseConstant): + self._attributes['diffuseConstant'] = diffuseConstant + def get_diffuseConstant(self): + return self._attributes.get('diffuseConstant') + + def set_kernelUnitLength(self, kernelUnitLength): + self._attributes['kernelUnitLength'] = kernelUnitLength + def get_kernelUnitLength(self): + return self._attributes.get('kernelUnitLength') + +class FeDisplacementMap(FeComponentTransfer): + """ + Class representing the feDisplacementMap element of an svg doc. + """ + def __init__(self, in2=None, scale=None, xChannelSelector=None, yChannelSelector=None, **kwargs): + BaseElement.__init__(self, 'feDisplacementMap') + self.set_in2(in2) + self.set_scale(scale) + self.set_xChannelSelector(xChannelSelector) + self.set_yChannelSelector(yChannelSelector) + self.setKWARGS(**kwargs) + + def set_in2(self, in2): + self._attributes['in2'] = in2 + def get_in2(self): + return self._attributes.get('in2') + + def set_scale(self, scale): + self._attributes['scale'] = scale + def get_scale(self): + return self._attributes.get('scale') + + def set_xChannelSelector(self, xChannelSelector): + self._attributes['xChannelSelector'] = xChannelSelector + def get_xChannelSelector(self): + return self._attributes.get('xChannelSelector') + + def set_yChannelSelector(self, yChannelSelector): + self._attributes['yChannelSelector'] = yChannelSelector + def get_yChannelSelector(self): + return self._attributes.get('yChannelSelector') + +class FeFlood(FeComponentTransfer, StyleAttrib, PaintAttrib, PresentationAttributes_feFlood): + """ + Class representing the feFlood element of an svg doc. + """ + def __init__(self, x=None, y=None, width=None, height=None, flood_color=None, flood_opacity=None, **kwargs): + BaseElement.__init__(self, 'feFlood') + self.set_x(x) + self.set_y(y) + self.set_height(height) + self.set_width(width) + self.set_flood_color(flood_color) + self.set_flood_opacity(flood_opacity) + self.setKWARGS(**kwargs) + +class FeGaussianBlur(FeComponentTransfer): + """ + Class representing the feGaussianBlur element of an svg doc. + """ + def __init__(self, inValue=None, x=None, y=None, width=None, height=None, stdDeviation=None, **kwargs): + BaseElement.__init__(self, 'feGaussianBlur') + self.set_x(x) + self.set_y(y) + self.set_height(height) + self.set_width(width) + self.set_in(inValue) + self.set_stdDeviation(stdDeviation) + self.setKWARGS(**kwargs) + + def set_stdDeviation(self, stdDeviation): + self._attributes['stdDeviation'] = stdDeviation + def get_stdDeviation(self): + return self._attributes.get('stdDeviation') + +class FeImage(BaseElement, CoreAttrib, XLinkAttrib, FilterColorAttrib, FilterPrimitiveAttrib, ExternalAttrib, StyleAttrib, PresentationAttributes_All): + """ + Class representing the feImage element of an svg doc. + """ + def __init__(self, xlink_href=None, x=None, y=None, width=None, height=None, result=None, **kwargs): + BaseElement.__init__(self, 'feImage') + self.set_xlink_href(xlink_href) + self.set_x(x) + self.set_y(y) + self.set_height(height) + self.set_width(width) + self.set_result(result) + self.setKWARGS(**kwargs) + +class FeMerge(BaseElement, CoreAttrib, FilterPrimitiveAttrib): + """ + Class representing the feMerge element of an svg doc. + """ + def __init__(self, x=None, y=None, width=None, height=None, **kwargs): + BaseElement.__init__(self, 'feMerge') + self.set_x(x) + self.set_y(y) + self.set_height(height) + self.set_width(width) + self.setKWARGS(**kwargs) + +class FeMergeNode(BaseElement, CoreAttrib, FilterColorAttrib, FilterPrimitiveWithInAttrib): + """ + Class representing the feMergeNode element of an svg doc. + """ + def __init__(self, inValue=None, **kwargs): + BaseElement.__init__(self, 'feMergeNode') + self.set_in(inValue) + self.setKWARGS(**kwargs) + +class FeMorphology(FeComponentTransfer): + """ + Class representing the feMorphology element of an svg doc. + """ + def __init__(self, x=None, y=None, width=None, height=None, operator=None, radius=None, **kwargs): + BaseElement.__init__(self, 'feMorphology') + self.set_x(x) + self.set_y(y) + self.set_height(height) + self.set_width(width) + self.set_operator(operator) + self.set_radius(radius) + self.setKWARGS(**kwargs) + + def set_operator(self, operator): + self._attributes['operator'] = operator + def get_operator(self): + return self._attributes.get('operator') + + def set_radius(self, radius): + self._attributes['radius'] = radius + def get_radius(self): + return self._attributes.get('radius') + +class FeOffset(FeComponentTransfer, DeltaPointAttrib): + """ + Class representing the feOffset element of an svg doc. + """ + def __init__(self, inValue=None, dx=None, dy=None, **kwargs): + BaseElement.__init__(self, 'feOffset') + self.set_in(inValue) + self.set_dx(dx) + self.set_dy(dy) + self.setKWARGS(**kwargs) + +class FeSpecularLighting(FeComponentTransfer, StyleAttrib, PaintAttrib, PresentationAttributes_LightingEffects): + """ + Class representing the feSpecularLighting element of an svg doc. + """ + def __init__(self, lighting_color=None, surfaceScale=None, specularConstant=None, specularExponent=None, kernelUnitLength=None, **kwargs): + BaseElement.__init__(self, 'feSpecularLighting') + self.set_lighting_color(lighting_color) + self.set_surfaceScale(surfaceScale) + self.set_specularConstant(specularConstant) + self.set_specularExponent(specularExponent) + self.set_kernelUnitLength(kernelUnitLength) + self.setKWARGS(**kwargs) + + def set_surfaceScale(self, surfaceScale): + self._attributes['surfaceScale'] = surfaceScale + def get_surfaceScale(self): + return self._attributes.get('surfaceScale') + + def set_specularConstant(self, specularConstant): + self._attributes['specularConstant'] = specularConstant + def get_specularConstant(self): + return self._attributes.get('specularConstant') + + def set_specularExponent(self, specularExponent): + self._attributes['specularExponent'] = specularExponent + def get_specularExponent(self): + return self._attributes.get('specularExponent') + + def set_kernelUnitLength(self, kernelUnitLength): + self._attributes['kernelUnitLength'] = kernelUnitLength + def get_kernelUnitLength(self): + return self._attributes.get('kernelUnitLength') + +class FeTile(FeComponentTransfer): + """ + Class representing the feTile element of an svg doc. + """ + def __init__(self, **kwargs): + BaseElement.__init__(self, 'feTile') + self.setKWARGS(**kwargs) + +class feTurbulence(BaseElement, CoreAttrib, FilterColorAttrib, FilterPrimitiveAttrib): + """ + Class representing the feTurbulence element of an svg doc. + """ + def __init__(self, **kwargs): + BaseElement.__init__(self, 'feTurbulence') + self.setKWARGS(**kwargs) + + def set_baseFrequency(self, baseFrequency): + self._attributes['baseFrequency'] = baseFrequency + def get_baseFrequency(self): + return self._attributes.get('baseFrequency') + + def set_numOctaves(self, numOctaves): + self._attributes['numOctaves'] = numOctaves + def get_numOctaves(self): + return self._attributes.get('numOctaves') + + def set_seed(self, seed): + self._attributes['seed'] = seed + def get_seed(self): + return self._attributes.get('seed') + + def set_stitchTiles(self, stitchTiles): + self._attributes['stitchTiles'] = stitchTiles + def get_stitchTiles(self): + return self._attributes.get('stitchTiles') + + def set_type(self, type): + self._attributes['type'] = type + def get_type(self): + return self._attributes.get('type') + +class FeDistantLight(BaseElement, CoreAttrib): + """ + Class representing the feDistantLight element of an svg doc. + """ + def __init__(self, azimuth=None, elevation=None, **kwargs): + BaseElement.__init__(self, 'feDistantLight') + self.set_azimuth(azimuth) + self.set_elevation(elevation) + self.setKWARGS(**kwargs) + + def set_azimuth(self, azimuth): + self._attributes['azimuth'] = azimuth + def get_azimuth(self): + return self._attributes.get('azimuth') + + def set_elevation(self, elevation): + self._attributes['elevation'] = elevation + def get_elevation(self): + return self._attributes.get('elevation') + +class FePointLight(BaseElement, CoreAttrib, PointAttrib): + """ + Class representing the fePointLight element of an svg doc. + """ + def __init__(self, x=None, y=None, z=None, **kwargs): + BaseElement.__init__(self, 'fePointLight') + self.set_x(x) + self.set_y(y) + self.set_z(z) + self.setKWARGS(**kwargs) + + def set_z(self, z): + self._attributes['z'] = z + def get_z(self): + return self._attributes.get('z') + +class FeSpotLight(FePointLight): + """ + Class representing the feSpotLight element of an svg doc. + """ + def __init__(self, x=None, y=None, z=None, pointsAtX=None, pointsAtY=None, pointsAtZ=None, specularExponent=None, limitingConeAngle=None, **kwargs): + BaseElement.__init__(self, 'feSpotLight') + self.set_x(x) + self.set_y(y) + self.set_z(z) + self.set_pointsAtX(pointsAtX) + self.set_pointsAtY(pointsAtY) + self.set_pointsAtZ(pointsAtZ) + self.set_specularExponent(specularExponent) + self.set_limitingConeAngle(limitingConeAngle) + self.setKWARGS(**kwargs) + + def set_pointsAtX(self, pointsAtX): + self._attributes['pointsAtX'] = pointsAtX + def get_pointsAtX(self): + return self._attributes.get('pointsAtX') + + def set_pointsAtY(self, pointsAtY): + self._attributes['pointsAtY'] = pointsAtY + def get_pointsAtY(self): + return self._attributes.get('pointsAtY') + + def set_pointsAtZ(self, pointsAtZ): + self._attributes['pointsAtZ'] = pointsAtZ + def get_pointsAtZ(self): + return self._attributes.get('pointsAtZ') + + def set_specularExponent(self, specularExponent): + self._attributes['specularExponent'] = specularExponent + def get_specularExponent(self): + return self._attributes.get('specularExponent') + + def set_limitingConeAngle(self, limitingConeAngle): + self._attributes['limitingConeAngle'] = limitingConeAngle + def get_limitingConeAngle(self): + return self._attributes.get('limitingConeAngle') + +class FeFuncR(BaseElement, CoreAttrib): + """ + Class representing the feFuncR element of an svg doc. + """ + def __init__(self, type=None, tableValues=None, slope=None, intercept=None, amplitude=None, exponent=None, offset=None, **kwargs): + BaseElement.__init__(self, 'feFuncR') + self.set_type(type) + self.set_tableValues(tableValues) + self.set_slope(slope) + self.set_intercept(intercept) + self.set_amplitude(amplitude) + self.set_exponent(exponent) + self.set_offset(offset) + self.setKWARGS(**kwargs) + + def set_type(self, type): + self._attributes['type'] = type + def get_type(self): + return self._attributes.get('type') + + def set_tableValues(self, tableValues): + self._attributes['tableValues'] = tableValues + def get_tableValues(self): + return self._attributes.get('tableValues') + + def set_slope(self, slope): + self._attributes['slope'] = slope + def get_slope(self): + return self._attributes.get('slope') + + def set_intercept(self, intercept): + self._attributes['intercept'] = intercept + def get_intercept(self): + return self._attributes.get('intercept') + + def set_amplitude(self, amplitude): + self._attributes['amplitude'] = amplitude + def get_amplitude(self): + return self._attributes.get('amplitude') + + def set_exponent(self, exponent): + self._attributes['exponent'] = exponent + def get_exponent(self): + return self._attributes.get('exponent') + + def set_offset(self, offset): + self._attributes['offset'] = offset + def get_offset(self): + return self._attributes.get('offset') + +class FeFuncG(FeFuncR): + """ + Class representing the feFuncG element of an svg doc. + """ + def __init__(self, type=None, tableValues=None, slope=None, intercept=None, amplitude=None, exponent=None, offset=None, **kwargs): + BaseElement.__init__(self, 'feFuncG') + self.set_type(type) + self.set_tableValues(tableValues) + self.set_slope(slope) + self.set_intercept(intercept) + self.set_amplitude(amplitude) + self.set_exponent(exponent) + self.set_offset(offset) + self.setKWARGS(**kwargs) + +class FeFuncB(FeFuncR): + """ + Class representing the feFuncB element of an svg doc. + """ + def __init__(self, type=None, tableValues=None, slope=None, intercept=None, amplitude=None, exponent=None, offset=None, **kwargs): + BaseElement.__init__(self, 'feFuncB') + self.set_type(type) + self.set_tableValues(tableValues) + self.set_slope(slope) + self.set_intercept(intercept) + self.set_amplitude(amplitude) + self.set_exponent(exponent) + self.set_offset(offset) + self.setKWARGS(**kwargs) + +class FeFuncA(FeFuncR): + """ + Class representing the feFuncA element of an svg doc. + """ + def __init__(self, type=None, tableValues=None, slope=None, intercept=None, amplitude=None, exponent=None, offset=None, **kwargs): + BaseElement.__init__(self, 'feFuncA') + self.set_type(type) + self.set_tableValues(tableValues) + self.set_slope(slope) + self.set_intercept(intercept) + self.set_amplitude(amplitude) + self.set_exponent(exponent) + self.set_offset(offset) + self.setKWARGS(**kwargs) diff --git a/appengine/2048-js/pysvg/filter.pyc b/appengine/2048-js/pysvg/filter.pyc new file mode 100644 index 0000000..cfb02e8 Binary files /dev/null and b/appengine/2048-js/pysvg/filter.pyc differ diff --git a/appengine/2048-js/pysvg/gradient.py b/appengine/2048-js/pysvg/gradient.py new file mode 100644 index 0000000..14c0bf9 --- /dev/null +++ b/appengine/2048-js/pysvg/gradient.py @@ -0,0 +1,170 @@ +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +''' +(C) 2008, 2009 Kerim Mansour +For licensing information please refer to license.txt +''' +from attributes import * +from core import BaseElement, PointAttrib, DimensionAttrib + + + +class LinearGradient(BaseElement, CoreAttrib, XLinkAttrib, PaintAttrib, StyleAttrib, ExternalAttrib): + """ + Class representing the linearGradient element of an svg doc. + """ + def __init__(self, x1=None, y1=None, x2=None, y2=None, **kwargs): + BaseElement.__init__(self, 'linearGradient') + self.set_x1(x1) + self.set_y1(y1) + self.set_x2(x2) + self.set_y2(y2) + self.setKWARGS(**kwargs) + + def set_x1(self, x1): + self._attributes['x1'] = x1 + def get_x1(self): + return self._attributes.get('x1') + + def set_y1(self, y1): + self._attributes['y1'] = y1 + def get_y1(self): + return self._attributes.get('y1') + + def set_x2(self, x2): + self._attributes['x2'] = x2 + def get_x2(self): + return self._attributes.get('x2') + + def set_y2(self, y2): + self._attributes['y2'] = y2 + def get_y2(self): + return self._attributes.get('y2') + + def set_gradientUnits(self, gradientUnits): + self._attributes['gradientUnits'] = gradientUnits + def get_gradientUnits(self): + return self._attributes.get('gradientUnits') + + def set_gradientTransform(self, gradientTransform): + self._attributes['gradientTransform'] = gradientTransform + def get_gradientTransform(self): + return self._attributes.get('gradientTransform') + + def set_spreadMethod(self, spreadMethod): + self._attributes['spreadMethod'] = spreadMethod + def get_spreadMethod(self): + return self._attributes.get('spreadMethod') + +class RadialGradient(BaseElement, CoreAttrib, XLinkAttrib, PaintAttrib, StyleAttrib, ExternalAttrib): + """ + Class representing the radialGradient element of an svg doc. + """ + def __init__(self, cx='50%', cy='50%', r='50%', fx='50%', fy='50%', **kwargs): + BaseElement.__init__(self, 'radialGradient') + self.set_cx(cx) + self.set_cy(cy) + self.set_fx(fx) + self.set_fy(fy) + self.set_r(r) + self.setKWARGS(**kwargs) + + def set_cx(self, cx): + self._attributes['cx'] = cx + def get_cx(self): + return self._attributes.get('cx') + + def set_cy(self, cy): + self._attributes['cy'] = cy + def get_cy(self): + return self._attributes.get('cy') + + def set_r(self, r): + self._attributes['r'] = r + def get_r(self): + return self._attributes.get('r') + + def set_fx(self, fx): + self._attributes['fx'] = fx + def get_fx(self): + return self._attributes.get('fx') + + def set_fy(self, fy): + self._attributes['fy'] = fy + def get_fy(self): + return self._attributes.get('fy') + + def set_gradientUnits(self, gradientUnits): + self._attributes['gradientUnits'] = gradientUnits + def get_gradientUnits(self): + return self._attributes.get('gradientUnits') + + def set_gradientTransform(self, gradientTransform): + self._attributes['gradientTransform'] = gradientTransform + def get_gradientTransform(self): + return self._attributes.get('gradientTransform') + + def set_spreadMethod(self, spreadMethod): + self._attributes['spreadMethod'] = spreadMethod + def get_spreadMethod(self): + return self._attributes.get('spreadMethod') + +class Stop(BaseElement, CoreAttrib, StyleAttrib, PaintAttrib, GradientAttrib): + """ + Class representing the stop element of an svg doc. + """ + def __init__(self, offset=None, **kwargs): + BaseElement.__init__(self, 'stop') + self.set_offset(offset) + self.setKWARGS(**kwargs) + + def set_offset(self, offset): + self._attributes['offset'] = offset + def get_offset(self): + return self._attributes.get('offset') + +class Pattern(BaseElement, CoreAttrib, XLinkAttrib, ConditionalAttrib, ExternalAttrib, StyleAttrib, PresentationAttributes_All, PointAttrib, DimensionAttrib): + """ + Class representing the pattern element of an svg doc. + """ + def __init__(self, x=None, y=None, width=None, height=None, patternUnits=None, patternContentUnits=None, patternTransform=None, viewBox=None, preserveAspectRatio=None, **kwargs): + BaseElement.__init__(self, 'pattern') + self.set_x(x) + self.set_y(y) + self.set_width(width) + self.set_height(height) + self.set_patternUnits(patternUnits) + self.set_patternContentUnits(patternContentUnits) + self.set_patternTransform(patternTransform) + self.set_viewBox(viewBox) + self.set_preserveAspectRatio(preserveAspectRatio) + self.setKWARGS(**kwargs) + + def set_viewBox(self, viewBox): + self._attributes['viewBox'] = viewBox + + def get_viewBox(self): + return self._attributes['viewBox'] + + def set_preserveAspectRatio(self, preserveAspectRatio): + self._attributes['preserveAspectRatio'] = preserveAspectRatio + + def get_preserveAspectRatio(self): + return self._attributes['preserveAspectRatio'] + + def set_patternUnits(self, patternUnits): + self._attributes['patternUnits'] = patternUnits + + def get_patternUnits(self): + return self._attributes['patternUnits'] + + def set_patternContentUnits(self, patternContentUnits): + self._attributes['patternContentUnits'] = patternContentUnits + def get_patternContentUnits(self): + return self._attributes['patternContentUnits'] + + def set_patternTransform(self, patternTransform): + self._attributes['patternTransform'] = patternTransform + + def get_patternTransform(self): + return self._attributes['patternTransform'] diff --git a/appengine/2048-js/pysvg/gradient.pyc b/appengine/2048-js/pysvg/gradient.pyc new file mode 100644 index 0000000..70c8029 Binary files /dev/null and b/appengine/2048-js/pysvg/gradient.pyc differ diff --git a/appengine/2048-js/pysvg/linking.py b/appengine/2048-js/pysvg/linking.py new file mode 100644 index 0000000..6bb90c6 --- /dev/null +++ b/appengine/2048-js/pysvg/linking.py @@ -0,0 +1,67 @@ +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +''' +(C) 2008, 2009 Kerim Mansour +For licensing information please refer to license.txt +''' +from attributes import * +from core import BaseElement + + + +class A(BaseElement, CoreAttrib, ConditionalAttrib, StyleAttrib, ExternalAttrib, PresentationAttributes_All, GraphicalEventsAttrib, XLinkAttrib): + """ + Class representing the a element of an svg doc. + """ + def __init__(self, target=None): + BaseElement.__init__(self,'a') + self.set_target(target) + + def set_transform(self, transform): + self._attributes['transform']=transform + def get_transform(self): + return self._attributes.get('transform') + + def set_target(self, target): + self._attributes['target']=target + def get_target(self): + return self._attributes.get('target') + +class View(BaseElement, CoreAttrib, ExternalAttrib): + """ + Class representing the view element of an svg doc. + """ + def __init__(self, ): + BaseElement.__init__(self,'view') + + def set_transform(self, transform): + self._attributes['transform']=transform + def get_transform(self): + return self._attributes.get('transform') + + def set_target(self, target): + self._attributes['target']=target + def get_target(self): + return self._attributes.get('target') + + def set_viewBox(self,viewBox): + self._attributes['viewBox']=viewBox + + def get_viewBox(self): + return self._attributes['viewBox'] + + def set_preserveAspectRatio(self,preserveAspectRatio): + self._attributes['preserveAspectRatio']=preserveAspectRatio + + def get_preserveAspectRatio(self): + return self._attributes['preserveAspectRatio'] + + def set_zoomAndPan(self,zoomAndPan): + self._attributes['zoomAndPan']=zoomAndPan + def get_zoomAndPan(self): + return self._attributes['zoomAndPan'] + + def set_viewTarget(self,viewTarget): + self._attributes['viewTarget']=viewTarget + def get_viewTarget(self): + return self._attributes['viewTarget'] \ No newline at end of file diff --git a/appengine/2048-js/pysvg/linking.pyc b/appengine/2048-js/pysvg/linking.pyc new file mode 100644 index 0000000..8617688 Binary files /dev/null and b/appengine/2048-js/pysvg/linking.pyc differ diff --git a/appengine/2048-js/pysvg/parser.py b/appengine/2048-js/pysvg/parser.py new file mode 100644 index 0000000..a09e3a7 --- /dev/null +++ b/appengine/2048-js/pysvg/parser.py @@ -0,0 +1,76 @@ +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +''' +(C) 2008, 2009 Kerim Mansour +For licensing information please refer to license.txt +''' +from xml.dom import minidom +from xml.dom import Node +import string +from pysvg.animate import * +from pysvg.filter import * +from pysvg.gradient import * +from pysvg.linking import * +from pysvg.script import * +from pysvg.shape import * +from pysvg.structure import * +from pysvg.style import * +from pysvg.text import * + +def calculateMethodName(attr): + name=attr + name=name.replace(':','_') + name=name.replace('-','_') + name='set_'+name + return name + +def setAttributes(attrs,obj): + for attr in attrs.keys(): + if hasattr(obj, calculateMethodName(attr)): + eval ('obj.'+calculateMethodName(attr))(attrs[attr].value) + else: + print calculateMethodName(attr)+' not found in:'+obj._elementName + +def build(node_, object): + attrs = node_.attributes + if attrs != None: + setAttributes(attrs, object) + for child_ in node_.childNodes: + nodeName_ = child_.nodeName.split(':')[-1] + if child_.nodeType == Node.ELEMENT_NODE: + try: + capitalLetter = string.upper(nodeName_[0]) + objectinstance=eval(capitalLetter+nodeName_[1:]) () + except: + print 'no class for: '+nodeName_ + continue + object.addElement(build(child_,objectinstance)) + elif child_.nodeType == Node.TEXT_NODE: + #print "TextNode:"+child_.nodeValue + #if child_.nodeValue.startswith('\n'): + # print "TextNode starts with return:"+child_.nodeValue + #else: +# print "TextNode is:"+child_.nodeValue + #object.setTextContent(child_.nodeValue) + if child_.nodeValue <> None: + object.appendTextContent(child_.nodeValue) + elif child_.nodeType == Node.CDATA_SECTION_NODE: + object.appendTextContent('') + elif child_.nodeType == Node.COMMENT_NODE: + object.appendTextContent('') + else: + print "Some node:"+nodeName_+" value: "+child_.nodeValue + return object + +#TODO: packageprefix ? +def parse(inFileName): + doc = minidom.parse(inFileName) + rootNode = doc.documentElement + rootObj = Svg() + build(rootNode,rootObj) + # Enable Python to collect the space used by the DOM. + doc = None + #print rootObj.getXML() + return rootObj + + diff --git a/appengine/2048-js/pysvg/script.py b/appengine/2048-js/pysvg/script.py new file mode 100644 index 0000000..2f530c2 --- /dev/null +++ b/appengine/2048-js/pysvg/script.py @@ -0,0 +1,23 @@ +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +''' +(C) 2008, 2009 Kerim Mansour +For licensing information please refer to license.txt +''' +from attributes import CoreAttrib, XLinkAttrib +from core import BaseElement + + + +class Script(BaseElement, CoreAttrib, XLinkAttrib): + """ + Class representing the script element of an svg doc. + """ + def __init__(self, **kwargs): + BaseElement.__init__(self,'script') + self.setKWARGS(**kwargs) + + def set_type(self, type): + self._attributes['type']=type + def get_type(self): + return self._attributes.get('type') diff --git a/appengine/2048-js/pysvg/script.pyc b/appengine/2048-js/pysvg/script.pyc new file mode 100644 index 0000000..2e068a2 Binary files /dev/null and b/appengine/2048-js/pysvg/script.pyc differ diff --git a/appengine/2048-js/pysvg/shape.py b/appengine/2048-js/pysvg/shape.py new file mode 100644 index 0000000..6094d7b --- /dev/null +++ b/appengine/2048-js/pysvg/shape.py @@ -0,0 +1,481 @@ +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +''' +(C) 2008, 2009 Kerim Mansour +For licensing information please refer to license.txt +''' +from attributes import * +from core import BaseElement, BaseShape, PointAttrib, DimensionAttrib, PointToAttrib + + +class Rect(BaseShape, PointAttrib, DimensionAttrib): + """ + Class representing the rect element of an svg doc. + """ + def __init__(self, x=None, y=None, width=None, height=None, rx=None, ry=None, **kwargs): + BaseElement.__init__(self,'rect') + self.set_x(x) + self.set_y(y) + self.set_height(height) + self.set_width(width) + self.set_rx(rx) + self.set_ry(ry) + self.setKWARGS(**kwargs) + + def set_rx(self, rx): + self._attributes['rx']=rx + def get_rx(self): + return self._attributes.get('rx') + + def set_ry(self, ry): + self._attributes['ry']=ry + def get_ry(self): + return self._attributes.get('ry') + + #extra methods. Methods do rely on number values in the attributes. You might get an exception else! + def getEdgePoints(self): + """ + Returns a list with the coordinates of the points at the edge of the rectangle as tuples. + e.g.[(x1,y1),(x2,y2)] + The sorting is counterclockwise starting with the lower left corner. + Coordinates must be numbers or an exception will be thrown. + """ + result = [(float(self.get_x()),float(self.get_y()))] + result.append((float(self.get_x())+float(self.get_width()),float(self.get_y()))) + result.append((float(self.get_x())+float(self.get_width()),float(self.get_y())+float(self.get_height()))) + result.append((float(self.get_x()),float(self.get_y())+float(self.get_height()))) + return result + + def getInnerEdgePoints(self): + """ + Returns a list with the coordinates of the points at the inner edge of a rounded rectangle as tuples. + e.g.[(x1,y1),(x2,y2)] + The sorting is counterclockwise starting with the lower left corner. + Coordinates must be numbers or an exception will be thrown. + """ + result = [] + result.append((float(self.get_x()) + float(self.get_rx()), float(self.get_y()) + float(self.get_ry()))) + result.append((float(self.get_x()) + float(self.get_width()) - float(self.get_rx()), float(self.get_y()) + float(self.get_ry()))) + result.append((float(self.get_x()) + float(self.get_width()) - float(self.get_rx()), float(self.get_y()) + float(self.get_height()) - float(self.get_ry()))) + result.append((float(self.get_x()) + float(self.get_rx()), float(self.get_y()) + float(self.get_height()) - float(self.get_ry()))) + return result + + def getBottomLeft(self): + """ + Retrieves a tuple with the x,y coordinates of the lower left point of the rect. + Requires the coordinates, width, height to be numbers + """ + return (float(self.get_x()), float(self.get_y())) + + def getBottomRight(self): + """ + Retrieves a tuple with the x,y coordinates of the lower right point of the rect. + Requires the coordinates, width, height to be numbers + """ + return (float(self.get_x()) + float(self.get_width()), float(self.get_y())) + + def getTopLeft(self): + """ + Retrieves a tuple with the x,y coordinates of the upper left point of the rect. + Requires the coordinates, width, height to be numbers + """ + return (float(self.get_x()), float(self.get_y())+ float(self.get_height())) + + def getTopRight(self): + """ + Retrieves a tuple with the x,y coordinates of the upper right point of the rect. + Requires the coordinates, width, height to be numbers + """ + return (float(self.get_x()) + float(self.get_width()), float(self.get_y()) + float(self.get_height())) + + def moveToPoint(self, (x,y)): + """ + Moves the rect to the point x,y + """ + self.set_x(float(self.get_x()) + float(x)) + self.set_y(float(self.get_y()) + float(y)) + + +class Circle(BaseShape): + """ + Class representing the circle element of an svg doc. + """ + def __init__(self, cx=None,cy=None,r=None, **kwargs): + BaseElement.__init__(self,'circle') + self.set_cx(cx) + self.set_cy(cy) + self.set_r(r) + self.setKWARGS(**kwargs) + + def set_cx(self, cx): + self._attributes['cx']=cx + def get_cx(self): + return self._attributes.get('cx') + + def set_cy(self, cy): + self._attributes['cy']=cy + def get_cy(self): + return self._attributes.get('cy') + + def set_r(self, r): + self._attributes['r']=r + def get_r(self): + return self._attributes.get('r') + + #extra methods. Methods do rely on number values in the attributes. You might get an exception else! + def getDiameter(self): + """ + Retrieves the diameter of the circle. Requires the radius to be a number + """ + return 2 * float(self.get_r()) + + def getWidth(self): + """ + Retrieves the width of the circle. Requires the radius to be a number + """ + return self.getDiameter() + + def getHeight(self): + """ + Retrieves the height of the circle. Requires the radius to be a number + """ + return self.getDiameter() + + def getBottomLeft(self): + """ + Retrieves a tuple with the x,y coordinates of the lower left point of the circle. + Requires the radius and the coordinates to be numbers + """ + return (float(self.get_cx()) - float(self.get_r()), float(self.get_cy()) - float(self.get_r())) + + def getBottomRight(self): + """ + Retrieves a tuple with the x,y coordinates of the lower right point of the circle. + Requires the radius and the coordinates to be numbers + """ + return (float(self.get_cx()) + float(self.get_r()), float(self.get_cy()) - float(self.get_r())) + + def getTopLeft(self): + """ + Retrieves a tuple with the x,y coordinates of the upper left point of the circle. + Requires the radius and the coordinates to be numbers + """ + return (float(self.get_cx()) - float(self.get_r()), float(self.get_cy()) + float(self.get_r())) + + def getTopRight(self): + """ + Retrieves a tuple with the x,y coordinates of the upper right point of the circle. + Requires the radius and the coordinates to be numbers + """ + return (float(self.get_cx()) + float(self.get_r()), float(self.get_cy()) + float(self.get_r())) + + def moveToPoint(self, (x,y)): + """ + Moves the circle to the point x,y + """ + self.set_cx(float(self.get_cx()) + float(x)) + self.set_cy(float(self.get_cy()) + float(y)) + +class Ellipse(BaseShape): + """ + Class representing the ellipse element of an svg doc. + """ + def __init__(self, cx=None,cy=None,rx=None,ry=None, **kwargs): + BaseElement.__init__(self,'ellipse') + self.set_cx(cx) + self.set_cy(cy) + self.set_rx(rx) + self.set_ry(ry) + self.setKWARGS(**kwargs) + + def set_cx(self, cx): + self._attributes['cx']=cx + def get_cx(self): + return self._attributes.get('cx') + + def set_cy(self, cy): + self._attributes['cy']=cy + def get_cy(self): + return self._attributes.get('cy') + + def set_rx(self, rx): + self._attributes['rx']=rx + def get_rx(self): + return self._attributes.get('rx') + + def set_ry(self, ry): + self._attributes['ry']=ry + def get_ry(self): + return self._attributes.get('ry') + + #extra methods. Methods do rely on number values in the attributes. You might get an exception else! + def getWidth(self): + return abs(2 * float(self.get_rx())) + + def getHeight(self): + return abs(2 * float(self.get_ry())) + + def getBottomLeft(self): + """ + Retrieves a tuple with the x,y coordinates of the lower left point of the ellipse. + Requires the radius and the coordinates to be numbers + """ + return (float(self.get_cx()) - float(self.get_rx()), float(self.get_cy()) - float(self.get_ry())) + + def getBottomRight(self): + """ + Retrieves a tuple with the x,y coordinates of the lower right point of the ellipse. + Requires the radius and the coordinates to be numbers + """ + return (float(self.get_cx()) + float(self.get_rx()), float(self.get_cy()) - float(self.get_ry())) + + def getTopLeft(self): + """ + Retrieves a tuple with the x,y coordinates of the upper left point of the ellipse. + Requires the radius and the coordinates to be numbers + """ + return (float(self.get_cx()) - float(self.get_rx()), float(self.get_cy()) + float(self.get_ry())) + + def getTopRight(self): + """ + Retrieves a tuple with the x,y coordinates of the upper right point of the ellipse. + Requires the radius and the coordinates to be numbers + """ + return (float(self.get_cx()) + float(self.get_rx()), float(self.get_cy()) + float(self.get_ry())) + +class Line(BaseShape, PointToAttrib): + """ + Class representing the line element of an svg doc. + Note that this element is NOT painted VISIBLY by default UNLESS you provide + a style including STROKE and STROKE-WIDTH + """ + def __init__(self, X1=None, Y1=None, X2=None, Y2=None, **kwargs): + """ + Creates a line + @type X1: string or int + @param X1: starting x-coordinate + @type Y1: string or int + @param Y1: starting y-coordinate + @type X2: string or int + @param X2: ending x-coordinate + @type Y2: string or int + @param Y2: ending y-coordinate + """ + BaseElement.__init__(self,'line') + self.set_x1(X1) + self.set_y1(Y1) + self.set_x2(X2) + self.set_y2(Y2) + self.setKWARGS(**kwargs) + + def set_x1(self, x1): + self._attributes['x1']=x1 + def get_x1(self): + return self._attributes.get('x1') + + def set_y1(self, y1): + self._attributes['y1']=y1 + def get_y1(self): + return self._attributes.get('y1') + + def set_x2(self, x2): + self._attributes['x2']=x2 + def get_x2(self): + return self._attributes.get('x2') + + def set_y2(self, y2): + self._attributes['y2']=y2 + def get_y2(self): + return self._attributes.get('y2') + + #extra methods. Methods do rely on number values in the attributes. You might get an exception else! + def getWidth(self): + """ + Retrieves the width of the line. This is always a positive number. + Coordinates must be numbers. + """ + return abs(float(self.get_x1()) - float(self.get_x2())) + + def getHeight(self): + """ + Retrieves the height of the line. This is always a positive number. + Coordinates must be numbers. + """ + return abs(float(self.get_y1()) - float(self.get_y2())) + + def getBottomLeft(self): + """ + Retrieves the the bottom left coordinate of the line as tuple. + Coordinates must be numbers. + """ + x1 = float(self.get_x1()) + x2 = float(self.get_x2()) + y1 = float(self.get_y1()) + y2 = float(self.get_y2()) + if x1 < x2: + if y1 < y2: + return (x1, y1) + else: + return (x1, y2) + else: + if y1 < y2: + return (x2, y1) + else: + return (x2, y2) + + def getBottomRight(self): + """ + Retrieves the the bottom right coordinate of the line as tuple. + Coordinates must be numbers. + """ + x1 = float(self.get_x1()) + x2 = float(self.get_x2()) + y1 = float(self.get_y1()) + y2 = float(self.get_y2()) + if x1 < x2: + if y1 < y2: + return (x2, y1) + else: + return (x2, y2) + else: + if y1 < y2: + return (x1, y1) + else: + return (x1, y2) + + def getTopRight(self): + """ + Retrieves the the top right coordinate of the line as tuple. + Coordinates must be numbers. + """ + x1 = float(self.get_x1()) + x2 = float(self.get_x2()) + y1 = float(self.get_y1()) + y2 = float(self.get_y2()) + if x1 < x2: + if y1 < y2: + return (x2, y2) + else: + return (x2, y1) + else: + if y1 < y2: + return (x1, y2) + else: + return (x1, y1) + + def getTopLeft(self): + """ + Retrieves the the top left coordinate of the line as tuple. + Coordinates must be numbers. + """ + x1 = float(self.get_x1()) + x2 = float(self.get_x2()) + y1 = float(self.get_y1()) + y2 = float(self.get_y2()) + if x1 < x2: + if y1 < y2: + return (x1, y2) + else: + return (x1, y1) + else: + if y1 < y2: + return (x2, y2) + else: + return (x2, y1) + + def moveToPoint(self, (x,y)): + """ + Moves the line to the point x,y + """ + self.set_x1(float(self.get_x1()) + float(x)) + self.set_x2(float(self.get_x2()) + float(x)) + self.set_y1(float(self.get_y1()) + float(y)) + self.set_y2(float(self.get_y2()) + float(y)) + +class Path(BaseShape, ExternalAttrib, MarkerAttrib): + """ + Class representing the path element of an svg doc. + """ + def __init__(self, pathData="",pathLength=None, style=None, focusable=None, **kwargs): + BaseElement.__init__(self,'path') + if pathData!='' and not pathData.endswith(' '): + pathData+=' ' + self.set_d(pathData) + if style!=None: + self.set_style(style) + self.setKWARGS(**kwargs) + + def set_d(self, d): + self._attributes['d']=d + def get_d(self): + return self._attributes.get('d') + + def set_pathLength(self, pathLength): + self._attributes['pathLength']=pathLength + def get_pathLength(self): + return self._attributes.get('pathLength') + + def __append__(self,command, params, relative=True): + d = self.get_d() + if relative==True: + d+=command.lower() + else: + d+=command.upper() + for param in params: + d+=' %s ' %(param) + self.set_d(d) + + def appendLineToPath(self,endx,endy, relative=True): + self.__append__('l',[endx,endy], relative) + + def appendHorizontalLineToPath(self,endx, relative=True): + self.__append__('h',[endx], relative) + + def appendVerticalLineToPath(self,endy, relative=True): + self.__append__('v',[endy], relative) + + def appendMoveToPath(self,endx,endy, relative=True): + self.__append__('m',[endx,endy], relative) + + def appendCloseCurve(self): + d = self.get_d() + d+="z" + self.set_d(d) + + def appendCubicCurveToPath(self, controlstartx, controlstarty, controlendx, controlendy, endx,endy,relative=True): + self.__append__('c',[controlstartx, controlstarty, controlendx, controlendy, endx,endy], relative) + + def appendCubicShorthandCurveToPath(self, controlendx, controlendy, endx,endy,relative=True): + self.__append__('s',[controlendx, controlendy, endx,endy], relative) + + def appendQuadraticCurveToPath(self, controlx, controly, endx,endy,relative=True): + self.__append__('q',[controlx, controly, endx,endy], relative) + + def appendQuadraticShorthandCurveToPath(self, endx,endy,relative=True): + self.__append__('t',[endx,endy], relative) + + def appendArcToPath(self,rx,ry,x,y,x_axis_rotation=0,large_arc_flag=0,sweep_flag=1 ,relative=True): + self.__append__('a',[rx,ry,x_axis_rotation,large_arc_flag,sweep_flag,x,y], relative) + +class Polyline(BaseShape): + """ + Class representing the polyline element of an svg doc. + """ + def __init__(self, points=None, **kwargs): + BaseElement.__init__(self,'polyline') + self.set_points(points) + self.setKWARGS(**kwargs) + + def set_points(self, points): + self._attributes['points']=points + def get_points(self): + return self._attributes.get('points') + +class Polygon(Polyline): + """ + Class representing the polygon element of an svg doc. + """ + def __init__(self, points=None, **kwargs): + BaseElement.__init__(self,'polygon') + self.set_points(points) + self.setKWARGS(**kwargs) \ No newline at end of file diff --git a/appengine/2048-js/pysvg/shape.pyc b/appengine/2048-js/pysvg/shape.pyc new file mode 100644 index 0000000..167ab0b Binary files /dev/null and b/appengine/2048-js/pysvg/shape.pyc differ diff --git a/appengine/2048-js/pysvg/structure.py b/appengine/2048-js/pysvg/structure.py new file mode 100644 index 0000000..4131b43 --- /dev/null +++ b/appengine/2048-js/pysvg/structure.py @@ -0,0 +1,222 @@ +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +''' +This module includes the elements found in http://www.w3.org/TR/SVG/struct.html + +(C) 2008, 2009 Kerim Mansour +For licensing information please refer to license.txt +''' +from attributes import * +from core import BaseElement, PointAttrib, DimensionAttrib + + + + +class G(BaseElement, CoreAttrib, ConditionalAttrib, StyleAttrib, ExternalAttrib, PresentationAttributes_All, GraphicalEventsAttrib): + """ + Class representing the g element of an svg doc. + """ + def __init__(self, **kwargs): + BaseElement.__init__(self, 'g') + self.setKWARGS(**kwargs) + + def set_transform(self, transform): + self._attributes['transform'] = transform + def get_transform(self): + return self._attributes.get('transform') + +class Defs(G): + """ + Class representing the defs element of an svg doc. + """ + def __init__(self,**kwargs): + BaseElement.__init__(self, 'defs') + self.setKWARGS(**kwargs) + + +class Desc(BaseElement, CoreAttrib, StyleAttrib): + """ + Class representing the desc element of an svg doc. + """ + def __init__(self,**kwargs): + BaseElement.__init__(self, 'desc') + self.setKWARGS(**kwargs) + +class Title(Desc): + """ + Class representing the title element of an svg doc. + """ + def __init__(self,**kwargs): + BaseElement.__init__(self, 'title') + self.setKWARGS(**kwargs) + +class Metadata(BaseElement, CoreAttrib): + """ + Class representing the metadata element of an svg doc. + """ + def __init__(self,**kwargs): + BaseElement.__init__(self, 'metadata') + self.setKWARGS(**kwargs) + +class Symbol(BaseElement, CoreAttrib, StyleAttrib, ExternalAttrib, PresentationAttributes_All, GraphicalEventsAttrib): + """ + Class representing the symbol element of an svg doc. + """ + def __init__(self,**kwargs): + BaseElement.__init__(self, 'symbol') + self.setKWARGS(**kwargs) + + def set_viewBox(self, viewBox): + self._attributes['viewBox'] = viewBox + + def get_viewBox(self): + return self._attributes['viewBox'] + + def set_preserveAspectRatio(self, preserveAspectRatio): + self._attributes['preserveAspectRatio'] = preserveAspectRatio + + def get_preserveAspectRatio(self): + return self._attributes['preserveAspectRatio'] + +class Use(BaseElement, CoreAttrib, StyleAttrib, ConditionalAttrib, PointAttrib, DimensionAttrib, XLinkAttrib, PresentationAttributes_All, GraphicalEventsAttrib): + """ + Class representing the use element of an svg doc. + """ + def __init__(self,**kwargs): + BaseElement.__init__(self, 'use') + self.setKWARGS(**kwargs) + + def set_transform(self, transform): + self._attributes['transform'] = transform + def get_transform(self): + return self._attributes.get('transform') + +class Svg(BaseElement, CoreAttrib, StyleAttrib, ConditionalAttrib, PointAttrib, DimensionAttrib, XLinkAttrib, PresentationAttributes_All, GraphicalEventsAttrib, DocumentEventsAttrib): + """ + Class representing the svg element of an svg doc. + """ + def __init__(self, x=None, y=None, width=None, height=None,**kwargs): + BaseElement.__init__(self, 'svg') + self.set_xmlns('http://www.w3.org/2000/svg') + self.set_xmlns_xlink('http://www.w3.org/1999/xlink') + self.set_version('1.1') + self.set_x(x) + self.set_y(y) + self.set_height(height) + self.set_width(width) + self.setKWARGS(**kwargs) + + def set_version(self, version): + self._attributes['version'] = version + + def get_version(self): + return self._attributes['version'] + + def set_xmlns(self, xmlns): + self._attributes['xmlns'] = xmlns + + def get_xmlns(self): + return self._attributes['xmlns'] + + def set_xmlns_xlink(self, xmlns_xlink): + self._attributes['xmlns:xlink'] = xmlns_xlink + + def get_xmlns_xlink(self): + return self._attributes.get('xmlns:xlink') + + def set_viewBox(self, viewBox): + self._attributes['viewBox'] = viewBox + def get_viewBox(self): + return self._attributes['viewBox'] + + def set_preserveAspectRatio(self, preserveAspectRatio): + self._attributes['preserveAspectRatio'] = preserveAspectRatio + def get_preserveAspectRatio(self): + return self._attributes['preserveAspectRatio'] + + def set_transform(self, transform): + self._attributes['transform'] = transform + def get_transform(self): + return self._attributes.get('transform') + + def set_zoomAndPan(self, zoomAndPan): + self._attributes['zoomAndPan'] = zoomAndPan + def get_zoomAndPan(self): + return self._attributes['zoomAndPan'] + + def set_contentScriptType(self, contentScriptType): + self._attributes['contentScriptType'] = contentScriptType + def get_contentScriptType(self): + return self._attributes['contentScriptType'] + + def set_contentStyleType(self, contentStyleType): + self._attributes['contentStyleType'] = contentStyleType + def get_contentStyleType(self): + return self._attributes['contentStyleType'] + + def set_baseProfile(self, baseProfile): + self._attributes['baseProfile'] = baseProfile + def get_baseProfile(self): + return self._attributes['baseProfile'] +#todo: check color.attrib and colorprofile.attrib. supposedly in image +class Image(BaseElement, CoreAttrib, ConditionalAttrib, StyleAttrib, ViewportAttrib, PaintAttrib, OpacityAttrib, GraphicsAttrib, ClipAttrib, MaskAttrib, FilterAttrib, GraphicalEventsAttrib, CursorAttrib, XLinkAttrib, ExternalAttrib, PointAttrib, DimensionAttrib): + """ + Class representing the image element of an svg doc. + """ + def __init__(self, x=None, y=None, width=None, height=None, preserveAspectRatio=None,**kwargs): + BaseElement.__init__(self, 'image') + self.set_x(x) + self.set_y(y) + self.set_height(height) + self.set_width(width) + self.set_preserveAspectRatio(preserveAspectRatio) + self.setKWARGS(**kwargs) + + #def set_embedded(self,embedded): + # self._attributes['embedded']=embedded + + def set_preserveAspectRatio(self, preserveAspectRatio): + self._attributes['preserveAspectRatio'] = preserveAspectRatio + def get_preserveAspectRatio(self): + return self._attributes['preserveAspectRatio'] + + def set_transform(self, transform): + self._attributes['transform'] = transform + def get_transform(self): + return self._attributes.get('transform') + +class Switch(BaseElement, CoreAttrib, ConditionalAttrib, StyleAttrib, PresentationAttributes_All, GraphicalEventsAttrib, ExternalAttrib): + """ + Class representing the switch element of an svg doc. + """ + def __init__(self,**kwargs): + BaseElement.__init__(self, 'switch') + self.setKWARGS(**kwargs) + + def set_transform(self, transform): + self._attributes['transform'] = transform + def get_transform(self): + return self._attributes.get('transform') + +class ClipPath(BaseElement, CoreAttrib, ConditionalAttrib, StyleAttrib, ExternalAttrib, PresentationAttributes_All, GraphicalEventsAttrib): + """ + Class representing the clipPath element of an svg doc. + """ + def __init__(self, id=None, transform=None, clipPathUnits=None,**kwargs): + BaseElement.__init__(self, 'clipPath') + self.set_id(id) + self.set_transform(transform) + self.set_clipPathUnits(clipPathUnits) + self.setKWARGS(**kwargs) + + + def set_transform(self, transform): + self._attributes['transform'] = transform + def get_transform(self): + return self._attributes.get('transform') + + def set_clipPathUnits(self, clipPathUnits): + self._attributes['clipPathUnits'] = clipPathUnits + + def get_clipPathUnits(self): + return self._attributes['clipPathUnits'] diff --git a/appengine/2048-js/pysvg/structure.pyc b/appengine/2048-js/pysvg/structure.pyc new file mode 100644 index 0000000..9e138e8 Binary files /dev/null and b/appengine/2048-js/pysvg/structure.pyc differ diff --git a/appengine/2048-js/pysvg/style.py b/appengine/2048-js/pysvg/style.py new file mode 100644 index 0000000..e32773d --- /dev/null +++ b/appengine/2048-js/pysvg/style.py @@ -0,0 +1,34 @@ +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +''' +(C) 2008, 2009 Kerim Mansour +For licensing information please refer to license.txt +''' +from attributes import CoreAttrib, XLinkAttrib +from core import BaseElement + + + +class Style(BaseElement, CoreAttrib, XLinkAttrib): + """ + Class representing the style element of an svg doc. + """ + def __init__(self, **kwargs): + BaseElement.__init__(self,'style') + self.setKWARGS(**kwargs) + + def set_type(self, type): + self._attributes['type']=type + def get_type(self): + return self._attributes.get('type') + + def set_media(self, media): + self._attributes['media']=media + def get_media(self): + return self._attributes.get('media') + + def set_title(self, title): + self._attributes['title']=title + def get_title(self): + return self._attributes.get('title') + diff --git a/appengine/2048-js/pysvg/style.pyc b/appengine/2048-js/pysvg/style.pyc new file mode 100644 index 0000000..a00658d Binary files /dev/null and b/appengine/2048-js/pysvg/style.pyc differ diff --git a/appengine/2048-js/pysvg/text.py b/appengine/2048-js/pysvg/text.py new file mode 100644 index 0000000..f7fc3dc --- /dev/null +++ b/appengine/2048-js/pysvg/text.py @@ -0,0 +1,169 @@ +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +''' +(C) 2008, 2009 Kerim Mansour +For licensing information please refer to license.txt +''' +from attributes import * +from core import BaseElement, PointAttrib, DeltaPointAttrib, RotateAttrib + +class AltGlyphDef(BaseElement, CoreAttrib): + """ + Class representing the altGlyphDef element of an svg doc. + """ + def __init__(self, **kwargs): + BaseElement.__init__(self, 'altGlypfDef') + self.setKWARGS(**kwargs) + +class AltGlyphItem(BaseElement, CoreAttrib): + """ + Class representing the altGlyphItem element of an svg doc. + """ + def __init__(self, **kwargs): + BaseElement.__init__(self, 'altGlypfItem') + self.setKWARGS(**kwargs) + +class GlyphRef(BaseElement, CoreAttrib, ExternalAttrib, StyleAttrib, FontAttrib, XLinkAttrib, PaintAttrib, PointAttrib, DeltaPointAttrib): + """ + Class representing the glyphRef element of an svg doc. + """ + def __init__(self, **kwargs): + BaseElement.__init__(self, 'glyphRef') + self.setKWARGS(**kwargs) + + def set_glyphRef(self, glyphRef): + self._attributes['glyphRef'] = glyphRef + def get_glyphRef(self): + return self._attributes.get('glyphRef') + + def set_format(self, format): + self._attributes['format'] = format + def get_format(self): + return self._attributes.get('format') + + def set_lengthAdjust(self, lengthAdjust): + self._attributes['lengthAdjust'] = lengthAdjust + def get_lengthAdjust(self): + return self._attributes.get('lengthAdjust') + +class AltGlyph(GlyphRef, ConditionalAttrib, GraphicalEventsAttrib, OpacityAttrib, GraphicsAttrib, CursorAttrib, FilterAttrib, MaskAttrib, ClipAttrib, TextContentAttrib, RotateAttrib): + """ + Class representing the altGlyph element of an svg doc. + """ + def __init__(self, **kwargs): + BaseElement.__init__(self, 'altGlyph') + self.setKWARGS(**kwargs) + + def set_textLength(self, textLength): + self._attributes['textLength'] = textLength + def get_textLength(self): + return self._attributes.get('textLength') + +class TextPath(BaseElement, CoreAttrib, ConditionalAttrib, ExternalAttrib, StyleAttrib, XLinkAttrib, FontAttrib, PaintAttrib, GraphicalEventsAttrib, OpacityAttrib, GraphicsAttrib, CursorAttrib, FilterAttrib, MaskAttrib, ClipAttrib, TextContentAttrib): + """ + Class representing the textPath element of an svg doc. + """ + def __init__(self, **kwargs): + BaseElement.__init__(self, 'textPath') + self.setKWARGS(**kwargs) + + def set_startOffset(self, startOffset): + self._attributes['startOffset'] = startOffset + def get_startOffset(self): + return self._attributes.get('startOffset') + + def set_textLength(self, textLength): + self._attributes['textLength'] = textLength + def get_textLength(self): + return self._attributes.get('textLength') + + def set_lengthAdjust(self, lengthAdjust): + self._attributes['lengthAdjust'] = lengthAdjust + def get_lengthAdjust(self): + return self._attributes.get('lengthAdjust') + + def set_method(self, method): + self._attributes['method'] = method + def get_method(self): + return self._attributes.get('method') + + def set_spacing(self, spacing): + self._attributes['spacing'] = spacing + def get_spacing(self): + return self._attributes.get('spacing') + +class Tref(BaseElement, CoreAttrib, ConditionalAttrib, ExternalAttrib, StyleAttrib, XLinkAttrib, PointAttrib, DeltaPointAttrib, RotateAttrib, GraphicalEventsAttrib, PaintAttrib, FontAttrib, OpacityAttrib, GraphicsAttrib, CursorAttrib, FilterAttrib, MaskAttrib, ClipAttrib, TextContentAttrib): + """ + Class representing the tref element of an svg doc. + """ + def __init__(self, **kwargs): + BaseElement.__init__(self, 'tref') + self.setKWARGS(**kwargs) + + def set_textLength(self, textLength): + self._attributes['textLength'] = textLength + def get_textLength(self): + return self._attributes.get('textLength') + + def set_lengthAdjust(self, lengthAdjust): + self._attributes['lengthAdjust'] = lengthAdjust + def get_lengthAdjust(self): + return self._attributes.get('lengthAdjust') + +class Tspan(BaseElement, CoreAttrib, ConditionalAttrib, ExternalAttrib, StyleAttrib, PointAttrib, DeltaPointAttrib, RotateAttrib, GraphicalEventsAttrib, PaintAttrib, FontAttrib, OpacityAttrib, GraphicsAttrib, CursorAttrib, FilterAttrib, MaskAttrib, ClipAttrib, TextContentAttrib): + """ + Class representing the tspan element of an svg doc. + """ + def __init__(self, x=None, y=None, dx=None, dy=None, rotate=None, textLength=None, lengthAdjust=None, **kwargs): + BaseElement.__init__(self, 'tspan') + self.set_x(x) + self.set_y(y) + self.set_dx(dx) + self.set_dy(dy) + self.set_rotate(rotate) + self.set_textLength(textLength) + self.set_lengthAdjust(lengthAdjust) + self.setKWARGS(**kwargs) + + def set_textLength(self, textLength): + self._attributes['textLength'] = textLength + def get_textLength(self): + return self._attributes.get('textLength') + + def set_lengthAdjust(self, lengthAdjust): + self._attributes['lengthAdjust'] = lengthAdjust + def get_lengthAdjust(self): + return self._attributes.get('lengthAdjust') + +class Text(BaseElement, CoreAttrib, ConditionalAttrib, ExternalAttrib, StyleAttrib, PointAttrib, DeltaPointAttrib, RotateAttrib, GraphicalEventsAttrib, PaintAttrib, FontAttrib, OpacityAttrib, GraphicsAttrib, CursorAttrib, FilterAttrib, MaskAttrib, ClipAttrib, TextContentAttrib, TextAttrib): + """ + Class representing the text element of an svg doc. + """ + def __init__(self, content=None, x=None, y=None, dx=None, dy=None, rotate=None, textLength=None, lengthAdjust=None, **kwargs): + BaseElement.__init__(self, 'text') + if content <> None: + self.appendTextContent(content) + self.set_x(x) + self.set_y(y) + self.set_dx(dx) + self.set_dy(dy) + self.set_rotate(rotate) + self.set_textLength(textLength) + self.set_lengthAdjust(lengthAdjust) + self.setKWARGS(**kwargs) + + def set_transform(self, transform): + self._attributes['transform'] = transform + def get_transform(self): + return self._attributes.get('transform') + + def set_textLength(self, textLength): + self._attributes['textLength'] = textLength + def get_textLength(self): + return self._attributes.get('textLength') + + def set_lengthAdjust(self, lengthAdjust): + self._attributes['lengthAdjust'] = lengthAdjust + def get_lengthAdjust(self): + return self._attributes.get('lengthAdjust') + diff --git a/appengine/2048-js/pysvg/text.pyc b/appengine/2048-js/pysvg/text.pyc new file mode 100644 index 0000000..412745d Binary files /dev/null and b/appengine/2048-js/pysvg/text.pyc differ diff --git a/appengine/2048-js/pysvg/turtle.py b/appengine/2048-js/pysvg/turtle.py new file mode 100644 index 0000000..9efa4c2 --- /dev/null +++ b/appengine/2048-js/pysvg/turtle.py @@ -0,0 +1,206 @@ +''' +(C) 2008, 2009, 2010 Kerim Mansour +For licensing information please refer to license.txt +''' +import math +from shape import Polyline + +class Vector(object): + """ + Class representing a vector. Used to determine position of the turtle as well as heading. + Also used to calculate movement. + Vector class is inspired by tips and code from: + - http://slowchop.com/2006/07/15/a-fast-python-vector-class/ + - http://www.kokkugia.com/wiki/index.php5?title=Python_vector_class + - http://xturtle.rg16.at/code/xturtle.py + """ + __slots__ = ('x', 'y') + def __init__(self, x, y): + """ Initializes the vector. + x and y are coordinates and should be numbers. + They will be cast to floats + """ + self.x = float(x) + self.y = float(y) + + def __add__(self, vector): + return Vector(self.x + vector.x, self.y + vector.y) + + def __sub__(self, vector): + return Vector(self.x - vector.x, self.y - vector.y) + + def __mul__(self, vector): + if isinstance(vector, Vector): + return self.x * vector.x + self.y * vector.y + return Vector(self.x * vector, self.y * vector) + + def __rmul__(self, vector): + if isinstance(vector, int) or isinstance(vector, float): + return Vector(self.x * vector, self.y * vector) + + def __neg__(self): + return Vector(-self.x, -self.y) + + def __abs__(self): + return (self.x ** 2 + self.y ** 2) ** 0.5 + + def rotate(self, angle): + """Rotates self counterclockwise by angle + (the angle must be given in a 360 degree system) + """ + perp = Vector(-self.y, self.x) + angle = angle * math.pi / 180.0 + c, s = math.cos(angle), math.sin(angle) + return Vector(self.x * c + perp.x * s, self.y * c + perp.y * s) + + def __getnewargs__(self): + return (self.x, self.y) + + def __repr__(self): + return "%.2f,%.2f " % (self.x, self.y) + +class Turtle(object): + """ + Class representing a classical turtle object known from logo and other implementations. + Note that currently each turtle has exactly ONE style of drawing, so if you intend to draw in multiple styles several instances of turtles are needed. + A turtle will only actually draw when the pen is down (default=false). + An xml representation usable for pysvg can ge retrieved using the getXML()-method. + To add the turtles paths to an svg you have two opions: + Either you simply call "addTurtlePathToSVG" or you can create an svg element and append the Elements of the turtle using a loop, e.g: + s=svg(...) + t=Turtle(...) + for element in t.getSVGElements(): + s.addElement(element) + """ + def __init__(self, initialPosition=Vector(0.0, 0.0), initialOrientation=Vector(1.0, 0.0), fill='white', stroke='black', strokeWidth='1', penDown=False): + """ Initializes a new Turtle with a new initial position and orientation as well as defaultvalues for style. + """ + self.fill = fill + self.stroke = stroke + self.strokeWidth = strokeWidth + self._position = initialPosition + self._orient = initialOrientation + self._penDown = penDown + self._svgElements = [] + self._pointsOfPolyline = [] + + + def forward(self, distance): + """ Moves the turtle forwards by distance in the direction it is facing. + If the pen is lowered it will also add to the currently drawn polyline. + """ + self._move(distance) + + def backward(self, distance): + """ Moves the turtle backwards by distance in the direction it is facing. + If the pen is lowered it will also add to the currently drawn polyline. + """ + self._move(-distance) + + def right(self, angle): + """Rotates the turtle to the right by angle. + """ + self._rotate(angle) + + def left(self, angle): + """Rotates the turtle to the left by angle. + """ + self._rotate(-angle) + + def moveTo(self, vector): + """ Moves the turtle to the new position. Orientation is kept as it is. + If the pen is lowered it will also add to the currently drawn polyline. + """ + self._position = vector + if self.isPenDown(): + self._pointsOfPolyline.append(self._position) + + def penUp(self): + """ Raises the pen. Any movement will not draw lines till pen is lowered again. + """ + if self._penDown==True: + self._penDown = False + self._addPolylineToElements() + + def penDown(self): + """ Lowers the pen down again. A new polyline will be created for drawing. + Old polylines will be stored in the stack + """ + #if self._penDown==False: + self._penDown = True + self._addPolylineToElements() + + def finish(self): + """MUST be called when drawing is finished. Else the last path will not be added to the stack. + """ + self._addPolylineToElements() + + def isPenDown(self): + """ Retrieve current status of the pen.(boolean) + """ + return self._penDown + + def getPosition(self): + """ Retrieve current position of the turtle.(Vector) + """ + return self._position + + def getOrientation(self): + """ Retrieve current orientation of the turtle.(Vector) + """ + return self._orient + + def setOrientation(self, vec): + """ Sets the orientation of the turtle.(Vector) + """ + self._orient=vec + + def _move(self, distance): + """ Moves the turtle by distance in the direction it is facing. + If the pen is lowered it will also add to the currently drawn polyline. + """ + self._position = self._position + self._orient * distance + if self.isPenDown(): + x = round(self._position.x, 2) + y = round(self._position.y, 2) + self._pointsOfPolyline.append(Vector(x, y)) + + def _rotate(self, angle): + """Rotates the turtle. + """ + self._orient = self._orient.rotate(angle) + + def _addPolylineToElements(self): + """Creates a new Polyline element that will be used for future movement/drawing. + The old one (if filled) will be stored on the movement stack. + """ + if (len(self._pointsOfPolyline) > 1): + s = '' + for point in self._pointsOfPolyline: + s += str(point) + ' '#str(point.x) + ',' + str(point.y) + ' ' + p = Polyline(s) + p.set_style('fill:' + self.fill + '; stroke:' + self.stroke + '; stroke-width:' + self.strokeWidth) + self._svgElements.append(p) + self._pointsOfPolyline = [] + self._pointsOfPolyline.append(Vector(self._position.x, self._position.y)) + + def getXML(self): + """Retrieves the pysvg elements that make up the turtles path and returns them as String in an xml representation. + """ + s = '' + for element in self._svgElements: + s += element.getXML() + return s + + def getSVGElements(self): + """Retrieves the pysvg elements that make up the turtles path and returns them as list. + """ + return self._svgElements + + def addTurtlePathToSVG(self, svgContainer): + """Adds the paths of the turtle to an existing svg container. + """ + for element in self.getSVGElements(): + svgContainer.addElement(element) + return svgContainer + \ No newline at end of file diff --git a/appengine/2048-js/pysvg/util.py b/appengine/2048-js/pysvg/util.py new file mode 100644 index 0000000..0ea8128 --- /dev/null +++ b/appengine/2048-js/pysvg/util.py @@ -0,0 +1,7 @@ +''' +Created on 12.04.2009 + +@author: kerim +''' + + diff --git a/appengine/2048-js/ui.py b/appengine/2048-js/ui.py new file mode 100644 index 0000000..194c897 --- /dev/null +++ b/appengine/2048-js/ui.py @@ -0,0 +1,31 @@ +from pysvg.builders import Svg, ShapeBuilder, StyleBuilder +from pysvg.text import * + +def createblock(number): + colors = {} + colors[None]=('#eee4da','#776e65') + colors[2]=('#eee4da','#776e65') + colors[4]=('#ede0c8','#776e65') + colors[8]=('#f2b179','#f9f6f2') + colors[16]=('#f59563','#f9f6f2') + colors[32]=('#f67c5f','#f9f6f2') + colors[64]=('#f65e3b','#f9f6f2') + colors[128]=('#edcf72','#f9f6f2') + colors[256]=('#edcc61','#f9f6f2') + colors[512]=('#eee4da','#776e65') + colors[1024]=('#edc53f','#f9f6f2') + colors[2048]=('#edc22e','#f9f6f2') + + canvas = Svg(0,0,100,100) + sb = ShapeBuilder() + canvas.addElement( sb.createRect(5,5,90,90,fill=colors[number][0]) ) + + t = Text(number,50,60) + t.set_style("font-family:FreeSans;font-weight:bold;font-size:36px;text-anchor:middle") + t.set_fill(colors[number][1]) + canvas.addElement(t) + return canvas.getXML() + #canvas.save('/tmp/try7.svg') + + +createblock(None) diff --git a/appengine/2048-js/ui.pyc b/appengine/2048-js/ui.pyc new file mode 100644 index 0000000..8a67929 Binary files /dev/null and b/appengine/2048-js/ui.pyc differ diff --git a/appengine/2048/app.yaml b/appengine/2048/app.yaml new file mode 100644 index 0000000..c7473d0 --- /dev/null +++ b/appengine/2048/app.yaml @@ -0,0 +1,15 @@ +application: bhutan2048 +version: 1 +runtime: python27 +api_version: 1 +threadsafe: true + +handlers: +- url: /.* + script: main.app + +libraries: +- name: webapp2 + version: latest +- name: jinja2 + version: latest diff --git a/appengine/2048/grid.py b/appengine/2048/grid.py new file mode 100644 index 0000000..d500dc7 --- /dev/null +++ b/appengine/2048/grid.py @@ -0,0 +1,64 @@ +import random + + +class Grid(object): + def __init__(self): + self.lol = [[None for i in range(4)] for i in range(4)] + self.addblock() + self.addblock() + + def get(self, a, b): + return self.lol[a][b] + + def content(self): + return self.lol + + def set(self, cord, value): + a, b = cord + self.lol[a][b] = value + + def addblock(self): + col = random.randint(0, 3) + row = random.randint(0, 3) + if not self.get(row, col): + self.set([row, col], 2) + else: + self.addblock() + + def move(self, way): + change = False + if way in ['down', 'right']: + rows = list(reversed(range(1, 4))) + elif way in ['up', 'left']: + rows = range(3) + + for col in range(4): + for row in rows: + if way == 'down': + curr = [row, col] + prev = [row-1, col] + if way == 'up': + curr = [row, col] + prev = [row+1, col] + if way == 'left': + curr = [col, row] + prev = [col, row+1] + if way == 'right': + curr = [col, row] + prev = [col, row-1] + if self.get(*prev): + if not self.get(*curr): + self.set(curr, self.get(*prev)) + self.set(prev, None) + change = True + if self.get(*prev) == self.get(*curr): + self.set(curr, self.get(*curr)*2) + self.set(prev, None) + change = False + if change: + self.move(way) + else: + self.addblock() + + +grid = Grid() diff --git a/appengine/2048/index.html b/appengine/2048/index.html new file mode 100644 index 0000000..c5ec801 --- /dev/null +++ b/appengine/2048/index.html @@ -0,0 +1,34 @@ + + + + +My 2048 + + + + + +{% for row in range(4) %} + + {% for column in range(4) %} + + {% endfor %} + +{% endfor %} +
{{ blocks[row][column] }}
+ +
+ + + + +
+ + diff --git a/appengine/2048/js/keyboard_input_manager-original.js b/appengine/2048/js/keyboard_input_manager-original.js new file mode 100644 index 0000000..e8768ed --- /dev/null +++ b/appengine/2048/js/keyboard_input_manager-original.js @@ -0,0 +1,153 @@ +function KeyboardInputManager() { + this.events = {}; + + if (window.navigator.msPointerEnabled) { + //Internet Explorer 10 style + this.eventTouchstart = "MSPointerDown"; + this.eventTouchmove = "MSPointerMove"; + this.eventTouchend = "MSPointerUp"; + } else { + this.eventTouchstart = "touchstart"; + this.eventTouchmove = "touchmove"; + this.eventTouchend = "touchend"; + } + + this.listen(); +} + +KeyboardInputManager.prototype.on = function (event, callback) { + if (!this.events[event]) { + this.events[event] = []; + } + this.events[event].push(callback); +}; + +KeyboardInputManager.prototype.emit = function (event, data) { + var callbacks = this.events[event]; + if (callbacks) { + callbacks.forEach(function (callback) { + callback(data); + }); + } +}; + +KeyboardInputManager.prototype.listen = function () { + var self = this; + + var map = { + 38: 0, // Up + 39: 1, // Right + 40: 2, // Down + 37: 3, // Left + 75: 0, // Vim up + 76: 1, // Vim right + 74: 2, // Vim down + 72: 3, // Vim left + 87: 0, // W + 68: 1, // D + 83: 2, // S + 65: 3 // A + }; + + // Respond to direction keys + document.addEventListener("keydown", function (event) { + var modifiers = event.altKey || event.ctrlKey || event.metaKey || + event.shiftKey; + var mapped = map[event.which]; + + // Ignore the event if it's happening in a text field + if (self.targetIsInput(event)) return; + + if (!modifiers) { + if (mapped !== undefined) { + event.preventDefault(); + self.emit("move", mapped); + } + } + + // R key restarts the game + if (!modifiers && event.which === 82) { + self.restart.call(self, event); + } + }); + + // Respond to button presses + this.bindButtonPress(".retry-button", this.restart); + this.bindButtonPress(".restart-button", this.restart); + this.bindButtonPress(".keep-playing-button", this.keepPlaying); + + // Respond to swipe events + var touchStartClientX, touchStartClientY; + var gameContainer = document.getElementsByClassName("game-container")[0]; + + gameContainer.addEventListener(this.eventTouchstart, function (event) { + if ((!window.navigator.msPointerEnabled && event.touches.length > 1) || + event.targetTouches > 1 || + self.targetIsInput(event)) { + return; // Ignore if touching with more than 1 finger or touching input + } + + if (window.navigator.msPointerEnabled) { + touchStartClientX = event.pageX; + touchStartClientY = event.pageY; + } else { + touchStartClientX = event.touches[0].clientX; + touchStartClientY = event.touches[0].clientY; + } + + event.preventDefault(); + }); + + gameContainer.addEventListener(this.eventTouchmove, function (event) { + event.preventDefault(); + }); + + gameContainer.addEventListener(this.eventTouchend, function (event) { + if ((!window.navigator.msPointerEnabled && event.touches.length > 0) || + event.targetTouches > 0 || + self.targetIsInput(event)) { + return; // Ignore if still touching with one or more fingers or input + } + + var touchEndClientX, touchEndClientY; + + if (window.navigator.msPointerEnabled) { + touchEndClientX = event.pageX; + touchEndClientY = event.pageY; + } else { + touchEndClientX = event.changedTouches[0].clientX; + touchEndClientY = event.changedTouches[0].clientY; + } + + var dx = touchEndClientX - touchStartClientX; + var absDx = Math.abs(dx); + + var dy = touchEndClientY - touchStartClientY; + var absDy = Math.abs(dy); + + if (Math.max(absDx, absDy) > 10) { + // (right : left) : (down : up) + self.emit("move", absDx > absDy ? (dx > 0 ? 1 : 3) : (dy > 0 ? 2 : 0)); + } + }); +}; + +KeyboardInputManager.prototype.restart = function (event) { + event.preventDefault(); + this.emit("restart"); +}; + +KeyboardInputManager.prototype.keepPlaying = function (event) { + event.preventDefault(); + this.emit("keepPlaying"); +}; + +KeyboardInputManager.prototype.bindButtonPress = function (selector, fn) { + var button = document.querySelector(selector); + button.addEventListener("click", fn.bind(this)); + button.addEventListener(this.eventTouchend, fn.bind(this)); +}; + +KeyboardInputManager.prototype.targetIsInput = function (event) { + return event.target.tagName.toLowerCase() === "input"; +}; diff --git a/appengine/2048/js/keyboard_input_manager.js b/appengine/2048/js/keyboard_input_manager.js new file mode 100644 index 0000000..6785cbe --- /dev/null +++ b/appengine/2048/js/keyboard_input_manager.js @@ -0,0 +1,28 @@ +document.onkeydown = checkKey; + +function checkKey(e) { + e = e || window.event; + + var map = { + 38: 0, // Up + 39: 1, // Right + 40: 2, // Down + 37: 3, // Left + 75: 0, // Vim up + 76: 1, // Vim right + 74: 2, // Vim down + 72: 3, // Vim left + 87: 0, // W + 68: 1, // D + 83: 2, // S + 65: 3 // A + }; + + if (e.keyCode == '38') { + alert map[e.keyCode]; + // up arrow + } + else if (e.keyCode == '40') { + // down arrow + } +} diff --git a/appengine/2048/main.py b/appengine/2048/main.py new file mode 100644 index 0000000..e2eb8d6 --- /dev/null +++ b/appengine/2048/main.py @@ -0,0 +1,38 @@ +import os + +import jinja2 +import webapp2 + +from ui import createblock +from grid import * + + +jinja_environment = jinja2.Environment( + loader=jinja2.FileSystemLoader(os.path.dirname(__file__))) + + +def format(grid): + blocks = [[None for i in range(4)] for i in range(4)] + for i in range(4): + for j in range(4): + blocks[i][j] = createblock(grid.get(i,j)) + return blocks + +class MainPage(webapp2.RequestHandler): + + def get(self): + template = jinja_environment.get_template('index.html') + nextmove = None + try: + nextmove = self.request.get('move') + grid.move(nextmove) + except: + pass + + blocks = format(grid) + template = jinja_environment.get_template('index.html') + self.response.out.write(template.render(blocks=blocks)) + +app = webapp2.WSGIApplication([ + ('/', MainPage), +], debug=True) diff --git a/appengine/2048/pysvg/__init__.py b/appengine/2048/pysvg/__init__.py new file mode 100644 index 0000000..b49afc2 --- /dev/null +++ b/appengine/2048/pysvg/__init__.py @@ -0,0 +1,14 @@ +#__all__ = [] +#for subpackage in ['core', 'filter', 'gradient', 'linking', 'script','shape','structure','style','text']: +# try: +# exec 'import ' + subpackage +# __all__.append( subpackage ) +# except ImportError: +# pass + + + + + + + \ No newline at end of file diff --git a/appengine/2048/pysvg/animate.py b/appengine/2048/pysvg/animate.py new file mode 100644 index 0000000..4bc8c94 --- /dev/null +++ b/appengine/2048/pysvg/animate.py @@ -0,0 +1,237 @@ +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +''' +(C) 2008, 2009 Kerim Mansour +For licensing information please refer to license.txt +''' +from attributes import * +from core import BaseShape, BaseElement + +##################################################### +# Attribute sets for animations +# Animation elements see below +##################################################### +class AnimationAttrib(XLinkAttrib): + """ + The AnimationAttrib class defines the Animation.attrib attribute set. + """ + +class AnimationAttributeAttrib: + """ + The AnimationAttributeAttrib class defines the AnimationAttribute.attrib attribute set. + """ + def set_attributeName(self, attributeName): + self._attributes['attributeName'] = attributeName + def get_attributeName(self): + return self._attributes.get('attributeName') + + def set_attributeType(self, attributeType): + self._attributes['attributeType'] = attributeType + def get_attributeType(self): + return self._attributes.get('attributeType') + +class AnimationTimingAttrib: + """ + The AnimationTimingAttrib class defines the AnimationTiming.attrib attribute set. + """ + def set_begin(self, begin): + self._attributes['begin'] = begin + def get_begin(self): + return self._attributes.get('begin') + + def set_dur(self, dur): + self._attributes['dur'] = dur + def get_dur(self): + return self._attributes.get('dur') + + def set_end(self, end): + self._attributes['end'] = end + def get_end(self): + return self._attributes.get('end') + + def set_min(self, min): + self._attributes['min'] = min + def get_min(self): + return self._attributes.get('min') + + def set_max(self, max): + self._attributes['max'] = max + def get_max(self): + return self._attributes.get('max') + + def set_restart(self, restart): + self._attributes['restart'] = restart + def get_restart(self): + return self._attributes.get('restart') + + def set_repeatCount(self, repeatCount): + self._attributes['repeatCount'] = repeatCount + def get_repeatCount(self): + return self._attributes.get('repeatCount') + + def set_repeatDur(self, repeatDur): + self._attributes['repeatDur'] = repeatDur + def get_repeatDur(self): + return self._attributes.get('repeatDur') + + def set_fill(self, fill): + self._attributes['fill'] = fill + def get_fill(self): + return self._attributes.get('fill') + +class AnimationValueAttrib: + """ + The AnimationValueAttrib class defines the AnimationValue.attrib attribute set. + """ + def set_calcMode(self, calcMode): + self._attributes['calcMode'] = calcMode + def get_calcMode(self): + return self._attributes.get('calcMode') + + def set_values(self, values): + self._attributes['values'] = values + def get_values(self): + return self._attributes.get('values') + + def set_keyTimes(self, keyTimes): + self._attributes['keyTimes'] = keyTimes + def get_keyTimes(self): + return self._attributes.get('keyTimes') + + def set_keySplines(self, keySplines): + self._attributes['keySplines'] = keySplines + def get_keySplines(self): + return self._attributes.get('keySplines') + + def set_from(self, fromField): + self._attributes['from'] = fromField + def get_from(self): + return self._attributes.get('from') + + def set_to(self, toField): + self._attributes['to'] = toField + def get_to(self): + return self._attributes.get('to') + + def set_by(self, by): + self._attributes['by'] = by + def get_by(self): + return self._attributes.get('by') + +class AnimationAdditionAttrib: + """ + The AnimationAdditionAttrib class defines the AnimationAddition.attrib attribute set. + """ + def set_additive(self, additive): + self._attributes['additive'] = additive + def get_additive(self): + return self._attributes.get('additive') + + def set_accumulate(self, accumulate): + self._attributes['accumulate'] = accumulate + def get_accumulate(self): + return self._attributes.get('accumulate') + +class AnimationEventsAttrib: + """ + The AnimationEventsAttrib class defines the AnimationEvents.attrib attribute set. + """ + def set_onbegin(self, onbegin): + self._attributes['onbegin'] = onbegin + def get_onbegin(self): + return self._attributes.get('onbegin') + + def set_onend(self, onend): + self._attributes['onend'] = onend + def get_onend(self): + return self._attributes.get('onend') + + def set_onrepeat(self, onrepeat): + self._attributes['onrepeat'] = onrepeat + def get_onrepeat(self): + return self._attributes.get('onrepeat') + + def set_onload(self, onload): + self._attributes['onload'] = onload + def get_onload(self): + return self._attributes.get('onload') +############################################## +# Animation Elements +############################################## +class Animate(BaseShape, CoreAttrib, ConditionalAttrib, ExternalAttrib, AnimationEventsAttrib, AnimationAttrib, AnimationAttributeAttrib, AnimationTimingAttrib, AnimationValueAttrib, AnimationAdditionAttrib): + """ + Class representing the animate element of an svg doc. + """ + def __init__(self, **kwargs): + BaseElement.__init__(self, 'animate') + self.setKWARGS(**kwargs) + +class Set(BaseShape, CoreAttrib, ConditionalAttrib, ExternalAttrib, AnimationEventsAttrib, AnimationAttrib, AnimationAttributeAttrib, AnimationTimingAttrib): + """ + Class representing the set element of an svg doc. + """ + def __init__(self, **kwargs): + BaseElement.__init__(self, 'set') + self.setKWARGS(**kwargs) + + def set_to(self, toField): + self._attributes['to'] = toField + def get_to(self): + return self._attributes.get('to') + +class AnimateMotion(BaseShape, CoreAttrib, ConditionalAttrib, ExternalAttrib, AnimationEventsAttrib, AnimationAttrib, AnimationTimingAttrib, AnimationValueAttrib, AnimationAdditionAttrib): + """ + Class representing the animateMotion element of an svg doc. + """ + def __init__(self, **kwargs): + BaseElement.__init__(self, 'animateMotion') + self.setKWARGS(**kwargs) + + def set_path(self, path): + self._attributes['path'] = path + def get_path(self): + return self._attributes.get('path') + + def set_keyPoints(self, keyPoints): + self._attributes['keyPoints'] = keyPoints + def get_keyPoints(self): + return self._attributes.get('keyPoints') + + def set_rotate(self, rotate): + self._attributes['rotate'] = rotate + def get_rotate(self): + return self._attributes.get('rotate') + + def set_origin(self, origin): + self._attributes['origin'] = origin + def get_origin(self): + return self._attributes.get('origin') + +class AnimateTransform(BaseShape, CoreAttrib, ConditionalAttrib, ExternalAttrib, AnimationEventsAttrib, AnimationAttrib, AnimationAttributeAttrib, AnimationTimingAttrib, AnimationValueAttrib, AnimationAdditionAttrib): + """ + Class representing the animateTransform element of an svg doc. + """ + def __init__(self, **kwargs): + BaseElement.__init__(self, 'animateTransform') + self.setKWARGS(**kwargs) + + def set_type(self, type): + self._attributes['type'] = type + def get_type(self): + return self._attributes.get('type') + +class AnimateColor(BaseShape, CoreAttrib, ConditionalAttrib, ExternalAttrib, AnimationEventsAttrib, AnimationAttrib, AnimationAttributeAttrib, AnimationTimingAttrib, AnimationValueAttrib, AnimationAdditionAttrib): + """ + Class representing the animateColor element of an svg doc. + """ + def __init__(self, **kwargs): + BaseElement.__init__(self, 'animateColor') + self.setKWARGS(**kwargs) + +class Mpath(BaseShape, CoreAttrib, XLinkAttrib, ExternalAttrib): + """ + Class representing the animateColor element of an svg doc. + """ + def __init__(self, **kwargs): + BaseElement.__init__(self, 'mpath') + self.setKWARGS(**kwargs) diff --git a/appengine/2048/pysvg/attributes.py b/appengine/2048/pysvg/attributes.py new file mode 100644 index 0000000..93caeee --- /dev/null +++ b/appengine/2048/pysvg/attributes.py @@ -0,0 +1,840 @@ +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +''' +(C) 2008, 2009 Kerim Mansour +For licensing information please refer to license.txt +''' +class CoreAttrib: + """ + The CoreAttrib class defines the attribute set Core.attrib + that is the core set of attributes that can be present on any element. + """ + def set_id(self, id): + self._attributes['id'] = id + + def get_id(self): + return self._attributes.get('id') + + def set_xml_base(self, xml_base): + self._attributes['xml:base'] = xml_base + + def get_xml_base(self): + return self._attributes.get('xml:base') + + def set_xml_lang(self, language_code): + self._attributes['xml:lang'] = language_code + + def get_xml_lang(self): + return self._attributes.get('xml:lang') + + def set_xml_space(self, xml_space): + self._attributes['xml:space'] = xml_space + + def get_xml_space(self): + return self._attributes.get('xml:space') + +class ConditionalAttrib: + """ + The ConditionalAttrib class defines the Conditional.attrib attribute set. + """ + def set_requiredFeatures(self, requiredFeatures): + self._attributes['requiredFeatures'] = requiredFeatures + + def get_requiredFeatures(self): + return self._attributes.get('requiredFeatures') + + def set_requiredExtensions(self, requiredExtensions): + self._attributes['requiredExtensions'] = requiredExtensions + + def get_requiredExtensions(self): + return self._attributes.get('requiredExtensions') + + def set_systemLanguage(self, language_code): + self._attributes['systemLanguage'] = language_code + + def get_systemLanguage(self): + return self._attributes.get('systemLanguage') + +class StyleAttrib: + """ + The StyleAttrib class defines the Style.attrib attribute set. + """ + def set_style(self, style): + self._attributes['style'] = style + + def get_style(self): + return self._attributes.get('style') + + def set_class(self, aClass): + self._attributes['class'] = aClass + + def get_class(self): + return self._attributes.get('class') + +class GraphicalEventsAttrib: + """ + The GraphicalEventsAttrib class defines the GraphicalEvents.attrib attribute set. + """ + def set_onfocusin(self, onfocusin): + self._attributes['onfocusin'] = onfocusin + + def get_onfocusin(self): + return self._attributes.get('onfocusin') + + def set_onfocusout(self, onfocusout): + self._attributes['onfocusout'] = onfocusout + + def get_onfocusout(self): + return self._attributes.get('onfocusout') + + def set_onactivate(self, onactivate): + self._attributes['onactivate'] = onactivate + + def get_onactivate(self): + return self._attributes.get('onactivate') + + def set_onclick(self, onclick): + self._attributes['onclick'] = onclick + + def get_onclick(self): + return self._attributes.get('onclick') + + def set_onmousedown(self, onmousedown): + self._attributes['onmousedown'] = onmousedown + + def get_onmousedown(self): + return self._attributes.get('onmousedown') + + def set_onmouseup(self, onmouseup): + self._attributes['onmouseup'] = onmouseup + + def get_onmouseup(self): + return self._attributes.get('onmouseup') + + def set_onmouseover(self, onmouseover): + self._attributes['onmouseover'] = onmouseover + + def get_onmouseover(self): + return self._attributes.get('onmouseover') + + def set_onmousemove(self, onmousemove): + self._attributes['onmousemove'] = onmousemove + + def get_onmousemove(self): + return self._attributes.get('onmousemove') + + def set_onmouseout(self, onmouseout): + self._attributes['onmouseout'] = onmouseout + + def get_onmouseout(self): + return self._attributes.get('onmouseout') + + def set_onload(self, onload): + self._attributes['onload'] = onload + + def get_onload(self): + return self._attributes.get('onload') + + + +class CursorAttrib: + """ + The CursorAttrib class defines the Cursor.attrib attribute set. + """ + def set_cursor(self, cursor): + self._attributes['cursor'] = cursor + + def get_cursor(self): + return self._attributes.get('cursor') + +class ExternalAttrib: + """ + The ExternalAttrib class defines the External.attrib attribute set. + """ + def set_externalResourcesRequired(self, externalResourcesRequired): + self._attributes['externalResourcesRequired'] = externalResourcesRequired + + def get_externalResourcesRequired(self): + return self._attributes.get('externalResourcesRequired') + +class DocumentEventsAttrib: + """ + The DocumentEventsAttrib class defines the DocumentEvents.attrib attribute set. + """ + def set_onunload(self, onunload): + self._attributes['onunload'] = onunload + + def get_onunload(self): + return self._attributes.get('onunload') + + def set_onabort(self, onabort): + self._attributes['onabort'] = onabort + + def get_onabort(self): + return self._attributes.get('onabort') + + def set_onerror(self, onerror): + self._attributes['onerror'] = onerror + + def get_onerror(self): + return self._attributes.get('onerror') + + def set_onresize(self, onresize): + self._attributes['onresize'] = onresize + + def get_onresize(self): + return self._attributes.get('onresize') + + def set_onscroll(self, onscroll): + self._attributes['onscroll'] = onscroll + + def get_onscroll(self): + return self._attributes.get('onscroll') + + def set_onzoom(self, onzoom): + self._attributes['onzoom'] = onzoom + + def get_onzoom(self): + return self._attributes.get('onzoom') + +class OpacityAttrib: + """ + The OpacityAttrib class defines the Opacity.attrib attribute set. + """ + def set_opacity(self, opacity): + self._attributes['opacity'] = opacity + + def get_opacity(self): + return self._attributes.get('opacity') + + def set_stroke_opacity(self, stroke_opacity): + self._attributes['stroke-opacity'] = stroke_opacity + + def get_stroke_opacity(self): + return self._attributes.get('stroke-opacity') + + def set_fill_opacity(self, fill_opacity): + self._attributes['fill-opacity'] = fill_opacity + + def get_fill_opacity(self): + return self._attributes.get('fill-opacity') + +class PaintAttrib: + """ + The PaintAttrib class defines the Paint.attrib attribute set. + """ + def set_color(self, color): + self._attributes['color'] = color + + def get_color(self): + return self._attributes.get('color') + + def set_color_interpolation(self, color_interpolation): + self._attributes['color-interpolation'] = color_interpolation + + def get_color_interpolation(self): + return self._attributes.get('color-interpolation') + + def set_color_rendering(self, color_rendering): + self._attributes['color-rendering'] = color_rendering + + def get_color_rendering(self): + return self._attributes.get('color-rendering') + + def set_fill(self, fill): + self._attributes['fill'] = fill + + def get_fill(self): + return self._attributes.get('fill') + + def set_fill_rule(self, fill_rule): + self._attributes['fill-rule'] = fill_rule + + def get_fill_rule(self): + return self._attributes.get('fill-rule') + + def set_stroke(self, stroke): + self._attributes['stroke'] = stroke + + def get_stroke(self): + return self._attributes.get('stroke') + + def set_stroke_dasharray(self, stroke_dasharray): + self._attributes['stroke-dasharray'] = stroke_dasharray + + def get_stroke_dasharray(self): + return self._attributes.get('stroke-dasharray') + + def set_stroke_dashoffset(self, stroke_dashoffset): + self._attributes['stroke-dashoffset'] = stroke_dashoffset + + def get_stroke_dashoffset(self): + return self._attributes.get('stroke-offset') + + def set_stroke_linecap(self, stroke_linecap): + self._attributes['stroke-linecap'] = stroke_linecap + + def get_stroke_linecap(self): + return self._attributes.get('stroke-linecap') + + def set_stroke_linejoin(self, stroke_linejoin): + self._attributes['stroke-linejoin'] = stroke_linejoin + + def get_stroke_linejoin(self): + return self._attributes.get('stroke-linejoin') + + def set_stroke_miterlimit(self, stroke_miterlimit): + self._attributes['stroke-miterlimit'] = stroke_miterlimit + + def get_stroke_miterlimit(self): + return self._attributes.get('stroke-miterlimit') + + def set_stroke_width(self, stroke_width): + self._attributes['stroke-width'] = stroke_width + + def get_stroke_width(self): + return self._attributes.get('stroke-width') + +class GraphicsAttrib: + """ + The GraphicsAttrib class defines the Graphics.attrib attribute set. + """ + def set_display(self, display): + self._attributes['display'] = display + + def get_display(self): + return self._attributes.get('display') + + def set_image_rendering(self, image_rendering): + self._attributes['image-rendering'] = image_rendering + + def get_image_rendering(self): + return self._attributes.get('image-rendering') + + def set_pointer_events(self, pointer_events): + self._attributes['pointer-events'] = pointer_events + + def get_pointer_events(self): + return self._attributes.get('pointer-events') + + def set_shape_rendering(self, shape_rendering): + self._attributes['shape-rendering'] = shape_rendering + + def get_shape_rendering(self): + return self._attributes.get('shape-rendering') + + def set_text_rendering(self, text_rendering): + self._attributes['text-rendering'] = text_rendering + + def get_text_rendering(self): + return self._attributes.get('text-rendering') + + def set_visibility(self, visibility): + self._attributes['visibility'] = visibility + + def get_visibility(self): + return self._attributes.get('visibility') + +class MarkerAttrib: + """ + The MarkerAttrib class defines the Marker.attrib attribute set. + """ + def set_marker_start(self, marker_start): + self._attributes['marker-start'] = marker_start + + def get_marker_start(self): + return self._attributes.get('marker-start') + + def set_marker_mid(self, marker_mid): + self._attributes['marker-mid'] = marker_mid + + def get_marker_mid(self): + return self._attributes.get('marker-mid') + + def set_marker_end(self, marker_end): + self._attributes['marker-end'] = marker_end + + def get_marker_end(self): + return self._attributes.get('marker-end') + +class ViewportAttrib: + """ + The ViewportAttrib class defines the Viewport.attrib attribute set. + """ + def set_clip(self, clip): + self._attributes['clip'] = clip + + def get_clip(self): + return self._attributes.get('clip') + + def set_overflow(self, overflow): + self._attributes['overflow'] = overflow + + def get_overflow(self): + return self._attributes.get('overflow') + +class FilterAttrib: + """ + The FilterAttrib class defines the Filter.attrib attribute sets. + """ + def set_filter(self, filter): + self._attributes['filter'] = filter + + def get_filter(self): + return self._attributes.get('filter') + +class FilterColorAttrib: + """ + The FilterColorAttrib class defines the FilterColor.attrib attribute sets. + """ + def set_color_interpolation_filters(self, color_interpolation_filters): + self._attributes['color-interpolation-filters'] = color_interpolation_filters + + def get_color_interpolation_filters(self): + return self._attributes.get('color-interpolation-filters') + +class FilterPrimitiveAttrib: + """ + The FilterPrimitiveAttrib class defines the FilterPrimitive.attrib attribute sets. + """ + def set_x(self, x): + self._attributes['x'] = x + + def get_x(self): + return self._attributes.get('x') + + def set_y(self, y): + self._attributes['y'] = y + + def get_y(self): + return self._attributes.get('y') + + def set_height(self, height): + self._attributes['height'] = height + + def get_height(self): + return self._attributes.get('height') + + def set_width(self, width): + self._attributes['width'] = width + + def get_width(self): + return self._attributes.get('width') + + def set_result(self, result): + self._attributes['result'] = result + + def get_result(self): + return self._attributes.get('result') + +class FilterPrimitiveWithInAttrib(FilterPrimitiveAttrib): + """ + The FilterPrimitiveWithInAttrib class defines the FilterPrimitiveWithIn.attrib attribute sets. + """ + def set_in(self, inValue): + self._attributes['in'] = inValue + + def get_in(self): + return self._attributes.get('in') + +class XLinkAttrib: + """ + The XLinkAttrib class defines the XLink.attrib, XLinkRequired.attrib, XLinkEmbed.attrib and XLinkReplace.attrib attribute sets. + """ + def set_xlink_type(self, xlink_type): + self._attributes['xlink:type'] = xlink_type + def get_xlink_type(self): + return self._attributes['xlink:type'] + + def set_xlink_href(self, xlink_href): + self._attributes['xlink:href'] = xlink_href + def get_xlink_href(self): + return self._attributes['xlink:href'] + + def set_xlink_role(self, xlink_role): + self._attributes['xlink:role'] = xlink_role + def get_xlink_role(self): + return self._attributes['xlink:role'] + + def set_xlink_arcrole(self, xlink_arcrole): + self._attributes['xlink:arcrole'] = xlink_arcrole + def get_xlink_arcrole(self): + return self._attributes['xlink:arcrole'] + + def set_xlink_title(self, xlink_title): + self._attributes['xlink:title'] = xlink_title + def get_xlink_title(self): + return self._attributes['xlink:title'] + + def set_xlink_show(self, xlink_show): + self._attributes['xlink:show'] = xlink_show + def get_xlink_show(self): + return self._attributes['xlink:show'] + + def set_xlink_actuate(self, xlink_actuate): + self._attributes['xlink:actuate'] = xlink_actuate + def get_xlink_actuate(self): + return self._attributes['xlink:actuate'] + +class TextAttrib: + """ + The TextAttrib class defines the Text.attrib attribute set. + """ + def set_writing_mode(self, writing_mode): + self._attributes['writing-mode'] = writing_mode + def get_writing_mode(self): + return self._attributes['writing-mode'] + +class TextContentAttrib: + """ + The TextContentAttrib class defines the TextContent.attrib attribute set. + """ + def set_alignment_baseline(self, alignment_baseline): + self._attributes['alignment-baseline'] = alignment_baseline + def get_alignment_baseline(self): + return self._attributes['alignment-baseline'] + + def set_baseline_shift(self, baseline_shift): + self._attributes['baseline-shift'] = baseline_shift + def get_baseline_shift(self): + return self._attributes['baseline-shift'] + + def set_direction(self, direction): + self._attributes['direction'] = direction + def get_direction(self): + return self._attributes['direction'] + + def set_dominant_baseline(self, dominant_baseline): + self._attributes['dominant-baseline'] = dominant_baseline + def get_dominant_baseline(self): + return self._attributes['dominant-baseline'] + + def set_glyph_orientation_horizontal(self, glyph_orientation_horizontal): + self._attributes['glyph-orientation-horizontal'] = glyph_orientation_horizontal + def get_glyph_orientation_horizontal(self): + return self._attributes['glyph-orientation-horizontal'] + + def set_glyph_orientation_vertical(self, glyph_orientation_vertical): + self._attributes['glyph-orientation-vertical'] = glyph_orientation_vertical + def get_glyph_orientation_vertical(self): + return self._attributes['glyph-orientation-vertical'] + + def set_kerning(self, kerning): + self._attributes['kerning'] = kerning + def get_kerning(self): + return self._attributes['kerning'] + + def set_letter_spacing(self, letter_spacing): + self._attributes['letter-spacing'] = letter_spacing + def get_letter_spacing(self): + return self._attributes['letter-spacing'] + + def set_text_anchor(self, text_anchor): + self._attributes['text-anchor'] = text_anchor + def get_text_anchor(self): + return self._attributes['text-anchor'] + + def set_text_decoration(self, text_decoration): + self._attributes['text-decoration'] = text_decoration + def get_text_decoration(self): + return self._attributes['text-decoration'] + + def set_unicode_bidi(self, unicode_bidi): + self._attributes['unicode-bidi'] = unicode_bidi + def get_unicode_bidi(self): + return self._attributes['unicode-bidi'] + + def set_word_spacing(self, word_spacing): + self._attributes['word-spacing'] = word_spacing + def get_word_spacing(self): + return self._attributes['word-spacing'] + +class FontAttrib: + """ + The FontAttrib class defines the Font.attrib attribute set. + """ + def set_font_family(self, font_family): + self._attributes['font-family'] = font_family + def get_font_family(self): + return self._attributes['font-family'] + + def set_font_size(self, font_size): + self._attributes['font-size'] = font_size + def get_font_size(self): + return self._attributes['font-size'] + + def set_font_size_adjust(self, font_size_adjust): + self._attributes['font-size-adjust'] = font_size_adjust + def get_font_size_adjust(self): + return self._attributes['font-size-adjust'] + + def set_font_stretch(self, font_stretch): + self._attributes['font-stretch'] = font_stretch + def get_font_stretch(self): + return self._attributes['font-stretch'] + + def set_font_style(self, font_style): + self._attributes['font-style'] = font_style + def get_font_style(self): + return self._attributes['font-style'] + + def set_font_variant(self, font_variant): + self._attributes['font-variant'] = font_variant + def get_font_variant(self): + return self._attributes['font-variant'] + + def set_font_weight(self, font_weight): + self._attributes['font-weight'] = font_weight + def get_font_weight(self): + return self._attributes['font-weight'] + +class MaskAttrib: + """ + The MaskAttrib class defines the Mask.attrib attribute set. + """ + def set_mask(self, mask): + self._attributes['mask'] = mask + + def get_mask(self): + return self._attributes.get('mask') + +class ClipAttrib: + """ + The ClipAttrib class defines the Clip.attrib attribute set. + """ + def set_clip_path(self, clip_path): + self._attributes['clip-path'] = clip_path + + def get_clip_path(self): + return self._attributes.get('clip-path') + + def set_clip_rule(self, clip_rule): + self._attributes['clip-rule'] = clip_rule + + def get_clip_rule(self): + return self._attributes.get('clip-rule') + +class GradientAttrib: + """ + The GradientAttrib class defines the Gradient.attrib attribute set. + """ + def set_stop_color(self, stop_color): + self._attributes['stop-color'] = stop_color + + def get_stop_color(self): + return self._attributes.get('stop-color') + + def set_stop_opacity(self, stop_opacity): + self._attributes['stop-opacity'] = stop_opacity + + def get_stop_opacity(self): + return self._attributes.get('stop-opacity') + +class PresentationAttributes_Color: + """ + The PresentationAttributes_Color class defines the PresentationAttributes_Color.attrib attribute set. + The following presentation attributes have to do with specifying color. + """ + def set_color(self, color): + self._attributes['color'] = color + + def get_color(self): + return self._attributes.get('color') + + def set_color_interpolation(self, color_interpolation): + self._attributes['color-interpolation'] = color_interpolation + + def get_color_interpolation(self): + return self._attributes.get('color-interpolation') + + def set_color_rendering(self, color_rendering): + self._attributes['color-rendering'] = color_rendering + + def get_color_rendering(self): + return self._attributes.get('color-rendering') + +class PresentationAttributes_Containers: + """ + The PresentationAttributes_Containers class defines the PresentationAttributes_Containers.attrib attribute set. + The following presentation attributes apply to container elements. + """ + def set_enable_background(self, enableBackground): + self._attributes['enable-background'] = enableBackground + + def get_enable_background(self): + return self._attributes.get('enable-background') + +class PresentationAttributes_feFlood: + """ + The PresentationAttributes_feFlood class defines the PresentationAttributes_feFlood.attrib attribute set. + The following presentation attributes apply to 'feFlood' elements. + """ + def set_flood_color(self, flood_color): + self._attributes['flood-color'] = flood_color + def get_flood_color(self): + return self._attributes.get('flood-color') + + def set_flood_opacity(self, flood_opacity): + self._attributes['flood-opacity'] = flood_opacity + def get_flood_opacity(self): + return self._attributes.get('flood-opacity') + +class PresentationAttributes_FilterPrimitives: + """ + The PresentationAttributes_FilterPrimitives class defines the PresentationAttributes_FilterPrimitives.attrib attribute set. + The following presentation attributes apply to filter primitives + """ + def set_color_interpolation_filters(self, color_interpolation_filters): + self._attributes['color-interpolation-filters'] = color_interpolation_filters + def get_color_interpolation_filters(self): + return self._attributes.get('color-interpolation-filters') + +class PresentationAttributes_FillStroke: + """ + The PresentationAttributes_FillStroke class defines the PresentationAttributes_FillStroke.attrib attribute set. + The following presentation attributes apply to filling and stroking operations. + """ + def set_fill(self, fill): + self._attributes['fill'] = fill + + def get_fill(self): + return self._attributes.get('fill') + + def set_fill_opacity(self, fill_opacity): + self._attributes['fill-opacity'] = fill_opacity + + def get_fill_opacity(self): + return self._attributes.get('fill-opacity') + + def set_fill_rule(self, fill_rule): + self._attributes['fill-rule'] = fill_rule + + def get_fill_rule(self): + return self._attributes.get('fill-rule') + + def set_stroke(self, stroke): + self._attributes['stroke'] = stroke + + def get_stroke(self): + return self._attributes.get('stroke') + + def set_stroke_opacity(self, stroke_opacity): + self._attributes['stroke-opacity'] = stroke_opacity + + def get_stroke_opacity(self): + return self._attributes.get('stroke-opacity') + + def set_stroke_dasharray(self, stroke_dasharray): + self._attributes['stroke-dasharray'] = stroke_dasharray + + def get_stroke_dasharray(self): + return self._attributes.get('stroke-dasharray') + + def set_stroke_dashoffset(self, stroke_dashoffset): + self._attributes['stroke-dashoffset'] = stroke_dashoffset + + def get_stroke_dashoffset(self): + return self._attributes.get('stroke-offset') + + def set_stroke_linecap(self, stroke_linecap): + self._attributes['stroke-linecap'] = stroke_linecap + + def get_stroke_linecap(self): + return self._attributes.get('stroke-linecap') + + def set_stroke_linejoin(self, stroke_linejoin): + self._attributes['stroke-linejoin'] = stroke_linejoin + + def get_stroke_linejoin(self): + return self._attributes.get('stroke-linejoin') + + def set_stroke_miterlimit(self, stroke_miterlimit): + self._attributes['stroke-miterlimit'] = stroke_miterlimit + + def get_stroke_miterlimit(self): + return self._attributes.get('stroke-miterlimit') + + def set_stroke_width(self, stroke_width): + self._attributes['stroke-width'] = stroke_width + + def get_stroke_width(self): + return self._attributes.get('stroke-width') + +class PresentationAttributes_FontSpecification(FontAttrib): + """ + The PresentationAttributes_FontSpecification class defines the PresentationAttributes_FontSpecification.attrib attribute set. + The following presentation attributes have to do with selecting a font to use. + """ + +class PresentationAttributes_Gradients(GradientAttrib): + """ + The PresentationAttributes_Gradients class defines the PresentationAttributes_Gradients.attrib attribute set. + The following presentation attributes apply to gradient 'stop' elements. + """ + +class PresentationAttributes_Graphics(ClipAttrib, CursorAttrib, GraphicsAttrib, MaskAttrib, FilterAttrib): + """ + The PresentationAttributes_Graphics class defines the PresentationAttributes_Graphics.attrib attribute set. + The following presentation attributes apply to graphics elements + """ + def set_opacity(self, opacity): + self._attributes['opacity'] = opacity + + def get_opacity(self): + return self._attributes.get('opacity') + +class PresentationAttributes_Images: + """ + The PresentationAttributes_Images class defines the PresentationAttributes_Images.attrib attribute set. + The following presentation attributes apply to 'image' elements + """ + def set_color_profile(self, color_profile): + self._attributes['color-profile'] = color_profile + + def get_color_profile(self): + return self._attributes.get('color-profile') + +class PresentationAttributes_LightingEffects: + """ + The PresentationAttributes_LightingEffects class defines the PresentationAttributes_LightingEffects.attrib attribute set. + The following presentation attributes apply to 'feDiffuseLighting' and 'feSpecularLighting' elements + """ + def set_lighting_color(self, lighting_color): + self._attributes['lighting-color'] = lighting_color + def get_lighting_color(self): + return self._attributes.get('lighting-color') + +class PresentationAttributes_Marker(MarkerAttrib): + """ + The PresentationAttributes_Marker class defines the PresentationAttributes_Marker.attrib attribute set. + The following presentation attributes apply to marker operations + """ + +class PresentationAttributes_TextContentElements(TextContentAttrib): + """ + The PresentationAttributes_TextContentElements class defines the PresentationAttributes_TextContentElements.attrib attribute set. + The following presentation attributes apply to text content elements + """ + +class PresentationAttributes_TextElements(TextAttrib): + """ + The following presentation attributes apply to 'text' elements + """ + +class PresentationAttributes_Viewports(ViewportAttrib): + """ + The following presentation attributes apply to elements that establish viewports + """ + +class PresentationAttributes_All(PresentationAttributes_Color, PresentationAttributes_Containers, PresentationAttributes_feFlood, PresentationAttributes_FillStroke, PresentationAttributes_FilterPrimitives, PresentationAttributes_FontSpecification, PresentationAttributes_Gradients, PresentationAttributes_Graphics, PresentationAttributes_Images, PresentationAttributes_LightingEffects, PresentationAttributes_Marker, PresentationAttributes_TextContentElements, PresentationAttributes_TextElements, PresentationAttributes_Viewports): + """ + The PresentationAttributes_All class defines the Presentation.attrib attribute set. + """ + + +class ColorAttrib: + """ + The ColorAttrib class defines the Color.attrib attribute set. + """ + + diff --git a/appengine/2048/pysvg/builders.py b/appengine/2048/pysvg/builders.py new file mode 100644 index 0000000..43ada5a --- /dev/null +++ b/appengine/2048/pysvg/builders.py @@ -0,0 +1,359 @@ +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +''' +(C) 2008, 2009 Kerim Mansour +For licensing information please refer to license.txt +''' +from pysvg.animate import * +from pysvg.filter import * +from pysvg.gradient import * +from pysvg.linking import * +from pysvg.script import * +from pysvg.shape import * +from pysvg.structure import * +from pysvg.style import * +from pysvg.text import * + +class ShapeBuilder: + """ + Helper class that creates commonly used objects and shapes with predefined styles and + few but often used parameters. Used to avoid more complex coding for common tasks. + """ + + def createCircle(self, cx, cy, r, strokewidth=1, stroke='black', fill='none'): + """ + Creates a circle + @type cx: string or int + @param cx: starting x-coordinate + @type cy: string or int + @param cy: starting y-coordinate + @type r: string or int + @param r: radius + @type strokewidth: string or int + @param strokewidth: width of the pen used to draw + @type stroke: string (either css constants like "black" or numerical values like "#FFFFFF") + @param stroke: color with which to draw the outer limits + @type fill: string (either css constants like "black" or numerical values like "#FFFFFF") + @param fill: color with which to fill the element (default: no filling) + @return: a circle object + """ + style_dict = {'fill':fill, 'stroke-width':strokewidth, 'stroke':stroke} + myStyle = StyleBuilder(style_dict) + c = Circle(cx, cy, r) + c.set_style(myStyle.getStyle()) + return c + + def createEllipse(self, cx, cy, rx, ry, strokewidth=1, stroke='black', fill='none'): + """ + Creates an ellipse + @type cx: string or int + @param cx: starting x-coordinate + @type cy: string or int + @param cy: starting y-coordinate + @type rx: string or int + @param rx: radius in x direction + @type ry: string or int + @param ry: radius in y direction + @type strokewidth: string or int + @param strokewidth: width of the pen used to draw + @type stroke: string (either css constants like "black" or numerical values like "#FFFFFF") + @param stroke: color with which to draw the outer limits + @type fill: string (either css constants like "black" or numerical values like "#FFFFFF") + @param fill: color with which to fill the element (default: no filling) + @return: an ellipse object + """ + style_dict = {'fill':fill, 'stroke-width':strokewidth, 'stroke':stroke} + myStyle = StyleBuilder(style_dict) + e = Ellipse(cx, cy, rx, ry) + e.set_style(myStyle.getStyle()) + return e + + def createRect(self, x, y, width, height, rx=None, ry=None, strokewidth=1, stroke='black', fill='none'): + """ + Creates a Rectangle + @type x: string or int + @param x: starting x-coordinate + @type y: string or int + @param y: starting y-coordinate + @type width: string or int + @param width: width of the rectangle + @type height: string or int + @param height: height of the rectangle + @type rx: string or int + @param rx: For rounded rectangles, the x-axis radius of the ellipse used to round off the corners of the rectangle. + @type ry: string or int + @param ry: For rounded rectangles, the y-axis radius of the ellipse used to round off the corners of the rectangle. + @type strokewidth: string or int + @param strokewidth: width of the pen used to draw + @type stroke: string (either css constants like "black" or numerical values like "#FFFFFF") + @param stroke: color with which to draw the outer limits + @type fill: string (either css constants like "black" or numerical values like "#FFFFFF") + @param fill: color with which to fill the element (default: no filling) + @return: a rect object + """ + style_dict = {'fill':fill, 'stroke-width':strokewidth, 'stroke':stroke} + myStyle = StyleBuilder(style_dict) + r = Rect(x, y, width, height, rx, ry) + r.set_style(myStyle.getStyle()) + return r + + def createPolygon(self, points, strokewidth=1, stroke='black', fill='none'): + """ + Creates a Polygon + @type points: string in the form "x1,y1 x2,y2 x3,y3" + @param points: all points relevant to the polygon + @type strokewidth: string or int + @param strokewidth: width of the pen used to draw + @type stroke: string (either css constants like "black" or numerical values like "#FFFFFF") + @param stroke: color with which to draw the outer limits + @type fill: string (either css constants like "black" or numerical values like "#FFFFFF") + @param fill: color with which to fill the element (default: no filling) + @return: a polygon object + """ + style_dict = {'fill':fill, 'stroke-width':strokewidth, 'stroke':stroke} + myStyle = StyleBuilder(style_dict) + p = Polygon(points=points) + p.set_style(myStyle.getStyle()) + return p + + def createPolyline(self, points, strokewidth=1, stroke='black'): + """ + Creates a Polyline + @type points: string in the form "x1,y1 x2,y2 x3,y3" + @param points: all points relevant to the polygon + @type strokewidth: string or int + @param strokewidth: width of the pen used to draw + @type stroke: string (either css constants like "black" or numerical values like "#FFFFFF") + @param stroke: color with which to draw the outer limits + @return: a polyline object + """ + style_dict = {'fill':'none', 'stroke-width':strokewidth, 'stroke':stroke} + myStyle = StyleBuilder(style_dict) + p = Polyline(points=points) + p.set_style(myStyle.getStyle()) + return p + + + def createLine(self, x1, y1, x2, y2, strokewidth=1, stroke="black"): + """ + Creates a line + @type x1: string or int + @param x1: starting x-coordinate + @type y1: string or int + @param y1: starting y-coordinate + @type x2: string or int + @param x2: ending x-coordinate + @type y2: string or int + @param y2: ending y-coordinate + @type strokewidth: string or int + @param strokewidth: width of the pen used to draw + @type stroke: string (either css constants like "black" or numerical values like "#FFFFFF") + @param stroke: color with which to draw the outer limits + @return: a line object + """ + style_dict = {'stroke-width':strokewidth, 'stroke':stroke} + myStyle = StyleBuilder(style_dict) + l = Line(x1, y1, x2, y2) + l.set_style(myStyle.getStyle()) + return l + + def convertTupleArrayToPoints(self, arrayOfPointTuples): + """Method used to convert an array of tuples (x,y) into a string + suitable for createPolygon or createPolyline + @type arrayOfPointTuples: An array containing tuples eg.[(x1,y1),(x2,y2] + @param arrayOfPointTuples: All points needed to create the shape + @return a string in the form "x1,y1 x2,y2 x3,y3" + """ + points = "" + for tuple in arrayOfPointTuples: + points += str(tuple[0]) + "," + str(tuple[1]) + " " + return points + + + +###################################################################### +# Style Builder. Utility class to create styles for your shapes etc. +###################################################################### +class StyleBuilder: + """ + Class to create a style string for those not familiar with svg attribute names. + How to use it: + 1) create an instance of StyleBuilder (builder=....) + 2) set the attributes you want to have + 3) create the shape (element) you want + 4) call set_style on the element with "builder.getStyle()" as parameter + """ + def __init__(self, aStyle_dict=None): + if aStyle_dict == None: + self.style_dict = {} + else: + self.style_dict = aStyle_dict + + + # tested below + def setFontFamily(self, fontfamily): + self.style_dict["font-family"] = fontfamily + + def setFontSize(self, fontsize): + self.style_dict["font-size"] = fontsize + + def setFontStyle(self, fontstyle): + self.style_dict["font-style"] = fontstyle + + def setFontWeight(self, fontweight): + self.style_dict["font-weight"] = fontweight + + #tested + def setFilling(self, fill): + self.style_dict["fill"] = fill + + def setFillOpacity(self, fillopacity): + self.style_dict["fill-opacity"] = fillopacity + + def setFillRule(self, fillrule): + self.style_dict["fill-rule"] = fillrule + + def setStrokeWidth(self, strokewidth): + self.style_dict["stroke-width"] = strokewidth + + def setStroke(self, stroke): + self.style_dict["stroke"] = stroke + + #untested below + def setStrokeDashArray(self, strokedasharray): + self.style_dict["stroke-dasharray"] = strokedasharray + def setStrokeDashOffset(self, strokedashoffset): + self.style_dict["stroke-dashoffset"] = strokedashoffset + def setStrokeLineCap(self, strikelinecap): + self.style_dict["stroke-linecap"] = strikelinecap + def setStrokeLineJoin(self, strokelinejoin): + self.style_dict["stroke-linejoin"] = strokelinejoin + def setStrokeMiterLimit(self, strokemiterlimit): + self.style_dict["stroke-miterlimit"] = strokemiterlimit + def setStrokeOpacity(self, strokeopacity): + self.style_dict["stroke-opacity"] = strokeopacity + + + #is used to provide a potential indirect value (currentColor) for the 'fill', 'stroke', 'stop-color' properties. + def setCurrentColor(self, color): + self.style_dict["color"] = color + + # Gradient properties: + def setStopColor(self, stopcolor): + self.style_dict["stop-color"] = stopcolor + + def setStopOpacity(self, stopopacity): + self.style_dict["stop-opacity"] = stopopacity + + #rendering properties + def setColorRendering(self, colorrendering): + self.style_dict["color-rendering"] = colorrendering + + def setImageRendering(self, imagerendering): + self.style_dict["image-rendering"] = imagerendering + + def setShapeRendering(self, shaperendering): + self.style_dict["shape-rendering"] = shaperendering + + def setTextRendering(self, textrendering): + self.style_dict["text-rendering"] = textrendering + + def setSolidColor(self, solidcolor): + self.style_dict["solid-color"] = solidcolor + + def setSolidOpacity(self, solidopacity): + self.style_dict["solid-opacity"] = solidopacity + + #Viewport properties + def setVectorEffect(self, vectoreffect): + self.style_dict["vector-effect"] = vectoreffect + + def setViewPortFill(self, viewportfill): + self.style_dict["viewport-fill"] = viewportfill + + def setViewPortOpacity(self, viewportfillopacity): + self.style_dict["viewport-fill_opacity"] = viewportfillopacity + + # Text properties + def setDisplayAlign(self, displayalign): + self.style_dict["display-align"] = displayalign + + def setLineIncrement(self, lineincrement): + self.style_dict["line-increment"] = lineincrement + + def setTextAnchor(self, textanchor): + self.style_dict["text-anchor"] = textanchor + + #def getStyleDict(self): + # return self.style_dict + + + def getStyle(self): + string = ''#style="' + for key, value in self.style_dict.items(): + if value <> None and value <> '': + string += str(key) + ':' + str(value) + '; ' + return string + +###################################################################### +# Transform Builder. Utility class to create transformations for your shapes etc. +###################################################################### +class TransformBuilder: + """ + Class to create a transform string for those not familiar with svg attribute names. + How to use it: + 1) create an instance of TransformBuilder (builder=....) + 2) set the attributes you want to have + 3) create the shape (element) you want + 4) call set_transform on the element with "builder.getTransform()" as parameter + """ + def __init__(self): + self.transform_dict = {} + + #def setMatrix(self, matrix): + # self.transform_dict["matrix"] = 'matrix(%s)' % matrix + + def setMatrix(self, a, b, c, d, e, f): + self.transform_dict["matrix"] = 'matrix(%s %s %s %s %s %s)' % (a, b, c, d, e, f) + + def setRotation(self, rotate): + self.transform_dict["rotate"] = 'rotate(%s)' % rotate + + #def setRotation(self, rotation, cx=None, cy=None): + # if cx != None and cy != None: + # self.transform_dict["rotate"] = 'rotate(%s %s %s)' % (rotation, cx, cy) + # else: + # self.transform_dict["rotate"] = 'rotate(%s)' % (rotation) + + def setTranslation(self, translate): + self.transform_dict["translate"] = 'translate(%s)' % (translate) + + #def setTranslation(self, x, y=0): + # self.transform_dict["translate"] = 'translate(%s %s)' % (x, y) + + #def setScaling(self, scale): + # self.transform_dict["scale"] = 'scale(%s)' % (scale) + + def setScaling(self, x=None, y=None): + if x == None and y != None: + x = y + elif x != None and y == None: + y = x + self.transform_dict["scale"] = 'scale(%s %s)' % (x, y) + + def setSkewY(self, skewY): + self.transform_dict["skewY"] = 'skewY(%s)' % (skewY) + + def setSkewX(self, skewX): + self.transform_dict["skewX"] = 'skewX(%s)' % (skewX) + + #def getTransformDict(self): + # return self.transform_dict + + def getTransform(self): + string = ''#style="' + for key, value in self.transform_dict.items(): + if value <> None and value <> '': + #string+=str(key)+':'+str(value)+'; ' + string += str(value) + ' ' + return string diff --git a/appengine/2048/pysvg/core.py b/appengine/2048/pysvg/core.py new file mode 100644 index 0000000..b2d2e71 --- /dev/null +++ b/appengine/2048/pysvg/core.py @@ -0,0 +1,265 @@ +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +''' +(C) 2008, 2009 Kerim Mansour +For licensing information please refer to license.txt +''' +from attributes import CoreAttrib, ConditionalAttrib, StyleAttrib, GraphicalEventsAttrib, PaintAttrib, OpacityAttrib, GraphicsAttrib, CursorAttrib, FilterAttrib, MaskAttrib, ClipAttrib +import codecs + +class BaseElement: + """ + This is the base class for all svg elements like title etc. It provides common functionality. + It should NOT be directly used by anyone. + """ + def __init__(self, elementName): + """ + initializes the object + @type elementName: string + @param elementName: name of the element (used for the xml tag) + """ + self._elementName=elementName + self._attributes={} #key value + self._textContent="" + self._subElements=[] + + def appendTextContent(self,text): + self.addElement(TextContent(text)) + + def addElement(self,element): + self._subElements.append(element) + + def getElementAt(self,pos): + """ returns the element at a specific position within this svg + """ + return self._subElements[pos] + + def getAllElements(self): + """ returns all elements contained within the top level element list of this element + """ + return self._subElements + + def getAllElementsOfHirarchy(self): + """ returns ALL elements of the complete hirarchy as a flat list + """ + allElements=[] + for element in self.getAllElements(): + allElements.append(element) + if isinstance(element, BaseElement): + allElements.extend(element.getAllElementsOfHirarchy()) + return allElements + + def getElementByID(self, id): + """ returns an element with the specific id and the position of that element within the svg elements array + """ + pos=0 + for element in self._subElements: + if element.get_id()==id: + return (element,pos) + pos+=1 + + def getElementsByType(self, type): + """ + retrieves all Elements that are of type type + @type type: class + @param type: type of the element + """ + foundElements=[] + for element in self.getAllElementsOfHirarchy(): + if isinstance(element, type): + foundElements.append(element) + + return foundElements + + def insertElementAt(self, element, pos): + return self._subElements.insert(pos, element) + + + def getXML(self): + """ + Return a XML representation of the current element. + This function can be used for debugging purposes. It is also used by getXML in SVG + + @return: the representation of the current element as an xml string + """ + xml='<'+self._elementName+' ' + for key,value in self._attributes.items(): + if value != None: + xml+=key+'="'+self.quote_attrib(str(value))+'" ' + if len(self._subElements)==0: #self._textContent==None and + xml+=' />\n' + else: + xml+=' >\n' + #if self._textContent==None: + for subelement in self._subElements: + s = subelement.getXML() + if type(s) != unicode: + s = str(s) + xml+=s + # xml+=str(subelement.getXML()) + #else: + #if self._textContent!=None: + # xml+=self._textContent + xml+='\n' + #print xml + return xml + + #generic methods to set and get atributes (should only be used if something is not supported yet + def setAttribute(self, attribute_name, attribute_value): + self._attributes[attribute_name]=attribute_value + + def getAttribute(self, attribute_name): + return self._attributes.get(attribute_name) + + def getAttributes(self): + """ get all atributes of the element + """ + return self._attributes + + def setKWARGS(self, **kwargs): + """ + Used to set all attributes given in a **kwargs parameter. + Might throw an Exception if attribute was not found. + #TODO: check if we should fix this using "setAttribute" + """ + for key in kwargs.keys(): + #try: + f = getattr(self,'set_' + key) + f(kwargs[key]) + #except: + # print('attribute not found via setter ') + # self.setAttribute(self, key, kwargs[key]) + + def wrap_xml(self, xml, encoding ='ISO-8859-1', standalone='no'): + """ + Method that provides a standard svg header string for a file + """ + header = '''''' %(encoding, standalone) + return header+xml + + def save(self, filename, encoding ='ISO-8859-1', standalone='no'): + """ + Stores any element in a svg file (including header). + Calling this method only makes sense if the root element is an svg elemnt + """ + f = codecs.open(filename, 'w', encoding) + s = self.wrap_xml(self.getXML(), encoding, standalone) + #s = s.replace("&", "&") + f.write(s) + f.close() + #f = open(filename, 'w') + #f.write(self.wrap_xml(self.getXML(), encoding, standalone)) + #f.close() + + def quote_attrib(self, inStr): + """ + Transforms characters between xml notation and python notation. + """ + s1 = (isinstance(inStr, basestring) and inStr or + '%s' % inStr) + s1 = s1.replace('&', '&') + s1 = s1.replace('<', '<') + s1 = s1.replace('>', '>') + if '"' in s1: + # if "'" in s1: + s1 = '%s' % s1.replace('"', """) + # else: + # s1 = "'%s'" % s1 + #else: + # s1 = '"%s"' % s1 + return s1 + +class TextContent: + """ + Class for the text content of an xml element. Can also include PCDATA + """ + def __init__(self,content): + self.content=content + def setContent(self,content): + self.content=content + def getXML(self): + return self.content + def get_id(self): + return None + +#--------------------------------------------------------------------------# +# Below are classes that define attribute sets that pysvg uses for convenience. +# There exist no corresponding attribute sets in svg. +# We simply use these classes as containers for often used attributes. +#--------------------------------------------------------------------------# +class PointAttrib: + """ + The PointAttrib class defines x and y. + """ + def set_x(self, x): + self._attributes['x']=x + def get_x(self): + return self._attributes.get('x') + + def set_y(self, y): + self._attributes['y']=y + def get_y(self): + return self._attributes.get('y') + +class DeltaPointAttrib: + """ + The DeltaPointAttrib class defines dx and dy. + """ + def set_dx(self, dx): + self._attributes['dx']=dx + def get_dx(self): + return self._attributes.get('dx') + + def set_dy(self, dy): + self._attributes['dy']=dy + def get_dy(self): + return self._attributes.get('dy') + +class PointToAttrib: + """ + The PointToAttrib class defines x2 and y2. + """ + def set_x2(self, x2): + self._attributes['x2']=x2 + def get_x2(self): + return self._attributes.get('x2') + + def set_y2(self, y2): + self._attributes['y2']=y2 + def get_y2(self): + return self._attributes.get('y2') + +class DimensionAttrib: + """ + The DimensionAttrib class defines height and width. + """ + def set_height(self, height): + self._attributes['height']=height + + def get_height(self): + return self._attributes.get('height') + + def set_width(self, width): + self._attributes['width']=width + + def get_width(self): + return self._attributes.get('width') + +class RotateAttrib: + """ + The RotateAttrib class defines rotation. + """ + def set_rotate(self, rotate): + self._attributes['rotate']=rotate + + def get_rotate(self): + return self._attributes.get('rotate') + +class BaseShape(BaseElement, CoreAttrib, ConditionalAttrib, StyleAttrib, GraphicalEventsAttrib, PaintAttrib, OpacityAttrib, GraphicsAttrib, CursorAttrib, FilterAttrib, MaskAttrib, ClipAttrib): + """ + Baseclass for all shapes. Do not use this class directly. There is no svg element for it + """ + def set_transform(self, transform): + self._attributes['transform']=transform + def get_transform(self): + return self._attributes.get('transform') diff --git a/appengine/2048/pysvg/filter.py b/appengine/2048/pysvg/filter.py new file mode 100644 index 0000000..e75fbde --- /dev/null +++ b/appengine/2048/pysvg/filter.py @@ -0,0 +1,600 @@ +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +''' +(C) 2008, 2009 Kerim Mansour +For licensing information please refer to license.txt +''' +from attributes import * +from core import BaseElement, DeltaPointAttrib, PointAttrib, DimensionAttrib + +class Filter(BaseElement, CoreAttrib, XLinkAttrib, ExternalAttrib, StyleAttrib, PresentationAttributes_All, PointAttrib, DimensionAttrib): + """ + Class representing the filter element of an svg doc. + """ + def __init__(self, x=None, y=None, width=None, height=None, filterRes=None, filterUnits=None, primitiveUnits=None, **kwargs): + BaseElement.__init__(self, 'filter') + self.set_x(x) + self.set_y(y) + self.set_height(height) + self.set_width(width) + self.set_filterRes(filterRes) + self.set_filterUnits(filterUnits) + self.set_primitiveUnits(primitiveUnits) + self.setKWARGS(**kwargs) + + def set_filterUnits(self, filterUnits): + self._attributes['filterUnits'] = filterUnits + def get_filterUnits(self): + return self._attributes.get('filterUnits') + + def set_primitiveUnits(self, primitiveUnits): + self._attributes['primitiveUnits'] = primitiveUnits + def get_primitiveUnits(self): + return self._attributes.get('primitiveUnits') + + def set_filterRes(self, filterRes): + self._attributes['filterRes'] = filterRes + def get_filterRes(self): + return self._attributes.get('filterRes') + +class FeComponentTransfer(BaseElement, CoreAttrib, FilterColorAttrib, FilterPrimitiveWithInAttrib): + """ + Class representing the feComponentTransfer element of an svg doc. + """ + def __init__(self, **kwargs): + BaseElement.__init__(self, 'feComponentTransfer') + self.setKWARGS(**kwargs) + + +class FeBlend(FeComponentTransfer): + """ + Class representing the feBlend element of an svg doc. + """ + def __init__(self, in2=None, mode=None, **kwargs): + BaseElement.__init__(self, 'feBlend') + self.set_in2(in2) + self.set_mode(mode) + self.setKWARGS(**kwargs) + + def set_in2(self, in2): + self._attributes['in2'] = in2 + def get_in2(self): + return self._attributes.get('in2') + + def set_mode(self, mode): + self._attributes['mode'] = mode + def get_mode(self): + return self._attributes.get('mode') + +class FeColorMatrix(FeComponentTransfer): + """ + Class representing the feColorMatrix element of an svg doc. + """ + def __init__(self, type=None, values=None, **kwargs): + BaseElement.__init__(self, 'feColorMatrix') + self.set_type(type) + self.set_values(values) + self.setKWARGS(**kwargs) + + def set_type(self, type): + self._attributes['type'] = type + def get_type(self): + return self._attributes.get('type') + + def set_values(self, values): + self._attributes['values'] = values + def get_values(self): + return self._attributes.get('values') + +class FeComposite(FeComponentTransfer): + """ + Class representing the feComposite element of an svg doc. + """ + def __init__(self, in2=None, operator=None, k1=None, k2=None, k3=None, k4=None, **kwargs): + BaseElement.__init__(self, 'feComposite') + self.set_in2(in2) + self.set_k1(k1) + self.set_k2(k2) + self.set_k3(k3) + self.set_k4(k4) + self.set_operator(operator) + self.setKWARGS(**kwargs) + + def set_in2(self, in2): + self._attributes['in2'] = in2 + def get_in2(self): + return self._attributes.get('in2') + + def set_operator(self, operator): + self._attributes['operator'] = operator + def get_operator(self): + return self._attributes.get('operator') + + def set_k1(self, k1): + self._attributes['k1'] = k1 + def get_k1(self): + return self._attributes.get('k1') + + def set_k2(self, k2): + self._attributes['k2'] = k2 + def get_k2(self): + return self._attributes.get('k2') + + def set_k3(self, k3): + self._attributes['k3'] = k3 + def get_k3(self): + return self._attributes.get('k3') + + def set_k4(self, k4): + self._attributes['k4'] = k4 + def get_k4(self): + return self._attributes.get('k4') + +class FeConvolveMatrix(FeComponentTransfer): + """ + Class representing the feConvolveMatrix element of an svg doc. + """ + def __init__(self, order=None, kernelMatrix=None, divisor=None, bias=None, targetX=None, targetY=None, edgeMode=None, kernelUnitLength=None, preserveAlpha=None, **kwargs): + BaseElement.__init__(self, 'feConvolveMatrix') + self.set_order(order) + self.set_kernelMatrix(kernelMatrix) + self.set_divisor(divisor) + self.set_bias(bias) + self.set_targetX(targetX) + self.set_targetY(targetY) + self.set_edgeMode(edgeMode) + self.set_kernelUnitLength(kernelUnitLength) + self.set_preserveAlpha(preserveAlpha) + self.setKWARGS(**kwargs) + + def set_order(self, order): + self._attributes['order'] = order + def get_order(self): + return self._attributes.get('order') + + def set_kernelMatrix(self, kernelMatrix): + self._attributes['kernelMatrix'] = kernelMatrix + def get_kernelMatrix(self): + return self._attributes.get('kernelMatrix') + + def set_divisor(self, divisor): + self._attributes['divisor'] = divisor + def get_divisor(self): + return self._attributes.get('divisor') + + def set_bias(self, bias): + self._attributes['bias'] = bias + def get_bias(self): + return self._attributes.get('bias') + + def set_targetX(self, targetX): + self._attributes['targetX'] = targetX + def get_targetX(self): + return self._attributes.get('targetX') + + def set_targetY(self, targetY): + self._attributes['targetY'] = targetY + def get_targetY(self): + return self._attributes.get('targetY') + + def set_edgeMode(self, edgeMode): + self._attributes['edgeMode'] = edgeMode + def get_edgeMode(self): + return self._attributes.get('edgeMode') + + def set_kernelUnitLength(self, kernelUnitLength): + self._attributes['kernelUnitLength'] = kernelUnitLength + def get_kernelUnitLength(self): + return self._attributes.get('kernelUnitLength') + + def set_preserveAlpha(self, preserveAlpha): + self._attributes['preserveAlpha'] = preserveAlpha + def get_preserveAlpha(self): + return self._attributes.get('preserveAlpha') + +class FeDiffuseLighting(FeComponentTransfer, StyleAttrib, PaintAttrib, PresentationAttributes_LightingEffects): + """ + Class representing the feDiffuseLighting element of an svg doc. + """ + def __init__(self, surfaceScale=None, diffuseConstant=None, kernelUnitLength=None , **kwargs): + BaseElement.__init__(self, 'feDiffuseLighting') + self.set_surfaceScale(surfaceScale) + self.set_diffuseConstant(diffuseConstant) + self.set_kernelUnitLength(kernelUnitLength) + self.setKWARGS(**kwargs) + + def set_surfaceScale(self, surfaceScale): + self._attributes['surfaceScale'] = surfaceScale + def get_surfaceScale(self): + return self._attributes.get('surfaceScale') + + def set_diffuseConstant(self, diffuseConstant): + self._attributes['diffuseConstant'] = diffuseConstant + def get_diffuseConstant(self): + return self._attributes.get('diffuseConstant') + + def set_kernelUnitLength(self, kernelUnitLength): + self._attributes['kernelUnitLength'] = kernelUnitLength + def get_kernelUnitLength(self): + return self._attributes.get('kernelUnitLength') + +class FeDisplacementMap(FeComponentTransfer): + """ + Class representing the feDisplacementMap element of an svg doc. + """ + def __init__(self, in2=None, scale=None, xChannelSelector=None, yChannelSelector=None, **kwargs): + BaseElement.__init__(self, 'feDisplacementMap') + self.set_in2(in2) + self.set_scale(scale) + self.set_xChannelSelector(xChannelSelector) + self.set_yChannelSelector(yChannelSelector) + self.setKWARGS(**kwargs) + + def set_in2(self, in2): + self._attributes['in2'] = in2 + def get_in2(self): + return self._attributes.get('in2') + + def set_scale(self, scale): + self._attributes['scale'] = scale + def get_scale(self): + return self._attributes.get('scale') + + def set_xChannelSelector(self, xChannelSelector): + self._attributes['xChannelSelector'] = xChannelSelector + def get_xChannelSelector(self): + return self._attributes.get('xChannelSelector') + + def set_yChannelSelector(self, yChannelSelector): + self._attributes['yChannelSelector'] = yChannelSelector + def get_yChannelSelector(self): + return self._attributes.get('yChannelSelector') + +class FeFlood(FeComponentTransfer, StyleAttrib, PaintAttrib, PresentationAttributes_feFlood): + """ + Class representing the feFlood element of an svg doc. + """ + def __init__(self, x=None, y=None, width=None, height=None, flood_color=None, flood_opacity=None, **kwargs): + BaseElement.__init__(self, 'feFlood') + self.set_x(x) + self.set_y(y) + self.set_height(height) + self.set_width(width) + self.set_flood_color(flood_color) + self.set_flood_opacity(flood_opacity) + self.setKWARGS(**kwargs) + +class FeGaussianBlur(FeComponentTransfer): + """ + Class representing the feGaussianBlur element of an svg doc. + """ + def __init__(self, inValue=None, x=None, y=None, width=None, height=None, stdDeviation=None, **kwargs): + BaseElement.__init__(self, 'feGaussianBlur') + self.set_x(x) + self.set_y(y) + self.set_height(height) + self.set_width(width) + self.set_in(inValue) + self.set_stdDeviation(stdDeviation) + self.setKWARGS(**kwargs) + + def set_stdDeviation(self, stdDeviation): + self._attributes['stdDeviation'] = stdDeviation + def get_stdDeviation(self): + return self._attributes.get('stdDeviation') + +class FeImage(BaseElement, CoreAttrib, XLinkAttrib, FilterColorAttrib, FilterPrimitiveAttrib, ExternalAttrib, StyleAttrib, PresentationAttributes_All): + """ + Class representing the feImage element of an svg doc. + """ + def __init__(self, xlink_href=None, x=None, y=None, width=None, height=None, result=None, **kwargs): + BaseElement.__init__(self, 'feImage') + self.set_xlink_href(xlink_href) + self.set_x(x) + self.set_y(y) + self.set_height(height) + self.set_width(width) + self.set_result(result) + self.setKWARGS(**kwargs) + +class FeMerge(BaseElement, CoreAttrib, FilterPrimitiveAttrib): + """ + Class representing the feMerge element of an svg doc. + """ + def __init__(self, x=None, y=None, width=None, height=None, **kwargs): + BaseElement.__init__(self, 'feMerge') + self.set_x(x) + self.set_y(y) + self.set_height(height) + self.set_width(width) + self.setKWARGS(**kwargs) + +class FeMergeNode(BaseElement, CoreAttrib, FilterColorAttrib, FilterPrimitiveWithInAttrib): + """ + Class representing the feMergeNode element of an svg doc. + """ + def __init__(self, inValue=None, **kwargs): + BaseElement.__init__(self, 'feMergeNode') + self.set_in(inValue) + self.setKWARGS(**kwargs) + +class FeMorphology(FeComponentTransfer): + """ + Class representing the feMorphology element of an svg doc. + """ + def __init__(self, x=None, y=None, width=None, height=None, operator=None, radius=None, **kwargs): + BaseElement.__init__(self, 'feMorphology') + self.set_x(x) + self.set_y(y) + self.set_height(height) + self.set_width(width) + self.set_operator(operator) + self.set_radius(radius) + self.setKWARGS(**kwargs) + + def set_operator(self, operator): + self._attributes['operator'] = operator + def get_operator(self): + return self._attributes.get('operator') + + def set_radius(self, radius): + self._attributes['radius'] = radius + def get_radius(self): + return self._attributes.get('radius') + +class FeOffset(FeComponentTransfer, DeltaPointAttrib): + """ + Class representing the feOffset element of an svg doc. + """ + def __init__(self, inValue=None, dx=None, dy=None, **kwargs): + BaseElement.__init__(self, 'feOffset') + self.set_in(inValue) + self.set_dx(dx) + self.set_dy(dy) + self.setKWARGS(**kwargs) + +class FeSpecularLighting(FeComponentTransfer, StyleAttrib, PaintAttrib, PresentationAttributes_LightingEffects): + """ + Class representing the feSpecularLighting element of an svg doc. + """ + def __init__(self, lighting_color=None, surfaceScale=None, specularConstant=None, specularExponent=None, kernelUnitLength=None, **kwargs): + BaseElement.__init__(self, 'feSpecularLighting') + self.set_lighting_color(lighting_color) + self.set_surfaceScale(surfaceScale) + self.set_specularConstant(specularConstant) + self.set_specularExponent(specularExponent) + self.set_kernelUnitLength(kernelUnitLength) + self.setKWARGS(**kwargs) + + def set_surfaceScale(self, surfaceScale): + self._attributes['surfaceScale'] = surfaceScale + def get_surfaceScale(self): + return self._attributes.get('surfaceScale') + + def set_specularConstant(self, specularConstant): + self._attributes['specularConstant'] = specularConstant + def get_specularConstant(self): + return self._attributes.get('specularConstant') + + def set_specularExponent(self, specularExponent): + self._attributes['specularExponent'] = specularExponent + def get_specularExponent(self): + return self._attributes.get('specularExponent') + + def set_kernelUnitLength(self, kernelUnitLength): + self._attributes['kernelUnitLength'] = kernelUnitLength + def get_kernelUnitLength(self): + return self._attributes.get('kernelUnitLength') + +class FeTile(FeComponentTransfer): + """ + Class representing the feTile element of an svg doc. + """ + def __init__(self, **kwargs): + BaseElement.__init__(self, 'feTile') + self.setKWARGS(**kwargs) + +class feTurbulence(BaseElement, CoreAttrib, FilterColorAttrib, FilterPrimitiveAttrib): + """ + Class representing the feTurbulence element of an svg doc. + """ + def __init__(self, **kwargs): + BaseElement.__init__(self, 'feTurbulence') + self.setKWARGS(**kwargs) + + def set_baseFrequency(self, baseFrequency): + self._attributes['baseFrequency'] = baseFrequency + def get_baseFrequency(self): + return self._attributes.get('baseFrequency') + + def set_numOctaves(self, numOctaves): + self._attributes['numOctaves'] = numOctaves + def get_numOctaves(self): + return self._attributes.get('numOctaves') + + def set_seed(self, seed): + self._attributes['seed'] = seed + def get_seed(self): + return self._attributes.get('seed') + + def set_stitchTiles(self, stitchTiles): + self._attributes['stitchTiles'] = stitchTiles + def get_stitchTiles(self): + return self._attributes.get('stitchTiles') + + def set_type(self, type): + self._attributes['type'] = type + def get_type(self): + return self._attributes.get('type') + +class FeDistantLight(BaseElement, CoreAttrib): + """ + Class representing the feDistantLight element of an svg doc. + """ + def __init__(self, azimuth=None, elevation=None, **kwargs): + BaseElement.__init__(self, 'feDistantLight') + self.set_azimuth(azimuth) + self.set_elevation(elevation) + self.setKWARGS(**kwargs) + + def set_azimuth(self, azimuth): + self._attributes['azimuth'] = azimuth + def get_azimuth(self): + return self._attributes.get('azimuth') + + def set_elevation(self, elevation): + self._attributes['elevation'] = elevation + def get_elevation(self): + return self._attributes.get('elevation') + +class FePointLight(BaseElement, CoreAttrib, PointAttrib): + """ + Class representing the fePointLight element of an svg doc. + """ + def __init__(self, x=None, y=None, z=None, **kwargs): + BaseElement.__init__(self, 'fePointLight') + self.set_x(x) + self.set_y(y) + self.set_z(z) + self.setKWARGS(**kwargs) + + def set_z(self, z): + self._attributes['z'] = z + def get_z(self): + return self._attributes.get('z') + +class FeSpotLight(FePointLight): + """ + Class representing the feSpotLight element of an svg doc. + """ + def __init__(self, x=None, y=None, z=None, pointsAtX=None, pointsAtY=None, pointsAtZ=None, specularExponent=None, limitingConeAngle=None, **kwargs): + BaseElement.__init__(self, 'feSpotLight') + self.set_x(x) + self.set_y(y) + self.set_z(z) + self.set_pointsAtX(pointsAtX) + self.set_pointsAtY(pointsAtY) + self.set_pointsAtZ(pointsAtZ) + self.set_specularExponent(specularExponent) + self.set_limitingConeAngle(limitingConeAngle) + self.setKWARGS(**kwargs) + + def set_pointsAtX(self, pointsAtX): + self._attributes['pointsAtX'] = pointsAtX + def get_pointsAtX(self): + return self._attributes.get('pointsAtX') + + def set_pointsAtY(self, pointsAtY): + self._attributes['pointsAtY'] = pointsAtY + def get_pointsAtY(self): + return self._attributes.get('pointsAtY') + + def set_pointsAtZ(self, pointsAtZ): + self._attributes['pointsAtZ'] = pointsAtZ + def get_pointsAtZ(self): + return self._attributes.get('pointsAtZ') + + def set_specularExponent(self, specularExponent): + self._attributes['specularExponent'] = specularExponent + def get_specularExponent(self): + return self._attributes.get('specularExponent') + + def set_limitingConeAngle(self, limitingConeAngle): + self._attributes['limitingConeAngle'] = limitingConeAngle + def get_limitingConeAngle(self): + return self._attributes.get('limitingConeAngle') + +class FeFuncR(BaseElement, CoreAttrib): + """ + Class representing the feFuncR element of an svg doc. + """ + def __init__(self, type=None, tableValues=None, slope=None, intercept=None, amplitude=None, exponent=None, offset=None, **kwargs): + BaseElement.__init__(self, 'feFuncR') + self.set_type(type) + self.set_tableValues(tableValues) + self.set_slope(slope) + self.set_intercept(intercept) + self.set_amplitude(amplitude) + self.set_exponent(exponent) + self.set_offset(offset) + self.setKWARGS(**kwargs) + + def set_type(self, type): + self._attributes['type'] = type + def get_type(self): + return self._attributes.get('type') + + def set_tableValues(self, tableValues): + self._attributes['tableValues'] = tableValues + def get_tableValues(self): + return self._attributes.get('tableValues') + + def set_slope(self, slope): + self._attributes['slope'] = slope + def get_slope(self): + return self._attributes.get('slope') + + def set_intercept(self, intercept): + self._attributes['intercept'] = intercept + def get_intercept(self): + return self._attributes.get('intercept') + + def set_amplitude(self, amplitude): + self._attributes['amplitude'] = amplitude + def get_amplitude(self): + return self._attributes.get('amplitude') + + def set_exponent(self, exponent): + self._attributes['exponent'] = exponent + def get_exponent(self): + return self._attributes.get('exponent') + + def set_offset(self, offset): + self._attributes['offset'] = offset + def get_offset(self): + return self._attributes.get('offset') + +class FeFuncG(FeFuncR): + """ + Class representing the feFuncG element of an svg doc. + """ + def __init__(self, type=None, tableValues=None, slope=None, intercept=None, amplitude=None, exponent=None, offset=None, **kwargs): + BaseElement.__init__(self, 'feFuncG') + self.set_type(type) + self.set_tableValues(tableValues) + self.set_slope(slope) + self.set_intercept(intercept) + self.set_amplitude(amplitude) + self.set_exponent(exponent) + self.set_offset(offset) + self.setKWARGS(**kwargs) + +class FeFuncB(FeFuncR): + """ + Class representing the feFuncB element of an svg doc. + """ + def __init__(self, type=None, tableValues=None, slope=None, intercept=None, amplitude=None, exponent=None, offset=None, **kwargs): + BaseElement.__init__(self, 'feFuncB') + self.set_type(type) + self.set_tableValues(tableValues) + self.set_slope(slope) + self.set_intercept(intercept) + self.set_amplitude(amplitude) + self.set_exponent(exponent) + self.set_offset(offset) + self.setKWARGS(**kwargs) + +class FeFuncA(FeFuncR): + """ + Class representing the feFuncA element of an svg doc. + """ + def __init__(self, type=None, tableValues=None, slope=None, intercept=None, amplitude=None, exponent=None, offset=None, **kwargs): + BaseElement.__init__(self, 'feFuncA') + self.set_type(type) + self.set_tableValues(tableValues) + self.set_slope(slope) + self.set_intercept(intercept) + self.set_amplitude(amplitude) + self.set_exponent(exponent) + self.set_offset(offset) + self.setKWARGS(**kwargs) diff --git a/appengine/2048/pysvg/gradient.py b/appengine/2048/pysvg/gradient.py new file mode 100644 index 0000000..14c0bf9 --- /dev/null +++ b/appengine/2048/pysvg/gradient.py @@ -0,0 +1,170 @@ +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +''' +(C) 2008, 2009 Kerim Mansour +For licensing information please refer to license.txt +''' +from attributes import * +from core import BaseElement, PointAttrib, DimensionAttrib + + + +class LinearGradient(BaseElement, CoreAttrib, XLinkAttrib, PaintAttrib, StyleAttrib, ExternalAttrib): + """ + Class representing the linearGradient element of an svg doc. + """ + def __init__(self, x1=None, y1=None, x2=None, y2=None, **kwargs): + BaseElement.__init__(self, 'linearGradient') + self.set_x1(x1) + self.set_y1(y1) + self.set_x2(x2) + self.set_y2(y2) + self.setKWARGS(**kwargs) + + def set_x1(self, x1): + self._attributes['x1'] = x1 + def get_x1(self): + return self._attributes.get('x1') + + def set_y1(self, y1): + self._attributes['y1'] = y1 + def get_y1(self): + return self._attributes.get('y1') + + def set_x2(self, x2): + self._attributes['x2'] = x2 + def get_x2(self): + return self._attributes.get('x2') + + def set_y2(self, y2): + self._attributes['y2'] = y2 + def get_y2(self): + return self._attributes.get('y2') + + def set_gradientUnits(self, gradientUnits): + self._attributes['gradientUnits'] = gradientUnits + def get_gradientUnits(self): + return self._attributes.get('gradientUnits') + + def set_gradientTransform(self, gradientTransform): + self._attributes['gradientTransform'] = gradientTransform + def get_gradientTransform(self): + return self._attributes.get('gradientTransform') + + def set_spreadMethod(self, spreadMethod): + self._attributes['spreadMethod'] = spreadMethod + def get_spreadMethod(self): + return self._attributes.get('spreadMethod') + +class RadialGradient(BaseElement, CoreAttrib, XLinkAttrib, PaintAttrib, StyleAttrib, ExternalAttrib): + """ + Class representing the radialGradient element of an svg doc. + """ + def __init__(self, cx='50%', cy='50%', r='50%', fx='50%', fy='50%', **kwargs): + BaseElement.__init__(self, 'radialGradient') + self.set_cx(cx) + self.set_cy(cy) + self.set_fx(fx) + self.set_fy(fy) + self.set_r(r) + self.setKWARGS(**kwargs) + + def set_cx(self, cx): + self._attributes['cx'] = cx + def get_cx(self): + return self._attributes.get('cx') + + def set_cy(self, cy): + self._attributes['cy'] = cy + def get_cy(self): + return self._attributes.get('cy') + + def set_r(self, r): + self._attributes['r'] = r + def get_r(self): + return self._attributes.get('r') + + def set_fx(self, fx): + self._attributes['fx'] = fx + def get_fx(self): + return self._attributes.get('fx') + + def set_fy(self, fy): + self._attributes['fy'] = fy + def get_fy(self): + return self._attributes.get('fy') + + def set_gradientUnits(self, gradientUnits): + self._attributes['gradientUnits'] = gradientUnits + def get_gradientUnits(self): + return self._attributes.get('gradientUnits') + + def set_gradientTransform(self, gradientTransform): + self._attributes['gradientTransform'] = gradientTransform + def get_gradientTransform(self): + return self._attributes.get('gradientTransform') + + def set_spreadMethod(self, spreadMethod): + self._attributes['spreadMethod'] = spreadMethod + def get_spreadMethod(self): + return self._attributes.get('spreadMethod') + +class Stop(BaseElement, CoreAttrib, StyleAttrib, PaintAttrib, GradientAttrib): + """ + Class representing the stop element of an svg doc. + """ + def __init__(self, offset=None, **kwargs): + BaseElement.__init__(self, 'stop') + self.set_offset(offset) + self.setKWARGS(**kwargs) + + def set_offset(self, offset): + self._attributes['offset'] = offset + def get_offset(self): + return self._attributes.get('offset') + +class Pattern(BaseElement, CoreAttrib, XLinkAttrib, ConditionalAttrib, ExternalAttrib, StyleAttrib, PresentationAttributes_All, PointAttrib, DimensionAttrib): + """ + Class representing the pattern element of an svg doc. + """ + def __init__(self, x=None, y=None, width=None, height=None, patternUnits=None, patternContentUnits=None, patternTransform=None, viewBox=None, preserveAspectRatio=None, **kwargs): + BaseElement.__init__(self, 'pattern') + self.set_x(x) + self.set_y(y) + self.set_width(width) + self.set_height(height) + self.set_patternUnits(patternUnits) + self.set_patternContentUnits(patternContentUnits) + self.set_patternTransform(patternTransform) + self.set_viewBox(viewBox) + self.set_preserveAspectRatio(preserveAspectRatio) + self.setKWARGS(**kwargs) + + def set_viewBox(self, viewBox): + self._attributes['viewBox'] = viewBox + + def get_viewBox(self): + return self._attributes['viewBox'] + + def set_preserveAspectRatio(self, preserveAspectRatio): + self._attributes['preserveAspectRatio'] = preserveAspectRatio + + def get_preserveAspectRatio(self): + return self._attributes['preserveAspectRatio'] + + def set_patternUnits(self, patternUnits): + self._attributes['patternUnits'] = patternUnits + + def get_patternUnits(self): + return self._attributes['patternUnits'] + + def set_patternContentUnits(self, patternContentUnits): + self._attributes['patternContentUnits'] = patternContentUnits + def get_patternContentUnits(self): + return self._attributes['patternContentUnits'] + + def set_patternTransform(self, patternTransform): + self._attributes['patternTransform'] = patternTransform + + def get_patternTransform(self): + return self._attributes['patternTransform'] diff --git a/appengine/2048/pysvg/linking.py b/appengine/2048/pysvg/linking.py new file mode 100644 index 0000000..6bb90c6 --- /dev/null +++ b/appengine/2048/pysvg/linking.py @@ -0,0 +1,67 @@ +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +''' +(C) 2008, 2009 Kerim Mansour +For licensing information please refer to license.txt +''' +from attributes import * +from core import BaseElement + + + +class A(BaseElement, CoreAttrib, ConditionalAttrib, StyleAttrib, ExternalAttrib, PresentationAttributes_All, GraphicalEventsAttrib, XLinkAttrib): + """ + Class representing the a element of an svg doc. + """ + def __init__(self, target=None): + BaseElement.__init__(self,'a') + self.set_target(target) + + def set_transform(self, transform): + self._attributes['transform']=transform + def get_transform(self): + return self._attributes.get('transform') + + def set_target(self, target): + self._attributes['target']=target + def get_target(self): + return self._attributes.get('target') + +class View(BaseElement, CoreAttrib, ExternalAttrib): + """ + Class representing the view element of an svg doc. + """ + def __init__(self, ): + BaseElement.__init__(self,'view') + + def set_transform(self, transform): + self._attributes['transform']=transform + def get_transform(self): + return self._attributes.get('transform') + + def set_target(self, target): + self._attributes['target']=target + def get_target(self): + return self._attributes.get('target') + + def set_viewBox(self,viewBox): + self._attributes['viewBox']=viewBox + + def get_viewBox(self): + return self._attributes['viewBox'] + + def set_preserveAspectRatio(self,preserveAspectRatio): + self._attributes['preserveAspectRatio']=preserveAspectRatio + + def get_preserveAspectRatio(self): + return self._attributes['preserveAspectRatio'] + + def set_zoomAndPan(self,zoomAndPan): + self._attributes['zoomAndPan']=zoomAndPan + def get_zoomAndPan(self): + return self._attributes['zoomAndPan'] + + def set_viewTarget(self,viewTarget): + self._attributes['viewTarget']=viewTarget + def get_viewTarget(self): + return self._attributes['viewTarget'] \ No newline at end of file diff --git a/appengine/2048/pysvg/parser.py b/appengine/2048/pysvg/parser.py new file mode 100644 index 0000000..a09e3a7 --- /dev/null +++ b/appengine/2048/pysvg/parser.py @@ -0,0 +1,76 @@ +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +''' +(C) 2008, 2009 Kerim Mansour +For licensing information please refer to license.txt +''' +from xml.dom import minidom +from xml.dom import Node +import string +from pysvg.animate import * +from pysvg.filter import * +from pysvg.gradient import * +from pysvg.linking import * +from pysvg.script import * +from pysvg.shape import * +from pysvg.structure import * +from pysvg.style import * +from pysvg.text import * + +def calculateMethodName(attr): + name=attr + name=name.replace(':','_') + name=name.replace('-','_') + name='set_'+name + return name + +def setAttributes(attrs,obj): + for attr in attrs.keys(): + if hasattr(obj, calculateMethodName(attr)): + eval ('obj.'+calculateMethodName(attr))(attrs[attr].value) + else: + print calculateMethodName(attr)+' not found in:'+obj._elementName + +def build(node_, object): + attrs = node_.attributes + if attrs != None: + setAttributes(attrs, object) + for child_ in node_.childNodes: + nodeName_ = child_.nodeName.split(':')[-1] + if child_.nodeType == Node.ELEMENT_NODE: + try: + capitalLetter = string.upper(nodeName_[0]) + objectinstance=eval(capitalLetter+nodeName_[1:]) () + except: + print 'no class for: '+nodeName_ + continue + object.addElement(build(child_,objectinstance)) + elif child_.nodeType == Node.TEXT_NODE: + #print "TextNode:"+child_.nodeValue + #if child_.nodeValue.startswith('\n'): + # print "TextNode starts with return:"+child_.nodeValue + #else: +# print "TextNode is:"+child_.nodeValue + #object.setTextContent(child_.nodeValue) + if child_.nodeValue <> None: + object.appendTextContent(child_.nodeValue) + elif child_.nodeType == Node.CDATA_SECTION_NODE: + object.appendTextContent('') + elif child_.nodeType == Node.COMMENT_NODE: + object.appendTextContent('') + else: + print "Some node:"+nodeName_+" value: "+child_.nodeValue + return object + +#TODO: packageprefix ? +def parse(inFileName): + doc = minidom.parse(inFileName) + rootNode = doc.documentElement + rootObj = Svg() + build(rootNode,rootObj) + # Enable Python to collect the space used by the DOM. + doc = None + #print rootObj.getXML() + return rootObj + + diff --git a/appengine/2048/pysvg/script.py b/appengine/2048/pysvg/script.py new file mode 100644 index 0000000..2f530c2 --- /dev/null +++ b/appengine/2048/pysvg/script.py @@ -0,0 +1,23 @@ +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +''' +(C) 2008, 2009 Kerim Mansour +For licensing information please refer to license.txt +''' +from attributes import CoreAttrib, XLinkAttrib +from core import BaseElement + + + +class Script(BaseElement, CoreAttrib, XLinkAttrib): + """ + Class representing the script element of an svg doc. + """ + def __init__(self, **kwargs): + BaseElement.__init__(self,'script') + self.setKWARGS(**kwargs) + + def set_type(self, type): + self._attributes['type']=type + def get_type(self): + return self._attributes.get('type') diff --git a/appengine/2048/pysvg/shape.py b/appengine/2048/pysvg/shape.py new file mode 100644 index 0000000..6094d7b --- /dev/null +++ b/appengine/2048/pysvg/shape.py @@ -0,0 +1,481 @@ +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +''' +(C) 2008, 2009 Kerim Mansour +For licensing information please refer to license.txt +''' +from attributes import * +from core import BaseElement, BaseShape, PointAttrib, DimensionAttrib, PointToAttrib + + +class Rect(BaseShape, PointAttrib, DimensionAttrib): + """ + Class representing the rect element of an svg doc. + """ + def __init__(self, x=None, y=None, width=None, height=None, rx=None, ry=None, **kwargs): + BaseElement.__init__(self,'rect') + self.set_x(x) + self.set_y(y) + self.set_height(height) + self.set_width(width) + self.set_rx(rx) + self.set_ry(ry) + self.setKWARGS(**kwargs) + + def set_rx(self, rx): + self._attributes['rx']=rx + def get_rx(self): + return self._attributes.get('rx') + + def set_ry(self, ry): + self._attributes['ry']=ry + def get_ry(self): + return self._attributes.get('ry') + + #extra methods. Methods do rely on number values in the attributes. You might get an exception else! + def getEdgePoints(self): + """ + Returns a list with the coordinates of the points at the edge of the rectangle as tuples. + e.g.[(x1,y1),(x2,y2)] + The sorting is counterclockwise starting with the lower left corner. + Coordinates must be numbers or an exception will be thrown. + """ + result = [(float(self.get_x()),float(self.get_y()))] + result.append((float(self.get_x())+float(self.get_width()),float(self.get_y()))) + result.append((float(self.get_x())+float(self.get_width()),float(self.get_y())+float(self.get_height()))) + result.append((float(self.get_x()),float(self.get_y())+float(self.get_height()))) + return result + + def getInnerEdgePoints(self): + """ + Returns a list with the coordinates of the points at the inner edge of a rounded rectangle as tuples. + e.g.[(x1,y1),(x2,y2)] + The sorting is counterclockwise starting with the lower left corner. + Coordinates must be numbers or an exception will be thrown. + """ + result = [] + result.append((float(self.get_x()) + float(self.get_rx()), float(self.get_y()) + float(self.get_ry()))) + result.append((float(self.get_x()) + float(self.get_width()) - float(self.get_rx()), float(self.get_y()) + float(self.get_ry()))) + result.append((float(self.get_x()) + float(self.get_width()) - float(self.get_rx()), float(self.get_y()) + float(self.get_height()) - float(self.get_ry()))) + result.append((float(self.get_x()) + float(self.get_rx()), float(self.get_y()) + float(self.get_height()) - float(self.get_ry()))) + return result + + def getBottomLeft(self): + """ + Retrieves a tuple with the x,y coordinates of the lower left point of the rect. + Requires the coordinates, width, height to be numbers + """ + return (float(self.get_x()), float(self.get_y())) + + def getBottomRight(self): + """ + Retrieves a tuple with the x,y coordinates of the lower right point of the rect. + Requires the coordinates, width, height to be numbers + """ + return (float(self.get_x()) + float(self.get_width()), float(self.get_y())) + + def getTopLeft(self): + """ + Retrieves a tuple with the x,y coordinates of the upper left point of the rect. + Requires the coordinates, width, height to be numbers + """ + return (float(self.get_x()), float(self.get_y())+ float(self.get_height())) + + def getTopRight(self): + """ + Retrieves a tuple with the x,y coordinates of the upper right point of the rect. + Requires the coordinates, width, height to be numbers + """ + return (float(self.get_x()) + float(self.get_width()), float(self.get_y()) + float(self.get_height())) + + def moveToPoint(self, (x,y)): + """ + Moves the rect to the point x,y + """ + self.set_x(float(self.get_x()) + float(x)) + self.set_y(float(self.get_y()) + float(y)) + + +class Circle(BaseShape): + """ + Class representing the circle element of an svg doc. + """ + def __init__(self, cx=None,cy=None,r=None, **kwargs): + BaseElement.__init__(self,'circle') + self.set_cx(cx) + self.set_cy(cy) + self.set_r(r) + self.setKWARGS(**kwargs) + + def set_cx(self, cx): + self._attributes['cx']=cx + def get_cx(self): + return self._attributes.get('cx') + + def set_cy(self, cy): + self._attributes['cy']=cy + def get_cy(self): + return self._attributes.get('cy') + + def set_r(self, r): + self._attributes['r']=r + def get_r(self): + return self._attributes.get('r') + + #extra methods. Methods do rely on number values in the attributes. You might get an exception else! + def getDiameter(self): + """ + Retrieves the diameter of the circle. Requires the radius to be a number + """ + return 2 * float(self.get_r()) + + def getWidth(self): + """ + Retrieves the width of the circle. Requires the radius to be a number + """ + return self.getDiameter() + + def getHeight(self): + """ + Retrieves the height of the circle. Requires the radius to be a number + """ + return self.getDiameter() + + def getBottomLeft(self): + """ + Retrieves a tuple with the x,y coordinates of the lower left point of the circle. + Requires the radius and the coordinates to be numbers + """ + return (float(self.get_cx()) - float(self.get_r()), float(self.get_cy()) - float(self.get_r())) + + def getBottomRight(self): + """ + Retrieves a tuple with the x,y coordinates of the lower right point of the circle. + Requires the radius and the coordinates to be numbers + """ + return (float(self.get_cx()) + float(self.get_r()), float(self.get_cy()) - float(self.get_r())) + + def getTopLeft(self): + """ + Retrieves a tuple with the x,y coordinates of the upper left point of the circle. + Requires the radius and the coordinates to be numbers + """ + return (float(self.get_cx()) - float(self.get_r()), float(self.get_cy()) + float(self.get_r())) + + def getTopRight(self): + """ + Retrieves a tuple with the x,y coordinates of the upper right point of the circle. + Requires the radius and the coordinates to be numbers + """ + return (float(self.get_cx()) + float(self.get_r()), float(self.get_cy()) + float(self.get_r())) + + def moveToPoint(self, (x,y)): + """ + Moves the circle to the point x,y + """ + self.set_cx(float(self.get_cx()) + float(x)) + self.set_cy(float(self.get_cy()) + float(y)) + +class Ellipse(BaseShape): + """ + Class representing the ellipse element of an svg doc. + """ + def __init__(self, cx=None,cy=None,rx=None,ry=None, **kwargs): + BaseElement.__init__(self,'ellipse') + self.set_cx(cx) + self.set_cy(cy) + self.set_rx(rx) + self.set_ry(ry) + self.setKWARGS(**kwargs) + + def set_cx(self, cx): + self._attributes['cx']=cx + def get_cx(self): + return self._attributes.get('cx') + + def set_cy(self, cy): + self._attributes['cy']=cy + def get_cy(self): + return self._attributes.get('cy') + + def set_rx(self, rx): + self._attributes['rx']=rx + def get_rx(self): + return self._attributes.get('rx') + + def set_ry(self, ry): + self._attributes['ry']=ry + def get_ry(self): + return self._attributes.get('ry') + + #extra methods. Methods do rely on number values in the attributes. You might get an exception else! + def getWidth(self): + return abs(2 * float(self.get_rx())) + + def getHeight(self): + return abs(2 * float(self.get_ry())) + + def getBottomLeft(self): + """ + Retrieves a tuple with the x,y coordinates of the lower left point of the ellipse. + Requires the radius and the coordinates to be numbers + """ + return (float(self.get_cx()) - float(self.get_rx()), float(self.get_cy()) - float(self.get_ry())) + + def getBottomRight(self): + """ + Retrieves a tuple with the x,y coordinates of the lower right point of the ellipse. + Requires the radius and the coordinates to be numbers + """ + return (float(self.get_cx()) + float(self.get_rx()), float(self.get_cy()) - float(self.get_ry())) + + def getTopLeft(self): + """ + Retrieves a tuple with the x,y coordinates of the upper left point of the ellipse. + Requires the radius and the coordinates to be numbers + """ + return (float(self.get_cx()) - float(self.get_rx()), float(self.get_cy()) + float(self.get_ry())) + + def getTopRight(self): + """ + Retrieves a tuple with the x,y coordinates of the upper right point of the ellipse. + Requires the radius and the coordinates to be numbers + """ + return (float(self.get_cx()) + float(self.get_rx()), float(self.get_cy()) + float(self.get_ry())) + +class Line(BaseShape, PointToAttrib): + """ + Class representing the line element of an svg doc. + Note that this element is NOT painted VISIBLY by default UNLESS you provide + a style including STROKE and STROKE-WIDTH + """ + def __init__(self, X1=None, Y1=None, X2=None, Y2=None, **kwargs): + """ + Creates a line + @type X1: string or int + @param X1: starting x-coordinate + @type Y1: string or int + @param Y1: starting y-coordinate + @type X2: string or int + @param X2: ending x-coordinate + @type Y2: string or int + @param Y2: ending y-coordinate + """ + BaseElement.__init__(self,'line') + self.set_x1(X1) + self.set_y1(Y1) + self.set_x2(X2) + self.set_y2(Y2) + self.setKWARGS(**kwargs) + + def set_x1(self, x1): + self._attributes['x1']=x1 + def get_x1(self): + return self._attributes.get('x1') + + def set_y1(self, y1): + self._attributes['y1']=y1 + def get_y1(self): + return self._attributes.get('y1') + + def set_x2(self, x2): + self._attributes['x2']=x2 + def get_x2(self): + return self._attributes.get('x2') + + def set_y2(self, y2): + self._attributes['y2']=y2 + def get_y2(self): + return self._attributes.get('y2') + + #extra methods. Methods do rely on number values in the attributes. You might get an exception else! + def getWidth(self): + """ + Retrieves the width of the line. This is always a positive number. + Coordinates must be numbers. + """ + return abs(float(self.get_x1()) - float(self.get_x2())) + + def getHeight(self): + """ + Retrieves the height of the line. This is always a positive number. + Coordinates must be numbers. + """ + return abs(float(self.get_y1()) - float(self.get_y2())) + + def getBottomLeft(self): + """ + Retrieves the the bottom left coordinate of the line as tuple. + Coordinates must be numbers. + """ + x1 = float(self.get_x1()) + x2 = float(self.get_x2()) + y1 = float(self.get_y1()) + y2 = float(self.get_y2()) + if x1 < x2: + if y1 < y2: + return (x1, y1) + else: + return (x1, y2) + else: + if y1 < y2: + return (x2, y1) + else: + return (x2, y2) + + def getBottomRight(self): + """ + Retrieves the the bottom right coordinate of the line as tuple. + Coordinates must be numbers. + """ + x1 = float(self.get_x1()) + x2 = float(self.get_x2()) + y1 = float(self.get_y1()) + y2 = float(self.get_y2()) + if x1 < x2: + if y1 < y2: + return (x2, y1) + else: + return (x2, y2) + else: + if y1 < y2: + return (x1, y1) + else: + return (x1, y2) + + def getTopRight(self): + """ + Retrieves the the top right coordinate of the line as tuple. + Coordinates must be numbers. + """ + x1 = float(self.get_x1()) + x2 = float(self.get_x2()) + y1 = float(self.get_y1()) + y2 = float(self.get_y2()) + if x1 < x2: + if y1 < y2: + return (x2, y2) + else: + return (x2, y1) + else: + if y1 < y2: + return (x1, y2) + else: + return (x1, y1) + + def getTopLeft(self): + """ + Retrieves the the top left coordinate of the line as tuple. + Coordinates must be numbers. + """ + x1 = float(self.get_x1()) + x2 = float(self.get_x2()) + y1 = float(self.get_y1()) + y2 = float(self.get_y2()) + if x1 < x2: + if y1 < y2: + return (x1, y2) + else: + return (x1, y1) + else: + if y1 < y2: + return (x2, y2) + else: + return (x2, y1) + + def moveToPoint(self, (x,y)): + """ + Moves the line to the point x,y + """ + self.set_x1(float(self.get_x1()) + float(x)) + self.set_x2(float(self.get_x2()) + float(x)) + self.set_y1(float(self.get_y1()) + float(y)) + self.set_y2(float(self.get_y2()) + float(y)) + +class Path(BaseShape, ExternalAttrib, MarkerAttrib): + """ + Class representing the path element of an svg doc. + """ + def __init__(self, pathData="",pathLength=None, style=None, focusable=None, **kwargs): + BaseElement.__init__(self,'path') + if pathData!='' and not pathData.endswith(' '): + pathData+=' ' + self.set_d(pathData) + if style!=None: + self.set_style(style) + self.setKWARGS(**kwargs) + + def set_d(self, d): + self._attributes['d']=d + def get_d(self): + return self._attributes.get('d') + + def set_pathLength(self, pathLength): + self._attributes['pathLength']=pathLength + def get_pathLength(self): + return self._attributes.get('pathLength') + + def __append__(self,command, params, relative=True): + d = self.get_d() + if relative==True: + d+=command.lower() + else: + d+=command.upper() + for param in params: + d+=' %s ' %(param) + self.set_d(d) + + def appendLineToPath(self,endx,endy, relative=True): + self.__append__('l',[endx,endy], relative) + + def appendHorizontalLineToPath(self,endx, relative=True): + self.__append__('h',[endx], relative) + + def appendVerticalLineToPath(self,endy, relative=True): + self.__append__('v',[endy], relative) + + def appendMoveToPath(self,endx,endy, relative=True): + self.__append__('m',[endx,endy], relative) + + def appendCloseCurve(self): + d = self.get_d() + d+="z" + self.set_d(d) + + def appendCubicCurveToPath(self, controlstartx, controlstarty, controlendx, controlendy, endx,endy,relative=True): + self.__append__('c',[controlstartx, controlstarty, controlendx, controlendy, endx,endy], relative) + + def appendCubicShorthandCurveToPath(self, controlendx, controlendy, endx,endy,relative=True): + self.__append__('s',[controlendx, controlendy, endx,endy], relative) + + def appendQuadraticCurveToPath(self, controlx, controly, endx,endy,relative=True): + self.__append__('q',[controlx, controly, endx,endy], relative) + + def appendQuadraticShorthandCurveToPath(self, endx,endy,relative=True): + self.__append__('t',[endx,endy], relative) + + def appendArcToPath(self,rx,ry,x,y,x_axis_rotation=0,large_arc_flag=0,sweep_flag=1 ,relative=True): + self.__append__('a',[rx,ry,x_axis_rotation,large_arc_flag,sweep_flag,x,y], relative) + +class Polyline(BaseShape): + """ + Class representing the polyline element of an svg doc. + """ + def __init__(self, points=None, **kwargs): + BaseElement.__init__(self,'polyline') + self.set_points(points) + self.setKWARGS(**kwargs) + + def set_points(self, points): + self._attributes['points']=points + def get_points(self): + return self._attributes.get('points') + +class Polygon(Polyline): + """ + Class representing the polygon element of an svg doc. + """ + def __init__(self, points=None, **kwargs): + BaseElement.__init__(self,'polygon') + self.set_points(points) + self.setKWARGS(**kwargs) \ No newline at end of file diff --git a/appengine/2048/pysvg/structure.py b/appengine/2048/pysvg/structure.py new file mode 100644 index 0000000..4131b43 --- /dev/null +++ b/appengine/2048/pysvg/structure.py @@ -0,0 +1,222 @@ +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +''' +This module includes the elements found in http://www.w3.org/TR/SVG/struct.html + +(C) 2008, 2009 Kerim Mansour +For licensing information please refer to license.txt +''' +from attributes import * +from core import BaseElement, PointAttrib, DimensionAttrib + + + + +class G(BaseElement, CoreAttrib, ConditionalAttrib, StyleAttrib, ExternalAttrib, PresentationAttributes_All, GraphicalEventsAttrib): + """ + Class representing the g element of an svg doc. + """ + def __init__(self, **kwargs): + BaseElement.__init__(self, 'g') + self.setKWARGS(**kwargs) + + def set_transform(self, transform): + self._attributes['transform'] = transform + def get_transform(self): + return self._attributes.get('transform') + +class Defs(G): + """ + Class representing the defs element of an svg doc. + """ + def __init__(self,**kwargs): + BaseElement.__init__(self, 'defs') + self.setKWARGS(**kwargs) + + +class Desc(BaseElement, CoreAttrib, StyleAttrib): + """ + Class representing the desc element of an svg doc. + """ + def __init__(self,**kwargs): + BaseElement.__init__(self, 'desc') + self.setKWARGS(**kwargs) + +class Title(Desc): + """ + Class representing the title element of an svg doc. + """ + def __init__(self,**kwargs): + BaseElement.__init__(self, 'title') + self.setKWARGS(**kwargs) + +class Metadata(BaseElement, CoreAttrib): + """ + Class representing the metadata element of an svg doc. + """ + def __init__(self,**kwargs): + BaseElement.__init__(self, 'metadata') + self.setKWARGS(**kwargs) + +class Symbol(BaseElement, CoreAttrib, StyleAttrib, ExternalAttrib, PresentationAttributes_All, GraphicalEventsAttrib): + """ + Class representing the symbol element of an svg doc. + """ + def __init__(self,**kwargs): + BaseElement.__init__(self, 'symbol') + self.setKWARGS(**kwargs) + + def set_viewBox(self, viewBox): + self._attributes['viewBox'] = viewBox + + def get_viewBox(self): + return self._attributes['viewBox'] + + def set_preserveAspectRatio(self, preserveAspectRatio): + self._attributes['preserveAspectRatio'] = preserveAspectRatio + + def get_preserveAspectRatio(self): + return self._attributes['preserveAspectRatio'] + +class Use(BaseElement, CoreAttrib, StyleAttrib, ConditionalAttrib, PointAttrib, DimensionAttrib, XLinkAttrib, PresentationAttributes_All, GraphicalEventsAttrib): + """ + Class representing the use element of an svg doc. + """ + def __init__(self,**kwargs): + BaseElement.__init__(self, 'use') + self.setKWARGS(**kwargs) + + def set_transform(self, transform): + self._attributes['transform'] = transform + def get_transform(self): + return self._attributes.get('transform') + +class Svg(BaseElement, CoreAttrib, StyleAttrib, ConditionalAttrib, PointAttrib, DimensionAttrib, XLinkAttrib, PresentationAttributes_All, GraphicalEventsAttrib, DocumentEventsAttrib): + """ + Class representing the svg element of an svg doc. + """ + def __init__(self, x=None, y=None, width=None, height=None,**kwargs): + BaseElement.__init__(self, 'svg') + self.set_xmlns('http://www.w3.org/2000/svg') + self.set_xmlns_xlink('http://www.w3.org/1999/xlink') + self.set_version('1.1') + self.set_x(x) + self.set_y(y) + self.set_height(height) + self.set_width(width) + self.setKWARGS(**kwargs) + + def set_version(self, version): + self._attributes['version'] = version + + def get_version(self): + return self._attributes['version'] + + def set_xmlns(self, xmlns): + self._attributes['xmlns'] = xmlns + + def get_xmlns(self): + return self._attributes['xmlns'] + + def set_xmlns_xlink(self, xmlns_xlink): + self._attributes['xmlns:xlink'] = xmlns_xlink + + def get_xmlns_xlink(self): + return self._attributes.get('xmlns:xlink') + + def set_viewBox(self, viewBox): + self._attributes['viewBox'] = viewBox + def get_viewBox(self): + return self._attributes['viewBox'] + + def set_preserveAspectRatio(self, preserveAspectRatio): + self._attributes['preserveAspectRatio'] = preserveAspectRatio + def get_preserveAspectRatio(self): + return self._attributes['preserveAspectRatio'] + + def set_transform(self, transform): + self._attributes['transform'] = transform + def get_transform(self): + return self._attributes.get('transform') + + def set_zoomAndPan(self, zoomAndPan): + self._attributes['zoomAndPan'] = zoomAndPan + def get_zoomAndPan(self): + return self._attributes['zoomAndPan'] + + def set_contentScriptType(self, contentScriptType): + self._attributes['contentScriptType'] = contentScriptType + def get_contentScriptType(self): + return self._attributes['contentScriptType'] + + def set_contentStyleType(self, contentStyleType): + self._attributes['contentStyleType'] = contentStyleType + def get_contentStyleType(self): + return self._attributes['contentStyleType'] + + def set_baseProfile(self, baseProfile): + self._attributes['baseProfile'] = baseProfile + def get_baseProfile(self): + return self._attributes['baseProfile'] +#todo: check color.attrib and colorprofile.attrib. supposedly in image +class Image(BaseElement, CoreAttrib, ConditionalAttrib, StyleAttrib, ViewportAttrib, PaintAttrib, OpacityAttrib, GraphicsAttrib, ClipAttrib, MaskAttrib, FilterAttrib, GraphicalEventsAttrib, CursorAttrib, XLinkAttrib, ExternalAttrib, PointAttrib, DimensionAttrib): + """ + Class representing the image element of an svg doc. + """ + def __init__(self, x=None, y=None, width=None, height=None, preserveAspectRatio=None,**kwargs): + BaseElement.__init__(self, 'image') + self.set_x(x) + self.set_y(y) + self.set_height(height) + self.set_width(width) + self.set_preserveAspectRatio(preserveAspectRatio) + self.setKWARGS(**kwargs) + + #def set_embedded(self,embedded): + # self._attributes['embedded']=embedded + + def set_preserveAspectRatio(self, preserveAspectRatio): + self._attributes['preserveAspectRatio'] = preserveAspectRatio + def get_preserveAspectRatio(self): + return self._attributes['preserveAspectRatio'] + + def set_transform(self, transform): + self._attributes['transform'] = transform + def get_transform(self): + return self._attributes.get('transform') + +class Switch(BaseElement, CoreAttrib, ConditionalAttrib, StyleAttrib, PresentationAttributes_All, GraphicalEventsAttrib, ExternalAttrib): + """ + Class representing the switch element of an svg doc. + """ + def __init__(self,**kwargs): + BaseElement.__init__(self, 'switch') + self.setKWARGS(**kwargs) + + def set_transform(self, transform): + self._attributes['transform'] = transform + def get_transform(self): + return self._attributes.get('transform') + +class ClipPath(BaseElement, CoreAttrib, ConditionalAttrib, StyleAttrib, ExternalAttrib, PresentationAttributes_All, GraphicalEventsAttrib): + """ + Class representing the clipPath element of an svg doc. + """ + def __init__(self, id=None, transform=None, clipPathUnits=None,**kwargs): + BaseElement.__init__(self, 'clipPath') + self.set_id(id) + self.set_transform(transform) + self.set_clipPathUnits(clipPathUnits) + self.setKWARGS(**kwargs) + + + def set_transform(self, transform): + self._attributes['transform'] = transform + def get_transform(self): + return self._attributes.get('transform') + + def set_clipPathUnits(self, clipPathUnits): + self._attributes['clipPathUnits'] = clipPathUnits + + def get_clipPathUnits(self): + return self._attributes['clipPathUnits'] diff --git a/appengine/2048/pysvg/style.py b/appengine/2048/pysvg/style.py new file mode 100644 index 0000000..e32773d --- /dev/null +++ b/appengine/2048/pysvg/style.py @@ -0,0 +1,34 @@ +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +''' +(C) 2008, 2009 Kerim Mansour +For licensing information please refer to license.txt +''' +from attributes import CoreAttrib, XLinkAttrib +from core import BaseElement + + + +class Style(BaseElement, CoreAttrib, XLinkAttrib): + """ + Class representing the style element of an svg doc. + """ + def __init__(self, **kwargs): + BaseElement.__init__(self,'style') + self.setKWARGS(**kwargs) + + def set_type(self, type): + self._attributes['type']=type + def get_type(self): + return self._attributes.get('type') + + def set_media(self, media): + self._attributes['media']=media + def get_media(self): + return self._attributes.get('media') + + def set_title(self, title): + self._attributes['title']=title + def get_title(self): + return self._attributes.get('title') + diff --git a/appengine/2048/pysvg/text.py b/appengine/2048/pysvg/text.py new file mode 100644 index 0000000..f7fc3dc --- /dev/null +++ b/appengine/2048/pysvg/text.py @@ -0,0 +1,169 @@ +#!/usr/bin/python +# -*- coding: iso-8859-1 -*- +''' +(C) 2008, 2009 Kerim Mansour +For licensing information please refer to license.txt +''' +from attributes import * +from core import BaseElement, PointAttrib, DeltaPointAttrib, RotateAttrib + +class AltGlyphDef(BaseElement, CoreAttrib): + """ + Class representing the altGlyphDef element of an svg doc. + """ + def __init__(self, **kwargs): + BaseElement.__init__(self, 'altGlypfDef') + self.setKWARGS(**kwargs) + +class AltGlyphItem(BaseElement, CoreAttrib): + """ + Class representing the altGlyphItem element of an svg doc. + """ + def __init__(self, **kwargs): + BaseElement.__init__(self, 'altGlypfItem') + self.setKWARGS(**kwargs) + +class GlyphRef(BaseElement, CoreAttrib, ExternalAttrib, StyleAttrib, FontAttrib, XLinkAttrib, PaintAttrib, PointAttrib, DeltaPointAttrib): + """ + Class representing the glyphRef element of an svg doc. + """ + def __init__(self, **kwargs): + BaseElement.__init__(self, 'glyphRef') + self.setKWARGS(**kwargs) + + def set_glyphRef(self, glyphRef): + self._attributes['glyphRef'] = glyphRef + def get_glyphRef(self): + return self._attributes.get('glyphRef') + + def set_format(self, format): + self._attributes['format'] = format + def get_format(self): + return self._attributes.get('format') + + def set_lengthAdjust(self, lengthAdjust): + self._attributes['lengthAdjust'] = lengthAdjust + def get_lengthAdjust(self): + return self._attributes.get('lengthAdjust') + +class AltGlyph(GlyphRef, ConditionalAttrib, GraphicalEventsAttrib, OpacityAttrib, GraphicsAttrib, CursorAttrib, FilterAttrib, MaskAttrib, ClipAttrib, TextContentAttrib, RotateAttrib): + """ + Class representing the altGlyph element of an svg doc. + """ + def __init__(self, **kwargs): + BaseElement.__init__(self, 'altGlyph') + self.setKWARGS(**kwargs) + + def set_textLength(self, textLength): + self._attributes['textLength'] = textLength + def get_textLength(self): + return self._attributes.get('textLength') + +class TextPath(BaseElement, CoreAttrib, ConditionalAttrib, ExternalAttrib, StyleAttrib, XLinkAttrib, FontAttrib, PaintAttrib, GraphicalEventsAttrib, OpacityAttrib, GraphicsAttrib, CursorAttrib, FilterAttrib, MaskAttrib, ClipAttrib, TextContentAttrib): + """ + Class representing the textPath element of an svg doc. + """ + def __init__(self, **kwargs): + BaseElement.__init__(self, 'textPath') + self.setKWARGS(**kwargs) + + def set_startOffset(self, startOffset): + self._attributes['startOffset'] = startOffset + def get_startOffset(self): + return self._attributes.get('startOffset') + + def set_textLength(self, textLength): + self._attributes['textLength'] = textLength + def get_textLength(self): + return self._attributes.get('textLength') + + def set_lengthAdjust(self, lengthAdjust): + self._attributes['lengthAdjust'] = lengthAdjust + def get_lengthAdjust(self): + return self._attributes.get('lengthAdjust') + + def set_method(self, method): + self._attributes['method'] = method + def get_method(self): + return self._attributes.get('method') + + def set_spacing(self, spacing): + self._attributes['spacing'] = spacing + def get_spacing(self): + return self._attributes.get('spacing') + +class Tref(BaseElement, CoreAttrib, ConditionalAttrib, ExternalAttrib, StyleAttrib, XLinkAttrib, PointAttrib, DeltaPointAttrib, RotateAttrib, GraphicalEventsAttrib, PaintAttrib, FontAttrib, OpacityAttrib, GraphicsAttrib, CursorAttrib, FilterAttrib, MaskAttrib, ClipAttrib, TextContentAttrib): + """ + Class representing the tref element of an svg doc. + """ + def __init__(self, **kwargs): + BaseElement.__init__(self, 'tref') + self.setKWARGS(**kwargs) + + def set_textLength(self, textLength): + self._attributes['textLength'] = textLength + def get_textLength(self): + return self._attributes.get('textLength') + + def set_lengthAdjust(self, lengthAdjust): + self._attributes['lengthAdjust'] = lengthAdjust + def get_lengthAdjust(self): + return self._attributes.get('lengthAdjust') + +class Tspan(BaseElement, CoreAttrib, ConditionalAttrib, ExternalAttrib, StyleAttrib, PointAttrib, DeltaPointAttrib, RotateAttrib, GraphicalEventsAttrib, PaintAttrib, FontAttrib, OpacityAttrib, GraphicsAttrib, CursorAttrib, FilterAttrib, MaskAttrib, ClipAttrib, TextContentAttrib): + """ + Class representing the tspan element of an svg doc. + """ + def __init__(self, x=None, y=None, dx=None, dy=None, rotate=None, textLength=None, lengthAdjust=None, **kwargs): + BaseElement.__init__(self, 'tspan') + self.set_x(x) + self.set_y(y) + self.set_dx(dx) + self.set_dy(dy) + self.set_rotate(rotate) + self.set_textLength(textLength) + self.set_lengthAdjust(lengthAdjust) + self.setKWARGS(**kwargs) + + def set_textLength(self, textLength): + self._attributes['textLength'] = textLength + def get_textLength(self): + return self._attributes.get('textLength') + + def set_lengthAdjust(self, lengthAdjust): + self._attributes['lengthAdjust'] = lengthAdjust + def get_lengthAdjust(self): + return self._attributes.get('lengthAdjust') + +class Text(BaseElement, CoreAttrib, ConditionalAttrib, ExternalAttrib, StyleAttrib, PointAttrib, DeltaPointAttrib, RotateAttrib, GraphicalEventsAttrib, PaintAttrib, FontAttrib, OpacityAttrib, GraphicsAttrib, CursorAttrib, FilterAttrib, MaskAttrib, ClipAttrib, TextContentAttrib, TextAttrib): + """ + Class representing the text element of an svg doc. + """ + def __init__(self, content=None, x=None, y=None, dx=None, dy=None, rotate=None, textLength=None, lengthAdjust=None, **kwargs): + BaseElement.__init__(self, 'text') + if content <> None: + self.appendTextContent(content) + self.set_x(x) + self.set_y(y) + self.set_dx(dx) + self.set_dy(dy) + self.set_rotate(rotate) + self.set_textLength(textLength) + self.set_lengthAdjust(lengthAdjust) + self.setKWARGS(**kwargs) + + def set_transform(self, transform): + self._attributes['transform'] = transform + def get_transform(self): + return self._attributes.get('transform') + + def set_textLength(self, textLength): + self._attributes['textLength'] = textLength + def get_textLength(self): + return self._attributes.get('textLength') + + def set_lengthAdjust(self, lengthAdjust): + self._attributes['lengthAdjust'] = lengthAdjust + def get_lengthAdjust(self): + return self._attributes.get('lengthAdjust') + diff --git a/appengine/2048/pysvg/turtle.py b/appengine/2048/pysvg/turtle.py new file mode 100644 index 0000000..9efa4c2 --- /dev/null +++ b/appengine/2048/pysvg/turtle.py @@ -0,0 +1,206 @@ +''' +(C) 2008, 2009, 2010 Kerim Mansour +For licensing information please refer to license.txt +''' +import math +from shape import Polyline + +class Vector(object): + """ + Class representing a vector. Used to determine position of the turtle as well as heading. + Also used to calculate movement. + Vector class is inspired by tips and code from: + - http://slowchop.com/2006/07/15/a-fast-python-vector-class/ + - http://www.kokkugia.com/wiki/index.php5?title=Python_vector_class + - http://xturtle.rg16.at/code/xturtle.py + """ + __slots__ = ('x', 'y') + def __init__(self, x, y): + """ Initializes the vector. + x and y are coordinates and should be numbers. + They will be cast to floats + """ + self.x = float(x) + self.y = float(y) + + def __add__(self, vector): + return Vector(self.x + vector.x, self.y + vector.y) + + def __sub__(self, vector): + return Vector(self.x - vector.x, self.y - vector.y) + + def __mul__(self, vector): + if isinstance(vector, Vector): + return self.x * vector.x + self.y * vector.y + return Vector(self.x * vector, self.y * vector) + + def __rmul__(self, vector): + if isinstance(vector, int) or isinstance(vector, float): + return Vector(self.x * vector, self.y * vector) + + def __neg__(self): + return Vector(-self.x, -self.y) + + def __abs__(self): + return (self.x ** 2 + self.y ** 2) ** 0.5 + + def rotate(self, angle): + """Rotates self counterclockwise by angle + (the angle must be given in a 360 degree system) + """ + perp = Vector(-self.y, self.x) + angle = angle * math.pi / 180.0 + c, s = math.cos(angle), math.sin(angle) + return Vector(self.x * c + perp.x * s, self.y * c + perp.y * s) + + def __getnewargs__(self): + return (self.x, self.y) + + def __repr__(self): + return "%.2f,%.2f " % (self.x, self.y) + +class Turtle(object): + """ + Class representing a classical turtle object known from logo and other implementations. + Note that currently each turtle has exactly ONE style of drawing, so if you intend to draw in multiple styles several instances of turtles are needed. + A turtle will only actually draw when the pen is down (default=false). + An xml representation usable for pysvg can ge retrieved using the getXML()-method. + To add the turtles paths to an svg you have two opions: + Either you simply call "addTurtlePathToSVG" or you can create an svg element and append the Elements of the turtle using a loop, e.g: + s=svg(...) + t=Turtle(...) + for element in t.getSVGElements(): + s.addElement(element) + """ + def __init__(self, initialPosition=Vector(0.0, 0.0), initialOrientation=Vector(1.0, 0.0), fill='white', stroke='black', strokeWidth='1', penDown=False): + """ Initializes a new Turtle with a new initial position and orientation as well as defaultvalues for style. + """ + self.fill = fill + self.stroke = stroke + self.strokeWidth = strokeWidth + self._position = initialPosition + self._orient = initialOrientation + self._penDown = penDown + self._svgElements = [] + self._pointsOfPolyline = [] + + + def forward(self, distance): + """ Moves the turtle forwards by distance in the direction it is facing. + If the pen is lowered it will also add to the currently drawn polyline. + """ + self._move(distance) + + def backward(self, distance): + """ Moves the turtle backwards by distance in the direction it is facing. + If the pen is lowered it will also add to the currently drawn polyline. + """ + self._move(-distance) + + def right(self, angle): + """Rotates the turtle to the right by angle. + """ + self._rotate(angle) + + def left(self, angle): + """Rotates the turtle to the left by angle. + """ + self._rotate(-angle) + + def moveTo(self, vector): + """ Moves the turtle to the new position. Orientation is kept as it is. + If the pen is lowered it will also add to the currently drawn polyline. + """ + self._position = vector + if self.isPenDown(): + self._pointsOfPolyline.append(self._position) + + def penUp(self): + """ Raises the pen. Any movement will not draw lines till pen is lowered again. + """ + if self._penDown==True: + self._penDown = False + self._addPolylineToElements() + + def penDown(self): + """ Lowers the pen down again. A new polyline will be created for drawing. + Old polylines will be stored in the stack + """ + #if self._penDown==False: + self._penDown = True + self._addPolylineToElements() + + def finish(self): + """MUST be called when drawing is finished. Else the last path will not be added to the stack. + """ + self._addPolylineToElements() + + def isPenDown(self): + """ Retrieve current status of the pen.(boolean) + """ + return self._penDown + + def getPosition(self): + """ Retrieve current position of the turtle.(Vector) + """ + return self._position + + def getOrientation(self): + """ Retrieve current orientation of the turtle.(Vector) + """ + return self._orient + + def setOrientation(self, vec): + """ Sets the orientation of the turtle.(Vector) + """ + self._orient=vec + + def _move(self, distance): + """ Moves the turtle by distance in the direction it is facing. + If the pen is lowered it will also add to the currently drawn polyline. + """ + self._position = self._position + self._orient * distance + if self.isPenDown(): + x = round(self._position.x, 2) + y = round(self._position.y, 2) + self._pointsOfPolyline.append(Vector(x, y)) + + def _rotate(self, angle): + """Rotates the turtle. + """ + self._orient = self._orient.rotate(angle) + + def _addPolylineToElements(self): + """Creates a new Polyline element that will be used for future movement/drawing. + The old one (if filled) will be stored on the movement stack. + """ + if (len(self._pointsOfPolyline) > 1): + s = '' + for point in self._pointsOfPolyline: + s += str(point) + ' '#str(point.x) + ',' + str(point.y) + ' ' + p = Polyline(s) + p.set_style('fill:' + self.fill + '; stroke:' + self.stroke + '; stroke-width:' + self.strokeWidth) + self._svgElements.append(p) + self._pointsOfPolyline = [] + self._pointsOfPolyline.append(Vector(self._position.x, self._position.y)) + + def getXML(self): + """Retrieves the pysvg elements that make up the turtles path and returns them as String in an xml representation. + """ + s = '' + for element in self._svgElements: + s += element.getXML() + return s + + def getSVGElements(self): + """Retrieves the pysvg elements that make up the turtles path and returns them as list. + """ + return self._svgElements + + def addTurtlePathToSVG(self, svgContainer): + """Adds the paths of the turtle to an existing svg container. + """ + for element in self.getSVGElements(): + svgContainer.addElement(element) + return svgContainer + \ No newline at end of file diff --git a/appengine/2048/pysvg/util.py b/appengine/2048/pysvg/util.py new file mode 100644 index 0000000..0ea8128 --- /dev/null +++ b/appengine/2048/pysvg/util.py @@ -0,0 +1,7 @@ +''' +Created on 12.04.2009 + +@author: kerim +''' + + diff --git a/appengine/2048/ui.py b/appengine/2048/ui.py new file mode 100644 index 0000000..194c897 --- /dev/null +++ b/appengine/2048/ui.py @@ -0,0 +1,31 @@ +from pysvg.builders import Svg, ShapeBuilder, StyleBuilder +from pysvg.text import * + +def createblock(number): + colors = {} + colors[None]=('#eee4da','#776e65') + colors[2]=('#eee4da','#776e65') + colors[4]=('#ede0c8','#776e65') + colors[8]=('#f2b179','#f9f6f2') + colors[16]=('#f59563','#f9f6f2') + colors[32]=('#f67c5f','#f9f6f2') + colors[64]=('#f65e3b','#f9f6f2') + colors[128]=('#edcf72','#f9f6f2') + colors[256]=('#edcc61','#f9f6f2') + colors[512]=('#eee4da','#776e65') + colors[1024]=('#edc53f','#f9f6f2') + colors[2048]=('#edc22e','#f9f6f2') + + canvas = Svg(0,0,100,100) + sb = ShapeBuilder() + canvas.addElement( sb.createRect(5,5,90,90,fill=colors[number][0]) ) + + t = Text(number,50,60) + t.set_style("font-family:FreeSans;font-weight:bold;font-size:36px;text-anchor:middle") + t.set_fill(colors[number][1]) + canvas.addElement(t) + return canvas.getXML() + #canvas.save('/tmp/try7.svg') + + +createblock(None) diff --git a/appengine/dashdemo-cached/app.yaml b/appengine/dashdemo-cached/app.yaml new file mode 100644 index 0000000..b00c405 --- /dev/null +++ b/appengine/dashdemo-cached/app.yaml @@ -0,0 +1,25 @@ +application: google.com:dashdemo +version: bq +runtime: python27 +api_version: 1 +threadsafe: no + +handlers: +- url: /favicon\.ico + static_files: favicon.ico + upload: favicon\.ico + +- url: .* + script: main.application + +- url: /stats.* + script: google.appengine.ext.appstats.ui.app + +libraries: +- name: django + version: latest +- name: webapp2 + version: latest + +builtins: +- appstats: on diff --git a/appengine/dashdemo-cached/appengine_config.py b/appengine/dashdemo-cached/appengine_config.py new file mode 100644 index 0000000..f7da1e3 --- /dev/null +++ b/appengine/dashdemo-cached/appengine_config.py @@ -0,0 +1,6 @@ +def webapp_add_wsgi_middleware(app): + from google.appengine.ext.appstats import recording + app = recording.appstats_wsgi_middleware(app) + return app + +appstats_SHELL_OK = True diff --git a/appengine/dashdemo-cached/bqclient.py b/appengine/dashdemo-cached/bqclient.py new file mode 100644 index 0000000..41a123b --- /dev/null +++ b/appengine/dashdemo-cached/bqclient.py @@ -0,0 +1,30 @@ +from googleapiclient.discovery import build + +class BigQueryClient(object): + def __init__(self, http, decorator): + """Creates the BigQuery client connection""" + self.service = build('bigquery', 'v2', http=http) + self.decorator = decorator + + def getTableData(self, project, dataset, table): + decorated = self.decorator.http() + return self.service.tables().get(projectId=project, datasetId=dataset, tableId=table).execute(decorated) + + def getLastModTime(self, project, dataset, table): + data = self.getTableData(project, dataset, table) + if data is not None and 'lastModifiedTime' in data: + return data['lastModifiedTime'] + else: + return None + + def Query(self, query, project, timeout_ms=10000): + query_config = { + 'query': query, + 'timeoutMs': timeout_ms + } + decorated = self.decorator.http() + result_json = (self.service.jobs() + .query(projectId=project, + body=query_config) + .execute(decorated)) + return result_json diff --git a/appengine/dashdemo-cached/client_secrets.json b/appengine/dashdemo-cached/client_secrets.json new file mode 100644 index 0000000..921b3ad --- /dev/null +++ b/appengine/dashdemo-cached/client_secrets.json @@ -0,0 +1 @@ +{"web":{"auth_uri":"https://accounts.google.com/o/oauth2/auth","client_secret":"jFfoJcG7Or7jZpchUjxQ4xmz","token_uri":"https://accounts.google.com/o/oauth2/token","client_email":"475473128136-7hs4hg3onoqva4h0d26fh0ptdntra1g1@developer.gserviceaccount.com","redirect_uris":["https://dashdemo.googleplex.com/oauth2callback","http://localhost:8080/oauth2callback"],"client_x509_cert_url":"https://www.googleapis.com/robot/v1/metadata/x509/475473128136-7hs4hg3onoqva4h0d26fh0ptdntra1g1@developer.gserviceaccount.com","client_id":"475473128136-7hs4hg3onoqva4h0d26fh0ptdntra1g1.apps.googleusercontent.com","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","javascript_origins":["https://dashdemo.googleplex.com","http://localhost:8080"]}} \ No newline at end of file diff --git a/appengine/dashdemo-cached/googleapiclient/__init__.py b/appengine/dashdemo-cached/googleapiclient/__init__.py new file mode 100644 index 0000000..fe31691 --- /dev/null +++ b/appengine/dashdemo-cached/googleapiclient/__init__.py @@ -0,0 +1,15 @@ +# Copyright (C) 2012 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "1.2" diff --git a/appengine/dashdemo-cached/googleapiclient/channel.py b/appengine/dashdemo-cached/googleapiclient/channel.py new file mode 100644 index 0000000..265273e --- /dev/null +++ b/appengine/dashdemo-cached/googleapiclient/channel.py @@ -0,0 +1,285 @@ +"""Channel notifications support. + +Classes and functions to support channel subscriptions and notifications +on those channels. + +Notes: + - This code is based on experimental APIs and is subject to change. + - Notification does not do deduplication of notification ids, that's up to + the receiver. + - Storing the Channel between calls is up to the caller. + + +Example setting up a channel: + + # Create a new channel that gets notifications via webhook. + channel = new_webhook_channel("https://example.com/my_web_hook") + + # Store the channel, keyed by 'channel.id'. Store it before calling the + # watch method because notifications may start arriving before the watch + # method returns. + ... + + resp = service.objects().watchAll( + bucket="some_bucket_id", body=channel.body()).execute() + channel.update(resp) + + # Store the channel, keyed by 'channel.id'. Store it after being updated + # since the resource_id value will now be correct, and that's needed to + # stop a subscription. + ... + + +An example Webhook implementation using webapp2. Note that webapp2 puts +headers in a case insensitive dictionary, as headers aren't guaranteed to +always be upper case. + + id = self.request.headers[X_GOOG_CHANNEL_ID] + + # Retrieve the channel by id. + channel = ... + + # Parse notification from the headers, including validating the id. + n = notification_from_headers(channel, self.request.headers) + + # Do app specific stuff with the notification here. + if n.resource_state == 'sync': + # Code to handle sync state. + elif n.resource_state == 'exists': + # Code to handle the exists state. + elif n.resource_state == 'not_exists': + # Code to handle the not exists state. + + +Example of unsubscribing. + + service.channels().stop(channel.body()) +""" + +import datetime +import uuid + +from googleapiclient import errors +from oauth2client import util + + +# The unix time epoch starts at midnight 1970. +EPOCH = datetime.datetime.utcfromtimestamp(0) + +# Map the names of the parameters in the JSON channel description to +# the parameter names we use in the Channel class. +CHANNEL_PARAMS = { + 'address': 'address', + 'id': 'id', + 'expiration': 'expiration', + 'params': 'params', + 'resourceId': 'resource_id', + 'resourceUri': 'resource_uri', + 'type': 'type', + 'token': 'token', + } + +X_GOOG_CHANNEL_ID = 'X-GOOG-CHANNEL-ID' +X_GOOG_MESSAGE_NUMBER = 'X-GOOG-MESSAGE-NUMBER' +X_GOOG_RESOURCE_STATE = 'X-GOOG-RESOURCE-STATE' +X_GOOG_RESOURCE_URI = 'X-GOOG-RESOURCE-URI' +X_GOOG_RESOURCE_ID = 'X-GOOG-RESOURCE-ID' + + +def _upper_header_keys(headers): + new_headers = {} + for k, v in headers.iteritems(): + new_headers[k.upper()] = v + return new_headers + + +class Notification(object): + """A Notification from a Channel. + + Notifications are not usually constructed directly, but are returned + from functions like notification_from_headers(). + + Attributes: + message_number: int, The unique id number of this notification. + state: str, The state of the resource being monitored. + uri: str, The address of the resource being monitored. + resource_id: str, The unique identifier of the version of the resource at + this event. + """ + @util.positional(5) + def __init__(self, message_number, state, resource_uri, resource_id): + """Notification constructor. + + Args: + message_number: int, The unique id number of this notification. + state: str, The state of the resource being monitored. Can be one + of "exists", "not_exists", or "sync". + resource_uri: str, The address of the resource being monitored. + resource_id: str, The identifier of the watched resource. + """ + self.message_number = message_number + self.state = state + self.resource_uri = resource_uri + self.resource_id = resource_id + + +class Channel(object): + """A Channel for notifications. + + Usually not constructed directly, instead it is returned from helper + functions like new_webhook_channel(). + + Attributes: + type: str, The type of delivery mechanism used by this channel. For + example, 'web_hook'. + id: str, A UUID for the channel. + token: str, An arbitrary string associated with the channel that + is delivered to the target address with each event delivered + over this channel. + address: str, The address of the receiving entity where events are + delivered. Specific to the channel type. + expiration: int, The time, in milliseconds from the epoch, when this + channel will expire. + params: dict, A dictionary of string to string, with additional parameters + controlling delivery channel behavior. + resource_id: str, An opaque id that identifies the resource that is + being watched. Stable across different API versions. + resource_uri: str, The canonicalized ID of the watched resource. + """ + + @util.positional(5) + def __init__(self, type, id, token, address, expiration=None, + params=None, resource_id="", resource_uri=""): + """Create a new Channel. + + In user code, this Channel constructor will not typically be called + manually since there are functions for creating channels for each specific + type with a more customized set of arguments to pass. + + Args: + type: str, The type of delivery mechanism used by this channel. For + example, 'web_hook'. + id: str, A UUID for the channel. + token: str, An arbitrary string associated with the channel that + is delivered to the target address with each event delivered + over this channel. + address: str, The address of the receiving entity where events are + delivered. Specific to the channel type. + expiration: int, The time, in milliseconds from the epoch, when this + channel will expire. + params: dict, A dictionary of string to string, with additional parameters + controlling delivery channel behavior. + resource_id: str, An opaque id that identifies the resource that is + being watched. Stable across different API versions. + resource_uri: str, The canonicalized ID of the watched resource. + """ + self.type = type + self.id = id + self.token = token + self.address = address + self.expiration = expiration + self.params = params + self.resource_id = resource_id + self.resource_uri = resource_uri + + def body(self): + """Build a body from the Channel. + + Constructs a dictionary that's appropriate for passing into watch() + methods as the value of body argument. + + Returns: + A dictionary representation of the channel. + """ + result = { + 'id': self.id, + 'token': self.token, + 'type': self.type, + 'address': self.address + } + if self.params: + result['params'] = self.params + if self.resource_id: + result['resourceId'] = self.resource_id + if self.resource_uri: + result['resourceUri'] = self.resource_uri + if self.expiration: + result['expiration'] = self.expiration + + return result + + def update(self, resp): + """Update a channel with information from the response of watch(). + + When a request is sent to watch() a resource, the response returned + from the watch() request is a dictionary with updated channel information, + such as the resource_id, which is needed when stopping a subscription. + + Args: + resp: dict, The response from a watch() method. + """ + for json_name, param_name in CHANNEL_PARAMS.iteritems(): + value = resp.get(json_name) + if value is not None: + setattr(self, param_name, value) + + +def notification_from_headers(channel, headers): + """Parse a notification from the webhook request headers, validate + the notification, and return a Notification object. + + Args: + channel: Channel, The channel that the notification is associated with. + headers: dict, A dictionary like object that contains the request headers + from the webhook HTTP request. + + Returns: + A Notification object. + + Raises: + errors.InvalidNotificationError if the notification is invalid. + ValueError if the X-GOOG-MESSAGE-NUMBER can't be converted to an int. + """ + headers = _upper_header_keys(headers) + channel_id = headers[X_GOOG_CHANNEL_ID] + if channel.id != channel_id: + raise errors.InvalidNotificationError( + 'Channel id mismatch: %s != %s' % (channel.id, channel_id)) + else: + message_number = int(headers[X_GOOG_MESSAGE_NUMBER]) + state = headers[X_GOOG_RESOURCE_STATE] + resource_uri = headers[X_GOOG_RESOURCE_URI] + resource_id = headers[X_GOOG_RESOURCE_ID] + return Notification(message_number, state, resource_uri, resource_id) + + +@util.positional(2) +def new_webhook_channel(url, token=None, expiration=None, params=None): + """Create a new webhook Channel. + + Args: + url: str, URL to post notifications to. + token: str, An arbitrary string associated with the channel that + is delivered to the target address with each notification delivered + over this channel. + expiration: datetime.datetime, A time in the future when the channel + should expire. Can also be None if the subscription should use the + default expiration. Note that different services may have different + limits on how long a subscription lasts. Check the response from the + watch() method to see the value the service has set for an expiration + time. + params: dict, Extra parameters to pass on channel creation. Currently + not used for webhook channels. + """ + expiration_ms = 0 + if expiration: + delta = expiration - EPOCH + expiration_ms = delta.microseconds/1000 + ( + delta.seconds + delta.days*24*3600)*1000 + if expiration_ms < 0: + expiration_ms = 0 + + return Channel('web_hook', str(uuid.uuid4()), + token, url, expiration=expiration_ms, + params=params) + diff --git a/appengine/dashdemo-cached/googleapiclient/discovery.py b/appengine/dashdemo-cached/googleapiclient/discovery.py new file mode 100644 index 0000000..1ed9921 --- /dev/null +++ b/appengine/dashdemo-cached/googleapiclient/discovery.py @@ -0,0 +1,959 @@ +# Copyright (C) 2010 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Client for discovery based APIs. + +A client library for Google's discovery based APIs. +""" + +__author__ = 'jcgregorio@google.com (Joe Gregorio)' +__all__ = [ + 'build', + 'build_from_document', + 'fix_method_name', + 'key2param', + ] + + +# Standard library imports +import copy +from email.mime.multipart import MIMEMultipart +from email.mime.nonmultipart import MIMENonMultipart +import keyword +import logging +import mimetypes +import os +import re +import urllib +import urlparse + +try: + from urlparse import parse_qsl +except ImportError: + from cgi import parse_qsl + +# Third-party imports +import httplib2 +import mimeparse +import uritemplate + +# Local imports +from googleapiclient.errors import HttpError +from googleapiclient.errors import InvalidJsonError +from googleapiclient.errors import MediaUploadSizeError +from googleapiclient.errors import UnacceptableMimeTypeError +from googleapiclient.errors import UnknownApiNameOrVersion +from googleapiclient.errors import UnknownFileType +from googleapiclient.http import HttpRequest +from googleapiclient.http import MediaFileUpload +from googleapiclient.http import MediaUpload +from googleapiclient.model import JsonModel +from googleapiclient.model import MediaModel +from googleapiclient.model import RawModel +from googleapiclient.schema import Schemas +from oauth2client.anyjson import simplejson +from oauth2client.util import _add_query_parameter +from oauth2client.util import positional + + +# The client library requires a version of httplib2 that supports RETRIES. +httplib2.RETRIES = 1 + +logger = logging.getLogger(__name__) + +URITEMPLATE = re.compile('{[^}]*}') +VARNAME = re.compile('[a-zA-Z0-9_-]+') +DISCOVERY_URI = ('https://www.googleapis.com/discovery/v1/apis/' + '{api}/{apiVersion}/rest') +DEFAULT_METHOD_DOC = 'A description of how to use this function' +HTTP_PAYLOAD_METHODS = frozenset(['PUT', 'POST', 'PATCH']) +_MEDIA_SIZE_BIT_SHIFTS = {'KB': 10, 'MB': 20, 'GB': 30, 'TB': 40} +BODY_PARAMETER_DEFAULT_VALUE = { + 'description': 'The request body.', + 'type': 'object', + 'required': True, +} +MEDIA_BODY_PARAMETER_DEFAULT_VALUE = { + 'description': ('The filename of the media request body, or an instance ' + 'of a MediaUpload object.'), + 'type': 'string', + 'required': False, +} + +# Parameters accepted by the stack, but not visible via discovery. +# TODO(dhermes): Remove 'userip' in 'v2'. +STACK_QUERY_PARAMETERS = frozenset(['trace', 'pp', 'userip', 'strict']) +STACK_QUERY_PARAMETER_DEFAULT_VALUE = {'type': 'string', 'location': 'query'} + +# Library-specific reserved words beyond Python keywords. +RESERVED_WORDS = frozenset(['body']) + + +def fix_method_name(name): + """Fix method names to avoid reserved word conflicts. + + Args: + name: string, method name. + + Returns: + The name with a '_' prefixed if the name is a reserved word. + """ + if keyword.iskeyword(name) or name in RESERVED_WORDS: + return name + '_' + else: + return name + + +def key2param(key): + """Converts key names into parameter names. + + For example, converting "max-results" -> "max_results" + + Args: + key: string, the method key name. + + Returns: + A safe method name based on the key name. + """ + result = [] + key = list(key) + if not key[0].isalpha(): + result.append('x') + for c in key: + if c.isalnum(): + result.append(c) + else: + result.append('_') + + return ''.join(result) + + +@positional(2) +def build(serviceName, + version, + http=None, + discoveryServiceUrl=DISCOVERY_URI, + developerKey=None, + model=None, + requestBuilder=HttpRequest): + """Construct a Resource for interacting with an API. + + Construct a Resource object for interacting with an API. The serviceName and + version are the names from the Discovery service. + + Args: + serviceName: string, name of the service. + version: string, the version of the service. + http: httplib2.Http, An instance of httplib2.Http or something that acts + like it that HTTP requests will be made through. + discoveryServiceUrl: string, a URI Template that points to the location of + the discovery service. It should have two parameters {api} and + {apiVersion} that when filled in produce an absolute URI to the discovery + document for that service. + developerKey: string, key obtained from + https://code.google.com/apis/console. + model: googleapiclient.Model, converts to and from the wire format. + requestBuilder: googleapiclient.http.HttpRequest, encapsulator for an HTTP + request. + + Returns: + A Resource object with methods for interacting with the service. + """ + params = { + 'api': serviceName, + 'apiVersion': version + } + + if http is None: + http = httplib2.Http() + + requested_url = uritemplate.expand(discoveryServiceUrl, params) + + # REMOTE_ADDR is defined by the CGI spec [RFC3875] as the environment + # variable that contains the network address of the client sending the + # request. If it exists then add that to the request for the discovery + # document to avoid exceeding the quota on discovery requests. + if 'REMOTE_ADDR' in os.environ: + requested_url = _add_query_parameter(requested_url, 'userIp', + os.environ['REMOTE_ADDR']) + logger.info('URL being requested: %s' % requested_url) + + resp, content = http.request(requested_url) + + if resp.status == 404: + raise UnknownApiNameOrVersion("name: %s version: %s" % (serviceName, + version)) + if resp.status >= 400: + raise HttpError(resp, content, uri=requested_url) + + try: + service = simplejson.loads(content) + except ValueError, e: + logger.error('Failed to parse as JSON: ' + content) + raise InvalidJsonError() + + return build_from_document(content, base=discoveryServiceUrl, http=http, + developerKey=developerKey, model=model, requestBuilder=requestBuilder) + + +@positional(1) +def build_from_document( + service, + base=None, + future=None, + http=None, + developerKey=None, + model=None, + requestBuilder=HttpRequest): + """Create a Resource for interacting with an API. + + Same as `build()`, but constructs the Resource object from a discovery + document that is it given, as opposed to retrieving one over HTTP. + + Args: + service: string or object, the JSON discovery document describing the API. + The value passed in may either be the JSON string or the deserialized + JSON. + base: string, base URI for all HTTP requests, usually the discovery URI. + This parameter is no longer used as rootUrl and servicePath are included + within the discovery document. (deprecated) + future: string, discovery document with future capabilities (deprecated). + http: httplib2.Http, An instance of httplib2.Http or something that acts + like it that HTTP requests will be made through. + developerKey: string, Key for controlling API usage, generated + from the API Console. + model: Model class instance that serializes and de-serializes requests and + responses. + requestBuilder: Takes an http request and packages it up to be executed. + + Returns: + A Resource object with methods for interacting with the service. + """ + + # future is no longer used. + future = {} + + if isinstance(service, basestring): + service = simplejson.loads(service) + base = urlparse.urljoin(service['rootUrl'], service['servicePath']) + schema = Schemas(service) + + if model is None: + features = service.get('features', []) + model = JsonModel('dataWrapper' in features) + return Resource(http=http, baseUrl=base, model=model, + developerKey=developerKey, requestBuilder=requestBuilder, + resourceDesc=service, rootDesc=service, schema=schema) + + +def _cast(value, schema_type): + """Convert value to a string based on JSON Schema type. + + See http://tools.ietf.org/html/draft-zyp-json-schema-03 for more details on + JSON Schema. + + Args: + value: any, the value to convert + schema_type: string, the type that value should be interpreted as + + Returns: + A string representation of 'value' based on the schema_type. + """ + if schema_type == 'string': + if type(value) == type('') or type(value) == type(u''): + return value + else: + return str(value) + elif schema_type == 'integer': + return str(int(value)) + elif schema_type == 'number': + return str(float(value)) + elif schema_type == 'boolean': + return str(bool(value)).lower() + else: + if type(value) == type('') or type(value) == type(u''): + return value + else: + return str(value) + + +def _media_size_to_long(maxSize): + """Convert a string media size, such as 10GB or 3TB into an integer. + + Args: + maxSize: string, size as a string, such as 2MB or 7GB. + + Returns: + The size as an integer value. + """ + if len(maxSize) < 2: + return 0L + units = maxSize[-2:].upper() + bit_shift = _MEDIA_SIZE_BIT_SHIFTS.get(units) + if bit_shift is not None: + return long(maxSize[:-2]) << bit_shift + else: + return long(maxSize) + + +def _media_path_url_from_info(root_desc, path_url): + """Creates an absolute media path URL. + + Constructed using the API root URI and service path from the discovery + document and the relative path for the API method. + + Args: + root_desc: Dictionary; the entire original deserialized discovery document. + path_url: String; the relative URL for the API method. Relative to the API + root, which is specified in the discovery document. + + Returns: + String; the absolute URI for media upload for the API method. + """ + return '%(root)supload/%(service_path)s%(path)s' % { + 'root': root_desc['rootUrl'], + 'service_path': root_desc['servicePath'], + 'path': path_url, + } + + +def _fix_up_parameters(method_desc, root_desc, http_method): + """Updates parameters of an API method with values specific to this library. + + Specifically, adds whatever global parameters are specified by the API to the + parameters for the individual method. Also adds parameters which don't + appear in the discovery document, but are available to all discovery based + APIs (these are listed in STACK_QUERY_PARAMETERS). + + SIDE EFFECTS: This updates the parameters dictionary object in the method + description. + + Args: + method_desc: Dictionary with metadata describing an API method. Value comes + from the dictionary of methods stored in the 'methods' key in the + deserialized discovery document. + root_desc: Dictionary; the entire original deserialized discovery document. + http_method: String; the HTTP method used to call the API method described + in method_desc. + + Returns: + The updated Dictionary stored in the 'parameters' key of the method + description dictionary. + """ + parameters = method_desc.setdefault('parameters', {}) + + # Add in the parameters common to all methods. + for name, description in root_desc.get('parameters', {}).iteritems(): + parameters[name] = description + + # Add in undocumented query parameters. + for name in STACK_QUERY_PARAMETERS: + parameters[name] = STACK_QUERY_PARAMETER_DEFAULT_VALUE.copy() + + # Add 'body' (our own reserved word) to parameters if the method supports + # a request payload. + if http_method in HTTP_PAYLOAD_METHODS and 'request' in method_desc: + body = BODY_PARAMETER_DEFAULT_VALUE.copy() + body.update(method_desc['request']) + parameters['body'] = body + + return parameters + + +def _fix_up_media_upload(method_desc, root_desc, path_url, parameters): + """Updates parameters of API by adding 'media_body' if supported by method. + + SIDE EFFECTS: If the method supports media upload and has a required body, + sets body to be optional (required=False) instead. Also, if there is a + 'mediaUpload' in the method description, adds 'media_upload' key to + parameters. + + Args: + method_desc: Dictionary with metadata describing an API method. Value comes + from the dictionary of methods stored in the 'methods' key in the + deserialized discovery document. + root_desc: Dictionary; the entire original deserialized discovery document. + path_url: String; the relative URL for the API method. Relative to the API + root, which is specified in the discovery document. + parameters: A dictionary describing method parameters for method described + in method_desc. + + Returns: + Triple (accept, max_size, media_path_url) where: + - accept is a list of strings representing what content types are + accepted for media upload. Defaults to empty list if not in the + discovery document. + - max_size is a long representing the max size in bytes allowed for a + media upload. Defaults to 0L if not in the discovery document. + - media_path_url is a String; the absolute URI for media upload for the + API method. Constructed using the API root URI and service path from + the discovery document and the relative path for the API method. If + media upload is not supported, this is None. + """ + media_upload = method_desc.get('mediaUpload', {}) + accept = media_upload.get('accept', []) + max_size = _media_size_to_long(media_upload.get('maxSize', '')) + media_path_url = None + + if media_upload: + media_path_url = _media_path_url_from_info(root_desc, path_url) + parameters['media_body'] = MEDIA_BODY_PARAMETER_DEFAULT_VALUE.copy() + if 'body' in parameters: + parameters['body']['required'] = False + + return accept, max_size, media_path_url + + +def _fix_up_method_description(method_desc, root_desc): + """Updates a method description in a discovery document. + + SIDE EFFECTS: Changes the parameters dictionary in the method description with + extra parameters which are used locally. + + Args: + method_desc: Dictionary with metadata describing an API method. Value comes + from the dictionary of methods stored in the 'methods' key in the + deserialized discovery document. + root_desc: Dictionary; the entire original deserialized discovery document. + + Returns: + Tuple (path_url, http_method, method_id, accept, max_size, media_path_url) + where: + - path_url is a String; the relative URL for the API method. Relative to + the API root, which is specified in the discovery document. + - http_method is a String; the HTTP method used to call the API method + described in the method description. + - method_id is a String; the name of the RPC method associated with the + API method, and is in the method description in the 'id' key. + - accept is a list of strings representing what content types are + accepted for media upload. Defaults to empty list if not in the + discovery document. + - max_size is a long representing the max size in bytes allowed for a + media upload. Defaults to 0L if not in the discovery document. + - media_path_url is a String; the absolute URI for media upload for the + API method. Constructed using the API root URI and service path from + the discovery document and the relative path for the API method. If + media upload is not supported, this is None. + """ + path_url = method_desc['path'] + http_method = method_desc['httpMethod'] + method_id = method_desc['id'] + + parameters = _fix_up_parameters(method_desc, root_desc, http_method) + # Order is important. `_fix_up_media_upload` needs `method_desc` to have a + # 'parameters' key and needs to know if there is a 'body' parameter because it + # also sets a 'media_body' parameter. + accept, max_size, media_path_url = _fix_up_media_upload( + method_desc, root_desc, path_url, parameters) + + return path_url, http_method, method_id, accept, max_size, media_path_url + + +# TODO(dhermes): Convert this class to ResourceMethod and make it callable +class ResourceMethodParameters(object): + """Represents the parameters associated with a method. + + Attributes: + argmap: Map from method parameter name (string) to query parameter name + (string). + required_params: List of required parameters (represented by parameter + name as string). + repeated_params: List of repeated parameters (represented by parameter + name as string). + pattern_params: Map from method parameter name (string) to regular + expression (as a string). If the pattern is set for a parameter, the + value for that parameter must match the regular expression. + query_params: List of parameters (represented by parameter name as string) + that will be used in the query string. + path_params: Set of parameters (represented by parameter name as string) + that will be used in the base URL path. + param_types: Map from method parameter name (string) to parameter type. Type + can be any valid JSON schema type; valid values are 'any', 'array', + 'boolean', 'integer', 'number', 'object', or 'string'. Reference: + http://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.1 + enum_params: Map from method parameter name (string) to list of strings, + where each list of strings is the list of acceptable enum values. + """ + + def __init__(self, method_desc): + """Constructor for ResourceMethodParameters. + + Sets default values and defers to set_parameters to populate. + + Args: + method_desc: Dictionary with metadata describing an API method. Value + comes from the dictionary of methods stored in the 'methods' key in + the deserialized discovery document. + """ + self.argmap = {} + self.required_params = [] + self.repeated_params = [] + self.pattern_params = {} + self.query_params = [] + # TODO(dhermes): Change path_params to a list if the extra URITEMPLATE + # parsing is gotten rid of. + self.path_params = set() + self.param_types = {} + self.enum_params = {} + + self.set_parameters(method_desc) + + def set_parameters(self, method_desc): + """Populates maps and lists based on method description. + + Iterates through each parameter for the method and parses the values from + the parameter dictionary. + + Args: + method_desc: Dictionary with metadata describing an API method. Value + comes from the dictionary of methods stored in the 'methods' key in + the deserialized discovery document. + """ + for arg, desc in method_desc.get('parameters', {}).iteritems(): + param = key2param(arg) + self.argmap[param] = arg + + if desc.get('pattern'): + self.pattern_params[param] = desc['pattern'] + if desc.get('enum'): + self.enum_params[param] = desc['enum'] + if desc.get('required'): + self.required_params.append(param) + if desc.get('repeated'): + self.repeated_params.append(param) + if desc.get('location') == 'query': + self.query_params.append(param) + if desc.get('location') == 'path': + self.path_params.add(param) + self.param_types[param] = desc.get('type', 'string') + + # TODO(dhermes): Determine if this is still necessary. Discovery based APIs + # should have all path parameters already marked with + # 'location: path'. + for match in URITEMPLATE.finditer(method_desc['path']): + for namematch in VARNAME.finditer(match.group(0)): + name = key2param(namematch.group(0)) + self.path_params.add(name) + if name in self.query_params: + self.query_params.remove(name) + + +def createMethod(methodName, methodDesc, rootDesc, schema): + """Creates a method for attaching to a Resource. + + Args: + methodName: string, name of the method to use. + methodDesc: object, fragment of deserialized discovery document that + describes the method. + rootDesc: object, the entire deserialized discovery document. + schema: object, mapping of schema names to schema descriptions. + """ + methodName = fix_method_name(methodName) + (pathUrl, httpMethod, methodId, accept, + maxSize, mediaPathUrl) = _fix_up_method_description(methodDesc, rootDesc) + + parameters = ResourceMethodParameters(methodDesc) + + def method(self, **kwargs): + # Don't bother with doc string, it will be over-written by createMethod. + + for name in kwargs.iterkeys(): + if name not in parameters.argmap: + raise TypeError('Got an unexpected keyword argument "%s"' % name) + + # Remove args that have a value of None. + keys = kwargs.keys() + for name in keys: + if kwargs[name] is None: + del kwargs[name] + + for name in parameters.required_params: + if name not in kwargs: + raise TypeError('Missing required parameter "%s"' % name) + + for name, regex in parameters.pattern_params.iteritems(): + if name in kwargs: + if isinstance(kwargs[name], basestring): + pvalues = [kwargs[name]] + else: + pvalues = kwargs[name] + for pvalue in pvalues: + if re.match(regex, pvalue) is None: + raise TypeError( + 'Parameter "%s" value "%s" does not match the pattern "%s"' % + (name, pvalue, regex)) + + for name, enums in parameters.enum_params.iteritems(): + if name in kwargs: + # We need to handle the case of a repeated enum + # name differently, since we want to handle both + # arg='value' and arg=['value1', 'value2'] + if (name in parameters.repeated_params and + not isinstance(kwargs[name], basestring)): + values = kwargs[name] + else: + values = [kwargs[name]] + for value in values: + if value not in enums: + raise TypeError( + 'Parameter "%s" value "%s" is not an allowed value in "%s"' % + (name, value, str(enums))) + + actual_query_params = {} + actual_path_params = {} + for key, value in kwargs.iteritems(): + to_type = parameters.param_types.get(key, 'string') + # For repeated parameters we cast each member of the list. + if key in parameters.repeated_params and type(value) == type([]): + cast_value = [_cast(x, to_type) for x in value] + else: + cast_value = _cast(value, to_type) + if key in parameters.query_params: + actual_query_params[parameters.argmap[key]] = cast_value + if key in parameters.path_params: + actual_path_params[parameters.argmap[key]] = cast_value + body_value = kwargs.get('body', None) + media_filename = kwargs.get('media_body', None) + + if self._developerKey: + actual_query_params['key'] = self._developerKey + + model = self._model + if methodName.endswith('_media'): + model = MediaModel() + elif 'response' not in methodDesc: + model = RawModel() + + headers = {} + headers, params, query, body = model.request(headers, + actual_path_params, actual_query_params, body_value) + + expanded_url = uritemplate.expand(pathUrl, params) + url = urlparse.urljoin(self._baseUrl, expanded_url + query) + + resumable = None + multipart_boundary = '' + + if media_filename: + # Ensure we end up with a valid MediaUpload object. + if isinstance(media_filename, basestring): + (media_mime_type, encoding) = mimetypes.guess_type(media_filename) + if media_mime_type is None: + raise UnknownFileType(media_filename) + if not mimeparse.best_match([media_mime_type], ','.join(accept)): + raise UnacceptableMimeTypeError(media_mime_type) + media_upload = MediaFileUpload(media_filename, + mimetype=media_mime_type) + elif isinstance(media_filename, MediaUpload): + media_upload = media_filename + else: + raise TypeError('media_filename must be str or MediaUpload.') + + # Check the maxSize + if maxSize > 0 and media_upload.size() > maxSize: + raise MediaUploadSizeError("Media larger than: %s" % maxSize) + + # Use the media path uri for media uploads + expanded_url = uritemplate.expand(mediaPathUrl, params) + url = urlparse.urljoin(self._baseUrl, expanded_url + query) + if media_upload.resumable(): + url = _add_query_parameter(url, 'uploadType', 'resumable') + + if media_upload.resumable(): + # This is all we need to do for resumable, if the body exists it gets + # sent in the first request, otherwise an empty body is sent. + resumable = media_upload + else: + # A non-resumable upload + if body is None: + # This is a simple media upload + headers['content-type'] = media_upload.mimetype() + body = media_upload.getbytes(0, media_upload.size()) + url = _add_query_parameter(url, 'uploadType', 'media') + else: + # This is a multipart/related upload. + msgRoot = MIMEMultipart('related') + # msgRoot should not write out it's own headers + setattr(msgRoot, '_write_headers', lambda self: None) + + # attach the body as one part + msg = MIMENonMultipart(*headers['content-type'].split('/')) + msg.set_payload(body) + msgRoot.attach(msg) + + # attach the media as the second part + msg = MIMENonMultipart(*media_upload.mimetype().split('/')) + msg['Content-Transfer-Encoding'] = 'binary' + + payload = media_upload.getbytes(0, media_upload.size()) + msg.set_payload(payload) + msgRoot.attach(msg) + body = msgRoot.as_string() + + multipart_boundary = msgRoot.get_boundary() + headers['content-type'] = ('multipart/related; ' + 'boundary="%s"') % multipart_boundary + url = _add_query_parameter(url, 'uploadType', 'multipart') + + logger.info('URL being requested: %s' % url) + return self._requestBuilder(self._http, + model.response, + url, + method=httpMethod, + body=body, + headers=headers, + methodId=methodId, + resumable=resumable) + + docs = [methodDesc.get('description', DEFAULT_METHOD_DOC), '\n\n'] + if len(parameters.argmap) > 0: + docs.append('Args:\n') + + # Skip undocumented params and params common to all methods. + skip_parameters = rootDesc.get('parameters', {}).keys() + skip_parameters.extend(STACK_QUERY_PARAMETERS) + + all_args = parameters.argmap.keys() + args_ordered = [key2param(s) for s in methodDesc.get('parameterOrder', [])] + + # Move body to the front of the line. + if 'body' in all_args: + args_ordered.append('body') + + for name in all_args: + if name not in args_ordered: + args_ordered.append(name) + + for arg in args_ordered: + if arg in skip_parameters: + continue + + repeated = '' + if arg in parameters.repeated_params: + repeated = ' (repeated)' + required = '' + if arg in parameters.required_params: + required = ' (required)' + paramdesc = methodDesc['parameters'][parameters.argmap[arg]] + paramdoc = paramdesc.get('description', 'A parameter') + if '$ref' in paramdesc: + docs.append( + (' %s: object, %s%s%s\n The object takes the' + ' form of:\n\n%s\n\n') % (arg, paramdoc, required, repeated, + schema.prettyPrintByName(paramdesc['$ref']))) + else: + paramtype = paramdesc.get('type', 'string') + docs.append(' %s: %s, %s%s%s\n' % (arg, paramtype, paramdoc, required, + repeated)) + enum = paramdesc.get('enum', []) + enumDesc = paramdesc.get('enumDescriptions', []) + if enum and enumDesc: + docs.append(' Allowed values\n') + for (name, desc) in zip(enum, enumDesc): + docs.append(' %s - %s\n' % (name, desc)) + if 'response' in methodDesc: + if methodName.endswith('_media'): + docs.append('\nReturns:\n The media object as a string.\n\n ') + else: + docs.append('\nReturns:\n An object of the form:\n\n ') + docs.append(schema.prettyPrintSchema(methodDesc['response'])) + + setattr(method, '__doc__', ''.join(docs)) + return (methodName, method) + + +def createNextMethod(methodName): + """Creates any _next methods for attaching to a Resource. + + The _next methods allow for easy iteration through list() responses. + + Args: + methodName: string, name of the method to use. + """ + methodName = fix_method_name(methodName) + + def methodNext(self, previous_request, previous_response): + """Retrieves the next page of results. + +Args: + previous_request: The request for the previous page. (required) + previous_response: The response from the request for the previous page. (required) + +Returns: + A request object that you can call 'execute()' on to request the next + page. Returns None if there are no more items in the collection. + """ + # Retrieve nextPageToken from previous_response + # Use as pageToken in previous_request to create new request. + + if 'nextPageToken' not in previous_response: + return None + + request = copy.copy(previous_request) + + pageToken = previous_response['nextPageToken'] + parsed = list(urlparse.urlparse(request.uri)) + q = parse_qsl(parsed[4]) + + # Find and remove old 'pageToken' value from URI + newq = [(key, value) for (key, value) in q if key != 'pageToken'] + newq.append(('pageToken', pageToken)) + parsed[4] = urllib.urlencode(newq) + uri = urlparse.urlunparse(parsed) + + request.uri = uri + + logger.info('URL being requested: %s' % uri) + + return request + + return (methodName, methodNext) + + +class Resource(object): + """A class for interacting with a resource.""" + + def __init__(self, http, baseUrl, model, requestBuilder, developerKey, + resourceDesc, rootDesc, schema): + """Build a Resource from the API description. + + Args: + http: httplib2.Http, Object to make http requests with. + baseUrl: string, base URL for the API. All requests are relative to this + URI. + model: googleapiclient.Model, converts to and from the wire format. + requestBuilder: class or callable that instantiates an + googleapiclient.HttpRequest object. + developerKey: string, key obtained from + https://code.google.com/apis/console + resourceDesc: object, section of deserialized discovery document that + describes a resource. Note that the top level discovery document + is considered a resource. + rootDesc: object, the entire deserialized discovery document. + schema: object, mapping of schema names to schema descriptions. + """ + self._dynamic_attrs = [] + + self._http = http + self._baseUrl = baseUrl + self._model = model + self._developerKey = developerKey + self._requestBuilder = requestBuilder + self._resourceDesc = resourceDesc + self._rootDesc = rootDesc + self._schema = schema + + self._set_service_methods() + + def _set_dynamic_attr(self, attr_name, value): + """Sets an instance attribute and tracks it in a list of dynamic attributes. + + Args: + attr_name: string; The name of the attribute to be set + value: The value being set on the object and tracked in the dynamic cache. + """ + self._dynamic_attrs.append(attr_name) + self.__dict__[attr_name] = value + + def __getstate__(self): + """Trim the state down to something that can be pickled. + + Uses the fact that the instance variable _dynamic_attrs holds attrs that + will be wiped and restored on pickle serialization. + """ + state_dict = copy.copy(self.__dict__) + for dynamic_attr in self._dynamic_attrs: + del state_dict[dynamic_attr] + del state_dict['_dynamic_attrs'] + return state_dict + + def __setstate__(self, state): + """Reconstitute the state of the object from being pickled. + + Uses the fact that the instance variable _dynamic_attrs holds attrs that + will be wiped and restored on pickle serialization. + """ + self.__dict__.update(state) + self._dynamic_attrs = [] + self._set_service_methods() + + def _set_service_methods(self): + self._add_basic_methods(self._resourceDesc, self._rootDesc, self._schema) + self._add_nested_resources(self._resourceDesc, self._rootDesc, self._schema) + self._add_next_methods(self._resourceDesc, self._schema) + + def _add_basic_methods(self, resourceDesc, rootDesc, schema): + # Add basic methods to Resource + if 'methods' in resourceDesc: + for methodName, methodDesc in resourceDesc['methods'].iteritems(): + fixedMethodName, method = createMethod( + methodName, methodDesc, rootDesc, schema) + self._set_dynamic_attr(fixedMethodName, + method.__get__(self, self.__class__)) + # Add in _media methods. The functionality of the attached method will + # change when it sees that the method name ends in _media. + if methodDesc.get('supportsMediaDownload', False): + fixedMethodName, method = createMethod( + methodName + '_media', methodDesc, rootDesc, schema) + self._set_dynamic_attr(fixedMethodName, + method.__get__(self, self.__class__)) + + def _add_nested_resources(self, resourceDesc, rootDesc, schema): + # Add in nested resources + if 'resources' in resourceDesc: + + def createResourceMethod(methodName, methodDesc): + """Create a method on the Resource to access a nested Resource. + + Args: + methodName: string, name of the method to use. + methodDesc: object, fragment of deserialized discovery document that + describes the method. + """ + methodName = fix_method_name(methodName) + + def methodResource(self): + return Resource(http=self._http, baseUrl=self._baseUrl, + model=self._model, developerKey=self._developerKey, + requestBuilder=self._requestBuilder, + resourceDesc=methodDesc, rootDesc=rootDesc, + schema=schema) + + setattr(methodResource, '__doc__', 'A collection resource.') + setattr(methodResource, '__is_resource__', True) + + return (methodName, methodResource) + + for methodName, methodDesc in resourceDesc['resources'].iteritems(): + fixedMethodName, method = createResourceMethod(methodName, methodDesc) + self._set_dynamic_attr(fixedMethodName, + method.__get__(self, self.__class__)) + + def _add_next_methods(self, resourceDesc, schema): + # Add _next() methods + # Look for response bodies in schema that contain nextPageToken, and methods + # that take a pageToken parameter. + if 'methods' in resourceDesc: + for methodName, methodDesc in resourceDesc['methods'].iteritems(): + if 'response' in methodDesc: + responseSchema = methodDesc['response'] + if '$ref' in responseSchema: + responseSchema = schema.get(responseSchema['$ref']) + hasNextPageToken = 'nextPageToken' in responseSchema.get('properties', + {}) + hasPageToken = 'pageToken' in methodDesc.get('parameters', {}) + if hasNextPageToken and hasPageToken: + fixedMethodName, method = createNextMethod(methodName + '_next') + self._set_dynamic_attr(fixedMethodName, + method.__get__(self, self.__class__)) diff --git a/appengine/dashdemo-cached/googleapiclient/errors.py b/appengine/dashdemo-cached/googleapiclient/errors.py new file mode 100644 index 0000000..ef2b161 --- /dev/null +++ b/appengine/dashdemo-cached/googleapiclient/errors.py @@ -0,0 +1,140 @@ +#!/usr/bin/python2.4 +# +# Copyright (C) 2010 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Errors for the library. + +All exceptions defined by the library +should be defined in this file. +""" + +__author__ = 'jcgregorio@google.com (Joe Gregorio)' + + +from oauth2client import util +from oauth2client.anyjson import simplejson + + +class Error(Exception): + """Base error for this module.""" + pass + + +class HttpError(Error): + """HTTP data was invalid or unexpected.""" + + @util.positional(3) + def __init__(self, resp, content, uri=None): + self.resp = resp + self.content = content + self.uri = uri + + def _get_reason(self): + """Calculate the reason for the error from the response content.""" + reason = self.resp.reason + try: + data = simplejson.loads(self.content) + reason = data['error']['message'] + except (ValueError, KeyError): + pass + if reason is None: + reason = '' + return reason + + def __repr__(self): + if self.uri: + return '' % ( + self.resp.status, self.uri, self._get_reason().strip()) + else: + return '' % (self.resp.status, self._get_reason()) + + __str__ = __repr__ + + +class InvalidJsonError(Error): + """The JSON returned could not be parsed.""" + pass + + +class UnknownFileType(Error): + """File type unknown or unexpected.""" + pass + + +class UnknownLinkType(Error): + """Link type unknown or unexpected.""" + pass + + +class UnknownApiNameOrVersion(Error): + """No API with that name and version exists.""" + pass + + +class UnacceptableMimeTypeError(Error): + """That is an unacceptable mimetype for this operation.""" + pass + + +class MediaUploadSizeError(Error): + """Media is larger than the method can accept.""" + pass + + +class ResumableUploadError(HttpError): + """Error occured during resumable upload.""" + pass + + +class InvalidChunkSizeError(Error): + """The given chunksize is not valid.""" + pass + +class InvalidNotificationError(Error): + """The channel Notification is invalid.""" + pass + +class BatchError(HttpError): + """Error occured during batch operations.""" + + @util.positional(2) + def __init__(self, reason, resp=None, content=None): + self.resp = resp + self.content = content + self.reason = reason + + def __repr__(self): + return '' % (self.resp.status, self.reason) + + __str__ = __repr__ + + +class UnexpectedMethodError(Error): + """Exception raised by RequestMockBuilder on unexpected calls.""" + + @util.positional(1) + def __init__(self, methodId=None): + """Constructor for an UnexpectedMethodError.""" + super(UnexpectedMethodError, self).__init__( + 'Received unexpected call %s' % methodId) + + +class UnexpectedBodyError(Error): + """Exception raised by RequestMockBuilder on unexpected bodies.""" + + def __init__(self, expected, provided): + """Constructor for an UnexpectedMethodError.""" + super(UnexpectedBodyError, self).__init__( + 'Expected: [%s] - Provided: [%s]' % (expected, provided)) diff --git a/appengine/dashdemo-cached/googleapiclient/http.py b/appengine/dashdemo-cached/googleapiclient/http.py new file mode 100644 index 0000000..2b65348 --- /dev/null +++ b/appengine/dashdemo-cached/googleapiclient/http.py @@ -0,0 +1,1609 @@ +# Copyright (C) 2012 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Classes to encapsulate a single HTTP request. + +The classes implement a command pattern, with every +object supporting an execute() method that does the +actuall HTTP request. +""" + +__author__ = 'jcgregorio@google.com (Joe Gregorio)' + +import StringIO +import base64 +import copy +import gzip +import httplib2 +import logging +import mimeparse +import mimetypes +import os +import random +import sys +import time +import urllib +import urlparse +import uuid + +from email.generator import Generator +from email.mime.multipart import MIMEMultipart +from email.mime.nonmultipart import MIMENonMultipart +from email.parser import FeedParser +from errors import BatchError +from errors import HttpError +from errors import InvalidChunkSizeError +from errors import ResumableUploadError +from errors import UnexpectedBodyError +from errors import UnexpectedMethodError +from model import JsonModel +from oauth2client import util +from oauth2client.anyjson import simplejson + + +DEFAULT_CHUNK_SIZE = 512*1024 + +MAX_URI_LENGTH = 2048 + + +class MediaUploadProgress(object): + """Status of a resumable upload.""" + + def __init__(self, resumable_progress, total_size): + """Constructor. + + Args: + resumable_progress: int, bytes sent so far. + total_size: int, total bytes in complete upload, or None if the total + upload size isn't known ahead of time. + """ + self.resumable_progress = resumable_progress + self.total_size = total_size + + def progress(self): + """Percent of upload completed, as a float. + + Returns: + the percentage complete as a float, returning 0.0 if the total size of + the upload is unknown. + """ + if self.total_size is not None: + return float(self.resumable_progress) / float(self.total_size) + else: + return 0.0 + + +class MediaDownloadProgress(object): + """Status of a resumable download.""" + + def __init__(self, resumable_progress, total_size): + """Constructor. + + Args: + resumable_progress: int, bytes received so far. + total_size: int, total bytes in complete download. + """ + self.resumable_progress = resumable_progress + self.total_size = total_size + + def progress(self): + """Percent of download completed, as a float. + + Returns: + the percentage complete as a float, returning 0.0 if the total size of + the download is unknown. + """ + if self.total_size is not None: + return float(self.resumable_progress) / float(self.total_size) + else: + return 0.0 + + +class MediaUpload(object): + """Describes a media object to upload. + + Base class that defines the interface of MediaUpload subclasses. + + Note that subclasses of MediaUpload may allow you to control the chunksize + when uploading a media object. It is important to keep the size of the chunk + as large as possible to keep the upload efficient. Other factors may influence + the size of the chunk you use, particularly if you are working in an + environment where individual HTTP requests may have a hardcoded time limit, + such as under certain classes of requests under Google App Engine. + + Streams are io.Base compatible objects that support seek(). Some MediaUpload + subclasses support using streams directly to upload data. Support for + streaming may be indicated by a MediaUpload sub-class and if appropriate for a + platform that stream will be used for uploading the media object. The support + for streaming is indicated by has_stream() returning True. The stream() method + should return an io.Base object that supports seek(). On platforms where the + underlying httplib module supports streaming, for example Python 2.6 and + later, the stream will be passed into the http library which will result in + less memory being used and possibly faster uploads. + + If you need to upload media that can't be uploaded using any of the existing + MediaUpload sub-class then you can sub-class MediaUpload for your particular + needs. + """ + + def chunksize(self): + """Chunk size for resumable uploads. + + Returns: + Chunk size in bytes. + """ + raise NotImplementedError() + + def mimetype(self): + """Mime type of the body. + + Returns: + Mime type. + """ + return 'application/octet-stream' + + def size(self): + """Size of upload. + + Returns: + Size of the body, or None of the size is unknown. + """ + return None + + def resumable(self): + """Whether this upload is resumable. + + Returns: + True if resumable upload or False. + """ + return False + + def getbytes(self, begin, end): + """Get bytes from the media. + + Args: + begin: int, offset from beginning of file. + length: int, number of bytes to read, starting at begin. + + Returns: + A string of bytes read. May be shorter than length if EOF was reached + first. + """ + raise NotImplementedError() + + def has_stream(self): + """Does the underlying upload support a streaming interface. + + Streaming means it is an io.IOBase subclass that supports seek, i.e. + seekable() returns True. + + Returns: + True if the call to stream() will return an instance of a seekable io.Base + subclass. + """ + return False + + def stream(self): + """A stream interface to the data being uploaded. + + Returns: + The returned value is an io.IOBase subclass that supports seek, i.e. + seekable() returns True. + """ + raise NotImplementedError() + + @util.positional(1) + def _to_json(self, strip=None): + """Utility function for creating a JSON representation of a MediaUpload. + + Args: + strip: array, An array of names of members to not include in the JSON. + + Returns: + string, a JSON representation of this instance, suitable to pass to + from_json(). + """ + t = type(self) + d = copy.copy(self.__dict__) + if strip is not None: + for member in strip: + del d[member] + d['_class'] = t.__name__ + d['_module'] = t.__module__ + return simplejson.dumps(d) + + def to_json(self): + """Create a JSON representation of an instance of MediaUpload. + + Returns: + string, a JSON representation of this instance, suitable to pass to + from_json(). + """ + return self._to_json() + + @classmethod + def new_from_json(cls, s): + """Utility class method to instantiate a MediaUpload subclass from a JSON + representation produced by to_json(). + + Args: + s: string, JSON from to_json(). + + Returns: + An instance of the subclass of MediaUpload that was serialized with + to_json(). + """ + data = simplejson.loads(s) + # Find and call the right classmethod from_json() to restore the object. + module = data['_module'] + m = __import__(module, fromlist=module.split('.')[:-1]) + kls = getattr(m, data['_class']) + from_json = getattr(kls, 'from_json') + return from_json(s) + + +class MediaIoBaseUpload(MediaUpload): + """A MediaUpload for a io.Base objects. + + Note that the Python file object is compatible with io.Base and can be used + with this class also. + + fh = io.BytesIO('...Some data to upload...') + media = MediaIoBaseUpload(fh, mimetype='image/png', + chunksize=1024*1024, resumable=True) + farm.animals().insert( + id='cow', + name='cow.png', + media_body=media).execute() + + Depending on the platform you are working on, you may pass -1 as the + chunksize, which indicates that the entire file should be uploaded in a single + request. If the underlying platform supports streams, such as Python 2.6 or + later, then this can be very efficient as it avoids multiple connections, and + also avoids loading the entire file into memory before sending it. Note that + Google App Engine has a 5MB limit on request size, so you should never set + your chunksize larger than 5MB, or to -1. + """ + + @util.positional(3) + def __init__(self, fd, mimetype, chunksize=DEFAULT_CHUNK_SIZE, + resumable=False): + """Constructor. + + Args: + fd: io.Base or file object, The source of the bytes to upload. MUST be + opened in blocking mode, do not use streams opened in non-blocking mode. + The given stream must be seekable, that is, it must be able to call + seek() on fd. + mimetype: string, Mime-type of the file. + chunksize: int, File will be uploaded in chunks of this many bytes. Only + used if resumable=True. Pass in a value of -1 if the file is to be + uploaded as a single chunk. Note that Google App Engine has a 5MB limit + on request size, so you should never set your chunksize larger than 5MB, + or to -1. + resumable: bool, True if this is a resumable upload. False means upload + in a single request. + """ + super(MediaIoBaseUpload, self).__init__() + self._fd = fd + self._mimetype = mimetype + if not (chunksize == -1 or chunksize > 0): + raise InvalidChunkSizeError() + self._chunksize = chunksize + self._resumable = resumable + + self._fd.seek(0, os.SEEK_END) + self._size = self._fd.tell() + + def chunksize(self): + """Chunk size for resumable uploads. + + Returns: + Chunk size in bytes. + """ + return self._chunksize + + def mimetype(self): + """Mime type of the body. + + Returns: + Mime type. + """ + return self._mimetype + + def size(self): + """Size of upload. + + Returns: + Size of the body, or None of the size is unknown. + """ + return self._size + + def resumable(self): + """Whether this upload is resumable. + + Returns: + True if resumable upload or False. + """ + return self._resumable + + def getbytes(self, begin, length): + """Get bytes from the media. + + Args: + begin: int, offset from beginning of file. + length: int, number of bytes to read, starting at begin. + + Returns: + A string of bytes read. May be shorted than length if EOF was reached + first. + """ + self._fd.seek(begin) + return self._fd.read(length) + + def has_stream(self): + """Does the underlying upload support a streaming interface. + + Streaming means it is an io.IOBase subclass that supports seek, i.e. + seekable() returns True. + + Returns: + True if the call to stream() will return an instance of a seekable io.Base + subclass. + """ + return True + + def stream(self): + """A stream interface to the data being uploaded. + + Returns: + The returned value is an io.IOBase subclass that supports seek, i.e. + seekable() returns True. + """ + return self._fd + + def to_json(self): + """This upload type is not serializable.""" + raise NotImplementedError('MediaIoBaseUpload is not serializable.') + + +class MediaFileUpload(MediaIoBaseUpload): + """A MediaUpload for a file. + + Construct a MediaFileUpload and pass as the media_body parameter of the + method. For example, if we had a service that allowed uploading images: + + + media = MediaFileUpload('cow.png', mimetype='image/png', + chunksize=1024*1024, resumable=True) + farm.animals().insert( + id='cow', + name='cow.png', + media_body=media).execute() + + Depending on the platform you are working on, you may pass -1 as the + chunksize, which indicates that the entire file should be uploaded in a single + request. If the underlying platform supports streams, such as Python 2.6 or + later, then this can be very efficient as it avoids multiple connections, and + also avoids loading the entire file into memory before sending it. Note that + Google App Engine has a 5MB limit on request size, so you should never set + your chunksize larger than 5MB, or to -1. + """ + + @util.positional(2) + def __init__(self, filename, mimetype=None, chunksize=DEFAULT_CHUNK_SIZE, + resumable=False): + """Constructor. + + Args: + filename: string, Name of the file. + mimetype: string, Mime-type of the file. If None then a mime-type will be + guessed from the file extension. + chunksize: int, File will be uploaded in chunks of this many bytes. Only + used if resumable=True. Pass in a value of -1 if the file is to be + uploaded in a single chunk. Note that Google App Engine has a 5MB limit + on request size, so you should never set your chunksize larger than 5MB, + or to -1. + resumable: bool, True if this is a resumable upload. False means upload + in a single request. + """ + self._filename = filename + fd = open(self._filename, 'rb') + if mimetype is None: + (mimetype, encoding) = mimetypes.guess_type(filename) + super(MediaFileUpload, self).__init__(fd, mimetype, chunksize=chunksize, + resumable=resumable) + + def to_json(self): + """Creating a JSON representation of an instance of MediaFileUpload. + + Returns: + string, a JSON representation of this instance, suitable to pass to + from_json(). + """ + return self._to_json(strip=['_fd']) + + @staticmethod + def from_json(s): + d = simplejson.loads(s) + return MediaFileUpload(d['_filename'], mimetype=d['_mimetype'], + chunksize=d['_chunksize'], resumable=d['_resumable']) + + +class MediaInMemoryUpload(MediaIoBaseUpload): + """MediaUpload for a chunk of bytes. + + DEPRECATED: Use MediaIoBaseUpload with either io.TextIOBase or StringIO for + the stream. + """ + + @util.positional(2) + def __init__(self, body, mimetype='application/octet-stream', + chunksize=DEFAULT_CHUNK_SIZE, resumable=False): + """Create a new MediaInMemoryUpload. + + DEPRECATED: Use MediaIoBaseUpload with either io.TextIOBase or StringIO for + the stream. + + Args: + body: string, Bytes of body content. + mimetype: string, Mime-type of the file or default of + 'application/octet-stream'. + chunksize: int, File will be uploaded in chunks of this many bytes. Only + used if resumable=True. + resumable: bool, True if this is a resumable upload. False means upload + in a single request. + """ + fd = StringIO.StringIO(body) + super(MediaInMemoryUpload, self).__init__(fd, mimetype, chunksize=chunksize, + resumable=resumable) + + +class MediaIoBaseDownload(object): + """"Download media resources. + + Note that the Python file object is compatible with io.Base and can be used + with this class also. + + + Example: + request = farms.animals().get_media(id='cow') + fh = io.FileIO('cow.png', mode='wb') + downloader = MediaIoBaseDownload(fh, request, chunksize=1024*1024) + + done = False + while done is False: + status, done = downloader.next_chunk() + if status: + print "Download %d%%." % int(status.progress() * 100) + print "Download Complete!" + """ + + @util.positional(3) + def __init__(self, fd, request, chunksize=DEFAULT_CHUNK_SIZE): + """Constructor. + + Args: + fd: io.Base or file object, The stream in which to write the downloaded + bytes. + request: googleapiclient.http.HttpRequest, the media request to perform in + chunks. + chunksize: int, File will be downloaded in chunks of this many bytes. + """ + self._fd = fd + self._request = request + self._uri = request.uri + self._chunksize = chunksize + self._progress = 0 + self._total_size = None + self._done = False + + # Stubs for testing. + self._sleep = time.sleep + self._rand = random.random + + @util.positional(1) + def next_chunk(self, num_retries=0): + """Get the next chunk of the download. + + Args: + num_retries: Integer, number of times to retry 500's with randomized + exponential backoff. If all retries fail, the raised HttpError + represents the last request. If zero (default), we attempt the + request only once. + + Returns: + (status, done): (MediaDownloadStatus, boolean) + The value of 'done' will be True when the media has been fully + downloaded. + + Raises: + googleapiclient.errors.HttpError if the response was not a 2xx. + httplib2.HttpLib2Error if a transport error has occured. + """ + headers = { + 'range': 'bytes=%d-%d' % ( + self._progress, self._progress + self._chunksize) + } + http = self._request.http + + for retry_num in xrange(num_retries + 1): + if retry_num > 0: + self._sleep(self._rand() * 2**retry_num) + logging.warning( + 'Retry #%d for media download: GET %s, following status: %d' + % (retry_num, self._uri, resp.status)) + + resp, content = http.request(self._uri, headers=headers) + if resp.status < 500: + break + + if resp.status in [200, 206]: + if 'content-location' in resp and resp['content-location'] != self._uri: + self._uri = resp['content-location'] + self._progress += len(content) + self._fd.write(content) + + if 'content-range' in resp: + content_range = resp['content-range'] + length = content_range.rsplit('/', 1)[1] + self._total_size = int(length) + + if self._progress == self._total_size: + self._done = True + return MediaDownloadProgress(self._progress, self._total_size), self._done + else: + raise HttpError(resp, content, uri=self._uri) + + +class _StreamSlice(object): + """Truncated stream. + + Takes a stream and presents a stream that is a slice of the original stream. + This is used when uploading media in chunks. In later versions of Python a + stream can be passed to httplib in place of the string of data to send. The + problem is that httplib just blindly reads to the end of the stream. This + wrapper presents a virtual stream that only reads to the end of the chunk. + """ + + def __init__(self, stream, begin, chunksize): + """Constructor. + + Args: + stream: (io.Base, file object), the stream to wrap. + begin: int, the seek position the chunk begins at. + chunksize: int, the size of the chunk. + """ + self._stream = stream + self._begin = begin + self._chunksize = chunksize + self._stream.seek(begin) + + def read(self, n=-1): + """Read n bytes. + + Args: + n, int, the number of bytes to read. + + Returns: + A string of length 'n', or less if EOF is reached. + """ + # The data left available to read sits in [cur, end) + cur = self._stream.tell() + end = self._begin + self._chunksize + if n == -1 or cur + n > end: + n = end - cur + return self._stream.read(n) + + +class HttpRequest(object): + """Encapsulates a single HTTP request.""" + + @util.positional(4) + def __init__(self, http, postproc, uri, + method='GET', + body=None, + headers=None, + methodId=None, + resumable=None): + """Constructor for an HttpRequest. + + Args: + http: httplib2.Http, the transport object to use to make a request + postproc: callable, called on the HTTP response and content to transform + it into a data object before returning, or raising an exception + on an error. + uri: string, the absolute URI to send the request to + method: string, the HTTP method to use + body: string, the request body of the HTTP request, + headers: dict, the HTTP request headers + methodId: string, a unique identifier for the API method being called. + resumable: MediaUpload, None if this is not a resumbale request. + """ + self.uri = uri + self.method = method + self.body = body + self.headers = headers or {} + self.methodId = methodId + self.http = http + self.postproc = postproc + self.resumable = resumable + self.response_callbacks = [] + self._in_error_state = False + + # Pull the multipart boundary out of the content-type header. + major, minor, params = mimeparse.parse_mime_type( + headers.get('content-type', 'application/json')) + + # The size of the non-media part of the request. + self.body_size = len(self.body or '') + + # The resumable URI to send chunks to. + self.resumable_uri = None + + # The bytes that have been uploaded. + self.resumable_progress = 0 + + # Stubs for testing. + self._rand = random.random + self._sleep = time.sleep + + @util.positional(1) + def execute(self, http=None, num_retries=0): + """Execute the request. + + Args: + http: httplib2.Http, an http object to be used in place of the + one the HttpRequest request object was constructed with. + num_retries: Integer, number of times to retry 500's with randomized + exponential backoff. If all retries fail, the raised HttpError + represents the last request. If zero (default), we attempt the + request only once. + + Returns: + A deserialized object model of the response body as determined + by the postproc. + + Raises: + googleapiclient.errors.HttpError if the response was not a 2xx. + httplib2.HttpLib2Error if a transport error has occured. + """ + if http is None: + http = self.http + + if self.resumable: + body = None + while body is None: + _, body = self.next_chunk(http=http, num_retries=num_retries) + return body + + # Non-resumable case. + + if 'content-length' not in self.headers: + self.headers['content-length'] = str(self.body_size) + # If the request URI is too long then turn it into a POST request. + if len(self.uri) > MAX_URI_LENGTH and self.method == 'GET': + self.method = 'POST' + self.headers['x-http-method-override'] = 'GET' + self.headers['content-type'] = 'application/x-www-form-urlencoded' + parsed = urlparse.urlparse(self.uri) + self.uri = urlparse.urlunparse( + (parsed.scheme, parsed.netloc, parsed.path, parsed.params, None, + None) + ) + self.body = parsed.query + self.headers['content-length'] = str(len(self.body)) + + # Handle retries for server-side errors. + for retry_num in xrange(num_retries + 1): + if retry_num > 0: + self._sleep(self._rand() * 2**retry_num) + logging.warning('Retry #%d for request: %s %s, following status: %d' + % (retry_num, self.method, self.uri, resp.status)) + + resp, content = http.request(str(self.uri), method=str(self.method), + body=self.body, headers=self.headers) + if resp.status < 500: + break + + for callback in self.response_callbacks: + callback(resp) + if resp.status >= 300: + raise HttpError(resp, content, uri=self.uri) + return self.postproc(resp, content) + + @util.positional(2) + def add_response_callback(self, cb): + """add_response_headers_callback + + Args: + cb: Callback to be called on receiving the response headers, of signature: + + def cb(resp): + # Where resp is an instance of httplib2.Response + """ + self.response_callbacks.append(cb) + + @util.positional(1) + def next_chunk(self, http=None, num_retries=0): + """Execute the next step of a resumable upload. + + Can only be used if the method being executed supports media uploads and + the MediaUpload object passed in was flagged as using resumable upload. + + Example: + + media = MediaFileUpload('cow.png', mimetype='image/png', + chunksize=1000, resumable=True) + request = farm.animals().insert( + id='cow', + name='cow.png', + media_body=media) + + response = None + while response is None: + status, response = request.next_chunk() + if status: + print "Upload %d%% complete." % int(status.progress() * 100) + + + Args: + http: httplib2.Http, an http object to be used in place of the + one the HttpRequest request object was constructed with. + num_retries: Integer, number of times to retry 500's with randomized + exponential backoff. If all retries fail, the raised HttpError + represents the last request. If zero (default), we attempt the + request only once. + + Returns: + (status, body): (ResumableMediaStatus, object) + The body will be None until the resumable media is fully uploaded. + + Raises: + googleapiclient.errors.HttpError if the response was not a 2xx. + httplib2.HttpLib2Error if a transport error has occured. + """ + if http is None: + http = self.http + + if self.resumable.size() is None: + size = '*' + else: + size = str(self.resumable.size()) + + if self.resumable_uri is None: + start_headers = copy.copy(self.headers) + start_headers['X-Upload-Content-Type'] = self.resumable.mimetype() + if size != '*': + start_headers['X-Upload-Content-Length'] = size + start_headers['content-length'] = str(self.body_size) + + for retry_num in xrange(num_retries + 1): + if retry_num > 0: + self._sleep(self._rand() * 2**retry_num) + logging.warning( + 'Retry #%d for resumable URI request: %s %s, following status: %d' + % (retry_num, self.method, self.uri, resp.status)) + + resp, content = http.request(self.uri, method=self.method, + body=self.body, + headers=start_headers) + if resp.status < 500: + break + + if resp.status == 200 and 'location' in resp: + self.resumable_uri = resp['location'] + else: + raise ResumableUploadError(resp, content) + elif self._in_error_state: + # If we are in an error state then query the server for current state of + # the upload by sending an empty PUT and reading the 'range' header in + # the response. + headers = { + 'Content-Range': 'bytes */%s' % size, + 'content-length': '0' + } + resp, content = http.request(self.resumable_uri, 'PUT', + headers=headers) + status, body = self._process_response(resp, content) + if body: + # The upload was complete. + return (status, body) + + # The httplib.request method can take streams for the body parameter, but + # only in Python 2.6 or later. If a stream is available under those + # conditions then use it as the body argument. + if self.resumable.has_stream() and sys.version_info[1] >= 6: + data = self.resumable.stream() + if self.resumable.chunksize() == -1: + data.seek(self.resumable_progress) + chunk_end = self.resumable.size() - self.resumable_progress - 1 + else: + # Doing chunking with a stream, so wrap a slice of the stream. + data = _StreamSlice(data, self.resumable_progress, + self.resumable.chunksize()) + chunk_end = min( + self.resumable_progress + self.resumable.chunksize() - 1, + self.resumable.size() - 1) + else: + data = self.resumable.getbytes( + self.resumable_progress, self.resumable.chunksize()) + + # A short read implies that we are at EOF, so finish the upload. + if len(data) < self.resumable.chunksize(): + size = str(self.resumable_progress + len(data)) + + chunk_end = self.resumable_progress + len(data) - 1 + + headers = { + 'Content-Range': 'bytes %d-%d/%s' % ( + self.resumable_progress, chunk_end, size), + # Must set the content-length header here because httplib can't + # calculate the size when working with _StreamSlice. + 'Content-Length': str(chunk_end - self.resumable_progress + 1) + } + + for retry_num in xrange(num_retries + 1): + if retry_num > 0: + self._sleep(self._rand() * 2**retry_num) + logging.warning( + 'Retry #%d for media upload: %s %s, following status: %d' + % (retry_num, self.method, self.uri, resp.status)) + + try: + resp, content = http.request(self.resumable_uri, method='PUT', + body=data, + headers=headers) + except: + self._in_error_state = True + raise + if resp.status < 500: + break + + return self._process_response(resp, content) + + def _process_response(self, resp, content): + """Process the response from a single chunk upload. + + Args: + resp: httplib2.Response, the response object. + content: string, the content of the response. + + Returns: + (status, body): (ResumableMediaStatus, object) + The body will be None until the resumable media is fully uploaded. + + Raises: + googleapiclient.errors.HttpError if the response was not a 2xx or a 308. + """ + if resp.status in [200, 201]: + self._in_error_state = False + return None, self.postproc(resp, content) + elif resp.status == 308: + self._in_error_state = False + # A "308 Resume Incomplete" indicates we are not done. + self.resumable_progress = int(resp['range'].split('-')[1]) + 1 + if 'location' in resp: + self.resumable_uri = resp['location'] + else: + self._in_error_state = True + raise HttpError(resp, content, uri=self.uri) + + return (MediaUploadProgress(self.resumable_progress, self.resumable.size()), + None) + + def to_json(self): + """Returns a JSON representation of the HttpRequest.""" + d = copy.copy(self.__dict__) + if d['resumable'] is not None: + d['resumable'] = self.resumable.to_json() + del d['http'] + del d['postproc'] + del d['_sleep'] + del d['_rand'] + + return simplejson.dumps(d) + + @staticmethod + def from_json(s, http, postproc): + """Returns an HttpRequest populated with info from a JSON object.""" + d = simplejson.loads(s) + if d['resumable'] is not None: + d['resumable'] = MediaUpload.new_from_json(d['resumable']) + return HttpRequest( + http, + postproc, + uri=d['uri'], + method=d['method'], + body=d['body'], + headers=d['headers'], + methodId=d['methodId'], + resumable=d['resumable']) + + +class BatchHttpRequest(object): + """Batches multiple HttpRequest objects into a single HTTP request. + + Example: + from googleapiclient.http import BatchHttpRequest + + def list_animals(request_id, response, exception): + \"\"\"Do something with the animals list response.\"\"\" + if exception is not None: + # Do something with the exception. + pass + else: + # Do something with the response. + pass + + def list_farmers(request_id, response, exception): + \"\"\"Do something with the farmers list response.\"\"\" + if exception is not None: + # Do something with the exception. + pass + else: + # Do something with the response. + pass + + service = build('farm', 'v2') + + batch = BatchHttpRequest() + + batch.add(service.animals().list(), list_animals) + batch.add(service.farmers().list(), list_farmers) + batch.execute(http=http) + """ + + @util.positional(1) + def __init__(self, callback=None, batch_uri=None): + """Constructor for a BatchHttpRequest. + + Args: + callback: callable, A callback to be called for each response, of the + form callback(id, response, exception). The first parameter is the + request id, and the second is the deserialized response object. The + third is an googleapiclient.errors.HttpError exception object if an HTTP error + occurred while processing the request, or None if no error occurred. + batch_uri: string, URI to send batch requests to. + """ + if batch_uri is None: + batch_uri = 'https://www.googleapis.com/batch' + self._batch_uri = batch_uri + + # Global callback to be called for each individual response in the batch. + self._callback = callback + + # A map from id to request. + self._requests = {} + + # A map from id to callback. + self._callbacks = {} + + # List of request ids, in the order in which they were added. + self._order = [] + + # The last auto generated id. + self._last_auto_id = 0 + + # Unique ID on which to base the Content-ID headers. + self._base_id = None + + # A map from request id to (httplib2.Response, content) response pairs + self._responses = {} + + # A map of id(Credentials) that have been refreshed. + self._refreshed_credentials = {} + + def _refresh_and_apply_credentials(self, request, http): + """Refresh the credentials and apply to the request. + + Args: + request: HttpRequest, the request. + http: httplib2.Http, the global http object for the batch. + """ + # For the credentials to refresh, but only once per refresh_token + # If there is no http per the request then refresh the http passed in + # via execute() + creds = None + if request.http is not None and hasattr(request.http.request, + 'credentials'): + creds = request.http.request.credentials + elif http is not None and hasattr(http.request, 'credentials'): + creds = http.request.credentials + if creds is not None: + if id(creds) not in self._refreshed_credentials: + creds.refresh(http) + self._refreshed_credentials[id(creds)] = 1 + + # Only apply the credentials if we are using the http object passed in, + # otherwise apply() will get called during _serialize_request(). + if request.http is None or not hasattr(request.http.request, + 'credentials'): + creds.apply(request.headers) + + def _id_to_header(self, id_): + """Convert an id to a Content-ID header value. + + Args: + id_: string, identifier of individual request. + + Returns: + A Content-ID header with the id_ encoded into it. A UUID is prepended to + the value because Content-ID headers are supposed to be universally + unique. + """ + if self._base_id is None: + self._base_id = uuid.uuid4() + + return '<%s+%s>' % (self._base_id, urllib.quote(id_)) + + def _header_to_id(self, header): + """Convert a Content-ID header value to an id. + + Presumes the Content-ID header conforms to the format that _id_to_header() + returns. + + Args: + header: string, Content-ID header value. + + Returns: + The extracted id value. + + Raises: + BatchError if the header is not in the expected format. + """ + if header[0] != '<' or header[-1] != '>': + raise BatchError("Invalid value for Content-ID: %s" % header) + if '+' not in header: + raise BatchError("Invalid value for Content-ID: %s" % header) + base, id_ = header[1:-1].rsplit('+', 1) + + return urllib.unquote(id_) + + def _serialize_request(self, request): + """Convert an HttpRequest object into a string. + + Args: + request: HttpRequest, the request to serialize. + + Returns: + The request as a string in application/http format. + """ + # Construct status line + parsed = urlparse.urlparse(request.uri) + request_line = urlparse.urlunparse( + (None, None, parsed.path, parsed.params, parsed.query, None) + ) + status_line = request.method + ' ' + request_line + ' HTTP/1.1\n' + major, minor = request.headers.get('content-type', 'application/json').split('/') + msg = MIMENonMultipart(major, minor) + headers = request.headers.copy() + + if request.http is not None and hasattr(request.http.request, + 'credentials'): + request.http.request.credentials.apply(headers) + + # MIMENonMultipart adds its own Content-Type header. + if 'content-type' in headers: + del headers['content-type'] + + for key, value in headers.iteritems(): + msg[key] = value + msg['Host'] = parsed.netloc + msg.set_unixfrom(None) + + if request.body is not None: + msg.set_payload(request.body) + msg['content-length'] = str(len(request.body)) + + # Serialize the mime message. + fp = StringIO.StringIO() + # maxheaderlen=0 means don't line wrap headers. + g = Generator(fp, maxheaderlen=0) + g.flatten(msg, unixfrom=False) + body = fp.getvalue() + + # Strip off the \n\n that the MIME lib tacks onto the end of the payload. + if request.body is None: + body = body[:-2] + + return status_line.encode('utf-8') + body + + def _deserialize_response(self, payload): + """Convert string into httplib2 response and content. + + Args: + payload: string, headers and body as a string. + + Returns: + A pair (resp, content), such as would be returned from httplib2.request. + """ + # Strip off the status line + status_line, payload = payload.split('\n', 1) + protocol, status, reason = status_line.split(' ', 2) + + # Parse the rest of the response + parser = FeedParser() + parser.feed(payload) + msg = parser.close() + msg['status'] = status + + # Create httplib2.Response from the parsed headers. + resp = httplib2.Response(msg) + resp.reason = reason + resp.version = int(protocol.split('/', 1)[1].replace('.', '')) + + content = payload.split('\r\n\r\n', 1)[1] + + return resp, content + + def _new_id(self): + """Create a new id. + + Auto incrementing number that avoids conflicts with ids already used. + + Returns: + string, a new unique id. + """ + self._last_auto_id += 1 + while str(self._last_auto_id) in self._requests: + self._last_auto_id += 1 + return str(self._last_auto_id) + + @util.positional(2) + def add(self, request, callback=None, request_id=None): + """Add a new request. + + Every callback added will be paired with a unique id, the request_id. That + unique id will be passed back to the callback when the response comes back + from the server. The default behavior is to have the library generate it's + own unique id. If the caller passes in a request_id then they must ensure + uniqueness for each request_id, and if they are not an exception is + raised. Callers should either supply all request_ids or nevery supply a + request id, to avoid such an error. + + Args: + request: HttpRequest, Request to add to the batch. + callback: callable, A callback to be called for this response, of the + form callback(id, response, exception). The first parameter is the + request id, and the second is the deserialized response object. The + third is an googleapiclient.errors.HttpError exception object if an HTTP error + occurred while processing the request, or None if no errors occurred. + request_id: string, A unique id for the request. The id will be passed to + the callback with the response. + + Returns: + None + + Raises: + BatchError if a media request is added to a batch. + KeyError is the request_id is not unique. + """ + if request_id is None: + request_id = self._new_id() + if request.resumable is not None: + raise BatchError("Media requests cannot be used in a batch request.") + if request_id in self._requests: + raise KeyError("A request with this ID already exists: %s" % request_id) + self._requests[request_id] = request + self._callbacks[request_id] = callback + self._order.append(request_id) + + def _execute(self, http, order, requests): + """Serialize batch request, send to server, process response. + + Args: + http: httplib2.Http, an http object to be used to make the request with. + order: list, list of request ids in the order they were added to the + batch. + request: list, list of request objects to send. + + Raises: + httplib2.HttpLib2Error if a transport error has occured. + googleapiclient.errors.BatchError if the response is the wrong format. + """ + message = MIMEMultipart('mixed') + # Message should not write out it's own headers. + setattr(message, '_write_headers', lambda self: None) + + # Add all the individual requests. + for request_id in order: + request = requests[request_id] + + msg = MIMENonMultipart('application', 'http') + msg['Content-Transfer-Encoding'] = 'binary' + msg['Content-ID'] = self._id_to_header(request_id) + + body = self._serialize_request(request) + msg.set_payload(body) + message.attach(msg) + + body = message.as_string() + + headers = {} + headers['content-type'] = ('multipart/mixed; ' + 'boundary="%s"') % message.get_boundary() + + resp, content = http.request(self._batch_uri, method='POST', body=body, + headers=headers) + + if resp.status >= 300: + raise HttpError(resp, content, uri=self._batch_uri) + + # Now break out the individual responses and store each one. + boundary, _ = content.split(None, 1) + + # Prepend with a content-type header so FeedParser can handle it. + header = 'content-type: %s\r\n\r\n' % resp['content-type'] + for_parser = header + content + + parser = FeedParser() + parser.feed(for_parser) + mime_response = parser.close() + + if not mime_response.is_multipart(): + raise BatchError("Response not in multipart/mixed format.", resp=resp, + content=content) + + for part in mime_response.get_payload(): + request_id = self._header_to_id(part['Content-ID']) + response, content = self._deserialize_response(part.get_payload()) + self._responses[request_id] = (response, content) + + @util.positional(1) + def execute(self, http=None): + """Execute all the requests as a single batched HTTP request. + + Args: + http: httplib2.Http, an http object to be used in place of the one the + HttpRequest request object was constructed with. If one isn't supplied + then use a http object from the requests in this batch. + + Returns: + None + + Raises: + httplib2.HttpLib2Error if a transport error has occured. + googleapiclient.errors.BatchError if the response is the wrong format. + """ + + # If http is not supplied use the first valid one given in the requests. + if http is None: + for request_id in self._order: + request = self._requests[request_id] + if request is not None: + http = request.http + break + + if http is None: + raise ValueError("Missing a valid http object.") + + self._execute(http, self._order, self._requests) + + # Loop over all the requests and check for 401s. For each 401 request the + # credentials should be refreshed and then sent again in a separate batch. + redo_requests = {} + redo_order = [] + + for request_id in self._order: + resp, content = self._responses[request_id] + if resp['status'] == '401': + redo_order.append(request_id) + request = self._requests[request_id] + self._refresh_and_apply_credentials(request, http) + redo_requests[request_id] = request + + if redo_requests: + self._execute(http, redo_order, redo_requests) + + # Now process all callbacks that are erroring, and raise an exception for + # ones that return a non-2xx response? Or add extra parameter to callback + # that contains an HttpError? + + for request_id in self._order: + resp, content = self._responses[request_id] + + request = self._requests[request_id] + callback = self._callbacks[request_id] + + response = None + exception = None + try: + if resp.status >= 300: + raise HttpError(resp, content, uri=request.uri) + response = request.postproc(resp, content) + except HttpError, e: + exception = e + + if callback is not None: + callback(request_id, response, exception) + if self._callback is not None: + self._callback(request_id, response, exception) + + +class HttpRequestMock(object): + """Mock of HttpRequest. + + Do not construct directly, instead use RequestMockBuilder. + """ + + def __init__(self, resp, content, postproc): + """Constructor for HttpRequestMock + + Args: + resp: httplib2.Response, the response to emulate coming from the request + content: string, the response body + postproc: callable, the post processing function usually supplied by + the model class. See model.JsonModel.response() as an example. + """ + self.resp = resp + self.content = content + self.postproc = postproc + if resp is None: + self.resp = httplib2.Response({'status': 200, 'reason': 'OK'}) + if 'reason' in self.resp: + self.resp.reason = self.resp['reason'] + + def execute(self, http=None): + """Execute the request. + + Same behavior as HttpRequest.execute(), but the response is + mocked and not really from an HTTP request/response. + """ + return self.postproc(self.resp, self.content) + + +class RequestMockBuilder(object): + """A simple mock of HttpRequest + + Pass in a dictionary to the constructor that maps request methodIds to + tuples of (httplib2.Response, content, opt_expected_body) that should be + returned when that method is called. None may also be passed in for the + httplib2.Response, in which case a 200 OK response will be generated. + If an opt_expected_body (str or dict) is provided, it will be compared to + the body and UnexpectedBodyError will be raised on inequality. + + Example: + response = '{"data": {"id": "tag:google.c...' + requestBuilder = RequestMockBuilder( + { + 'plus.activities.get': (None, response), + } + ) + googleapiclient.discovery.build("plus", "v1", requestBuilder=requestBuilder) + + Methods that you do not supply a response for will return a + 200 OK with an empty string as the response content or raise an excpetion + if check_unexpected is set to True. The methodId is taken from the rpcName + in the discovery document. + + For more details see the project wiki. + """ + + def __init__(self, responses, check_unexpected=False): + """Constructor for RequestMockBuilder + + The constructed object should be a callable object + that can replace the class HttpResponse. + + responses - A dictionary that maps methodIds into tuples + of (httplib2.Response, content). The methodId + comes from the 'rpcName' field in the discovery + document. + check_unexpected - A boolean setting whether or not UnexpectedMethodError + should be raised on unsupplied method. + """ + self.responses = responses + self.check_unexpected = check_unexpected + + def __call__(self, http, postproc, uri, method='GET', body=None, + headers=None, methodId=None, resumable=None): + """Implements the callable interface that discovery.build() expects + of requestBuilder, which is to build an object compatible with + HttpRequest.execute(). See that method for the description of the + parameters and the expected response. + """ + if methodId in self.responses: + response = self.responses[methodId] + resp, content = response[:2] + if len(response) > 2: + # Test the body against the supplied expected_body. + expected_body = response[2] + if bool(expected_body) != bool(body): + # Not expecting a body and provided one + # or expecting a body and not provided one. + raise UnexpectedBodyError(expected_body, body) + if isinstance(expected_body, str): + expected_body = simplejson.loads(expected_body) + body = simplejson.loads(body) + if body != expected_body: + raise UnexpectedBodyError(expected_body, body) + return HttpRequestMock(resp, content, postproc) + elif self.check_unexpected: + raise UnexpectedMethodError(methodId=methodId) + else: + model = JsonModel(False) + return HttpRequestMock(None, '{}', model.response) + + +class HttpMock(object): + """Mock of httplib2.Http""" + + def __init__(self, filename=None, headers=None): + """ + Args: + filename: string, absolute filename to read response from + headers: dict, header to return with response + """ + if headers is None: + headers = {'status': '200 OK'} + if filename: + f = file(filename, 'r') + self.data = f.read() + f.close() + else: + self.data = None + self.response_headers = headers + self.headers = None + self.uri = None + self.method = None + self.body = None + self.headers = None + + + def request(self, uri, + method='GET', + body=None, + headers=None, + redirections=1, + connection_type=None): + self.uri = uri + self.method = method + self.body = body + self.headers = headers + return httplib2.Response(self.response_headers), self.data + + +class HttpMockSequence(object): + """Mock of httplib2.Http + + Mocks a sequence of calls to request returning different responses for each + call. Create an instance initialized with the desired response headers + and content and then use as if an httplib2.Http instance. + + http = HttpMockSequence([ + ({'status': '401'}, ''), + ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'), + ({'status': '200'}, 'echo_request_headers'), + ]) + resp, content = http.request("http://examples.com") + + There are special values you can pass in for content to trigger + behavours that are helpful in testing. + + 'echo_request_headers' means return the request headers in the response body + 'echo_request_headers_as_json' means return the request headers in + the response body + 'echo_request_body' means return the request body in the response body + 'echo_request_uri' means return the request uri in the response body + """ + + def __init__(self, iterable): + """ + Args: + iterable: iterable, a sequence of pairs of (headers, body) + """ + self._iterable = iterable + self.follow_redirects = True + + def request(self, uri, + method='GET', + body=None, + headers=None, + redirections=1, + connection_type=None): + resp, content = self._iterable.pop(0) + if content == 'echo_request_headers': + content = headers + elif content == 'echo_request_headers_as_json': + content = simplejson.dumps(headers) + elif content == 'echo_request_body': + if hasattr(body, 'read'): + content = body.read() + else: + content = body + elif content == 'echo_request_uri': + content = uri + return httplib2.Response(resp), content + + +def set_user_agent(http, user_agent): + """Set the user-agent on every request. + + Args: + http - An instance of httplib2.Http + or something that acts like it. + user_agent: string, the value for the user-agent header. + + Returns: + A modified instance of http that was passed in. + + Example: + + h = httplib2.Http() + h = set_user_agent(h, "my-app-name/6.0") + + Most of the time the user-agent will be set doing auth, this is for the rare + cases where you are accessing an unauthenticated endpoint. + """ + request_orig = http.request + + # The closure that will replace 'httplib2.Http.request'. + def new_request(uri, method='GET', body=None, headers=None, + redirections=httplib2.DEFAULT_MAX_REDIRECTS, + connection_type=None): + """Modify the request headers to add the user-agent.""" + if headers is None: + headers = {} + if 'user-agent' in headers: + headers['user-agent'] = user_agent + ' ' + headers['user-agent'] + else: + headers['user-agent'] = user_agent + resp, content = request_orig(uri, method, body, headers, + redirections, connection_type) + return resp, content + + http.request = new_request + return http + + +def tunnel_patch(http): + """Tunnel PATCH requests over POST. + Args: + http - An instance of httplib2.Http + or something that acts like it. + + Returns: + A modified instance of http that was passed in. + + Example: + + h = httplib2.Http() + h = tunnel_patch(h, "my-app-name/6.0") + + Useful if you are running on a platform that doesn't support PATCH. + Apply this last if you are using OAuth 1.0, as changing the method + will result in a different signature. + """ + request_orig = http.request + + # The closure that will replace 'httplib2.Http.request'. + def new_request(uri, method='GET', body=None, headers=None, + redirections=httplib2.DEFAULT_MAX_REDIRECTS, + connection_type=None): + """Modify the request headers to add the user-agent.""" + if headers is None: + headers = {} + if method == 'PATCH': + if 'oauth_token' in headers.get('authorization', ''): + logging.warning( + 'OAuth 1.0 request made with Credentials after tunnel_patch.') + headers['x-http-method-override'] = "PATCH" + method = 'POST' + resp, content = request_orig(uri, method, body, headers, + redirections, connection_type) + return resp, content + + http.request = new_request + return http diff --git a/appengine/dashdemo-cached/googleapiclient/mimeparse.py b/appengine/dashdemo-cached/googleapiclient/mimeparse.py new file mode 100644 index 0000000..cbb9d07 --- /dev/null +++ b/appengine/dashdemo-cached/googleapiclient/mimeparse.py @@ -0,0 +1,172 @@ +# Copyright (C) 2007 Joe Gregorio +# +# Licensed under the MIT License + +"""MIME-Type Parser + +This module provides basic functions for handling mime-types. It can handle +matching mime-types against a list of media-ranges. See section 14.1 of the +HTTP specification [RFC 2616] for a complete explanation. + + http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1 + +Contents: + - parse_mime_type(): Parses a mime-type into its component parts. + - parse_media_range(): Media-ranges are mime-types with wild-cards and a 'q' + quality parameter. + - quality(): Determines the quality ('q') of a mime-type when + compared against a list of media-ranges. + - quality_parsed(): Just like quality() except the second parameter must be + pre-parsed. + - best_match(): Choose the mime-type with the highest quality ('q') + from a list of candidates. +""" + +__version__ = '0.1.3' +__author__ = 'Joe Gregorio' +__email__ = 'joe@bitworking.org' +__license__ = 'MIT License' +__credits__ = '' + + +def parse_mime_type(mime_type): + """Parses a mime-type into its component parts. + + Carves up a mime-type and returns a tuple of the (type, subtype, params) + where 'params' is a dictionary of all the parameters for the media range. + For example, the media range 'application/xhtml;q=0.5' would get parsed + into: + + ('application', 'xhtml', {'q', '0.5'}) + """ + parts = mime_type.split(';') + params = dict([tuple([s.strip() for s in param.split('=', 1)])\ + for param in parts[1:] + ]) + full_type = parts[0].strip() + # Java URLConnection class sends an Accept header that includes a + # single '*'. Turn it into a legal wildcard. + if full_type == '*': + full_type = '*/*' + (type, subtype) = full_type.split('/') + + return (type.strip(), subtype.strip(), params) + + +def parse_media_range(range): + """Parse a media-range into its component parts. + + Carves up a media range and returns a tuple of the (type, subtype, + params) where 'params' is a dictionary of all the parameters for the media + range. For example, the media range 'application/*;q=0.5' would get parsed + into: + + ('application', '*', {'q', '0.5'}) + + In addition this function also guarantees that there is a value for 'q' + in the params dictionary, filling it in with a proper default if + necessary. + """ + (type, subtype, params) = parse_mime_type(range) + if not params.has_key('q') or not params['q'] or \ + not float(params['q']) or float(params['q']) > 1\ + or float(params['q']) < 0: + params['q'] = '1' + + return (type, subtype, params) + + +def fitness_and_quality_parsed(mime_type, parsed_ranges): + """Find the best match for a mime-type amongst parsed media-ranges. + + Find the best match for a given mime-type against a list of media_ranges + that have already been parsed by parse_media_range(). Returns a tuple of + the fitness value and the value of the 'q' quality parameter of the best + match, or (-1, 0) if no match was found. Just as for quality_parsed(), + 'parsed_ranges' must be a list of parsed media ranges. + """ + best_fitness = -1 + best_fit_q = 0 + (target_type, target_subtype, target_params) =\ + parse_media_range(mime_type) + for (type, subtype, params) in parsed_ranges: + type_match = (type == target_type or\ + type == '*' or\ + target_type == '*') + subtype_match = (subtype == target_subtype or\ + subtype == '*' or\ + target_subtype == '*') + if type_match and subtype_match: + param_matches = reduce(lambda x, y: x + y, [1 for (key, value) in \ + target_params.iteritems() if key != 'q' and \ + params.has_key(key) and value == params[key]], 0) + fitness = (type == target_type) and 100 or 0 + fitness += (subtype == target_subtype) and 10 or 0 + fitness += param_matches + if fitness > best_fitness: + best_fitness = fitness + best_fit_q = params['q'] + + return best_fitness, float(best_fit_q) + + +def quality_parsed(mime_type, parsed_ranges): + """Find the best match for a mime-type amongst parsed media-ranges. + + Find the best match for a given mime-type against a list of media_ranges + that have already been parsed by parse_media_range(). Returns the 'q' + quality parameter of the best match, 0 if no match was found. This function + bahaves the same as quality() except that 'parsed_ranges' must be a list of + parsed media ranges. + """ + + return fitness_and_quality_parsed(mime_type, parsed_ranges)[1] + + +def quality(mime_type, ranges): + """Return the quality ('q') of a mime-type against a list of media-ranges. + + Returns the quality 'q' of a mime-type when compared against the + media-ranges in ranges. For example: + + >>> quality('text/html','text/*;q=0.3, text/html;q=0.7, + text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5') + 0.7 + + """ + parsed_ranges = [parse_media_range(r) for r in ranges.split(',')] + + return quality_parsed(mime_type, parsed_ranges) + + +def best_match(supported, header): + """Return mime-type with the highest quality ('q') from list of candidates. + + Takes a list of supported mime-types and finds the best match for all the + media-ranges listed in header. The value of header must be a string that + conforms to the format of the HTTP Accept: header. The value of 'supported' + is a list of mime-types. The list of supported mime-types should be sorted + in order of increasing desirability, in case of a situation where there is + a tie. + + >>> best_match(['application/xbel+xml', 'text/xml'], + 'text/*;q=0.5,*/*; q=0.1') + 'text/xml' + """ + split_header = _filter_blank(header.split(',')) + parsed_header = [parse_media_range(r) for r in split_header] + weighted_matches = [] + pos = 0 + for mime_type in supported: + weighted_matches.append((fitness_and_quality_parsed(mime_type, + parsed_header), pos, mime_type)) + pos += 1 + weighted_matches.sort() + + return weighted_matches[-1][0][1] and weighted_matches[-1][2] or '' + + +def _filter_blank(i): + for s in i: + if s.strip(): + yield s diff --git a/appengine/dashdemo-cached/googleapiclient/model.py b/appengine/dashdemo-cached/googleapiclient/model.py new file mode 100644 index 0000000..566a233 --- /dev/null +++ b/appengine/dashdemo-cached/googleapiclient/model.py @@ -0,0 +1,383 @@ +#!/usr/bin/python2.4 +# +# Copyright (C) 2010 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Model objects for requests and responses. + +Each API may support one or more serializations, such +as JSON, Atom, etc. The model classes are responsible +for converting between the wire format and the Python +object representation. +""" + +__author__ = 'jcgregorio@google.com (Joe Gregorio)' + +import logging +import urllib + +from googleapiclient import __version__ +from errors import HttpError +from oauth2client.anyjson import simplejson + + +dump_request_response = False + + +def _abstract(): + raise NotImplementedError('You need to override this function') + + +class Model(object): + """Model base class. + + All Model classes should implement this interface. + The Model serializes and de-serializes between a wire + format such as JSON and a Python object representation. + """ + + def request(self, headers, path_params, query_params, body_value): + """Updates outgoing requests with a serialized body. + + Args: + headers: dict, request headers + path_params: dict, parameters that appear in the request path + query_params: dict, parameters that appear in the query + body_value: object, the request body as a Python object, which must be + serializable. + Returns: + A tuple of (headers, path_params, query, body) + + headers: dict, request headers + path_params: dict, parameters that appear in the request path + query: string, query part of the request URI + body: string, the body serialized in the desired wire format. + """ + _abstract() + + def response(self, resp, content): + """Convert the response wire format into a Python object. + + Args: + resp: httplib2.Response, the HTTP response headers and status + content: string, the body of the HTTP response + + Returns: + The body de-serialized as a Python object. + + Raises: + googleapiclient.errors.HttpError if a non 2xx response is received. + """ + _abstract() + + +class BaseModel(Model): + """Base model class. + + Subclasses should provide implementations for the "serialize" and + "deserialize" methods, as well as values for the following class attributes. + + Attributes: + accept: The value to use for the HTTP Accept header. + content_type: The value to use for the HTTP Content-type header. + no_content_response: The value to return when deserializing a 204 "No + Content" response. + alt_param: The value to supply as the "alt" query parameter for requests. + """ + + accept = None + content_type = None + no_content_response = None + alt_param = None + + def _log_request(self, headers, path_params, query, body): + """Logs debugging information about the request if requested.""" + if dump_request_response: + logging.info('--request-start--') + logging.info('-headers-start-') + for h, v in headers.iteritems(): + logging.info('%s: %s', h, v) + logging.info('-headers-end-') + logging.info('-path-parameters-start-') + for h, v in path_params.iteritems(): + logging.info('%s: %s', h, v) + logging.info('-path-parameters-end-') + logging.info('body: %s', body) + logging.info('query: %s', query) + logging.info('--request-end--') + + def request(self, headers, path_params, query_params, body_value): + """Updates outgoing requests with a serialized body. + + Args: + headers: dict, request headers + path_params: dict, parameters that appear in the request path + query_params: dict, parameters that appear in the query + body_value: object, the request body as a Python object, which must be + serializable by simplejson. + Returns: + A tuple of (headers, path_params, query, body) + + headers: dict, request headers + path_params: dict, parameters that appear in the request path + query: string, query part of the request URI + body: string, the body serialized as JSON + """ + query = self._build_query(query_params) + headers['accept'] = self.accept + headers['accept-encoding'] = 'gzip, deflate' + if 'user-agent' in headers: + headers['user-agent'] += ' ' + else: + headers['user-agent'] = '' + headers['user-agent'] += 'google-api-python-client/%s (gzip)' % __version__ + + if body_value is not None: + headers['content-type'] = self.content_type + body_value = self.serialize(body_value) + self._log_request(headers, path_params, query, body_value) + return (headers, path_params, query, body_value) + + def _build_query(self, params): + """Builds a query string. + + Args: + params: dict, the query parameters + + Returns: + The query parameters properly encoded into an HTTP URI query string. + """ + if self.alt_param is not None: + params.update({'alt': self.alt_param}) + astuples = [] + for key, value in params.iteritems(): + if type(value) == type([]): + for x in value: + x = x.encode('utf-8') + astuples.append((key, x)) + else: + if getattr(value, 'encode', False) and callable(value.encode): + value = value.encode('utf-8') + astuples.append((key, value)) + return '?' + urllib.urlencode(astuples) + + def _log_response(self, resp, content): + """Logs debugging information about the response if requested.""" + if dump_request_response: + logging.info('--response-start--') + for h, v in resp.iteritems(): + logging.info('%s: %s', h, v) + if content: + logging.info(content) + logging.info('--response-end--') + + def response(self, resp, content): + """Convert the response wire format into a Python object. + + Args: + resp: httplib2.Response, the HTTP response headers and status + content: string, the body of the HTTP response + + Returns: + The body de-serialized as a Python object. + + Raises: + googleapiclient.errors.HttpError if a non 2xx response is received. + """ + self._log_response(resp, content) + # Error handling is TBD, for example, do we retry + # for some operation/error combinations? + if resp.status < 300: + if resp.status == 204: + # A 204: No Content response should be treated differently + # to all the other success states + return self.no_content_response + return self.deserialize(content) + else: + logging.debug('Content from bad request was: %s' % content) + raise HttpError(resp, content) + + def serialize(self, body_value): + """Perform the actual Python object serialization. + + Args: + body_value: object, the request body as a Python object. + + Returns: + string, the body in serialized form. + """ + _abstract() + + def deserialize(self, content): + """Perform the actual deserialization from response string to Python + object. + + Args: + content: string, the body of the HTTP response + + Returns: + The body de-serialized as a Python object. + """ + _abstract() + + +class JsonModel(BaseModel): + """Model class for JSON. + + Serializes and de-serializes between JSON and the Python + object representation of HTTP request and response bodies. + """ + accept = 'application/json' + content_type = 'application/json' + alt_param = 'json' + + def __init__(self, data_wrapper=False): + """Construct a JsonModel. + + Args: + data_wrapper: boolean, wrap requests and responses in a data wrapper + """ + self._data_wrapper = data_wrapper + + def serialize(self, body_value): + if (isinstance(body_value, dict) and 'data' not in body_value and + self._data_wrapper): + body_value = {'data': body_value} + return simplejson.dumps(body_value) + + def deserialize(self, content): + content = content.decode('utf-8') + body = simplejson.loads(content) + if self._data_wrapper and isinstance(body, dict) and 'data' in body: + body = body['data'] + return body + + @property + def no_content_response(self): + return {} + + +class RawModel(JsonModel): + """Model class for requests that don't return JSON. + + Serializes and de-serializes between JSON and the Python + object representation of HTTP request, and returns the raw bytes + of the response body. + """ + accept = '*/*' + content_type = 'application/json' + alt_param = None + + def deserialize(self, content): + return content + + @property + def no_content_response(self): + return '' + + +class MediaModel(JsonModel): + """Model class for requests that return Media. + + Serializes and de-serializes between JSON and the Python + object representation of HTTP request, and returns the raw bytes + of the response body. + """ + accept = '*/*' + content_type = 'application/json' + alt_param = 'media' + + def deserialize(self, content): + return content + + @property + def no_content_response(self): + return '' + + +class ProtocolBufferModel(BaseModel): + """Model class for protocol buffers. + + Serializes and de-serializes the binary protocol buffer sent in the HTTP + request and response bodies. + """ + accept = 'application/x-protobuf' + content_type = 'application/x-protobuf' + alt_param = 'proto' + + def __init__(self, protocol_buffer): + """Constructs a ProtocolBufferModel. + + The serialzed protocol buffer returned in an HTTP response will be + de-serialized using the given protocol buffer class. + + Args: + protocol_buffer: The protocol buffer class used to de-serialize a + response from the API. + """ + self._protocol_buffer = protocol_buffer + + def serialize(self, body_value): + return body_value.SerializeToString() + + def deserialize(self, content): + return self._protocol_buffer.FromString(content) + + @property + def no_content_response(self): + return self._protocol_buffer() + + +def makepatch(original, modified): + """Create a patch object. + + Some methods support PATCH, an efficient way to send updates to a resource. + This method allows the easy construction of patch bodies by looking at the + differences between a resource before and after it was modified. + + Args: + original: object, the original deserialized resource + modified: object, the modified deserialized resource + Returns: + An object that contains only the changes from original to modified, in a + form suitable to pass to a PATCH method. + + Example usage: + item = service.activities().get(postid=postid, userid=userid).execute() + original = copy.deepcopy(item) + item['object']['content'] = 'This is updated.' + service.activities.patch(postid=postid, userid=userid, + body=makepatch(original, item)).execute() + """ + patch = {} + for key, original_value in original.iteritems(): + modified_value = modified.get(key, None) + if modified_value is None: + # Use None to signal that the element is deleted + patch[key] = None + elif original_value != modified_value: + if type(original_value) == type({}): + # Recursively descend objects + patch[key] = makepatch(original_value, modified_value) + else: + # In the case of simple types or arrays we just replace + patch[key] = modified_value + else: + # Don't add anything to patch if there's no change + pass + for key in modified: + if key not in original: + patch[key] = modified[key] + + return patch diff --git a/appengine/dashdemo-cached/googleapiclient/sample_tools.py b/appengine/dashdemo-cached/googleapiclient/sample_tools.py new file mode 100644 index 0000000..09f9057 --- /dev/null +++ b/appengine/dashdemo-cached/googleapiclient/sample_tools.py @@ -0,0 +1,93 @@ +# Copyright (C) 2013 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Utilities for making samples. + +Consolidates a lot of code commonly repeated in sample applications. +""" + +__author__ = 'jcgregorio@google.com (Joe Gregorio)' +__all__ = ['init'] + + +import argparse +import httplib2 +import os + +from googleapiclient import discovery +from oauth2client import client +from oauth2client import file +from oauth2client import tools + + +def init(argv, name, version, doc, filename, scope=None, parents=[]): + """A common initialization routine for samples. + + Many of the sample applications do the same initialization, which has now + been consolidated into this function. This function uses common idioms found + in almost all the samples, i.e. for an API with name 'apiname', the + credentials are stored in a file named apiname.dat, and the + client_secrets.json file is stored in the same directory as the application + main file. + + Args: + argv: list of string, the command-line parameters of the application. + name: string, name of the API. + version: string, version of the API. + doc: string, description of the application. Usually set to __doc__. + file: string, filename of the application. Usually set to __file__. + parents: list of argparse.ArgumentParser, additional command-line flags. + scope: string, The OAuth scope used. + + Returns: + A tuple of (service, flags), where service is the service object and flags + is the parsed command-line flags. + """ + if scope is None: + scope = 'https://www.googleapis.com/auth/' + name + + # Parser command-line arguments. + parent_parsers = [tools.argparser] + parent_parsers.extend(parents) + parser = argparse.ArgumentParser( + description=doc, + formatter_class=argparse.RawDescriptionHelpFormatter, + parents=parent_parsers) + flags = parser.parse_args(argv[1:]) + + # Name of a file containing the OAuth 2.0 information for this + # application, including client_id and client_secret, which are found + # on the API Access tab on the Google APIs + # Console . + client_secrets = os.path.join(os.path.dirname(filename), + 'client_secrets.json') + + # Set up a Flow object to be used if we need to authenticate. + flow = client.flow_from_clientsecrets(client_secrets, + scope=scope, + message=tools.message_if_missing(client_secrets)) + + # Prepare credentials, and authorize HTTP object with them. + # If the credentials don't exist or are invalid run through the native client + # flow. The Storage object will ensure that if successful the good + # credentials will get written back to a file. + storage = file.Storage(name + '.dat') + credentials = storage.get() + if credentials is None or credentials.invalid: + credentials = tools.run_flow(flow, storage, flags) + http = credentials.authorize(http = httplib2.Http()) + + # Construct a service object via the discovery service. + service = discovery.build(name, version, http=http) + return (service, flags) diff --git a/appengine/dashdemo-cached/googleapiclient/schema.py b/appengine/dashdemo-cached/googleapiclient/schema.py new file mode 100644 index 0000000..d076a86 --- /dev/null +++ b/appengine/dashdemo-cached/googleapiclient/schema.py @@ -0,0 +1,312 @@ +# Copyright (C) 2010 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Schema processing for discovery based APIs + +Schemas holds an APIs discovery schemas. It can return those schema as +deserialized JSON objects, or pretty print them as prototype objects that +conform to the schema. + +For example, given the schema: + + schema = \"\"\"{ + "Foo": { + "type": "object", + "properties": { + "etag": { + "type": "string", + "description": "ETag of the collection." + }, + "kind": { + "type": "string", + "description": "Type of the collection ('calendar#acl').", + "default": "calendar#acl" + }, + "nextPageToken": { + "type": "string", + "description": "Token used to access the next + page of this result. Omitted if no further results are available." + } + } + } + }\"\"\" + + s = Schemas(schema) + print s.prettyPrintByName('Foo') + + Produces the following output: + + { + "nextPageToken": "A String", # Token used to access the + # next page of this result. Omitted if no further results are available. + "kind": "A String", # Type of the collection ('calendar#acl'). + "etag": "A String", # ETag of the collection. + }, + +The constructor takes a discovery document in which to look up named schema. +""" + +# TODO(jcgregorio) support format, enum, minimum, maximum + +__author__ = 'jcgregorio@google.com (Joe Gregorio)' + +import copy + +from oauth2client import util +from oauth2client.anyjson import simplejson + + +class Schemas(object): + """Schemas for an API.""" + + def __init__(self, discovery): + """Constructor. + + Args: + discovery: object, Deserialized discovery document from which we pull + out the named schema. + """ + self.schemas = discovery.get('schemas', {}) + + # Cache of pretty printed schemas. + self.pretty = {} + + @util.positional(2) + def _prettyPrintByName(self, name, seen=None, dent=0): + """Get pretty printed object prototype from the schema name. + + Args: + name: string, Name of schema in the discovery document. + seen: list of string, Names of schema already seen. Used to handle + recursive definitions. + + Returns: + string, A string that contains a prototype object with + comments that conforms to the given schema. + """ + if seen is None: + seen = [] + + if name in seen: + # Do not fall into an infinite loop over recursive definitions. + return '# Object with schema name: %s' % name + seen.append(name) + + if name not in self.pretty: + self.pretty[name] = _SchemaToStruct(self.schemas[name], + seen, dent=dent).to_str(self._prettyPrintByName) + + seen.pop() + + return self.pretty[name] + + def prettyPrintByName(self, name): + """Get pretty printed object prototype from the schema name. + + Args: + name: string, Name of schema in the discovery document. + + Returns: + string, A string that contains a prototype object with + comments that conforms to the given schema. + """ + # Return with trailing comma and newline removed. + return self._prettyPrintByName(name, seen=[], dent=1)[:-2] + + @util.positional(2) + def _prettyPrintSchema(self, schema, seen=None, dent=0): + """Get pretty printed object prototype of schema. + + Args: + schema: object, Parsed JSON schema. + seen: list of string, Names of schema already seen. Used to handle + recursive definitions. + + Returns: + string, A string that contains a prototype object with + comments that conforms to the given schema. + """ + if seen is None: + seen = [] + + return _SchemaToStruct(schema, seen, dent=dent).to_str(self._prettyPrintByName) + + def prettyPrintSchema(self, schema): + """Get pretty printed object prototype of schema. + + Args: + schema: object, Parsed JSON schema. + + Returns: + string, A string that contains a prototype object with + comments that conforms to the given schema. + """ + # Return with trailing comma and newline removed. + return self._prettyPrintSchema(schema, dent=1)[:-2] + + def get(self, name): + """Get deserialized JSON schema from the schema name. + + Args: + name: string, Schema name. + """ + return self.schemas[name] + + +class _SchemaToStruct(object): + """Convert schema to a prototype object.""" + + @util.positional(3) + def __init__(self, schema, seen, dent=0): + """Constructor. + + Args: + schema: object, Parsed JSON schema. + seen: list, List of names of schema already seen while parsing. Used to + handle recursive definitions. + dent: int, Initial indentation depth. + """ + # The result of this parsing kept as list of strings. + self.value = [] + + # The final value of the parsing. + self.string = None + + # The parsed JSON schema. + self.schema = schema + + # Indentation level. + self.dent = dent + + # Method that when called returns a prototype object for the schema with + # the given name. + self.from_cache = None + + # List of names of schema already seen while parsing. + self.seen = seen + + def emit(self, text): + """Add text as a line to the output. + + Args: + text: string, Text to output. + """ + self.value.extend([" " * self.dent, text, '\n']) + + def emitBegin(self, text): + """Add text to the output, but with no line terminator. + + Args: + text: string, Text to output. + """ + self.value.extend([" " * self.dent, text]) + + def emitEnd(self, text, comment): + """Add text and comment to the output with line terminator. + + Args: + text: string, Text to output. + comment: string, Python comment. + """ + if comment: + divider = '\n' + ' ' * (self.dent + 2) + '# ' + lines = comment.splitlines() + lines = [x.rstrip() for x in lines] + comment = divider.join(lines) + self.value.extend([text, ' # ', comment, '\n']) + else: + self.value.extend([text, '\n']) + + def indent(self): + """Increase indentation level.""" + self.dent += 1 + + def undent(self): + """Decrease indentation level.""" + self.dent -= 1 + + def _to_str_impl(self, schema): + """Prototype object based on the schema, in Python code with comments. + + Args: + schema: object, Parsed JSON schema file. + + Returns: + Prototype object based on the schema, in Python code with comments. + """ + stype = schema.get('type') + if stype == 'object': + self.emitEnd('{', schema.get('description', '')) + self.indent() + if 'properties' in schema: + for pname, pschema in schema.get('properties', {}).iteritems(): + self.emitBegin('"%s": ' % pname) + self._to_str_impl(pschema) + elif 'additionalProperties' in schema: + self.emitBegin('"a_key": ') + self._to_str_impl(schema['additionalProperties']) + self.undent() + self.emit('},') + elif '$ref' in schema: + schemaName = schema['$ref'] + description = schema.get('description', '') + s = self.from_cache(schemaName, seen=self.seen) + parts = s.splitlines() + self.emitEnd(parts[0], description) + for line in parts[1:]: + self.emit(line.rstrip()) + elif stype == 'boolean': + value = schema.get('default', 'True or False') + self.emitEnd('%s,' % str(value), schema.get('description', '')) + elif stype == 'string': + value = schema.get('default', 'A String') + self.emitEnd('"%s",' % str(value), schema.get('description', '')) + elif stype == 'integer': + value = schema.get('default', '42') + self.emitEnd('%s,' % str(value), schema.get('description', '')) + elif stype == 'number': + value = schema.get('default', '3.14') + self.emitEnd('%s,' % str(value), schema.get('description', '')) + elif stype == 'null': + self.emitEnd('None,', schema.get('description', '')) + elif stype == 'any': + self.emitEnd('"",', schema.get('description', '')) + elif stype == 'array': + self.emitEnd('[', schema.get('description')) + self.indent() + self.emitBegin('') + self._to_str_impl(schema['items']) + self.undent() + self.emit('],') + else: + self.emit('Unknown type! %s' % stype) + self.emitEnd('', '') + + self.string = ''.join(self.value) + return self.string + + def to_str(self, from_cache): + """Prototype object based on the schema, in Python code with comments. + + Args: + from_cache: callable(name, seen), Callable that retrieves an object + prototype for a schema with the given name. Seen is a list of schema + names already seen as we recursively descend the schema definition. + + Returns: + Prototype object based on the schema, in Python code with comments. + The lines of the code will all be properly indented. + """ + self.from_cache = from_cache + return self._to_str_impl(self.schema) diff --git a/appengine/dashdemo-cached/httplib2/__init__.py b/appengine/dashdemo-cached/httplib2/__init__.py new file mode 100644 index 0000000..d1212b5 --- /dev/null +++ b/appengine/dashdemo-cached/httplib2/__init__.py @@ -0,0 +1,1680 @@ +from __future__ import generators +""" +httplib2 + +A caching http interface that supports ETags and gzip +to conserve bandwidth. + +Requires Python 2.3 or later + +Changelog: +2007-08-18, Rick: Modified so it's able to use a socks proxy if needed. + +""" + +__author__ = "Joe Gregorio (joe@bitworking.org)" +__copyright__ = "Copyright 2006, Joe Gregorio" +__contributors__ = ["Thomas Broyer (t.broyer@ltgt.net)", + "James Antill", + "Xavier Verges Farrero", + "Jonathan Feinberg", + "Blair Zajac", + "Sam Ruby", + "Louis Nyffenegger"] +__license__ = "MIT" +__version__ = "0.9" + +import re +import sys +import email +import email.Utils +import email.Message +import email.FeedParser +import StringIO +import gzip +import zlib +import httplib +import urlparse +import urllib +import base64 +import os +import copy +import calendar +import time +import random +import errno +try: + from hashlib import sha1 as _sha, md5 as _md5 +except ImportError: + # prior to Python 2.5, these were separate modules + import sha + import md5 + _sha = sha.new + _md5 = md5.new +import hmac +from gettext import gettext as _ +import socket + +try: + from httplib2 import socks +except ImportError: + try: + import socks + except (ImportError, AttributeError): + socks = None + +# Build the appropriate socket wrapper for ssl +try: + import ssl # python 2.6 + ssl_SSLError = ssl.SSLError + def _ssl_wrap_socket(sock, key_file, cert_file, + disable_validation, ca_certs): + if disable_validation: + cert_reqs = ssl.CERT_NONE + else: + cert_reqs = ssl.CERT_REQUIRED + # We should be specifying SSL version 3 or TLS v1, but the ssl module + # doesn't expose the necessary knobs. So we need to go with the default + # of SSLv23. + return ssl.wrap_socket(sock, keyfile=key_file, certfile=cert_file, + cert_reqs=cert_reqs, ca_certs=ca_certs) +except (AttributeError, ImportError): + ssl_SSLError = None + def _ssl_wrap_socket(sock, key_file, cert_file, + disable_validation, ca_certs): + if not disable_validation: + raise CertificateValidationUnsupported( + "SSL certificate validation is not supported without " + "the ssl module installed. To avoid this error, install " + "the ssl module, or explicity disable validation.") + ssl_sock = socket.ssl(sock, key_file, cert_file) + return httplib.FakeSocket(sock, ssl_sock) + + +if sys.version_info >= (2,3): + from iri2uri import iri2uri +else: + def iri2uri(uri): + return uri + +def has_timeout(timeout): # python 2.6 + if hasattr(socket, '_GLOBAL_DEFAULT_TIMEOUT'): + return (timeout is not None and timeout is not socket._GLOBAL_DEFAULT_TIMEOUT) + return (timeout is not None) + +__all__ = [ + 'Http', 'Response', 'ProxyInfo', 'HttpLib2Error', 'RedirectMissingLocation', + 'RedirectLimit', 'FailedToDecompressContent', + 'UnimplementedDigestAuthOptionError', + 'UnimplementedHmacDigestAuthOptionError', + 'debuglevel', 'ProxiesUnavailableError'] + + +# The httplib debug level, set to a non-zero value to get debug output +debuglevel = 0 + +# A request will be tried 'RETRIES' times if it fails at the socket/connection level. +RETRIES = 2 + +# Python 2.3 support +if sys.version_info < (2,4): + def sorted(seq): + seq.sort() + return seq + +# Python 2.3 support +def HTTPResponse__getheaders(self): + """Return list of (header, value) tuples.""" + if self.msg is None: + raise httplib.ResponseNotReady() + return self.msg.items() + +if not hasattr(httplib.HTTPResponse, 'getheaders'): + httplib.HTTPResponse.getheaders = HTTPResponse__getheaders + +# All exceptions raised here derive from HttpLib2Error +class HttpLib2Error(Exception): pass + +# Some exceptions can be caught and optionally +# be turned back into responses. +class HttpLib2ErrorWithResponse(HttpLib2Error): + def __init__(self, desc, response, content): + self.response = response + self.content = content + HttpLib2Error.__init__(self, desc) + +class RedirectMissingLocation(HttpLib2ErrorWithResponse): pass +class RedirectLimit(HttpLib2ErrorWithResponse): pass +class FailedToDecompressContent(HttpLib2ErrorWithResponse): pass +class UnimplementedDigestAuthOptionError(HttpLib2ErrorWithResponse): pass +class UnimplementedHmacDigestAuthOptionError(HttpLib2ErrorWithResponse): pass + +class MalformedHeader(HttpLib2Error): pass +class RelativeURIError(HttpLib2Error): pass +class ServerNotFoundError(HttpLib2Error): pass +class ProxiesUnavailableError(HttpLib2Error): pass +class CertificateValidationUnsupported(HttpLib2Error): pass +class SSLHandshakeError(HttpLib2Error): pass +class NotSupportedOnThisPlatform(HttpLib2Error): pass +class CertificateHostnameMismatch(SSLHandshakeError): + def __init__(self, desc, host, cert): + HttpLib2Error.__init__(self, desc) + self.host = host + self.cert = cert + +# Open Items: +# ----------- +# Proxy support + +# Are we removing the cached content too soon on PUT (only delete on 200 Maybe?) + +# Pluggable cache storage (supports storing the cache in +# flat files by default. We need a plug-in architecture +# that can support Berkeley DB and Squid) + +# == Known Issues == +# Does not handle a resource that uses conneg and Last-Modified but no ETag as a cache validator. +# Does not handle Cache-Control: max-stale +# Does not use Age: headers when calculating cache freshness. + + +# The number of redirections to follow before giving up. +# Note that only GET redirects are automatically followed. +# Will also honor 301 requests by saving that info and never +# requesting that URI again. +DEFAULT_MAX_REDIRECTS = 5 + +try: + # Users can optionally provide a module that tells us where the CA_CERTS + # are located. + import ca_certs_locater + CA_CERTS = ca_certs_locater.get() +except ImportError: + # Default CA certificates file bundled with httplib2. + CA_CERTS = os.path.join( + os.path.dirname(os.path.abspath(__file__ )), "cacerts.txt") + +# Which headers are hop-by-hop headers by default +HOP_BY_HOP = ['connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailers', 'transfer-encoding', 'upgrade'] + +def _get_end2end_headers(response): + hopbyhop = list(HOP_BY_HOP) + hopbyhop.extend([x.strip() for x in response.get('connection', '').split(',')]) + return [header for header in response.keys() if header not in hopbyhop] + +URI = re.compile(r"^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?") + +def parse_uri(uri): + """Parses a URI using the regex given in Appendix B of RFC 3986. + + (scheme, authority, path, query, fragment) = parse_uri(uri) + """ + groups = URI.match(uri).groups() + return (groups[1], groups[3], groups[4], groups[6], groups[8]) + +def urlnorm(uri): + (scheme, authority, path, query, fragment) = parse_uri(uri) + if not scheme or not authority: + raise RelativeURIError("Only absolute URIs are allowed. uri = %s" % uri) + authority = authority.lower() + scheme = scheme.lower() + if not path: + path = "/" + # Could do syntax based normalization of the URI before + # computing the digest. See Section 6.2.2 of Std 66. + request_uri = query and "?".join([path, query]) or path + scheme = scheme.lower() + defrag_uri = scheme + "://" + authority + request_uri + return scheme, authority, request_uri, defrag_uri + + +# Cache filename construction (original borrowed from Venus http://intertwingly.net/code/venus/) +re_url_scheme = re.compile(r'^\w+://') +re_slash = re.compile(r'[?/:|]+') + +def safename(filename): + """Return a filename suitable for the cache. + + Strips dangerous and common characters to create a filename we + can use to store the cache in. + """ + + try: + if re_url_scheme.match(filename): + if isinstance(filename,str): + filename = filename.decode('utf-8') + filename = filename.encode('idna') + else: + filename = filename.encode('idna') + except UnicodeError: + pass + if isinstance(filename,unicode): + filename=filename.encode('utf-8') + filemd5 = _md5(filename).hexdigest() + filename = re_url_scheme.sub("", filename) + filename = re_slash.sub(",", filename) + + # limit length of filename + if len(filename)>200: + filename=filename[:200] + return ",".join((filename, filemd5)) + +NORMALIZE_SPACE = re.compile(r'(?:\r\n)?[ \t]+') +def _normalize_headers(headers): + return dict([ (key.lower(), NORMALIZE_SPACE.sub(value, ' ').strip()) for (key, value) in headers.iteritems()]) + +def _parse_cache_control(headers): + retval = {} + if headers.has_key('cache-control'): + parts = headers['cache-control'].split(',') + parts_with_args = [tuple([x.strip().lower() for x in part.split("=", 1)]) for part in parts if -1 != part.find("=")] + parts_wo_args = [(name.strip().lower(), 1) for name in parts if -1 == name.find("=")] + retval = dict(parts_with_args + parts_wo_args) + return retval + +# Whether to use a strict mode to parse WWW-Authenticate headers +# Might lead to bad results in case of ill-formed header value, +# so disabled by default, falling back to relaxed parsing. +# Set to true to turn on, usefull for testing servers. +USE_WWW_AUTH_STRICT_PARSING = 0 + +# In regex below: +# [^\0-\x1f\x7f-\xff()<>@,;:\\\"/[\]?={} \t]+ matches a "token" as defined by HTTP +# "(?:[^\0-\x08\x0A-\x1f\x7f-\xff\\\"]|\\[\0-\x7f])*?" matches a "quoted-string" as defined by HTTP, when LWS have already been replaced by a single space +# Actually, as an auth-param value can be either a token or a quoted-string, they are combined in a single pattern which matches both: +# \"?((?<=\")(?:[^\0-\x1f\x7f-\xff\\\"]|\\[\0-\x7f])*?(?=\")|(?@,;:\\\"/[\]?={} \t]+(?!\"))\"? +WWW_AUTH_STRICT = re.compile(r"^(?:\s*(?:,\s*)?([^\0-\x1f\x7f-\xff()<>@,;:\\\"/[\]?={} \t]+)\s*=\s*\"?((?<=\")(?:[^\0-\x08\x0A-\x1f\x7f-\xff\\\"]|\\[\0-\x7f])*?(?=\")|(?@,;:\\\"/[\]?={} \t]+(?!\"))\"?)(.*)$") +WWW_AUTH_RELAXED = re.compile(r"^(?:\s*(?:,\s*)?([^ \t\r\n=]+)\s*=\s*\"?((?<=\")(?:[^\\\"]|\\.)*?(?=\")|(? current_age: + retval = "FRESH" + return retval + +def _decompressContent(response, new_content): + content = new_content + try: + encoding = response.get('content-encoding', None) + if encoding in ['gzip', 'deflate']: + if encoding == 'gzip': + content = gzip.GzipFile(fileobj=StringIO.StringIO(new_content)).read() + if encoding == 'deflate': + content = zlib.decompress(content) + response['content-length'] = str(len(content)) + # Record the historical presence of the encoding in a way the won't interfere. + response['-content-encoding'] = response['content-encoding'] + del response['content-encoding'] + except IOError: + content = "" + raise FailedToDecompressContent(_("Content purported to be compressed with %s but failed to decompress.") % response.get('content-encoding'), response, content) + return content + +def _updateCache(request_headers, response_headers, content, cache, cachekey): + if cachekey: + cc = _parse_cache_control(request_headers) + cc_response = _parse_cache_control(response_headers) + if cc.has_key('no-store') or cc_response.has_key('no-store'): + cache.delete(cachekey) + else: + info = email.Message.Message() + for key, value in response_headers.iteritems(): + if key not in ['status','content-encoding','transfer-encoding']: + info[key] = value + + # Add annotations to the cache to indicate what headers + # are variant for this request. + vary = response_headers.get('vary', None) + if vary: + vary_headers = vary.lower().replace(' ', '').split(',') + for header in vary_headers: + key = '-varied-%s' % header + try: + info[key] = request_headers[header] + except KeyError: + pass + + status = response_headers.status + if status == 304: + status = 200 + + status_header = 'status: %d\r\n' % status + + header_str = info.as_string() + + header_str = re.sub("\r(?!\n)|(? 0: + service = "cl" + # No point in guessing Base or Spreadsheet + #elif request_uri.find("spreadsheets") > 0: + # service = "wise" + + auth = dict(Email=credentials[0], Passwd=credentials[1], service=service, source=headers['user-agent']) + resp, content = self.http.request("https://www.google.com/accounts/ClientLogin", method="POST", body=urlencode(auth), headers={'Content-Type': 'application/x-www-form-urlencoded'}) + lines = content.split('\n') + d = dict([tuple(line.split("=", 1)) for line in lines if line]) + if resp.status == 403: + self.Auth = "" + else: + self.Auth = d['Auth'] + + def request(self, method, request_uri, headers, content): + """Modify the request headers to add the appropriate + Authorization header.""" + headers['authorization'] = 'GoogleLogin Auth=' + self.Auth + + +AUTH_SCHEME_CLASSES = { + "basic": BasicAuthentication, + "wsse": WsseAuthentication, + "digest": DigestAuthentication, + "hmacdigest": HmacDigestAuthentication, + "googlelogin": GoogleLoginAuthentication +} + +AUTH_SCHEME_ORDER = ["hmacdigest", "googlelogin", "digest", "wsse", "basic"] + +class FileCache(object): + """Uses a local directory as a store for cached files. + Not really safe to use if multiple threads or processes are going to + be running on the same cache. + """ + def __init__(self, cache, safe=safename): # use safe=lambda x: md5.new(x).hexdigest() for the old behavior + self.cache = cache + self.safe = safe + if not os.path.exists(cache): + os.makedirs(self.cache) + + def get(self, key): + retval = None + cacheFullPath = os.path.join(self.cache, self.safe(key)) + try: + f = file(cacheFullPath, "rb") + retval = f.read() + f.close() + except IOError: + pass + return retval + + def set(self, key, value): + cacheFullPath = os.path.join(self.cache, self.safe(key)) + f = file(cacheFullPath, "wb") + f.write(value) + f.close() + + def delete(self, key): + cacheFullPath = os.path.join(self.cache, self.safe(key)) + if os.path.exists(cacheFullPath): + os.remove(cacheFullPath) + +class Credentials(object): + def __init__(self): + self.credentials = [] + + def add(self, name, password, domain=""): + self.credentials.append((domain.lower(), name, password)) + + def clear(self): + self.credentials = [] + + def iter(self, domain): + for (cdomain, name, password) in self.credentials: + if cdomain == "" or domain == cdomain: + yield (name, password) + +class KeyCerts(Credentials): + """Identical to Credentials except that + name/password are mapped to key/cert.""" + pass + +class AllHosts(object): + pass + +class ProxyInfo(object): + """Collect information required to use a proxy.""" + bypass_hosts = () + + def __init__(self, proxy_type, proxy_host, proxy_port, + proxy_rdns=None, proxy_user=None, proxy_pass=None): + """The parameter proxy_type must be set to one of socks.PROXY_TYPE_XXX + constants. For example: + + p = ProxyInfo(proxy_type=socks.PROXY_TYPE_HTTP, + proxy_host='localhost', proxy_port=8000) + """ + self.proxy_type = proxy_type + self.proxy_host = proxy_host + self.proxy_port = proxy_port + self.proxy_rdns = proxy_rdns + self.proxy_user = proxy_user + self.proxy_pass = proxy_pass + + def astuple(self): + return (self.proxy_type, self.proxy_host, self.proxy_port, + self.proxy_rdns, self.proxy_user, self.proxy_pass) + + def isgood(self): + return (self.proxy_host != None) and (self.proxy_port != None) + + def applies_to(self, hostname): + return not self.bypass_host(hostname) + + def bypass_host(self, hostname): + """Has this host been excluded from the proxy config""" + if self.bypass_hosts is AllHosts: + return True + + bypass = False + for domain in self.bypass_hosts: + if hostname.endswith(domain): + bypass = True + + return bypass + + +def proxy_info_from_environment(method='http'): + """ + Read proxy info from the environment variables. + """ + if method not in ['http', 'https']: + return + + env_var = method + '_proxy' + url = os.environ.get(env_var, os.environ.get(env_var.upper())) + if not url: + return + pi = proxy_info_from_url(url, method) + + no_proxy = os.environ.get('no_proxy', os.environ.get('NO_PROXY', '')) + bypass_hosts = [] + if no_proxy: + bypass_hosts = no_proxy.split(',') + # special case, no_proxy=* means all hosts bypassed + if no_proxy == '*': + bypass_hosts = AllHosts + + pi.bypass_hosts = bypass_hosts + return pi + +def proxy_info_from_url(url, method='http'): + """ + Construct a ProxyInfo from a URL (such as http_proxy env var) + """ + url = urlparse.urlparse(url) + username = None + password = None + port = None + if '@' in url[1]: + ident, host_port = url[1].split('@', 1) + if ':' in ident: + username, password = ident.split(':', 1) + else: + password = ident + else: + host_port = url[1] + if ':' in host_port: + host, port = host_port.split(':', 1) + else: + host = host_port + + if port: + port = int(port) + else: + port = dict(https=443, http=80)[method] + + proxy_type = 3 # socks.PROXY_TYPE_HTTP + return ProxyInfo( + proxy_type = proxy_type, + proxy_host = host, + proxy_port = port, + proxy_user = username or None, + proxy_pass = password or None, + ) + + +class HTTPConnectionWithTimeout(httplib.HTTPConnection): + """ + HTTPConnection subclass that supports timeouts + + All timeouts are in seconds. If None is passed for timeout then + Python's default timeout for sockets will be used. See for example + the docs of socket.setdefaulttimeout(): + http://docs.python.org/library/socket.html#socket.setdefaulttimeout + """ + + def __init__(self, host, port=None, strict=None, timeout=None, proxy_info=None): + httplib.HTTPConnection.__init__(self, host, port, strict) + self.timeout = timeout + self.proxy_info = proxy_info + + def connect(self): + """Connect to the host and port specified in __init__.""" + # Mostly verbatim from httplib.py. + if self.proxy_info and socks is None: + raise ProxiesUnavailableError( + 'Proxy support missing but proxy use was requested!') + msg = "getaddrinfo returns an empty list" + if self.proxy_info and self.proxy_info.isgood(): + use_proxy = True + proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass = self.proxy_info.astuple() + else: + use_proxy = False + if use_proxy and proxy_rdns: + host = proxy_host + port = proxy_port + else: + host = self.host + port = self.port + + for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM): + af, socktype, proto, canonname, sa = res + try: + if use_proxy: + self.sock = socks.socksocket(af, socktype, proto) + self.sock.setproxy(proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass) + else: + self.sock = socket.socket(af, socktype, proto) + self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + # Different from httplib: support timeouts. + if has_timeout(self.timeout): + self.sock.settimeout(self.timeout) + # End of difference from httplib. + if self.debuglevel > 0: + print "connect: (%s, %s) ************" % (self.host, self.port) + if use_proxy: + print "proxy: %s ************" % str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass)) + + self.sock.connect((self.host, self.port) + sa[2:]) + except socket.error, msg: + if self.debuglevel > 0: + print "connect fail: (%s, %s)" % (self.host, self.port) + if use_proxy: + print "proxy: %s" % str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass)) + if self.sock: + self.sock.close() + self.sock = None + continue + break + if not self.sock: + raise socket.error, msg + +class HTTPSConnectionWithTimeout(httplib.HTTPSConnection): + """ + This class allows communication via SSL. + + All timeouts are in seconds. If None is passed for timeout then + Python's default timeout for sockets will be used. See for example + the docs of socket.setdefaulttimeout(): + http://docs.python.org/library/socket.html#socket.setdefaulttimeout + """ + def __init__(self, host, port=None, key_file=None, cert_file=None, + strict=None, timeout=None, proxy_info=None, + ca_certs=None, disable_ssl_certificate_validation=False): + httplib.HTTPSConnection.__init__(self, host, port=port, + key_file=key_file, + cert_file=cert_file, strict=strict) + self.timeout = timeout + self.proxy_info = proxy_info + if ca_certs is None: + ca_certs = CA_CERTS + self.ca_certs = ca_certs + self.disable_ssl_certificate_validation = \ + disable_ssl_certificate_validation + + # The following two methods were adapted from https_wrapper.py, released + # with the Google Appengine SDK at + # http://googleappengine.googlecode.com/svn-history/r136/trunk/python/google/appengine/tools/https_wrapper.py + # under the following license: + # + # Copyright 2007 Google Inc. + # + # Licensed under the Apache License, Version 2.0 (the "License"); + # you may not use this file except in compliance with the License. + # You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License is distributed on an "AS IS" BASIS, + # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + # See the License for the specific language governing permissions and + # limitations under the License. + # + + def _GetValidHostsForCert(self, cert): + """Returns a list of valid host globs for an SSL certificate. + + Args: + cert: A dictionary representing an SSL certificate. + Returns: + list: A list of valid host globs. + """ + if 'subjectAltName' in cert: + return [x[1] for x in cert['subjectAltName'] + if x[0].lower() == 'dns'] + else: + return [x[0][1] for x in cert['subject'] + if x[0][0].lower() == 'commonname'] + + def _ValidateCertificateHostname(self, cert, hostname): + """Validates that a given hostname is valid for an SSL certificate. + + Args: + cert: A dictionary representing an SSL certificate. + hostname: The hostname to test. + Returns: + bool: Whether or not the hostname is valid for this certificate. + """ + hosts = self._GetValidHostsForCert(cert) + for host in hosts: + host_re = host.replace('.', '\.').replace('*', '[^.]*') + if re.search('^%s$' % (host_re,), hostname, re.I): + return True + return False + + def connect(self): + "Connect to a host on a given (SSL) port." + + msg = "getaddrinfo returns an empty list" + if self.proxy_info and self.proxy_info.isgood(): + use_proxy = True + proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass = self.proxy_info.astuple() + else: + use_proxy = False + if use_proxy and proxy_rdns: + host = proxy_host + port = proxy_port + else: + host = self.host + port = self.port + + address_info = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM) + for family, socktype, proto, canonname, sockaddr in address_info: + try: + if use_proxy: + sock = socks.socksocket(family, socktype, proto) + + sock.setproxy(proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass) + else: + sock = socket.socket(family, socktype, proto) + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + + if has_timeout(self.timeout): + sock.settimeout(self.timeout) + sock.connect((self.host, self.port)) + self.sock =_ssl_wrap_socket( + sock, self.key_file, self.cert_file, + self.disable_ssl_certificate_validation, self.ca_certs) + if self.debuglevel > 0: + print "connect: (%s, %s)" % (self.host, self.port) + if use_proxy: + print "proxy: %s" % str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass)) + if not self.disable_ssl_certificate_validation: + cert = self.sock.getpeercert() + hostname = self.host.split(':', 0)[0] + if not self._ValidateCertificateHostname(cert, hostname): + raise CertificateHostnameMismatch( + 'Server presented certificate that does not match ' + 'host %s: %s' % (hostname, cert), hostname, cert) + except ssl_SSLError, e: + if sock: + sock.close() + if self.sock: + self.sock.close() + self.sock = None + # Unfortunately the ssl module doesn't seem to provide any way + # to get at more detailed error information, in particular + # whether the error is due to certificate validation or + # something else (such as SSL protocol mismatch). + if e.errno == ssl.SSL_ERROR_SSL: + raise SSLHandshakeError(e) + else: + raise + except (socket.timeout, socket.gaierror): + raise + except socket.error, msg: + if self.debuglevel > 0: + print "connect fail: (%s, %s)" % (self.host, self.port) + if use_proxy: + print "proxy: %s" % str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass)) + if self.sock: + self.sock.close() + self.sock = None + continue + break + if not self.sock: + raise socket.error, msg + +SCHEME_TO_CONNECTION = { + 'http': HTTPConnectionWithTimeout, + 'https': HTTPSConnectionWithTimeout +} + +# Use a different connection object for Google App Engine +try: + try: + from google.appengine.api import apiproxy_stub_map + if apiproxy_stub_map.apiproxy.GetStub('urlfetch') is None: + raise ImportError # Bail out; we're not actually running on App Engine. + from google.appengine.api.urlfetch import fetch + from google.appengine.api.urlfetch import InvalidURLError + except (ImportError, AttributeError): + from google3.apphosting.api import apiproxy_stub_map + if apiproxy_stub_map.apiproxy.GetStub('urlfetch') is None: + raise ImportError # Bail out; we're not actually running on App Engine. + from google3.apphosting.api.urlfetch import fetch + from google3.apphosting.api.urlfetch import InvalidURLError + + def _new_fixed_fetch(validate_certificate): + def fixed_fetch(url, payload=None, method="GET", headers={}, + allow_truncated=False, follow_redirects=True, + deadline=None): + if deadline is None: + deadline = socket.getdefaulttimeout() or 5 + return fetch(url, payload=payload, method=method, headers=headers, + allow_truncated=allow_truncated, + follow_redirects=follow_redirects, deadline=deadline, + validate_certificate=validate_certificate) + return fixed_fetch + + class AppEngineHttpConnection(httplib.HTTPConnection): + """Use httplib on App Engine, but compensate for its weirdness. + + The parameters key_file, cert_file, proxy_info, ca_certs, and + disable_ssl_certificate_validation are all dropped on the ground. + """ + def __init__(self, host, port=None, key_file=None, cert_file=None, + strict=None, timeout=None, proxy_info=None, ca_certs=None, + disable_ssl_certificate_validation=False): + httplib.HTTPConnection.__init__(self, host, port=port, + strict=strict, timeout=timeout) + + class AppEngineHttpsConnection(httplib.HTTPSConnection): + """Same as AppEngineHttpConnection, but for HTTPS URIs.""" + def __init__(self, host, port=None, key_file=None, cert_file=None, + strict=None, timeout=None, proxy_info=None, ca_certs=None, + disable_ssl_certificate_validation=False): + httplib.HTTPSConnection.__init__(self, host, port=port, + key_file=key_file, + cert_file=cert_file, strict=strict, + timeout=timeout) + self._fetch = _new_fixed_fetch( + not disable_ssl_certificate_validation) + + # Update the connection classes to use the Googel App Engine specific ones. + SCHEME_TO_CONNECTION = { + 'http': AppEngineHttpConnection, + 'https': AppEngineHttpsConnection + } +except (ImportError, AttributeError): + pass + + +class Http(object): + """An HTTP client that handles: + + - all methods + - caching + - ETags + - compression, + - HTTPS + - Basic + - Digest + - WSSE + + and more. + """ + def __init__(self, cache=None, timeout=None, + proxy_info=proxy_info_from_environment, + ca_certs=None, disable_ssl_certificate_validation=False): + """If 'cache' is a string then it is used as a directory name for + a disk cache. Otherwise it must be an object that supports the + same interface as FileCache. + + All timeouts are in seconds. If None is passed for timeout + then Python's default timeout for sockets will be used. See + for example the docs of socket.setdefaulttimeout(): + http://docs.python.org/library/socket.html#socket.setdefaulttimeout + + `proxy_info` may be: + - a callable that takes the http scheme ('http' or 'https') and + returns a ProxyInfo instance per request. By default, uses + proxy_nfo_from_environment. + - a ProxyInfo instance (static proxy config). + - None (proxy disabled). + + ca_certs is the path of a file containing root CA certificates for SSL + server certificate validation. By default, a CA cert file bundled with + httplib2 is used. + + If disable_ssl_certificate_validation is true, SSL cert validation will + not be performed. + """ + self.proxy_info = proxy_info + self.ca_certs = ca_certs + self.disable_ssl_certificate_validation = \ + disable_ssl_certificate_validation + + # Map domain name to an httplib connection + self.connections = {} + # The location of the cache, for now a directory + # where cached responses are held. + if cache and isinstance(cache, basestring): + self.cache = FileCache(cache) + else: + self.cache = cache + + # Name/password + self.credentials = Credentials() + + # Key/cert + self.certificates = KeyCerts() + + # authorization objects + self.authorizations = [] + + # If set to False then no redirects are followed, even safe ones. + self.follow_redirects = True + + # Which HTTP methods do we apply optimistic concurrency to, i.e. + # which methods get an "if-match:" etag header added to them. + self.optimistic_concurrency_methods = ["PUT", "PATCH"] + + # If 'follow_redirects' is True, and this is set to True then + # all redirecs are followed, including unsafe ones. + self.follow_all_redirects = False + + self.ignore_etag = False + + self.force_exception_to_status_code = False + + self.timeout = timeout + + # Keep Authorization: headers on a redirect. + self.forward_authorization_headers = False + + def __getstate__(self): + state_dict = copy.copy(self.__dict__) + # In case request is augmented by some foreign object such as + # credentials which handle auth + if 'request' in state_dict: + del state_dict['request'] + if 'connections' in state_dict: + del state_dict['connections'] + return state_dict + + def __setstate__(self, state): + self.__dict__.update(state) + self.connections = {} + + def _auth_from_challenge(self, host, request_uri, headers, response, content): + """A generator that creates Authorization objects + that can be applied to requests. + """ + challenges = _parse_www_authenticate(response, 'www-authenticate') + for cred in self.credentials.iter(host): + for scheme in AUTH_SCHEME_ORDER: + if challenges.has_key(scheme): + yield AUTH_SCHEME_CLASSES[scheme](cred, host, request_uri, headers, response, content, self) + + def add_credentials(self, name, password, domain=""): + """Add a name and password that will be used + any time a request requires authentication.""" + self.credentials.add(name, password, domain) + + def add_certificate(self, key, cert, domain): + """Add a key and cert that will be used + any time a request requires authentication.""" + self.certificates.add(key, cert, domain) + + def clear_credentials(self): + """Remove all the names and passwords + that are used for authentication""" + self.credentials.clear() + self.authorizations = [] + + def _conn_request(self, conn, request_uri, method, body, headers): + i = 0 + seen_bad_status_line = False + while i < RETRIES: + i += 1 + try: + if hasattr(conn, 'sock') and conn.sock is None: + conn.connect() + conn.request(method, request_uri, body, headers) + except socket.timeout: + raise + except socket.gaierror: + conn.close() + raise ServerNotFoundError("Unable to find the server at %s" % conn.host) + except ssl_SSLError: + conn.close() + raise + except socket.error, e: + err = 0 + if hasattr(e, 'args'): + err = getattr(e, 'args')[0] + else: + err = e.errno + if err == errno.ECONNREFUSED: # Connection refused + raise + except httplib.HTTPException: + # Just because the server closed the connection doesn't apparently mean + # that the server didn't send a response. + if hasattr(conn, 'sock') and conn.sock is None: + if i < RETRIES-1: + conn.close() + conn.connect() + continue + else: + conn.close() + raise + if i < RETRIES-1: + conn.close() + conn.connect() + continue + try: + response = conn.getresponse() + except httplib.BadStatusLine: + # If we get a BadStatusLine on the first try then that means + # the connection just went stale, so retry regardless of the + # number of RETRIES set. + if not seen_bad_status_line and i == 1: + i = 0 + seen_bad_status_line = True + conn.close() + conn.connect() + continue + else: + conn.close() + raise + except (socket.error, httplib.HTTPException): + if i < RETRIES-1: + conn.close() + conn.connect() + continue + else: + conn.close() + raise + else: + content = "" + if method == "HEAD": + conn.close() + else: + content = response.read() + response = Response(response) + if method != "HEAD": + content = _decompressContent(response, content) + break + return (response, content) + + + def _request(self, conn, host, absolute_uri, request_uri, method, body, headers, redirections, cachekey): + """Do the actual request using the connection object + and also follow one level of redirects if necessary""" + + auths = [(auth.depth(request_uri), auth) for auth in self.authorizations if auth.inscope(host, request_uri)] + auth = auths and sorted(auths)[0][1] or None + if auth: + auth.request(method, request_uri, headers, body) + + (response, content) = self._conn_request(conn, request_uri, method, body, headers) + + if auth: + if auth.response(response, body): + auth.request(method, request_uri, headers, body) + (response, content) = self._conn_request(conn, request_uri, method, body, headers ) + response._stale_digest = 1 + + if response.status == 401: + for authorization in self._auth_from_challenge(host, request_uri, headers, response, content): + authorization.request(method, request_uri, headers, body) + (response, content) = self._conn_request(conn, request_uri, method, body, headers, ) + if response.status != 401: + self.authorizations.append(authorization) + authorization.response(response, body) + break + + if (self.follow_all_redirects or (method in ["GET", "HEAD"]) or response.status == 303): + if self.follow_redirects and response.status in [300, 301, 302, 303, 307]: + # Pick out the location header and basically start from the beginning + # remembering first to strip the ETag header and decrement our 'depth' + if redirections: + if not response.has_key('location') and response.status != 300: + raise RedirectMissingLocation( _("Redirected but the response is missing a Location: header."), response, content) + # Fix-up relative redirects (which violate an RFC 2616 MUST) + if response.has_key('location'): + location = response['location'] + (scheme, authority, path, query, fragment) = parse_uri(location) + if authority == None: + response['location'] = urlparse.urljoin(absolute_uri, location) + if response.status == 301 and method in ["GET", "HEAD"]: + response['-x-permanent-redirect-url'] = response['location'] + if not response.has_key('content-location'): + response['content-location'] = absolute_uri + _updateCache(headers, response, content, self.cache, cachekey) + if headers.has_key('if-none-match'): + del headers['if-none-match'] + if headers.has_key('if-modified-since'): + del headers['if-modified-since'] + if 'authorization' in headers and not self.forward_authorization_headers: + del headers['authorization'] + if response.has_key('location'): + location = response['location'] + old_response = copy.deepcopy(response) + if not old_response.has_key('content-location'): + old_response['content-location'] = absolute_uri + redirect_method = method + if response.status in [302, 303]: + redirect_method = "GET" + body = None + (response, content) = self.request( + location, method=redirect_method, + body=body, headers=headers, + redirections=redirections - 1) + response.previous = old_response + else: + raise RedirectLimit("Redirected more times than rediection_limit allows.", response, content) + elif response.status in [200, 203] and method in ["GET", "HEAD"]: + # Don't cache 206's since we aren't going to handle byte range requests + if not response.has_key('content-location'): + response['content-location'] = absolute_uri + _updateCache(headers, response, content, self.cache, cachekey) + + return (response, content) + + def _normalize_headers(self, headers): + return _normalize_headers(headers) + +# Need to catch and rebrand some exceptions +# Then need to optionally turn all exceptions into status codes +# including all socket.* and httplib.* exceptions. + + + def request(self, uri, method="GET", body=None, headers=None, redirections=DEFAULT_MAX_REDIRECTS, connection_type=None): + """ Performs a single HTTP request. + + The 'uri' is the URI of the HTTP resource and can begin with either + 'http' or 'https'. The value of 'uri' must be an absolute URI. + + The 'method' is the HTTP method to perform, such as GET, POST, DELETE, + etc. There is no restriction on the methods allowed. + + The 'body' is the entity body to be sent with the request. It is a + string object. + + Any extra headers that are to be sent with the request should be + provided in the 'headers' dictionary. + + The maximum number of redirect to follow before raising an + exception is 'redirections. The default is 5. + + The return value is a tuple of (response, content), the first + being and instance of the 'Response' class, the second being + a string that contains the response entity body. + """ + try: + if headers is None: + headers = {} + else: + headers = self._normalize_headers(headers) + + if not headers.has_key('user-agent'): + headers['user-agent'] = "Python-httplib2/%s (gzip)" % __version__ + + uri = iri2uri(uri) + + (scheme, authority, request_uri, defrag_uri) = urlnorm(uri) + domain_port = authority.split(":")[0:2] + if len(domain_port) == 2 and domain_port[1] == '443' and scheme == 'http': + scheme = 'https' + authority = domain_port[0] + + proxy_info = self._get_proxy_info(scheme, authority) + + conn_key = scheme+":"+authority + if conn_key in self.connections: + conn = self.connections[conn_key] + else: + if not connection_type: + connection_type = SCHEME_TO_CONNECTION[scheme] + certs = list(self.certificates.iter(authority)) + if scheme == 'https': + if certs: + conn = self.connections[conn_key] = connection_type( + authority, key_file=certs[0][0], + cert_file=certs[0][1], timeout=self.timeout, + proxy_info=proxy_info, + ca_certs=self.ca_certs, + disable_ssl_certificate_validation= + self.disable_ssl_certificate_validation) + else: + conn = self.connections[conn_key] = connection_type( + authority, timeout=self.timeout, + proxy_info=proxy_info, + ca_certs=self.ca_certs, + disable_ssl_certificate_validation= + self.disable_ssl_certificate_validation) + else: + conn = self.connections[conn_key] = connection_type( + authority, timeout=self.timeout, + proxy_info=proxy_info) + conn.set_debuglevel(debuglevel) + + if 'range' not in headers and 'accept-encoding' not in headers: + headers['accept-encoding'] = 'gzip, deflate' + + info = email.Message.Message() + cached_value = None + if self.cache: + cachekey = defrag_uri + cached_value = self.cache.get(cachekey) + if cached_value: + # info = email.message_from_string(cached_value) + # + # Need to replace the line above with the kludge below + # to fix the non-existent bug not fixed in this + # bug report: http://mail.python.org/pipermail/python-bugs-list/2005-September/030289.html + try: + info, content = cached_value.split('\r\n\r\n', 1) + feedparser = email.FeedParser.FeedParser() + feedparser.feed(info) + info = feedparser.close() + feedparser._parse = None + except (IndexError, ValueError): + self.cache.delete(cachekey) + cachekey = None + cached_value = None + else: + cachekey = None + + if method in self.optimistic_concurrency_methods and self.cache and info.has_key('etag') and not self.ignore_etag and 'if-match' not in headers: + # http://www.w3.org/1999/04/Editing/ + headers['if-match'] = info['etag'] + + if method not in ["GET", "HEAD"] and self.cache and cachekey: + # RFC 2616 Section 13.10 + self.cache.delete(cachekey) + + # Check the vary header in the cache to see if this request + # matches what varies in the cache. + if method in ['GET', 'HEAD'] and 'vary' in info: + vary = info['vary'] + vary_headers = vary.lower().replace(' ', '').split(',') + for header in vary_headers: + key = '-varied-%s' % header + value = info[key] + if headers.get(header, None) != value: + cached_value = None + break + + if cached_value and method in ["GET", "HEAD"] and self.cache and 'range' not in headers: + if info.has_key('-x-permanent-redirect-url'): + # Should cached permanent redirects be counted in our redirection count? For now, yes. + if redirections <= 0: + raise RedirectLimit("Redirected more times than rediection_limit allows.", {}, "") + (response, new_content) = self.request( + info['-x-permanent-redirect-url'], method='GET', + headers=headers, redirections=redirections - 1) + response.previous = Response(info) + response.previous.fromcache = True + else: + # Determine our course of action: + # Is the cached entry fresh or stale? + # Has the client requested a non-cached response? + # + # There seems to be three possible answers: + # 1. [FRESH] Return the cache entry w/o doing a GET + # 2. [STALE] Do the GET (but add in cache validators if available) + # 3. [TRANSPARENT] Do a GET w/o any cache validators (Cache-Control: no-cache) on the request + entry_disposition = _entry_disposition(info, headers) + + if entry_disposition == "FRESH": + if not cached_value: + info['status'] = '504' + content = "" + response = Response(info) + if cached_value: + response.fromcache = True + return (response, content) + + if entry_disposition == "STALE": + if info.has_key('etag') and not self.ignore_etag and not 'if-none-match' in headers: + headers['if-none-match'] = info['etag'] + if info.has_key('last-modified') and not 'last-modified' in headers: + headers['if-modified-since'] = info['last-modified'] + elif entry_disposition == "TRANSPARENT": + pass + + (response, new_content) = self._request(conn, authority, uri, request_uri, method, body, headers, redirections, cachekey) + + if response.status == 304 and method == "GET": + # Rewrite the cache entry with the new end-to-end headers + # Take all headers that are in response + # and overwrite their values in info. + # unless they are hop-by-hop, or are listed in the connection header. + + for key in _get_end2end_headers(response): + info[key] = response[key] + merged_response = Response(info) + if hasattr(response, "_stale_digest"): + merged_response._stale_digest = response._stale_digest + _updateCache(headers, merged_response, content, self.cache, cachekey) + response = merged_response + response.status = 200 + response.fromcache = True + + elif response.status == 200: + content = new_content + else: + self.cache.delete(cachekey) + content = new_content + else: + cc = _parse_cache_control(headers) + if cc.has_key('only-if-cached'): + info['status'] = '504' + response = Response(info) + content = "" + else: + (response, content) = self._request(conn, authority, uri, request_uri, method, body, headers, redirections, cachekey) + except Exception, e: + if self.force_exception_to_status_code: + if isinstance(e, HttpLib2ErrorWithResponse): + response = e.response + content = e.content + response.status = 500 + response.reason = str(e) + elif isinstance(e, socket.timeout): + content = "Request Timeout" + response = Response({ + "content-type": "text/plain", + "status": "408", + "content-length": len(content) + }) + response.reason = "Request Timeout" + else: + content = str(e) + response = Response({ + "content-type": "text/plain", + "status": "400", + "content-length": len(content) + }) + response.reason = "Bad Request" + else: + raise + + + return (response, content) + + def _get_proxy_info(self, scheme, authority): + """Return a ProxyInfo instance (or None) based on the scheme + and authority. + """ + hostname, port = urllib.splitport(authority) + proxy_info = self.proxy_info + if callable(proxy_info): + proxy_info = proxy_info(scheme) + + if (hasattr(proxy_info, 'applies_to') + and not proxy_info.applies_to(hostname)): + proxy_info = None + return proxy_info + + +class Response(dict): + """An object more like email.Message than httplib.HTTPResponse.""" + + """Is this response from our local cache""" + fromcache = False + + """HTTP protocol version used by server. 10 for HTTP/1.0, 11 for HTTP/1.1. """ + version = 11 + + "Status code returned by server. " + status = 200 + + """Reason phrase returned by server.""" + reason = "Ok" + + previous = None + + def __init__(self, info): + # info is either an email.Message or + # an httplib.HTTPResponse object. + if isinstance(info, httplib.HTTPResponse): + for key, value in info.getheaders(): + self[key.lower()] = value + self.status = info.status + self['status'] = str(self.status) + self.reason = info.reason + self.version = info.version + elif isinstance(info, email.Message.Message): + for key, value in info.items(): + self[key.lower()] = value + self.status = int(self['status']) + else: + for key, value in info.iteritems(): + self[key.lower()] = value + self.status = int(self.get('status', self.status)) + self.reason = self.get('reason', self.reason) + + + def __getattr__(self, name): + if name == 'dict': + return self + else: + raise AttributeError, name diff --git a/appengine/dashdemo-cached/httplib2/cacerts.txt b/appengine/dashdemo-cached/httplib2/cacerts.txt new file mode 100644 index 0000000..70990f1 --- /dev/null +++ b/appengine/dashdemo-cached/httplib2/cacerts.txt @@ -0,0 +1,2183 @@ +# Issuer: CN=GTE CyberTrust Global Root O=GTE Corporation OU=GTE CyberTrust Solutions, Inc. +# Subject: CN=GTE CyberTrust Global Root O=GTE Corporation OU=GTE CyberTrust Solutions, Inc. +# Label: "GTE CyberTrust Global Root" +# Serial: 421 +# MD5 Fingerprint: ca:3d:d3:68:f1:03:5c:d0:32:fa:b8:2b:59:e8:5a:db +# SHA1 Fingerprint: 97:81:79:50:d8:1c:96:70:cc:34:d8:09:cf:79:44:31:36:7e:f4:74 +# SHA256 Fingerprint: a5:31:25:18:8d:21:10:aa:96:4b:02:c7:b7:c6:da:32:03:17:08:94:e5:fb:71:ff:fb:66:67:d5:e6:81:0a:36 +-----BEGIN CERTIFICATE----- +MIICWjCCAcMCAgGlMA0GCSqGSIb3DQEBBAUAMHUxCzAJBgNVBAYTAlVTMRgwFgYD +VQQKEw9HVEUgQ29ycG9yYXRpb24xJzAlBgNVBAsTHkdURSBDeWJlclRydXN0IFNv +bHV0aW9ucywgSW5jLjEjMCEGA1UEAxMaR1RFIEN5YmVyVHJ1c3QgR2xvYmFsIFJv +b3QwHhcNOTgwODEzMDAyOTAwWhcNMTgwODEzMjM1OTAwWjB1MQswCQYDVQQGEwJV +UzEYMBYGA1UEChMPR1RFIENvcnBvcmF0aW9uMScwJQYDVQQLEx5HVEUgQ3liZXJU +cnVzdCBTb2x1dGlvbnMsIEluYy4xIzAhBgNVBAMTGkdURSBDeWJlclRydXN0IEds +b2JhbCBSb290MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCVD6C28FCc6HrH +iM3dFw4usJTQGz0O9pTAipTHBsiQl8i4ZBp6fmw8U+E3KHNgf7KXUwefU/ltWJTS +r41tiGeA5u2ylc9yMcqlHHK6XALnZELn+aks1joNrI1CqiQBOeacPwGFVw1Yh0X4 +04Wqk2kmhXBIgD8SFcd5tB8FLztimQIDAQABMA0GCSqGSIb3DQEBBAUAA4GBAG3r +GwnpXtlR22ciYaQqPEh346B8pt5zohQDhT37qw4wxYMWM4ETCJ57NE7fQMh017l9 +3PR2VX2bY1QY6fDq81yx2YtCHrnAlU66+tXifPVoYb+O7AWXX1uw16OFNMQkpw0P +lZPvy5TYnh+dXIVtx6quTx8itc2VrbqnzPmrC3p/ +-----END CERTIFICATE----- + +# Issuer: CN=Thawte Server CA O=Thawte Consulting cc OU=Certification Services Division +# Subject: CN=Thawte Server CA O=Thawte Consulting cc OU=Certification Services Division +# Label: "Thawte Server CA" +# Serial: 1 +# MD5 Fingerprint: c5:70:c4:a2:ed:53:78:0c:c8:10:53:81:64:cb:d0:1d +# SHA1 Fingerprint: 23:e5:94:94:51:95:f2:41:48:03:b4:d5:64:d2:a3:a3:f5:d8:8b:8c +# SHA256 Fingerprint: b4:41:0b:73:e2:e6:ea:ca:47:fb:c4:2f:8f:a4:01:8a:f4:38:1d:c5:4c:fa:a8:44:50:46:1e:ed:09:45:4d:e9 +-----BEGIN CERTIFICATE----- +MIIDEzCCAnygAwIBAgIBATANBgkqhkiG9w0BAQQFADCBxDELMAkGA1UEBhMCWkEx +FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYD +VQQKExRUaGF3dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlv +biBTZXJ2aWNlcyBEaXZpc2lvbjEZMBcGA1UEAxMQVGhhd3RlIFNlcnZlciBDQTEm +MCQGCSqGSIb3DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5jb20wHhcNOTYwODAx +MDAwMDAwWhcNMjAxMjMxMjM1OTU5WjCBxDELMAkGA1UEBhMCWkExFTATBgNVBAgT +DFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYDVQQKExRUaGF3 +dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNl +cyBEaXZpc2lvbjEZMBcGA1UEAxMQVGhhd3RlIFNlcnZlciBDQTEmMCQGCSqGSIb3 +DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQAD +gY0AMIGJAoGBANOkUG7I/1Zr5s9dtuoMaHVHoqrC2oQl/Kj0R1HahbUgdJSGHg91 +yekIYfUGbTBuFRkC6VLAYttNmZ7iagxEOM3+vuNkCXDF/rFrKbYvScg71CcEJRCX +L+eQbcAoQpnXTEPew/UhbVSfXcNY4cDk2VuwuNy0e982OsK1ZiIS1ocNAgMBAAGj +EzARMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEEBQADgYEAB/pMaVz7lcxG +7oWDTSEwjsrZqG9JGubaUeNgcGyEYRGhGshIPllDfU+VPaGLtwtimHp1it2ITk6e +QNuozDJ0uW8NxuOzRAvZim+aKZuZGCg70eNAKJpaPNW15yAbi8qkq43pUdniTCxZ +qdq5snUb9kLy78fyGPmJvKP/iiMucEc= +-----END CERTIFICATE----- + +# Issuer: CN=Thawte Premium Server CA O=Thawte Consulting cc OU=Certification Services Division +# Subject: CN=Thawte Premium Server CA O=Thawte Consulting cc OU=Certification Services Division +# Label: "Thawte Premium Server CA" +# Serial: 1 +# MD5 Fingerprint: 06:9f:69:79:16:66:90:02:1b:8c:8c:a2:c3:07:6f:3a +# SHA1 Fingerprint: 62:7f:8d:78:27:65:63:99:d2:7d:7f:90:44:c9:fe:b3:f3:3e:fa:9a +# SHA256 Fingerprint: ab:70:36:36:5c:71:54:aa:29:c2:c2:9f:5d:41:91:16:3b:16:2a:22:25:01:13:57:d5:6d:07:ff:a7:bc:1f:72 +-----BEGIN CERTIFICATE----- +MIIDJzCCApCgAwIBAgIBATANBgkqhkiG9w0BAQQFADCBzjELMAkGA1UEBhMCWkEx +FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYD +VQQKExRUaGF3dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlv +biBTZXJ2aWNlcyBEaXZpc2lvbjEhMB8GA1UEAxMYVGhhd3RlIFByZW1pdW0gU2Vy +dmVyIENBMSgwJgYJKoZIhvcNAQkBFhlwcmVtaXVtLXNlcnZlckB0aGF3dGUuY29t +MB4XDTk2MDgwMTAwMDAwMFoXDTIwMTIzMTIzNTk1OVowgc4xCzAJBgNVBAYTAlpB +MRUwEwYDVQQIEwxXZXN0ZXJuIENhcGUxEjAQBgNVBAcTCUNhcGUgVG93bjEdMBsG +A1UEChMUVGhhd3RlIENvbnN1bHRpbmcgY2MxKDAmBgNVBAsTH0NlcnRpZmljYXRp +b24gU2VydmljZXMgRGl2aXNpb24xITAfBgNVBAMTGFRoYXd0ZSBQcmVtaXVtIFNl +cnZlciBDQTEoMCYGCSqGSIb3DQEJARYZcHJlbWl1bS1zZXJ2ZXJAdGhhd3RlLmNv +bTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA0jY2aovXwlue2oFBYo847kkE +VdbQ7xwblRZH7xhINTpS9CtqBo87L+pW46+GjZ4X9560ZXUCTe/LCaIhUdib0GfQ +ug2SBhRz1JPLlyoAnFxODLz6FVL88kRu2hFKbgifLy3j+ao6hnO2RlNYyIkFvYMR +uHM/qgeN9EJN50CdHDcCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG +9w0BAQQFAAOBgQAmSCwWwlj66BZ0DKqqX1Q/8tfJeGBeXm43YyJ3Nn6yF8Q0ufUI +hfzJATj/Tb7yFkJD57taRvvBxhEf8UqwKEbJw8RCfbz6q1lu1bdRiBHjpIUZa4JM +pAwSremkrj/xw0llmozFyD4lt5SZu5IycQfwhl7tUCemDaYj+bvLpgcUQg== +-----END CERTIFICATE----- + +# Issuer: O=Equifax OU=Equifax Secure Certificate Authority +# Subject: O=Equifax OU=Equifax Secure Certificate Authority +# Label: "Equifax Secure CA" +# Serial: 903804111 +# MD5 Fingerprint: 67:cb:9d:c0:13:24:8a:82:9b:b2:17:1e:d1:1b:ec:d4 +# SHA1 Fingerprint: d2:32:09:ad:23:d3:14:23:21:74:e4:0d:7f:9d:62:13:97:86:63:3a +# SHA256 Fingerprint: 08:29:7a:40:47:db:a2:36:80:c7:31:db:6e:31:76:53:ca:78:48:e1:be:bd:3a:0b:01:79:a7:07:f9:2c:f1:78 +-----BEGIN CERTIFICATE----- +MIIDIDCCAomgAwIBAgIENd70zzANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJV +UzEQMA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2Vy +dGlmaWNhdGUgQXV0aG9yaXR5MB4XDTk4MDgyMjE2NDE1MVoXDTE4MDgyMjE2NDE1 +MVowTjELMAkGA1UEBhMCVVMxEDAOBgNVBAoTB0VxdWlmYXgxLTArBgNVBAsTJEVx +dWlmYXggU2VjdXJlIENlcnRpZmljYXRlIEF1dGhvcml0eTCBnzANBgkqhkiG9w0B +AQEFAAOBjQAwgYkCgYEAwV2xWGcIYu6gmi0fCG2RFGiYCh7+2gRvE4RiIcPRfM6f +BeC4AfBONOziipUEZKzxa1NfBbPLZ4C/QgKO/t0BCezhABRP/PvwDN1Dulsr4R+A +cJkVV5MW8Q+XarfCaCMczE1ZMKxRHjuvK9buY0V7xdlfUNLjUA86iOe/FP3gx7kC +AwEAAaOCAQkwggEFMHAGA1UdHwRpMGcwZaBjoGGkXzBdMQswCQYDVQQGEwJVUzEQ +MA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2VydGlm +aWNhdGUgQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMBoGA1UdEAQTMBGBDzIwMTgw +ODIyMTY0MTUxWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUSOZo+SvSspXXR9gj +IBBPM5iQn9QwHQYDVR0OBBYEFEjmaPkr0rKV10fYIyAQTzOYkJ/UMAwGA1UdEwQF +MAMBAf8wGgYJKoZIhvZ9B0EABA0wCxsFVjMuMGMDAgbAMA0GCSqGSIb3DQEBBQUA +A4GBAFjOKer89961zgK5F7WF0bnj4JXMJTENAKaSbn+2kmOeUJXRmm/kEd5jhW6Y +7qj/WsjTVbJmcVfewCHrPSqnI0kBBIZCe/zuf6IWUrVnZ9NA2zsmWLIodz2uFHdh +1voqZiegDfqnc1zqcPGUIWVEX/r87yloqaKHee9570+sB3c4 +-----END CERTIFICATE----- + +# Issuer: O=VeriSign, Inc. OU=Class 3 Public Primary Certification Authority +# Subject: O=VeriSign, Inc. OU=Class 3 Public Primary Certification Authority +# Label: "Verisign Class 3 Public Primary Certification Authority" +# Serial: 149843929435818692848040365716851702463 +# MD5 Fingerprint: 10:fc:63:5d:f6:26:3e:0d:f3:25:be:5f:79:cd:67:67 +# SHA1 Fingerprint: 74:2c:31:92:e6:07:e4:24:eb:45:49:54:2b:e1:bb:c5:3e:61:74:e2 +# SHA256 Fingerprint: e7:68:56:34:ef:ac:f6:9a:ce:93:9a:6b:25:5b:7b:4f:ab:ef:42:93:5b:50:a2:65:ac:b5:cb:60:27:e4:4e:70 +-----BEGIN CERTIFICATE----- +MIICPDCCAaUCEHC65B0Q2Sk0tjjKewPMur8wDQYJKoZIhvcNAQECBQAwXzELMAkG +A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz +cyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2 +MDEyOTAwMDAwMFoXDTI4MDgwMTIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV +BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmlt +YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN +ADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhE +BarsAx94f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/is +I19wKTakyYbnsZogy1Olhec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0G +CSqGSIb3DQEBAgUAA4GBALtMEivPLCYATxQT3ab7/AoRhIzzKBxnki98tsX63/Do +lbwdj2wsqFHMc9ikwFPwTtYmwHYBV4GSXiHx0bH/59AhWM1pF+NEHJwZRDmJXNyc +AA9WjQKZ7aKQRUzkuxCkPfAyAw7xzvjoyVGM5mKf5p/AfbdynMk2OmufTqj/ZA1k +-----END CERTIFICATE----- + +# Issuer: O=VeriSign, Inc. OU=Class 3 Public Primary Certification Authority - G2/(c) 1998 VeriSign, Inc. - For authorized use only/VeriSign Trust Network +# Subject: O=VeriSign, Inc. OU=Class 3 Public Primary Certification Authority - G2/(c) 1998 VeriSign, Inc. - For authorized use only/VeriSign Trust Network +# Label: "Verisign Class 3 Public Primary Certification Authority - G2" +# Serial: 167285380242319648451154478808036881606 +# MD5 Fingerprint: a2:33:9b:4c:74:78:73:d4:6c:e7:c1:f3:8d:cb:5c:e9 +# SHA1 Fingerprint: 85:37:1c:a6:e5:50:14:3d:ce:28:03:47:1b:de:3a:09:e8:f8:77:0f +# SHA256 Fingerprint: 83:ce:3c:12:29:68:8a:59:3d:48:5f:81:97:3c:0f:91:95:43:1e:da:37:cc:5e:36:43:0e:79:c7:a8:88:63:8b +-----BEGIN CERTIFICATE----- +MIIDAjCCAmsCEH3Z/gfPqB63EHln+6eJNMYwDQYJKoZIhvcNAQEFBQAwgcExCzAJ +BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xh +c3MgMyBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcy +MTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3Jp +emVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMB4X +DTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVTMRcw +FQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMyBQdWJsaWMg +UHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEo +YykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5 +MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEB +AQUAA4GNADCBiQKBgQDMXtERXVxp0KvTuWpMmR9ZmDCOFoUgRm1HP9SFIIThbbP4 +pO0M8RcPO/mn+SXXwc+EY/J8Y8+iR/LGWzOOZEAEaMGAuWQcRXfH2G71lSk8UOg0 +13gfqLptQ5GVj0VXXn7F+8qkBOvqlzdUMG+7AUcyM83cV5tkaWH4mx0ciU9cZwID +AQABMA0GCSqGSIb3DQEBBQUAA4GBAFFNzb5cy5gZnBWyATl4Lk0PZ3BwmcYQWpSk +U01UbSuvDV1Ai2TT1+7eVmGSX6bEHRBhNtMsJzzoKQm5EWR0zLVznxxIqbxhAe7i +F6YM40AIOw7n60RzKprxaZLvcRTDOaxxp5EJb+RxBrO6WVcmeQD2+A2iMzAo1KpY +oJ2daZH9 +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign Root CA O=GlobalSign nv-sa OU=Root CA +# Subject: CN=GlobalSign Root CA O=GlobalSign nv-sa OU=Root CA +# Label: "GlobalSign Root CA" +# Serial: 4835703278459707669005204 +# MD5 Fingerprint: 3e:45:52:15:09:51:92:e1:b7:5d:37:9f:b1:87:29:8a +# SHA1 Fingerprint: b1:bc:96:8b:d4:f4:9d:62:2a:a8:9a:81:f2:15:01:52:a4:1d:82:9c +# SHA256 Fingerprint: eb:d4:10:40:e4:bb:3e:c7:42:c9:e3:81:d3:1e:f2:a4:1a:48:b6:68:5c:96:e7:ce:f3:c1:df:6c:d4:33:1c:99 +-----BEGIN CERTIFICATE----- +MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG +A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv +b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw +MDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i +YWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT +aWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ +jc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp +xy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp +1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG +snUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ +U26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8 +9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B +AQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz +yj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE +38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP +AbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad +DKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME +HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A== +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R2 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R2 +# Label: "GlobalSign Root CA - R2" +# Serial: 4835703278459682885658125 +# MD5 Fingerprint: 94:14:77:7e:3e:5e:fd:8f:30:bd:41:b0:cf:e7:d0:30 +# SHA1 Fingerprint: 75:e0:ab:b6:13:85:12:27:1c:04:f8:5f:dd:de:38:e4:b7:24:2e:fe +# SHA256 Fingerprint: ca:42:dd:41:74:5f:d0:b8:1e:b9:02:36:2c:f9:d8:bf:71:9d:a1:bd:1b:1e:fc:94:6f:5b:4c:99:f4:2c:1b:9e +-----BEGIN CERTIFICATE----- +MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4G +A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNp +Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1 +MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEG +A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6ErPL +v4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8 +eoLrvozps6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklq +tTleiDTsvHgMCJiEbKjNS7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzd +C9XZzPnqJworc5HGnRusyMvo4KD0L5CLTfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pa +zq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6CygPCm48CAwEAAaOBnDCB +mTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm+IH +V2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5n +bG9iYWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG +3lm0mi3f3BmGLjANBgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4Gs +J0/WwbgcQ3izDJr86iw8bmEbTUsp9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO +291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavS +ot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG79G+dwfCMNYxd +AfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7 +TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg== +-----END CERTIFICATE----- + +# Issuer: CN=http://www.valicert.com/ O=ValiCert, Inc. OU=ValiCert Class 1 Policy Validation Authority +# Subject: CN=http://www.valicert.com/ O=ValiCert, Inc. OU=ValiCert Class 1 Policy Validation Authority +# Label: "ValiCert Class 1 VA" +# Serial: 1 +# MD5 Fingerprint: 65:58:ab:15:ad:57:6c:1e:a8:a7:b5:69:ac:bf:ff:eb +# SHA1 Fingerprint: e5:df:74:3c:b6:01:c4:9b:98:43:dc:ab:8c:e8:6a:81:10:9f:e4:8e +# SHA256 Fingerprint: f4:c1:49:55:1a:30:13:a3:5b:c7:bf:fe:17:a7:f3:44:9b:c1:ab:5b:5a:0a:e7:4b:06:c2:3b:90:00:4c:01:04 +-----BEGIN CERTIFICATE----- +MIIC5zCCAlACAQEwDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0 +IFZhbGlkYXRpb24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAz +BgNVBAsTLFZhbGlDZXJ0IENsYXNzIDEgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9y +aXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG +9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTk5MDYyNTIyMjM0OFoXDTE5MDYy +NTIyMjM0OFowgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRpb24gTmV0d29y +azEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENs +YXNzIDEgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRw +Oi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNl +cnQuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDYWYJ6ibiWuqYvaG9Y +LqdUHAZu9OqNSLwxlBfw8068srg1knaw0KWlAdcAAxIiGQj4/xEjm84H9b9pGib+ +TunRf50sQB1ZaG6m+FiwnRqP0z/x3BkGgagO4DrdyFNFCQbmD3DD+kCmDuJWBQ8Y +TfwggtFzVXSNdnKgHZ0dwN0/cQIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAFBoPUn0 +LBwGlN+VYH+Wexf+T3GtZMjdd9LvWVXoP+iOBSoh8gfStadS/pyxtuJbdxdA6nLW +I8sogTLDAHkY7FkXicnGah5xyf23dKUlRWnFSKsZ4UWKJWsZ7uW7EvV/96aNUcPw +nXS3qT6gpf+2SQMT2iLM7XGCK5nPOrf1LXLI +-----END CERTIFICATE----- + +# Issuer: CN=http://www.valicert.com/ O=ValiCert, Inc. OU=ValiCert Class 2 Policy Validation Authority +# Subject: CN=http://www.valicert.com/ O=ValiCert, Inc. OU=ValiCert Class 2 Policy Validation Authority +# Label: "ValiCert Class 2 VA" +# Serial: 1 +# MD5 Fingerprint: a9:23:75:9b:ba:49:36:6e:31:c2:db:f2:e7:66:ba:87 +# SHA1 Fingerprint: 31:7a:2a:d0:7f:2b:33:5e:f5:a1:c3:4e:4b:57:e8:b7:d8:f1:fc:a6 +# SHA256 Fingerprint: 58:d0:17:27:9c:d4:dc:63:ab:dd:b1:96:a6:c9:90:6c:30:c4:e0:87:83:ea:e8:c1:60:99:54:d6:93:55:59:6b +-----BEGIN CERTIFICATE----- +MIIC5zCCAlACAQEwDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0 +IFZhbGlkYXRpb24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAz +BgNVBAsTLFZhbGlDZXJ0IENsYXNzIDIgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9y +aXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG +9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTk5MDYyNjAwMTk1NFoXDTE5MDYy +NjAwMTk1NFowgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRpb24gTmV0d29y +azEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENs +YXNzIDIgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRw +Oi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNl +cnQuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDOOnHK5avIWZJV16vY +dA757tn2VUdZZUcOBVXc65g2PFxTXdMwzzjsvUGJ7SVCCSRrCl6zfN1SLUzm1NZ9 +WlmpZdRJEy0kTRxQb7XBhVQ7/nHk01xC+YDgkRoKWzk2Z/M/VXwbP7RfZHM047QS +v4dk+NoS/zcnwbNDu+97bi5p9wIDAQABMA0GCSqGSIb3DQEBBQUAA4GBADt/UG9v +UJSZSWI4OB9L+KXIPqeCgfYrx+jFzug6EILLGACOTb2oWH+heQC1u+mNr0HZDzTu +IYEZoDJJKPTEjlbVUjP9UNV+mWwD5MlM/Mtsq2azSiGM5bUMMj4QssxsodyamEwC +W/POuZ6lcg5Ktz885hZo+L7tdEy8W9ViH0Pd +-----END CERTIFICATE----- + +# Issuer: CN=http://www.valicert.com/ O=ValiCert, Inc. OU=ValiCert Class 3 Policy Validation Authority +# Subject: CN=http://www.valicert.com/ O=ValiCert, Inc. OU=ValiCert Class 3 Policy Validation Authority +# Label: "RSA Root Certificate 1" +# Serial: 1 +# MD5 Fingerprint: a2:6f:53:b7:ee:40:db:4a:68:e7:fa:18:d9:10:4b:72 +# SHA1 Fingerprint: 69:bd:8c:f4:9c:d3:00:fb:59:2e:17:93:ca:55:6a:f3:ec:aa:35:fb +# SHA256 Fingerprint: bc:23:f9:8a:31:3c:b9:2d:e3:bb:fc:3a:5a:9f:44:61:ac:39:49:4c:4a:e1:5a:9e:9d:f1:31:e9:9b:73:01:9a +-----BEGIN CERTIFICATE----- +MIIC5zCCAlACAQEwDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0 +IFZhbGlkYXRpb24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAz +BgNVBAsTLFZhbGlDZXJ0IENsYXNzIDMgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9y +aXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG +9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTk5MDYyNjAwMjIzM1oXDTE5MDYy +NjAwMjIzM1owgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRpb24gTmV0d29y +azEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENs +YXNzIDMgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRw +Oi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNl +cnQuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDjmFGWHOjVsQaBalfD +cnWTq8+epvzzFlLWLU2fNUSoLgRNB0mKOCn1dzfnt6td3zZxFJmP3MKS8edgkpfs +2Ejcv8ECIMYkpChMMFp2bbFc893enhBxoYjHW5tBbcqwuI4V7q0zK89HBFx1cQqY +JJgpp0lZpd34t0NiYfPT4tBVPwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAFa7AliE +Zwgs3x/be0kz9dNnnfS0ChCzycUs4pJqcXgn8nCDQtM+z6lU9PHYkhaM0QTLS6vJ +n0WuPIqpsHEzXcjFV9+vqDWzf4mH6eglkrh/hXqu1rweN1gqZ8mRzyqBPu3GOd/A +PhmcGcwTTYJBtYze4D1gCCAPRX5ron+jjBXu +-----END CERTIFICATE----- + +# Issuer: CN=VeriSign Class 3 Public Primary Certification Authority - G3 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 1999 VeriSign, Inc. - For authorized use only +# Subject: CN=VeriSign Class 3 Public Primary Certification Authority - G3 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 1999 VeriSign, Inc. - For authorized use only +# Label: "Verisign Class 3 Public Primary Certification Authority - G3" +# Serial: 206684696279472310254277870180966723415 +# MD5 Fingerprint: cd:68:b6:a7:c7:c4:ce:75:e0:1d:4f:57:44:61:92:09 +# SHA1 Fingerprint: 13:2d:0d:45:53:4b:69:97:cd:b2:d5:c3:39:e2:55:76:60:9b:5c:c6 +# SHA256 Fingerprint: eb:04:cf:5e:b1:f3:9a:fa:76:2f:2b:b1:20:f2:96:cb:a5:20:c1:b9:7d:b1:58:95:65:b8:1c:b9:a1:7b:72:44 +-----BEGIN CERTIFICATE----- +MIIEGjCCAwICEQCbfgZJoz5iudXukEhxKe9XMA0GCSqGSIb3DQEBBQUAMIHKMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl +cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu +LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT +aWduIENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD +VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT +aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ +bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu +IENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg +LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMu6nFL8eB8aHm8b +N3O9+MlrlBIwT/A2R/XQkQr1F8ilYcEWQE37imGQ5XYgwREGfassbqb1EUGO+i2t +KmFZpGcmTNDovFJbcCAEWNF6yaRpvIMXZK0Fi7zQWM6NjPXr8EJJC52XJ2cybuGu +kxUccLwgTS8Y3pKI6GyFVxEa6X7jJhFUokWWVYPKMIno3Nij7SqAP395ZVc+FSBm +CC+Vk7+qRy+oRpfwEuL+wgorUeZ25rdGt+INpsyow0xZVYnm6FNcHOqd8GIWC6fJ +Xwzw3sJ2zq/3avL6QaaiMxTJ5Xpj055iN9WFZZ4O5lMkdBteHRJTW8cs54NJOxWu +imi5V5cCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAERSWwauSCPc/L8my/uRan2Te +2yFPhpk0djZX3dAVL8WtfxUfN2JzPtTnX84XA9s1+ivbrmAJXx5fj267Cz3qWhMe +DGBvtcC1IyIuBwvLqXTLR7sdwdela8wv0kL9Sd2nic9TutoAWii/gt/4uhMdUIaC +/Y4wjylGsB49Ndo4YhYYSq3mtlFs3q9i6wHQHiT+eo8SGhJouPtmmRQURVyu565p +F4ErWjfJXir0xuKhXFSbplQAz/DxwceYMBo7Nhbbo27q/a2ywtrvAkcTisDxszGt +TxzhT5yvDwyd93gN2PQ1VoDat20Xj50egWTh/sVFuq1ruQp6Tk9LhO5L8X3dEQ== +-----END CERTIFICATE----- + +# Issuer: CN=VeriSign Class 4 Public Primary Certification Authority - G3 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 1999 VeriSign, Inc. - For authorized use only +# Subject: CN=VeriSign Class 4 Public Primary Certification Authority - G3 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 1999 VeriSign, Inc. - For authorized use only +# Label: "Verisign Class 4 Public Primary Certification Authority - G3" +# Serial: 314531972711909413743075096039378935511 +# MD5 Fingerprint: db:c8:f2:27:2e:b1:ea:6a:29:23:5d:fe:56:3e:33:df +# SHA1 Fingerprint: c8:ec:8c:87:92:69:cb:4b:ab:39:e9:8d:7e:57:67:f3:14:95:73:9d +# SHA256 Fingerprint: e3:89:36:0d:0f:db:ae:b3:d2:50:58:4b:47:30:31:4e:22:2f:39:c1:56:a0:20:14:4e:8d:96:05:61:79:15:06 +-----BEGIN CERTIFICATE----- +MIIEGjCCAwICEQDsoKeLbnVqAc/EfMwvlF7XMA0GCSqGSIb3DQEBBQUAMIHKMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl +cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu +LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT +aWduIENsYXNzIDQgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD +VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT +aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ +bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu +IENsYXNzIDQgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg +LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK3LpRFpxlmr8Y+1 +GQ9Wzsy1HyDkniYlS+BzZYlZ3tCD5PUPtbut8XzoIfzk6AzufEUiGXaStBO3IFsJ ++mGuqPKljYXCKtbeZjbSmwL0qJJgfJxptI8kHtCGUvYynEFYHiK9zUVilQhu0Gbd +U6LM8BDcVHOLBKFGMzNcF0C5nk3T875Vg+ixiY5afJqWIpA7iCXy0lOIAgwLePLm +NxdLMEYH5IBtptiWLugs+BGzOA1mppvqySNb247i8xOOGlktqgLw7KSHZtzBP/XY +ufTsgsbSPZUd5cBPhMnZo0QoBmrXRazwa2rvTl/4EYIeOGM0ZlDUPpNz+jDDZq3/ +ky2X7wMCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAj/ola09b5KROJ1WrIhVZPMq1 +CtRK26vdoV9TxaBXOcLORyu+OshWv8LZJxA6sQU8wHcxuzrTBXttmhwwjIDLk5Mq +g6sFUYICABFna/OIYUdfA5PVWw3g8dShMjWFsjrbsIKr0csKvE+MW8VLADsfKoKm +fjaF3H48ZwC15DtS4KjrXRX5xm3wrR0OhbepmnMUWluPQSjA1egtTaRezarZ7c7c +2NU8Qh0XwRJdRTjDOPP8hS6DRkiy1yBfkjaP53kPmF6Z6PDQpLv1U70qzlmwr25/ +bLvSHgCwIe34QWKCudiyxLtGUPMxxY8BqHTr9Xgn2uf3ZkPznoM+IKrDNWCRzg== +-----END CERTIFICATE----- + +# Issuer: CN=Entrust.net Secure Server Certification Authority O=Entrust.net OU=www.entrust.net/CPS incorp. by ref. (limits liab.)/(c) 1999 Entrust.net Limited +# Subject: CN=Entrust.net Secure Server Certification Authority O=Entrust.net OU=www.entrust.net/CPS incorp. by ref. (limits liab.)/(c) 1999 Entrust.net Limited +# Label: "Entrust.net Secure Server CA" +# Serial: 927650371 +# MD5 Fingerprint: df:f2:80:73:cc:f1:e6:61:73:fc:f5:42:e9:c5:7c:ee +# SHA1 Fingerprint: 99:a6:9b:e6:1a:fe:88:6b:4d:2b:82:00:7c:b8:54:fc:31:7e:15:39 +# SHA256 Fingerprint: 62:f2:40:27:8c:56:4c:4d:d8:bf:7d:9d:4f:6f:36:6e:a8:94:d2:2f:5f:34:d9:89:a9:83:ac:ec:2f:ff:ed:50 +-----BEGIN CERTIFICATE----- +MIIE2DCCBEGgAwIBAgIEN0rSQzANBgkqhkiG9w0BAQUFADCBwzELMAkGA1UEBhMC +VVMxFDASBgNVBAoTC0VudHJ1c3QubmV0MTswOQYDVQQLEzJ3d3cuZW50cnVzdC5u +ZXQvQ1BTIGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxpYWIuKTElMCMGA1UECxMc +KGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDE6MDgGA1UEAxMxRW50cnVzdC5u +ZXQgU2VjdXJlIFNlcnZlciBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw05OTA1 +MjUxNjA5NDBaFw0xOTA1MjUxNjM5NDBaMIHDMQswCQYDVQQGEwJVUzEUMBIGA1UE +ChMLRW50cnVzdC5uZXQxOzA5BgNVBAsTMnd3dy5lbnRydXN0Lm5ldC9DUFMgaW5j +b3JwLiBieSByZWYuIChsaW1pdHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMTk5OSBF +bnRydXN0Lm5ldCBMaW1pdGVkMTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUg +U2VydmVyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGdMA0GCSqGSIb3DQEBAQUA +A4GLADCBhwKBgQDNKIM0VBuJ8w+vN5Ex/68xYMmo6LIQaO2f55M28Qpku0f1BBc/ +I0dNxScZgSYMVHINiC3ZH5oSn7yzcdOAGT9HZnuMNSjSuQrfJNqc1lB5gXpa0zf3 +wkrYKZImZNHkmGw6AIr1NJtl+O3jEP/9uElY3KDegjlrgbEWGWG5VLbmQwIBA6OC +AdcwggHTMBEGCWCGSAGG+EIBAQQEAwIABzCCARkGA1UdHwSCARAwggEMMIHeoIHb +oIHYpIHVMIHSMQswCQYDVQQGEwJVUzEUMBIGA1UEChMLRW50cnVzdC5uZXQxOzA5 +BgNVBAsTMnd3dy5lbnRydXN0Lm5ldC9DUFMgaW5jb3JwLiBieSByZWYuIChsaW1p +dHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMTk5OSBFbnRydXN0Lm5ldCBMaW1pdGVk +MTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUgU2VydmVyIENlcnRpZmljYXRp +b24gQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMCmgJ6AlhiNodHRwOi8vd3d3LmVu +dHJ1c3QubmV0L0NSTC9uZXQxLmNybDArBgNVHRAEJDAigA8xOTk5MDUyNTE2MDk0 +MFqBDzIwMTkwNTI1MTYwOTQwWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAU8Bdi +E1U9s/8KAGv7UISX8+1i0BowHQYDVR0OBBYEFPAXYhNVPbP/CgBr+1CEl/PtYtAa +MAwGA1UdEwQFMAMBAf8wGQYJKoZIhvZ9B0EABAwwChsEVjQuMAMCBJAwDQYJKoZI +hvcNAQEFBQADgYEAkNwwAvpkdMKnCqV8IY00F6j7Rw7/JXyNEwr75Ji174z4xRAN +95K+8cPV1ZVqBLssziY2ZcgxxufuP+NXdYR6Ee9GTxj005i7qIcyunL2POI9n9cd +2cNgQ4xYDiKWL2KjLB+6rQXvqzJ4h6BUcxm1XAX5Uj5tLUUL9wqT6u0G+bI= +-----END CERTIFICATE----- + +# Issuer: CN=Entrust.net Certification Authority (2048) O=Entrust.net OU=www.entrust.net/CPS_2048 incorp. by ref. (limits liab.)/(c) 1999 Entrust.net Limited +# Subject: CN=Entrust.net Certification Authority (2048) O=Entrust.net OU=www.entrust.net/CPS_2048 incorp. by ref. (limits liab.)/(c) 1999 Entrust.net Limited +# Label: "Entrust.net Premium 2048 Secure Server CA" +# Serial: 946059622 +# MD5 Fingerprint: ba:21:ea:20:d6:dd:db:8f:c1:57:8b:40:ad:a1:fc:fc +# SHA1 Fingerprint: 80:1d:62:d0:7b:44:9d:5c:5c:03:5c:98:ea:61:fa:44:3c:2a:58:fe +# SHA256 Fingerprint: d1:c3:39:ea:27:84:eb:87:0f:93:4f:c5:63:4e:4a:a9:ad:55:05:01:64:01:f2:64:65:d3:7a:57:46:63:35:9f +-----BEGIN CERTIFICATE----- +MIIEXDCCA0SgAwIBAgIEOGO5ZjANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChML +RW50cnVzdC5uZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBp +bmNvcnAuIGJ5IHJlZi4gKGxpbWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5 +IEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNVBAMTKkVudHJ1c3QubmV0IENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5ICgyMDQ4KTAeFw05OTEyMjQxNzUwNTFaFw0xOTEy +MjQxODIwNTFaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3d3d3 +LmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxp +YWIuKTElMCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEG +A1UEAxMqRW50cnVzdC5uZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKDIwNDgp +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArU1LqRKGsuqjIAcVFmQq +K0vRvwtKTY7tgHalZ7d4QMBzQshowNtTK91euHaYNZOLGp18EzoOH1u3Hs/lJBQe +sYGpjX24zGtLA/ECDNyrpUAkAH90lKGdCCmziAv1h3edVc3kw37XamSrhRSGlVuX +MlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZy/Gn7xxGWC4LeksyZB2ZnuU4q941mVT +XTzWnLLPKQP5L6RQstRIzgUyVYr9smRMDuSYB3Xbf9+5CFVghTAp+XtIpGmG4zU/ +HoZdenoVve8AjhUiVBcAkCaTvA5JaJG/+EfTnZVCwQ5N328mz8MYIWJmQ3DW1cAH +4QIDAQABo3QwcjARBglghkgBhvhCAQEEBAMCAAcwHwYDVR0jBBgwFoAUVeSB0RGA +vtiJuQijMfmhJAkWuXAwHQYDVR0OBBYEFFXkgdERgL7YibkIozH5oSQJFrlwMB0G +CSqGSIb2fQdBAAQQMA4bCFY1LjA6NC4wAwIEkDANBgkqhkiG9w0BAQUFAAOCAQEA +WUesIYSKF8mciVMeuoCFGsY8Tj6xnLZ8xpJdGGQC49MGCBFhfGPjK50xA3B20qMo +oPS7mmNz7W3lKtvtFKkrxjYR0CvrB4ul2p5cGZ1WEvVUKcgF7bISKo30Axv/55IQ +h7A6tcOdBTcSo8f0FbnVpDkWm1M6I5HxqIKiaohowXkCIryqptau37AUX7iH0N18 +f3v/rxzP5tsHrV7bhZ3QKw0z2wTR5klAEyt2+z7pnIkPFc4YsIV4IU9rTw76NmfN +B/L/CNDi3tm/Kq+4h4YhPATKt5Rof8886ZjXOP/swNlQ8C5LWK5Gb9Auw2DaclVy +vUxFnmG6v4SBkgPR0ml8xQ== +-----END CERTIFICATE----- + +# Issuer: CN=Baltimore CyberTrust Root O=Baltimore OU=CyberTrust +# Subject: CN=Baltimore CyberTrust Root O=Baltimore OU=CyberTrust +# Label: "Baltimore CyberTrust Root" +# Serial: 33554617 +# MD5 Fingerprint: ac:b6:94:a5:9c:17:e0:d7:91:52:9b:b1:97:06:a6:e4 +# SHA1 Fingerprint: d4:de:20:d0:5e:66:fc:53:fe:1a:50:88:2c:78:db:28:52:ca:e4:74 +# SHA256 Fingerprint: 16:af:57:a9:f6:76:b0:ab:12:60:95:aa:5e:ba:de:f2:2a:b3:11:19:d6:44:ac:95:cd:4b:93:db:f3:f2:6a:eb +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ +RTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYD +VQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoX +DTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMCSUUxEjAQBgNVBAoTCUJhbHRpbW9y +ZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFsdGltb3JlIEN5YmVy +VHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKMEuyKr +mD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjr +IZ3AQSsBUnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeK +mpYcqWe4PwzV9/lSEy/CG9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSu +XmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9XbIGevOF6uvUA65ehD5f/xXtabz5OTZy +dc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjprl3RjM71oGDHweI12v/ye +jl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoIVDaGezq1 +BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3 +DQEBBQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT92 +9hkTI7gQCvlYpNRhcL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3Wgx +jkzSswF07r51XgdIGn9w/xZchMB5hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0 +Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsaY71k5h+3zvDyny67G7fyUIhz +ksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS +R9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp +-----END CERTIFICATE----- + +# Issuer: CN=Equifax Secure Global eBusiness CA-1 O=Equifax Secure Inc. +# Subject: CN=Equifax Secure Global eBusiness CA-1 O=Equifax Secure Inc. +# Label: "Equifax Secure Global eBusiness CA" +# Serial: 1 +# MD5 Fingerprint: 8f:5d:77:06:27:c4:98:3c:5b:93:78:e7:d7:7d:9b:cc +# SHA1 Fingerprint: 7e:78:4a:10:1c:82:65:cc:2d:e1:f1:6d:47:b4:40:ca:d9:0a:19:45 +# SHA256 Fingerprint: 5f:0b:62:ea:b5:e3:53:ea:65:21:65:16:58:fb:b6:53:59:f4:43:28:0a:4a:fb:d1:04:d7:7d:10:f9:f0:4c:07 +-----BEGIN CERTIFICATE----- +MIICkDCCAfmgAwIBAgIBATANBgkqhkiG9w0BAQQFADBaMQswCQYDVQQGEwJVUzEc +MBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5jLjEtMCsGA1UEAxMkRXF1aWZheCBT +ZWN1cmUgR2xvYmFsIGVCdXNpbmVzcyBDQS0xMB4XDTk5MDYyMTA0MDAwMFoXDTIw +MDYyMTA0MDAwMFowWjELMAkGA1UEBhMCVVMxHDAaBgNVBAoTE0VxdWlmYXggU2Vj +dXJlIEluYy4xLTArBgNVBAMTJEVxdWlmYXggU2VjdXJlIEdsb2JhbCBlQnVzaW5l +c3MgQ0EtMTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAuucXkAJlsTRVPEnC +UdXfp9E3j9HngXNBUmCbnaEXJnitx7HoJpQytd4zjTov2/KaelpzmKNc6fuKcxtc +58O/gGzNqfTWK8D3+ZmqY6KxRwIP1ORROhI8bIpaVIRw28HFkM9yRcuoWcDNM50/ +o5brhTMhHD4ePmBudpxnhcXIw2ECAwEAAaNmMGQwEQYJYIZIAYb4QgEBBAQDAgAH +MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUvqigdHJQa0S3ySPY+6j/s1dr +aGwwHQYDVR0OBBYEFL6ooHRyUGtEt8kj2Puo/7NXa2hsMA0GCSqGSIb3DQEBBAUA +A4GBADDiAVGqx+pf2rnQZQ8w1j7aDRRJbpGTJxQx78T3LUX47Me/okENI7SS+RkA +Z70Br83gcfxaz2TE4JaY0KNA4gGK7ycH8WUBikQtBmV1UsCGECAhX2xrD2yuCRyv +8qIYNMR1pHMc8Y3c7635s3a0kr/clRAevsvIO1qEYBlWlKlV +-----END CERTIFICATE----- + +# Issuer: CN=Equifax Secure eBusiness CA-1 O=Equifax Secure Inc. +# Subject: CN=Equifax Secure eBusiness CA-1 O=Equifax Secure Inc. +# Label: "Equifax Secure eBusiness CA 1" +# Serial: 4 +# MD5 Fingerprint: 64:9c:ef:2e:44:fc:c6:8f:52:07:d0:51:73:8f:cb:3d +# SHA1 Fingerprint: da:40:18:8b:91:89:a3:ed:ee:ae:da:97:fe:2f:9d:f5:b7:d1:8a:41 +# SHA256 Fingerprint: cf:56:ff:46:a4:a1:86:10:9d:d9:65:84:b5:ee:b5:8a:51:0c:42:75:b0:e5:f9:4f:40:bb:ae:86:5e:19:f6:73 +-----BEGIN CERTIFICATE----- +MIICgjCCAeugAwIBAgIBBDANBgkqhkiG9w0BAQQFADBTMQswCQYDVQQGEwJVUzEc +MBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5jLjEmMCQGA1UEAxMdRXF1aWZheCBT +ZWN1cmUgZUJ1c2luZXNzIENBLTEwHhcNOTkwNjIxMDQwMDAwWhcNMjAwNjIxMDQw +MDAwWjBTMQswCQYDVQQGEwJVUzEcMBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5j +LjEmMCQGA1UEAxMdRXF1aWZheCBTZWN1cmUgZUJ1c2luZXNzIENBLTEwgZ8wDQYJ +KoZIhvcNAQEBBQADgY0AMIGJAoGBAM4vGbwXt3fek6lfWg0XTzQaDJj0ItlZ1MRo +RvC0NcWFAyDGr0WlIVFFQesWWDYyb+JQYmT5/VGcqiTZ9J2DKocKIdMSODRsjQBu +WqDZQu4aIZX5UkxVWsUPOE9G+m34LjXWHXzr4vCwdYDIqROsvojvOm6rXyo4YgKw +Env+j6YDAgMBAAGjZjBkMBEGCWCGSAGG+EIBAQQEAwIABzAPBgNVHRMBAf8EBTAD +AQH/MB8GA1UdIwQYMBaAFEp4MlIR21kWNl7fwRQ2QGpHfEyhMB0GA1UdDgQWBBRK +eDJSEdtZFjZe38EUNkBqR3xMoTANBgkqhkiG9w0BAQQFAAOBgQB1W6ibAxHm6VZM +zfmpTMANmvPMZWnmJXbMWbfWVMMdzZmsGd20hdXgPfxiIKeES1hl8eL5lSE/9dR+ +WB5Hh1Q+WKG1tfgq73HnvMP2sUlG4tega+VWeponmHxGYhTnyfxuAxJ5gDgdSIKN +/Bf+KpYrtWKmpj29f5JZzVoqgrI3eQ== +-----END CERTIFICATE----- + +# Issuer: O=Equifax Secure OU=Equifax Secure eBusiness CA-2 +# Subject: O=Equifax Secure OU=Equifax Secure eBusiness CA-2 +# Label: "Equifax Secure eBusiness CA 2" +# Serial: 930140085 +# MD5 Fingerprint: aa:bf:bf:64:97:da:98:1d:6f:c6:08:3a:95:70:33:ca +# SHA1 Fingerprint: 39:4f:f6:85:0b:06:be:52:e5:18:56:cc:10:e1:80:e8:82:b3:85:cc +# SHA256 Fingerprint: 2f:27:4e:48:ab:a4:ac:7b:76:59:33:10:17:75:50:6d:c3:0e:e3:8e:f6:ac:d5:c0:49:32:cf:e0:41:23:42:20 +-----BEGIN CERTIFICATE----- +MIIDIDCCAomgAwIBAgIEN3DPtTANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJV +UzEXMBUGA1UEChMORXF1aWZheCBTZWN1cmUxJjAkBgNVBAsTHUVxdWlmYXggU2Vj +dXJlIGVCdXNpbmVzcyBDQS0yMB4XDTk5MDYyMzEyMTQ0NVoXDTE5MDYyMzEyMTQ0 +NVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkVxdWlmYXggU2VjdXJlMSYwJAYD +VQQLEx1FcXVpZmF4IFNlY3VyZSBlQnVzaW5lc3MgQ0EtMjCBnzANBgkqhkiG9w0B +AQEFAAOBjQAwgYkCgYEA5Dk5kx5SBhsoNviyoynF7Y6yEb3+6+e0dMKP/wXn2Z0G +vxLIPw7y1tEkshHe0XMJitSxLJgJDR5QRrKDpkWNYmi7hRsgcDKqQM2mll/EcTc/ +BPO3QSQ5BxoeLmFYoBIL5aXfxavqN3HMHMg3OrmXUqesxWoklE6ce8/AatbfIb0C +AwEAAaOCAQkwggEFMHAGA1UdHwRpMGcwZaBjoGGkXzBdMQswCQYDVQQGEwJVUzEX +MBUGA1UEChMORXF1aWZheCBTZWN1cmUxJjAkBgNVBAsTHUVxdWlmYXggU2VjdXJl +IGVCdXNpbmVzcyBDQS0yMQ0wCwYDVQQDEwRDUkwxMBoGA1UdEAQTMBGBDzIwMTkw +NjIzMTIxNDQ1WjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUUJ4L6q9euSBIplBq +y/3YIHqngnYwHQYDVR0OBBYEFFCeC+qvXrkgSKZQasv92CB6p4J2MAwGA1UdEwQF +MAMBAf8wGgYJKoZIhvZ9B0EABA0wCxsFVjMuMGMDAgbAMA0GCSqGSIb3DQEBBQUA +A4GBAAyGgq3oThr1jokn4jVYPSm0B482UJW/bsGe68SQsoWou7dC4A8HOd/7npCy +0cE+U58DRLB+S/Rv5Hwf5+Kx5Lia78O9zt4LMjTZ3ijtM2vE1Nc9ElirfQkty3D1 +E4qUoSek1nDFbZS1yX2doNLGCEnZZpum0/QL3MUmV+GRMOrN +-----END CERTIFICATE----- + +# Issuer: CN=AddTrust Class 1 CA Root O=AddTrust AB OU=AddTrust TTP Network +# Subject: CN=AddTrust Class 1 CA Root O=AddTrust AB OU=AddTrust TTP Network +# Label: "AddTrust Low-Value Services Root" +# Serial: 1 +# MD5 Fingerprint: 1e:42:95:02:33:92:6b:b9:5f:c0:7f:da:d6:b2:4b:fc +# SHA1 Fingerprint: cc:ab:0e:a0:4c:23:01:d6:69:7b:dd:37:9f:cd:12:eb:24:e3:94:9d +# SHA256 Fingerprint: 8c:72:09:27:9a:c0:4e:27:5e:16:d0:7f:d3:b7:75:e8:01:54:b5:96:80:46:e3:1f:52:dd:25:76:63:24:e9:a7 +-----BEGIN CERTIFICATE----- +MIIEGDCCAwCgAwIBAgIBATANBgkqhkiG9w0BAQUFADBlMQswCQYDVQQGEwJTRTEU +MBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3 +b3JrMSEwHwYDVQQDExhBZGRUcnVzdCBDbGFzcyAxIENBIFJvb3QwHhcNMDAwNTMw +MTAzODMxWhcNMjAwNTMwMTAzODMxWjBlMQswCQYDVQQGEwJTRTEUMBIGA1UEChML +QWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3b3JrMSEwHwYD +VQQDExhBZGRUcnVzdCBDbGFzcyAxIENBIFJvb3QwggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQCWltQhSWDia+hBBwzexODcEyPNwTXH+9ZOEQpnXvUGW2ul +CDtbKRY654eyNAbFvAWlA3yCyykQruGIgb3WntP+LVbBFc7jJp0VLhD7Bo8wBN6n +tGO0/7Gcrjyvd7ZWxbWroulpOj0OM3kyP3CCkplhbY0wCI9xP6ZIVxn4JdxLZlyl +dI+Yrsj5wAYi56xz36Uu+1LcsRVlIPo1Zmne3yzxbrww2ywkEtvrNTVokMsAsJch +PXQhI2U0K7t4WaPW4XY5mqRJjox0r26kmqPZm9I4XJuiGMx1I4S+6+JNM3GOGvDC ++Mcdoq0Dlyz4zyXG9rgkMbFjXZJ/Y/AlyVMuH79NAgMBAAGjgdIwgc8wHQYDVR0O +BBYEFJWxtPCUtr3H2tERCSG+wa9J/RB7MAsGA1UdDwQEAwIBBjAPBgNVHRMBAf8E +BTADAQH/MIGPBgNVHSMEgYcwgYSAFJWxtPCUtr3H2tERCSG+wa9J/RB7oWmkZzBl +MQswCQYDVQQGEwJTRTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFk +ZFRydXN0IFRUUCBOZXR3b3JrMSEwHwYDVQQDExhBZGRUcnVzdCBDbGFzcyAxIENB +IFJvb3SCAQEwDQYJKoZIhvcNAQEFBQADggEBACxtZBsfzQ3duQH6lmM0MkhHma6X +7f1yFqZzR1r0693p9db7RcwpiURdv0Y5PejuvE1Uhh4dbOMXJ0PhiVYrqW9yTkkz +43J8KiOavD7/KCrto/8cI7pDVwlnTUtiBi34/2ydYB7YHEt9tTEv2dB8Xfjea4MY +eDdXL+gzB2ffHsdrKpV2ro9Xo/D0UrSpUwjP4E/TelOL/bscVjby/rK25Xa71SJl +pz/+0WatC7xrmYbvP33zGDLKe8bjq2RGlfgmadlVg3sslgf/WSxEo8bl6ancoWOA +WiFeIc9TVPC6b4nbqKqVz4vjccweGyBECMB6tkD9xOQ14R0WHNC8K47Wcdk= +-----END CERTIFICATE----- + +# Issuer: CN=AddTrust External CA Root O=AddTrust AB OU=AddTrust External TTP Network +# Subject: CN=AddTrust External CA Root O=AddTrust AB OU=AddTrust External TTP Network +# Label: "AddTrust External Root" +# Serial: 1 +# MD5 Fingerprint: 1d:35:54:04:85:78:b0:3f:42:42:4d:bf:20:73:0a:3f +# SHA1 Fingerprint: 02:fa:f3:e2:91:43:54:68:60:78:57:69:4d:f5:e4:5b:68:85:18:68 +# SHA256 Fingerprint: 68:7f:a4:51:38:22:78:ff:f0:c8:b1:1f:8d:43:d5:76:67:1c:6e:b2:bc:ea:b4:13:fb:83:d9:65:d0:6d:2f:f2 +-----BEGIN CERTIFICATE----- +MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEU +MBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFkZFRydXN0IEV4dGVybmFs +IFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBFeHRlcm5hbCBDQSBSb290 +MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFowbzELMAkGA1UEBhMCU0Ux +FDASBgNVBAoTC0FkZFRydXN0IEFCMSYwJAYDVQQLEx1BZGRUcnVzdCBFeHRlcm5h +bCBUVFAgTmV0d29yazEiMCAGA1UEAxMZQWRkVHJ1c3QgRXh0ZXJuYWwgQ0EgUm9v +dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALf3GjPm8gAELTngTlvt +H7xsD821+iO2zt6bETOXpClMfZOfvUq8k+0DGuOPz+VtUFrWlymUWoCwSXrbLpX9 +uMq/NzgtHj6RQa1wVsfwTz/oMp50ysiQVOnGXw94nZpAPA6sYapeFI+eh6FqUNzX +mk6vBbOmcZSccbNQYArHE504B4YCqOmoaSYYkKtMsE8jqzpPhNjfzp/haW+710LX +a0Tkx63ubUFfclpxCDezeWWkWaCUN/cALw3CknLa0Dhy2xSoRcRdKn23tNbE7qzN +E0S3ySvdQwAl+mG5aWpYIxG3pzOPVnVZ9c0p10a3CitlttNCbxWyuHv77+ldU9U0 +WicCAwEAAaOB3DCB2TAdBgNVHQ4EFgQUrb2YejS0Jvf6xCZU7wO94CTLVBowCwYD +VR0PBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wgZkGA1UdIwSBkTCBjoAUrb2YejS0 +Jvf6xCZU7wO94CTLVBqhc6RxMG8xCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRU +cnVzdCBBQjEmMCQGA1UECxMdQWRkVHJ1c3QgRXh0ZXJuYWwgVFRQIE5ldHdvcmsx +IjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENBIFJvb3SCAQEwDQYJKoZIhvcN +AQEFBQADggEBALCb4IUlwtYj4g+WBpKdQZic2YR5gdkeWxQHIzZlj7DYd7usQWxH +YINRsPkyPef89iYTx4AWpb9a/IfPeHmJIZriTAcKhjW88t5RxNKWt9x+Tu5w/Rw5 +6wwCURQtjr0W4MHfRnXnJK3s9EK0hZNwEGe6nQY1ShjTK3rMUUKhemPR5ruhxSvC +Nr4TDea9Y355e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEX +c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a +mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ= +-----END CERTIFICATE----- + +# Issuer: CN=AddTrust Public CA Root O=AddTrust AB OU=AddTrust TTP Network +# Subject: CN=AddTrust Public CA Root O=AddTrust AB OU=AddTrust TTP Network +# Label: "AddTrust Public Services Root" +# Serial: 1 +# MD5 Fingerprint: c1:62:3e:23:c5:82:73:9c:03:59:4b:2b:e9:77:49:7f +# SHA1 Fingerprint: 2a:b6:28:48:5e:78:fb:f3:ad:9e:79:10:dd:6b:df:99:72:2c:96:e5 +# SHA256 Fingerprint: 07:91:ca:07:49:b2:07:82:aa:d3:c7:d7:bd:0c:df:c9:48:58:35:84:3e:b2:d7:99:60:09:ce:43:ab:6c:69:27 +-----BEGIN CERTIFICATE----- +MIIEFTCCAv2gAwIBAgIBATANBgkqhkiG9w0BAQUFADBkMQswCQYDVQQGEwJTRTEU +MBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3 +b3JrMSAwHgYDVQQDExdBZGRUcnVzdCBQdWJsaWMgQ0EgUm9vdDAeFw0wMDA1MzAx +MDQxNTBaFw0yMDA1MzAxMDQxNTBaMGQxCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtB +ZGRUcnVzdCBBQjEdMBsGA1UECxMUQWRkVHJ1c3QgVFRQIE5ldHdvcmsxIDAeBgNV +BAMTF0FkZFRydXN0IFB1YmxpYyBDQSBSb290MIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEA6Rowj4OIFMEg2Dybjxt+A3S72mnTRqX4jsIMEZBRpS9mVEBV +6tsfSlbunyNu9DnLoblv8n75XYcmYZ4c+OLspoH4IcUkzBEMP9smcnrHAZcHF/nX +GCwwfQ56HmIexkvA/X1id9NEHif2P0tEs7c42TkfYNVRknMDtABp4/MUTu7R3AnP +dzRGULD4EfL+OHn3Bzn+UZKXC1sIXzSGAa2Il+tmzV7R/9x98oTaunet3IAIx6eH +1lWfl2royBFkuucZKT8Rs3iQhCBSWxHveNCD9tVIkNAwHM+A+WD+eeSI8t0A65RF +62WUaUC6wNW0uLp9BBGo6zEFlpROWCGOn9Bg/QIDAQABo4HRMIHOMB0GA1UdDgQW +BBSBPjfYkrAfd59ctKtzquf2NGAv+jALBgNVHQ8EBAMCAQYwDwYDVR0TAQH/BAUw +AwEB/zCBjgYDVR0jBIGGMIGDgBSBPjfYkrAfd59ctKtzquf2NGAv+qFopGYwZDEL +MAkGA1UEBhMCU0UxFDASBgNVBAoTC0FkZFRydXN0IEFCMR0wGwYDVQQLExRBZGRU +cnVzdCBUVFAgTmV0d29yazEgMB4GA1UEAxMXQWRkVHJ1c3QgUHVibGljIENBIFJv +b3SCAQEwDQYJKoZIhvcNAQEFBQADggEBAAP3FUr4JNojVhaTdt02KLmuG7jD8WS6 +IBh4lSknVwW8fCr0uVFV2ocC3g8WFzH4qnkuCRO7r7IgGRLlk/lL+YPoRNWyQSW/ +iHVv/xD8SlTQX/D67zZzfRs2RcYhbbQVuE7PnFylPVoAjgbjPGsye/Kf8Lb93/Ao +GEjwxrzQvzSAlsJKsW2Ox5BF3i9nrEUEo3rcVZLJR2bYGozH7ZxOmuASu7VqTITh +4SINhwBk/ox9Yjllpu9CtoAlEmEBqCQTcAARJl/6NVDFSMwGR+gn2HCNX2TmoUQm +XiLsks3/QppEIW1cxeMiHV9HEufOX1362KqxMy3ZdvJOOjMMK7MtkAY= +-----END CERTIFICATE----- + +# Issuer: CN=AddTrust Qualified CA Root O=AddTrust AB OU=AddTrust TTP Network +# Subject: CN=AddTrust Qualified CA Root O=AddTrust AB OU=AddTrust TTP Network +# Label: "AddTrust Qualified Certificates Root" +# Serial: 1 +# MD5 Fingerprint: 27:ec:39:47:cd:da:5a:af:e2:9a:01:65:21:a9:4c:bb +# SHA1 Fingerprint: 4d:23:78:ec:91:95:39:b5:00:7f:75:8f:03:3b:21:1e:c5:4d:8b:cf +# SHA256 Fingerprint: 80:95:21:08:05:db:4b:bc:35:5e:44:28:d8:fd:6e:c2:cd:e3:ab:5f:b9:7a:99:42:98:8e:b8:f4:dc:d0:60:16 +-----BEGIN CERTIFICATE----- +MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJTRTEU +MBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3 +b3JrMSMwIQYDVQQDExpBZGRUcnVzdCBRdWFsaWZpZWQgQ0EgUm9vdDAeFw0wMDA1 +MzAxMDQ0NTBaFw0yMDA1MzAxMDQ0NTBaMGcxCzAJBgNVBAYTAlNFMRQwEgYDVQQK +EwtBZGRUcnVzdCBBQjEdMBsGA1UECxMUQWRkVHJ1c3QgVFRQIE5ldHdvcmsxIzAh +BgNVBAMTGkFkZFRydXN0IFF1YWxpZmllZCBDQSBSb290MIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA5B6a/twJWoekn0e+EV+vhDTbYjx5eLfpMLXsDBwq +xBb/4Oxx64r1EW7tTw2R0hIYLUkVAcKkIhPHEWT/IhKauY5cLwjPcWqzZwFZ8V1G +87B4pfYOQnrjfxvM0PC3KP0q6p6zsLkEqv32x7SxuCqg+1jxGaBvcCV+PmlKfw8i +2O+tCBGaKZnhqkRFmhJePp1tUvznoD1oL/BLcHwTOK28FSXx1s6rosAx1i+f4P8U +WfyEk9mHfExUE+uf0S0R+Bg6Ot4l2ffTQO2kBhLEO+GRwVY18BTcZTYJbqukB8c1 +0cIDMzZbdSZtQvESa0NvS3GU+jQd7RNuyoB/mC9suWXY6QIDAQABo4HUMIHRMB0G +A1UdDgQWBBQ5lYtii1zJ1IC6WA+XPxUIQ8yYpzALBgNVHQ8EBAMCAQYwDwYDVR0T +AQH/BAUwAwEB/zCBkQYDVR0jBIGJMIGGgBQ5lYtii1zJ1IC6WA+XPxUIQ8yYp6Fr +pGkwZzELMAkGA1UEBhMCU0UxFDASBgNVBAoTC0FkZFRydXN0IEFCMR0wGwYDVQQL +ExRBZGRUcnVzdCBUVFAgTmV0d29yazEjMCEGA1UEAxMaQWRkVHJ1c3QgUXVhbGlm +aWVkIENBIFJvb3SCAQEwDQYJKoZIhvcNAQEFBQADggEBABmrder4i2VhlRO6aQTv +hsoToMeqT2QbPxj2qC0sVY8FtzDqQmodwCVRLae/DLPt7wh/bDxGGuoYQ992zPlm +hpwsaPXpF/gxsxjE1kh9I0xowX67ARRvxdlu3rsEQmr49lx95dr6h+sNNVJn0J6X +dgWTP5XHAeZpVTh/EGGZyeNfpso+gmNIquIISD6q8rKFYqa0p9m9N5xotS1WfbC3 +P6CxB9bpT9zeRXEwMn8bLgn5v1Kh7sKAPgZcLlVAwRv1cEWw3F369nJad9Jjzc9Y +iQBCYz95OdBEsIJuQRno3eDBiFrRHnGTHyQwdOUeqN48Jzd/g66ed8/wMLH/S5no +xqE= +-----END CERTIFICATE----- + +# Issuer: CN=Entrust Root Certification Authority O=Entrust, Inc. OU=www.entrust.net/CPS is incorporated by reference/(c) 2006 Entrust, Inc. +# Subject: CN=Entrust Root Certification Authority O=Entrust, Inc. OU=www.entrust.net/CPS is incorporated by reference/(c) 2006 Entrust, Inc. +# Label: "Entrust Root Certification Authority" +# Serial: 1164660820 +# MD5 Fingerprint: d6:a5:c3:ed:5d:dd:3e:00:c1:3d:87:92:1f:1d:3f:e4 +# SHA1 Fingerprint: b3:1e:b1:b7:40:e3:6c:84:02:da:dc:37:d4:4d:f5:d4:67:49:52:f9 +# SHA256 Fingerprint: 73:c1:76:43:4f:1b:c6:d5:ad:f4:5b:0e:76:e7:27:28:7c:8d:e5:76:16:c1:e6:e6:14:1a:2b:2c:bc:7d:8e:4c +-----BEGIN CERTIFICATE----- +MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMC +VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0 +Lm5ldC9DUFMgaXMgaW5jb3Jwb3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMW +KGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsGA1UEAxMkRW50cnVzdCBSb290IENl +cnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0MloXDTI2MTEyNzIw +NTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMTkw +NwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSBy +ZWZlcmVuY2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNV +BAMTJEVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBALaVtkNC+sZtKm9I35RMOVcF7sN5EUFo +Nu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYszA9u3g3s+IIRe7bJWKKf4 +4LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOwwCj0Yzfv9 +KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGI +rb68j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi +94DkZfs0Nw4pgHBNrziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOB +sDCBrTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAi +gA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1MzQyWjAfBgNVHSMEGDAWgBRo +kORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DHhmak8fdLQ/uE +vW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA +A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9t +O1KzKtvn1ISMY/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6Zua +AGAT/3B+XxFNSRuzFVJ7yVTav52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP +9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTSW3iDVuycNsMm4hH2Z0kdkquM++v/ +eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m +0vdXcDazv/wor3ElhVsT/h5/WrQ8 +-----END CERTIFICATE----- + +# Issuer: CN=GeoTrust Global CA O=GeoTrust Inc. +# Subject: CN=GeoTrust Global CA O=GeoTrust Inc. +# Label: "GeoTrust Global CA" +# Serial: 144470 +# MD5 Fingerprint: f7:75:ab:29:fb:51:4e:b7:77:5e:ff:05:3c:99:8e:f5 +# SHA1 Fingerprint: de:28:f4:a4:ff:e5:b9:2f:a3:c5:03:d1:a3:49:a7:f9:96:2a:82:12 +# SHA256 Fingerprint: ff:85:6a:2d:25:1d:cd:88:d3:66:56:f4:50:12:67:98:cf:ab:aa:de:40:79:9c:72:2d:e4:d2:b5:db:36:a7:3a +-----BEGIN CERTIFICATE----- +MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT +MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i +YWwgQ0EwHhcNMDIwNTIxMDQwMDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQG +EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UEAxMSR2VvVHJ1c3Qg +R2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2swYYzD9 +9BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjoBbdq +fnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDv +iS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU +1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+ +bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5aszPeE4uwc2hGKceeoW +MPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTA +ephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVkDBF9qn1l +uMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKIn +Z57QzxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfS +tQWVYrmm3ok9Nns4d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcF +PseKUgzbFbS9bZvlxrFUaKnjaZC2mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Un +hw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6pXE0zX5IJL4hmXXeXxx12E6nV +5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvmMw== +-----END CERTIFICATE----- + +# Issuer: CN=GeoTrust Global CA 2 O=GeoTrust Inc. +# Subject: CN=GeoTrust Global CA 2 O=GeoTrust Inc. +# Label: "GeoTrust Global CA 2" +# Serial: 1 +# MD5 Fingerprint: 0e:40:a7:6c:de:03:5d:8f:d1:0f:e4:d1:8d:f9:6c:a9 +# SHA1 Fingerprint: a9:e9:78:08:14:37:58:88:f2:05:19:b0:6d:2b:0d:2b:60:16:90:7d +# SHA256 Fingerprint: ca:2d:82:a0:86:77:07:2f:8a:b6:76:4f:f0:35:67:6c:fe:3e:5e:32:5e:01:21:72:df:3f:92:09:6d:b7:9b:85 +-----BEGIN CERTIFICATE----- +MIIDZjCCAk6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBEMQswCQYDVQQGEwJVUzEW +MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEdMBsGA1UEAxMUR2VvVHJ1c3QgR2xvYmFs +IENBIDIwHhcNMDQwMzA0MDUwMDAwWhcNMTkwMzA0MDUwMDAwWjBEMQswCQYDVQQG +EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEdMBsGA1UEAxMUR2VvVHJ1c3Qg +R2xvYmFsIENBIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDvPE1A +PRDfO1MA4Wf+lGAVPoWI8YkNkMgoI5kF6CsgncbzYEbYwbLVjDHZ3CB5JIG/NTL8 +Y2nbsSpr7iFY8gjpeMtvy/wWUsiRxP89c96xPqfCfWbB9X5SJBri1WeR0IIQ13hL +TytCOb1kLUCgsBDTOEhGiKEMuzozKmKY+wCdE1l/bztyqu6mD4b5BWHqZ38MN5aL +5mkWRxHCJ1kDs6ZgwiFAVvqgx306E+PsV8ez1q6diYD3Aecs9pYrEw15LNnA5IZ7 +S4wMcoKK+xfNAGw6EzywhIdLFnopsk/bHdQL82Y3vdj2V7teJHq4PIu5+pIaGoSe +2HSPqht/XvT+RSIhAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE +FHE4NvICMVNHK266ZUapEBVYIAUJMB8GA1UdIwQYMBaAFHE4NvICMVNHK266ZUap +EBVYIAUJMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQUFAAOCAQEAA/e1K6td +EPx7srJerJsOflN4WT5CBP51o62sgU7XAotexC3IUnbHLB/8gTKY0UvGkpMzNTEv +/NgdRN3ggX+d6YvhZJFiCzkIjKx0nVnZellSlxG5FntvRdOW2TF9AjYPnDtuzywN +A0ZF66D0f0hExghAzN4bcLUprbqLOzRldRtxIR0sFAqwlpW41uryZfspuk/qkZN0 +abby/+Ea0AzRdoXLiiW9l14sbxWZJue2Kf8i7MkCx1YAzUm5s2x7UwQa4qjJqhIF +I8LO57sEAszAR6LkxCkvW0VXiVHuPOtSCP8HNR6fNWpHSlaY0VqFH4z1Ir+rzoPz +4iIprn2DQKi6bA== +-----END CERTIFICATE----- + +# Issuer: CN=GeoTrust Universal CA O=GeoTrust Inc. +# Subject: CN=GeoTrust Universal CA O=GeoTrust Inc. +# Label: "GeoTrust Universal CA" +# Serial: 1 +# MD5 Fingerprint: 92:65:58:8b:a2:1a:31:72:73:68:5c:b4:a5:7a:07:48 +# SHA1 Fingerprint: e6:21:f3:35:43:79:05:9a:4b:68:30:9d:8a:2f:74:22:15:87:ec:79 +# SHA256 Fingerprint: a0:45:9b:9f:63:b2:25:59:f5:fa:5d:4c:6d:b3:f9:f7:2f:f1:93:42:03:35:78:f0:73:bf:1d:1b:46:cb:b9:12 +-----BEGIN CERTIFICATE----- +MIIFaDCCA1CgAwIBAgIBATANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJVUzEW +MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEeMBwGA1UEAxMVR2VvVHJ1c3QgVW5pdmVy +c2FsIENBMB4XDTA0MDMwNDA1MDAwMFoXDTI5MDMwNDA1MDAwMFowRTELMAkGA1UE +BhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xHjAcBgNVBAMTFUdlb1RydXN0 +IFVuaXZlcnNhbCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKYV +VaCjxuAfjJ0hUNfBvitbtaSeodlyWL0AG0y/YckUHUWCq8YdgNY96xCcOq9tJPi8 +cQGeBvV8Xx7BDlXKg5pZMK4ZyzBIle0iN430SppyZj6tlcDgFgDgEB8rMQ7XlFTT +QjOgNB0eRXbdT8oYN+yFFXoZCPzVx5zw8qkuEKmS5j1YPakWaDwvdSEYfyh3peFh +F7em6fgemdtzbvQKoiFs7tqqhZJmr/Z6a4LauiIINQ/PQvE1+mrufislzDoR5G2v +c7J2Ha3QsnhnGqQ5HFELZ1aD/ThdDc7d8Lsrlh/eezJS/R27tQahsiFepdaVaH/w +mZ7cRQg+59IJDTWU3YBOU5fXtQlEIGQWFwMCTFMNaN7VqnJNk22CDtucvc+081xd +VHppCZbW2xHBjXWotM85yM48vCR85mLK4b19p71XZQvk/iXttmkQ3CgaRr0BHdCX +teGYO8A3ZNY9lO4L4fUorgtWv3GLIylBjobFS1J72HGrH4oVpjuDWtdYAVHGTEHZ +f9hBZ3KiKN9gg6meyHv8U3NyWfWTehd2Ds735VzZC1U0oqpbtWpU5xPKV+yXbfRe +Bi9Fi1jUIxaS5BZuKGNZMN9QAZxjiRqf2xeUgnA3wySemkfWWspOqGmJch+RbNt+ +nhutxx9z3SxPGWX9f5NAEC7S8O08ni4oPmkmM8V7AgMBAAGjYzBhMA8GA1UdEwEB +/wQFMAMBAf8wHQYDVR0OBBYEFNq7LqqwDLiIJlF0XG0D08DYj3rWMB8GA1UdIwQY +MBaAFNq7LqqwDLiIJlF0XG0D08DYj3rWMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG +9w0BAQUFAAOCAgEAMXjmx7XfuJRAyXHEqDXsRh3ChfMoWIawC/yOsjmPRFWrZIRc +aanQmjg8+uUfNeVE44B5lGiku8SfPeE0zTBGi1QrlaXv9z+ZhP015s8xxtxqv6fX +IwjhmF7DWgh2qaavdy+3YL1ERmrvl/9zlcGO6JP7/TG37FcREUWbMPEaiDnBTzyn +ANXH/KttgCJwpQzgXQQpAvvLoJHRfNbDflDVnVi+QTjruXU8FdmbyUqDWcDaU/0z +uzYYm4UPFd3uLax2k7nZAY1IEKj79TiG8dsKxr2EoyNB3tZ3b4XUhRxQ4K5RirqN +Pnbiucon8l+f725ZDQbYKxek0nxru18UGkiPGkzns0ccjkxFKyDuSN/n3QmOGKja +QI2SJhFTYXNd673nxE0pN2HrrDktZy4W1vUAg4WhzH92xH3kt0tm7wNFYGm2DFKW +koRepqO1pD4r2czYG0eq8kTaT/kD6PAUyz/zg97QwVTjt+gKN02LIFkDMBmhLMi9 +ER/frslKxfMnZmaGrGiR/9nmUxwPi1xpZQomyB40w11Re9epnAahNt3ViZS82eQt +DF4JbAiXfKM9fJP/P6EUp8+1Xevb2xzEdt+Iub1FBZUbrvxGakyvSOPOrg/Sfuvm +bJxPgWp6ZKy7PtXny3YuxadIwVyQD8vIP/rmMuGNG2+k5o7Y+SlIis5z/iw= +-----END CERTIFICATE----- + +# Issuer: CN=GeoTrust Universal CA 2 O=GeoTrust Inc. +# Subject: CN=GeoTrust Universal CA 2 O=GeoTrust Inc. +# Label: "GeoTrust Universal CA 2" +# Serial: 1 +# MD5 Fingerprint: 34:fc:b8:d0:36:db:9e:14:b3:c2:f2:db:8f:e4:94:c7 +# SHA1 Fingerprint: 37:9a:19:7b:41:85:45:35:0c:a6:03:69:f3:3c:2e:af:47:4f:20:79 +# SHA256 Fingerprint: a0:23:4f:3b:c8:52:7c:a5:62:8e:ec:81:ad:5d:69:89:5d:a5:68:0d:c9:1d:1c:b8:47:7f:33:f8:78:b9:5b:0b +-----BEGIN CERTIFICATE----- +MIIFbDCCA1SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBHMQswCQYDVQQGEwJVUzEW +MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1c3QgVW5pdmVy +c2FsIENBIDIwHhcNMDQwMzA0MDUwMDAwWhcNMjkwMzA0MDUwMDAwWjBHMQswCQYD +VQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1 +c3QgVW5pdmVyc2FsIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC +AQCzVFLByT7y2dyxUxpZKeexw0Uo5dfR7cXFS6GqdHtXr0om/Nj1XqduGdt0DE81 +WzILAePb63p3NeqqWuDW6KFXlPCQo3RWlEQwAx5cTiuFJnSCegx2oG9NzkEtoBUG +FF+3Qs17j1hhNNwqCPkuwwGmIkQcTAeC5lvO0Ep8BNMZcyfwqph/Lq9O64ceJHdq +XbboW0W63MOhBW9Wjo8QJqVJwy7XQYci4E+GymC16qFjwAGXEHm9ADwSbSsVsaxL +se4YuU6W3Nx2/zu+z18DwPw76L5GG//aQMJS9/7jOvdqdzXQ2o3rXhhqMcceujwb +KNZrVMaqW9eiLBsZzKIC9ptZvTdrhrVtgrrY6slWvKk2WP0+GfPtDCapkzj4T8Fd +IgbQl+rhrcZV4IErKIM6+vR7IVEAvlI4zs1meaj0gVbi0IMJR1FbUGrP20gaXT73 +y/Zl92zxlfgCOzJWgjl6W70viRu/obTo/3+NjN8D8WBOWBFM66M/ECuDmgFz2ZRt +hAAnZqzwcEAJQpKtT5MNYQlRJNiS1QuUYbKHsu3/mjX/hVTK7URDrBs8FmtISgoc +QIgfksILAAX/8sgCSqSqqcyZlpwvWOB94b67B9xfBHJcMTTD7F8t4D1kkCLm0ey4 +Lt1ZrtmhN79UNdxzMk+MBB4zsslG8dhcyFVQyWi9qLo2CQIDAQABo2MwYTAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAfBgNV +HSMEGDAWgBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAOBgNVHQ8BAf8EBAMCAYYwDQYJ +KoZIhvcNAQEFBQADggIBAGbBxiPz2eAubl/oz66wsCVNK/g7WJtAJDday6sWSf+z +dXkzoS9tcBc0kf5nfo/sm+VegqlVHy/c1FEHEv6sFj4sNcZj/NwQ6w2jqtB8zNHQ +L1EuxBRa3ugZ4T7GzKQp5y6EqgYweHZUcyiYWTjgAA1i00J9IZ+uPTqM1fp3DRgr +Fg5fNuH8KrUwJM/gYwx7WBr+mbpCErGR9Hxo4sjoryzqyX6uuyo9DRXcNJW2GHSo +ag/HtPQTxORb7QrSpJdMKu0vbBKJPfEncKpqA1Ihn0CoZ1Dy81of398j9tx4TuaY +T1U6U+Pv8vSfx3zYWK8pIpe44L2RLrB27FcRz+8pRPPphXpgY+RdM4kX2TGq2tbz +GDVyz4crL2MjhF2EjD9XoIj8mZEoJmmZ1I+XRL6O1UixpCgp8RW04eWe3fiPpm8m +1wk8OhwRDqZsN/etRIcsKMfYdIKz0G9KV7s1KSegi+ghp4dkNl3M2Basx7InQJJV +OCiNUW7dFGdTbHFcJoRNdVq2fmBWqU2t+5sel/MN2dKXVHfaPRK34B7vCAas+YWH +6aLcr34YEoP9VhdBLtUpgn2Z9DH2canPLAEnpQW5qrJITirvn5NSUZU8UnOOVkwX +QMAJKOSLakhT2+zNVVXxxvjpoixMptEmX36vWkzaH6byHCx+rgIW0lbQL1dTR+iS +-----END CERTIFICATE----- + +# Issuer: CN=America Online Root Certification Authority 1 O=America Online Inc. +# Subject: CN=America Online Root Certification Authority 1 O=America Online Inc. +# Label: "America Online Root Certification Authority 1" +# Serial: 1 +# MD5 Fingerprint: 14:f1:08:ad:9d:fa:64:e2:89:e7:1c:cf:a8:ad:7d:5e +# SHA1 Fingerprint: 39:21:c1:15:c1:5d:0e:ca:5c:cb:5b:c4:f0:7d:21:d8:05:0b:56:6a +# SHA256 Fingerprint: 77:40:73:12:c6:3a:15:3d:5b:c0:0b:4e:51:75:9c:df:da:c2:37:dc:2a:33:b6:79:46:e9:8e:9b:fa:68:0a:e3 +-----BEGIN CERTIFICATE----- +MIIDpDCCAoygAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEc +MBoGA1UEChMTQW1lcmljYSBPbmxpbmUgSW5jLjE2MDQGA1UEAxMtQW1lcmljYSBP +bmxpbmUgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAxMB4XDTAyMDUyODA2 +MDAwMFoXDTM3MTExOTIwNDMwMFowYzELMAkGA1UEBhMCVVMxHDAaBgNVBAoTE0Ft +ZXJpY2EgT25saW5lIEluYy4xNjA0BgNVBAMTLUFtZXJpY2EgT25saW5lIFJvb3Qg +Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkgMTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAKgv6KRpBgNHw+kqmP8ZonCaxlCyfqXfaE0bfA+2l2h9LaaLl+lk +hsmj76CGv2BlnEtUiMJIxUo5vxTjWVXlGbR0yLQFOVwWpeKVBeASrlmLojNoWBym +1BW32J/X3HGrfpq/m44zDyL9Hy7nBzbvYjnF3cu6JRQj3gzGPTzOggjmZj7aUTsW +OqMFf6Dch9Wc/HKpoH145LcxVR5lu9RhsCFg7RAycsWSJR74kEoYeEfffjA3PlAb +2xzTa5qGUwew76wGePiEmf4hjUyAtgyC9mZweRrTT6PP8c9GsEsPPt2IYriMqQko +O3rHl+Ee5fSfwMCuJKDIodkP1nsmgmkyPacCAwEAAaNjMGEwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUAK3Zo/Z59m50qX8zPYEX10zPM94wHwYDVR0jBBgwFoAU +AK3Zo/Z59m50qX8zPYEX10zPM94wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB +BQUAA4IBAQB8itEfGDeC4Liwo+1WlchiYZwFos3CYiZhzRAW18y0ZTTQEYqtqKkF +Zu90821fnZmv9ov761KyBZiibyrFVL0lvV+uyIbqRizBs73B6UlwGBaXCBOMIOAb +LjpHyx7kADCVW/RFo8AasAFOq73AI25jP4BKxQft3OJvx8Fi8eNy1gTIdGcL+oir +oQHIb/AUr9KZzVGTfu0uOMe9zkZQPXLjeSWdm4grECDdpbgyn43gKd8hdIaC2y+C +MMbHNYaz+ZZfRtsMRf3zUMNvxsNIrUam4SdHCh0Om7bCd39j8uB9Gr784N/Xx6ds +sPmuujz9dLQR6FgNgLzTqIA6me11zEZ7 +-----END CERTIFICATE----- + +# Issuer: CN=America Online Root Certification Authority 2 O=America Online Inc. +# Subject: CN=America Online Root Certification Authority 2 O=America Online Inc. +# Label: "America Online Root Certification Authority 2" +# Serial: 1 +# MD5 Fingerprint: d6:ed:3c:ca:e2:66:0f:af:10:43:0d:77:9b:04:09:bf +# SHA1 Fingerprint: 85:b5:ff:67:9b:0c:79:96:1f:c8:6e:44:22:00:46:13:db:17:92:84 +# SHA256 Fingerprint: 7d:3b:46:5a:60:14:e5:26:c0:af:fc:ee:21:27:d2:31:17:27:ad:81:1c:26:84:2d:00:6a:f3:73:06:cc:80:bd +-----BEGIN CERTIFICATE----- +MIIFpDCCA4ygAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEc +MBoGA1UEChMTQW1lcmljYSBPbmxpbmUgSW5jLjE2MDQGA1UEAxMtQW1lcmljYSBP +bmxpbmUgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAyMB4XDTAyMDUyODA2 +MDAwMFoXDTM3MDkyOTE0MDgwMFowYzELMAkGA1UEBhMCVVMxHDAaBgNVBAoTE0Ft +ZXJpY2EgT25saW5lIEluYy4xNjA0BgNVBAMTLUFtZXJpY2EgT25saW5lIFJvb3Qg +Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBAMxBRR3pPU0Q9oyxQcngXssNt79Hc9PwVU3dxgz6sWYFas14tNwC +206B89enfHG8dWOgXeMHDEjsJcQDIPT/DjsS/5uN4cbVG7RtIuOx238hZK+GvFci +KtZHgVdEglZTvYYUAQv8f3SkWq7xuhG1m1hagLQ3eAkzfDJHA1zEpYNI9FdWboE2 +JxhP7JsowtS013wMPgwr38oE18aO6lhOqKSlGBxsRZijQdEt0sdtjRnxrXm3gT+9 +BoInLRBYBbV4Bbkv2wxrkJB+FFk4u5QkE+XRnRTf04JNRvCAOVIyD+OEsnpD8l7e +Xz8d3eOyG6ChKiMDbi4BFYdcpnV1x5dhvt6G3NRI270qv0pV2uh9UPu0gBe4lL8B +PeraunzgWGcXuVjgiIZGZ2ydEEdYMtA1fHkqkKJaEBEjNa0vzORKW6fIJ/KD3l67 +Xnfn6KVuY8INXWHQjNJsWiEOyiijzirplcdIz5ZvHZIlyMbGwcEMBawmxNJ10uEq +Z8A9W6Wa6897GqidFEXlD6CaZd4vKL3Ob5Rmg0gp2OpljK+T2WSfVVcmv2/LNzGZ +o2C7HK2JNDJiuEMhBnIMoVxtRsX6Kc8w3onccVvdtjc+31D1uAclJuW8tf48ArO3 ++L5DwYcRlJ4jbBeKuIonDFRH8KmzwICMoCfrHRnjB453cMor9H124HhnAgMBAAGj +YzBhMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFE1FwWg4u3OpaaEg5+31IqEj +FNeeMB8GA1UdIwQYMBaAFE1FwWg4u3OpaaEg5+31IqEjFNeeMA4GA1UdDwEB/wQE +AwIBhjANBgkqhkiG9w0BAQUFAAOCAgEAZ2sGuV9FOypLM7PmG2tZTiLMubekJcmn +xPBUlgtk87FYT15R/LKXeydlwuXK5w0MJXti4/qftIe3RUavg6WXSIylvfEWK5t2 +LHo1YGwRgJfMqZJS5ivmae2p+DYtLHe/YUjRYwu5W1LtGLBDQiKmsXeu3mnFzccc +obGlHBD7GL4acN3Bkku+KVqdPzW+5X1R+FXgJXUjhx5c3LqdsKyzadsXg8n33gy8 +CNyRnqjQ1xU3c6U1uPx+xURABsPr+CKAXEfOAuMRn0T//ZoyzH1kUQ7rVyZ2OuMe +IjzCpjbdGe+n/BLzJsBZMYVMnNjP36TMzCmT/5RtdlwTCJfy7aULTd3oyWgOZtMA +DjMSW7yV5TKQqLPGbIOtd+6Lfn6xqavT4fG2wLHqiMDn05DpKJKUe2h7lyoKZy2F +AjgQ5ANh1NolNscIWC2hp1GvMApJ9aZphwctREZ2jirlmjvXGKL8nDgQzMY70rUX +Om/9riW99XJZZLF0KjhfGEzfz3EEWjbUvy+ZnOjZurGV5gJLIaFb1cFPj65pbVPb +AZO1XB4Y3WRayhgoPmMEEf0cjQAPuDffZ4qdZqkCapH/E8ovXYO8h5Ns3CRRFgQl +Zvqz2cK6Kb6aSDiCmfS/O0oxGfm/jiEzFMpPVF/7zvuPcX/9XhmgD0uRuMRUvAaw +RY8mkaKO/qk= +-----END CERTIFICATE----- + +# Issuer: CN=AAA Certificate Services O=Comodo CA Limited +# Subject: CN=AAA Certificate Services O=Comodo CA Limited +# Label: "Comodo AAA Services root" +# Serial: 1 +# MD5 Fingerprint: 49:79:04:b0:eb:87:19:ac:47:b0:bc:11:51:9b:74:d0 +# SHA1 Fingerprint: d1:eb:23:a4:6d:17:d6:8f:d9:25:64:c2:f1:f1:60:17:64:d8:e3:49 +# SHA256 Fingerprint: d7:a7:a0:fb:5d:7e:27:31:d7:71:e9:48:4e:bc:de:f7:1d:5f:0c:3e:0a:29:48:78:2b:c8:3e:e0:ea:69:9e:f4 +-----BEGIN CERTIFICATE----- +MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb +MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow +GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj +YXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM +GEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua +BtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe +3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4 +YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR +rOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm +ez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU +oBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF +MAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v +QUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t +b2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF +AAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q +GE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz +Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2 +G9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi +l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3 +smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg== +-----END CERTIFICATE----- + +# Issuer: CN=Secure Certificate Services O=Comodo CA Limited +# Subject: CN=Secure Certificate Services O=Comodo CA Limited +# Label: "Comodo Secure Services root" +# Serial: 1 +# MD5 Fingerprint: d3:d9:bd:ae:9f:ac:67:24:b3:c8:1b:52:e1:b9:a9:bd +# SHA1 Fingerprint: 4a:65:d5:f4:1d:ef:39:b8:b8:90:4a:4a:d3:64:81:33:cf:c7:a1:d1 +# SHA256 Fingerprint: bd:81:ce:3b:4f:65:91:d1:1a:67:b5:fc:7a:47:fd:ef:25:52:1b:f9:aa:4e:18:b9:e3:df:2e:34:a7:80:3b:e8 +-----BEGIN CERTIFICATE----- +MIIEPzCCAyegAwIBAgIBATANBgkqhkiG9w0BAQUFADB+MQswCQYDVQQGEwJHQjEb +MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow +GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEkMCIGA1UEAwwbU2VjdXJlIENlcnRp +ZmljYXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVow +fjELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxJDAiBgNV +BAMMG1NlY3VyZSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBAMBxM4KK0HDrc4eCQNUd5MvJDkKQ+d40uaG6EfQlhfPM +cm3ye5drswfxdySRXyWP9nQ95IDC+DwN879A6vfIUtFyb+/Iq0G4bi4XKpVpDM3S +HpR7LZQdqnXXs5jLrLxkU0C8j6ysNstcrbvd4JQX7NFc0L/vpZXJkMWwrPsbQ996 +CF23uPJAGysnnlDOXmWCiIxe004MeuoIkbY2qitC++rCoznl2yY4rYsK7hljxxwk +3wN42ubqwUcaCwtGCd0C/N7Lh1/XMGNooa7cMqG6vv5Eq2i2pRcV/b3Vp6ea5EQz +6YiO/O1R65NxTq0B50SOqy3LqP4BSUjwwN3HaNiS/j0CAwEAAaOBxzCBxDAdBgNV +HQ4EFgQUPNiTiMLAggnMAZkGkyDpnnAJY08wDgYDVR0PAQH/BAQDAgEGMA8GA1Ud +EwEB/wQFMAMBAf8wgYEGA1UdHwR6MHgwO6A5oDeGNWh0dHA6Ly9jcmwuY29tb2Rv +Y2EuY29tL1NlY3VyZUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDmgN6A1hjNodHRw +Oi8vY3JsLmNvbW9kby5uZXQvU2VjdXJlQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmww +DQYJKoZIhvcNAQEFBQADggEBAIcBbSMdflsXfcFhMs+P5/OKlFlm4J4oqF7Tt/Q0 +5qo5spcWxYJvMqTpjOev/e/C6LlLqqP05tqNZSH7uoDrJiiFGv45jN5bBAS0VPmj +Z55B+glSzAVIqMk/IQQezkhr/IXownuvf7fM+F86/TXGDe+X3EyrEeFryzHRbPtI +gKvcnDe4IRRLDXE97IMzbtFuMhbsmMcWi1mmNKsFVy2T96oTy9IT4rcuO81rUBcJ +aD61JlfutuC23bkpgHl9j6PwpCikFcSF9CfUa7/lXORlAnZUtOM3ZiTTGWHIUhDl +izeauan5Hb/qmZJhlv8BzaFfDbxxvA6sCx1HRR3B7Hzs/Sk= +-----END CERTIFICATE----- + +# Issuer: CN=Trusted Certificate Services O=Comodo CA Limited +# Subject: CN=Trusted Certificate Services O=Comodo CA Limited +# Label: "Comodo Trusted Services root" +# Serial: 1 +# MD5 Fingerprint: 91:1b:3f:6e:cd:9e:ab:ee:07:fe:1f:71:d2:b3:61:27 +# SHA1 Fingerprint: e1:9f:e3:0e:8b:84:60:9e:80:9b:17:0d:72:a8:c5:ba:6e:14:09:bd +# SHA256 Fingerprint: 3f:06:e5:56:81:d4:96:f5:be:16:9e:b5:38:9f:9f:2b:8f:f6:1e:17:08:df:68:81:72:48:49:cd:5d:27:cb:69 +-----BEGIN CERTIFICATE----- +MIIEQzCCAyugAwIBAgIBATANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJHQjEb +MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow +GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDElMCMGA1UEAwwcVHJ1c3RlZCBDZXJ0 +aWZpY2F0ZSBTZXJ2aWNlczAeFw0wNDAxMDEwMDAwMDBaFw0yODEyMzEyMzU5NTla +MH8xCzAJBgNVBAYTAkdCMRswGQYDVQQIDBJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAO +BgNVBAcMB1NhbGZvcmQxGjAYBgNVBAoMEUNvbW9kbyBDQSBMaW1pdGVkMSUwIwYD +VQQDDBxUcnVzdGVkIENlcnRpZmljYXRlIFNlcnZpY2VzMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA33FvNlhTWvI2VFeAxHQIIO0Yfyod5jWaHiWsnOWW +fnJSoBVC21ndZHoa0Lh73TkVvFVIxO06AOoxEbrycXQaZ7jPM8yoMa+j49d/vzMt +TGo87IvDktJTdyR0nAducPy9C1t2ul/y/9c3S0pgePfw+spwtOpZqqPOSC+pw7IL +fhdyFgymBwwbOM/JYrc/oJOlh0Hyt3BAd9i+FHzjqMB6juljatEPmsbS9Is6FARW +1O24zG71++IsWL1/T2sr92AkWCTOJu80kTrV44HQsvAEAtdbtz6SrGsSivnkBbA7 +kUlcsutT6vifR4buv5XAwAaf0lteERv0xwQ1KdJVXOTt6wIDAQABo4HJMIHGMB0G +A1UdDgQWBBTFe1i97doladL3WRaoszLAeydb9DAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zCBgwYDVR0fBHwwejA8oDqgOIY2aHR0cDovL2NybC5jb21v +ZG9jYS5jb20vVHJ1c3RlZENlcnRpZmljYXRlU2VydmljZXMuY3JsMDqgOKA2hjRo +dHRwOi8vY3JsLmNvbW9kby5uZXQvVHJ1c3RlZENlcnRpZmljYXRlU2VydmljZXMu +Y3JsMA0GCSqGSIb3DQEBBQUAA4IBAQDIk4E7ibSvuIQSTI3S8NtwuleGFTQQuS9/ +HrCoiWChisJ3DFBKmwCL2Iv0QeLQg4pKHBQGsKNoBXAxMKdTmw7pSqBYaWcOrp32 +pSxBvzwGa+RZzG0Q8ZZvH9/0BAKkn0U+yNj6NkZEUD+Cl5EfKNsYEYwq5GWDVxIS +jBc/lDb+XbDABHcTuPQV1T84zJQ6VdCsmPW6AF/ghhmBeC8owH7TzEIK9a5QoNE+ +xqFx7D+gIIxmOom0jtTYsU0lR+4viMi14QVFwL4Ucd56/Y57fU0IlqUSc/Atyjcn +dBInTMu2l+nZrghtWjlA3QVHdWpaIbOjGM9O9y5Xt5hwXsjEeLBi +-----END CERTIFICATE----- + +# Issuer: CN=UTN - DATACorp SGC O=The USERTRUST Network OU=http://www.usertrust.com +# Subject: CN=UTN - DATACorp SGC O=The USERTRUST Network OU=http://www.usertrust.com +# Label: "UTN DATACorp SGC Root CA" +# Serial: 91374294542884689855167577680241077609 +# MD5 Fingerprint: b3:a5:3e:77:21:6d:ac:4a:c0:c9:fb:d5:41:3d:ca:06 +# SHA1 Fingerprint: 58:11:9f:0e:12:82:87:ea:50:fd:d9:87:45:6f:4f:78:dc:fa:d6:d4 +# SHA256 Fingerprint: 85:fb:2f:91:dd:12:27:5a:01:45:b6:36:53:4f:84:02:4a:d6:8b:69:b8:ee:88:68:4f:f7:11:37:58:05:b3:48 +-----BEGIN CERTIFICATE----- +MIIEXjCCA0agAwIBAgIQRL4Mi1AAIbQR0ypoBqmtaTANBgkqhkiG9w0BAQUFADCB +kzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug +Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho +dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xGzAZBgNVBAMTElVUTiAtIERBVEFDb3Jw +IFNHQzAeFw05OTA2MjQxODU3MjFaFw0xOTA2MjQxOTA2MzBaMIGTMQswCQYDVQQG +EwJVUzELMAkGA1UECBMCVVQxFzAVBgNVBAcTDlNhbHQgTGFrZSBDaXR5MR4wHAYD +VQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxITAfBgNVBAsTGGh0dHA6Ly93d3cu +dXNlcnRydXN0LmNvbTEbMBkGA1UEAxMSVVROIC0gREFUQUNvcnAgU0dDMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3+5YEKIrblXEjr8uRgnn4AgPLit6 +E5Qbvfa2gI5lBZMAHryv4g+OGQ0SR+ysraP6LnD43m77VkIVni5c7yPeIbkFdicZ +D0/Ww5y0vpQZY/KmEQrrU0icvvIpOxboGqBMpsn0GFlowHDyUwDAXlCCpVZvNvlK +4ESGoE1O1kduSUrLZ9emxAW5jh70/P/N5zbgnAVssjMiFdC04MwXwLLA9P4yPykq +lXvY8qdOD1R8oQ2AswkDwf9c3V6aPryuvEeKaq5xyh+xKrhfQgUL7EYw0XILyulW +bfXv33i+Ybqypa4ETLyorGkVl73v67SMvzX41MPRKA5cOp9wGDMgd8SirwIDAQAB +o4GrMIGoMAsGA1UdDwQEAwIBxjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRT +MtGzz3/64PGgXYVOktKeRR20TzA9BgNVHR8ENjA0MDKgMKAuhixodHRwOi8vY3Js +LnVzZXJ0cnVzdC5jb20vVVROLURBVEFDb3JwU0dDLmNybDAqBgNVHSUEIzAhBggr +BgEFBQcDAQYKKwYBBAGCNwoDAwYJYIZIAYb4QgQBMA0GCSqGSIb3DQEBBQUAA4IB +AQAnNZcAiosovcYzMB4p/OL31ZjUQLtgyr+rFywJNn9Q+kHcrpY6CiM+iVnJowft +Gzet/Hy+UUla3joKVAgWRcKZsYfNjGjgaQPpxE6YsjuMFrMOoAyYUJuTqXAJyCyj +j98C5OBxOvG0I3KgqgHf35g+FFCgMSa9KOlaMCZ1+XtgHI3zzVAmbQQnmt/VDUVH +KWss5nbZqSl9Mt3JNjy9rjXxEZ4du5A/EkdOjtd+D2JzHVImOBwYSf0wdJrE5SIv +2MCN7ZF6TACPcn9d2t0bi0Vr591pl6jFVkwPDPafepE39peC4N1xaf92P2BNPM/3 +mfnGV/TJVTl4uix5yaaIK/QI +-----END CERTIFICATE----- + +# Issuer: CN=UTN-USERFirst-Hardware O=The USERTRUST Network OU=http://www.usertrust.com +# Subject: CN=UTN-USERFirst-Hardware O=The USERTRUST Network OU=http://www.usertrust.com +# Label: "UTN USERFirst Hardware Root CA" +# Serial: 91374294542884704022267039221184531197 +# MD5 Fingerprint: 4c:56:41:e5:0d:bb:2b:e8:ca:a3:ed:18:08:ad:43:39 +# SHA1 Fingerprint: 04:83:ed:33:99:ac:36:08:05:87:22:ed:bc:5e:46:00:e3:be:f9:d7 +# SHA256 Fingerprint: 6e:a5:47:41:d0:04:66:7e:ed:1b:48:16:63:4a:a3:a7:9e:6e:4b:96:95:0f:82:79:da:fc:8d:9b:d8:81:21:37 +-----BEGIN CERTIFICATE----- +MIIEdDCCA1ygAwIBAgIQRL4Mi1AAJLQR0zYq/mUK/TANBgkqhkiG9w0BAQUFADCB +lzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug +Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho +dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xHzAdBgNVBAMTFlVUTi1VU0VSRmlyc3Qt +SGFyZHdhcmUwHhcNOTkwNzA5MTgxMDQyWhcNMTkwNzA5MTgxOTIyWjCBlzELMAkG +A1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2UgQ2l0eTEe +MBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExhodHRwOi8v +d3d3LnVzZXJ0cnVzdC5jb20xHzAdBgNVBAMTFlVUTi1VU0VSRmlyc3QtSGFyZHdh +cmUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCx98M4P7Sof885glFn +0G2f0v9Y8+efK+wNiVSZuTiZFvfgIXlIwrthdBKWHTxqctU8EGc6Oe0rE81m65UJ +M6Rsl7HoxuzBdXmcRl6Nq9Bq/bkqVRcQVLMZ8Jr28bFdtqdt++BxF2uiiPsA3/4a +MXcMmgF6sTLjKwEHOG7DpV4jvEWbe1DByTCP2+UretNb+zNAHqDVmBe8i4fDidNd +oI6yqqr2jmmIBsX6iSHzCJ1pLgkzmykNRg+MzEk0sGlRvfkGzWitZky8PqxhvQqI +DsjfPe58BEydCl5rkdbux+0ojatNh4lz0G6k0B4WixThdkQDf2Os5M1JnMWS9Ksy +oUhbAgMBAAGjgbkwgbYwCwYDVR0PBAQDAgHGMA8GA1UdEwEB/wQFMAMBAf8wHQYD +VR0OBBYEFKFyXyYbKJhDlV0HN9WFlp1L0sNFMEQGA1UdHwQ9MDswOaA3oDWGM2h0 +dHA6Ly9jcmwudXNlcnRydXN0LmNvbS9VVE4tVVNFUkZpcnN0LUhhcmR3YXJlLmNy +bDAxBgNVHSUEKjAoBggrBgEFBQcDAQYIKwYBBQUHAwUGCCsGAQUFBwMGBggrBgEF +BQcDBzANBgkqhkiG9w0BAQUFAAOCAQEARxkP3nTGmZev/K0oXnWO6y1n7k57K9cM +//bey1WiCuFMVGWTYGufEpytXoMs61quwOQt9ABjHbjAbPLPSbtNk28Gpgoiskli +CE7/yMgUsogWXecB5BKV5UU0s4tpvc+0hY91UZ59Ojg6FEgSxvunOxqNDYJAB+gE +CJChicsZUN/KHAG8HQQZexB2lzvukJDKxA4fFm517zP4029bHpbj4HR3dHuKom4t +3XbWOTCC8KucUvIqx69JXn7HaOWCgchqJ/kniCrVWFCVH/A7HFe7fRQ5YiuayZSS +KqMiDP+JJn1fIytH1xUdqWqeUQ0qUZ6B+dQ7XnASfxAynB67nfhmqA== +-----END CERTIFICATE----- + +# Issuer: CN=XRamp Global Certification Authority O=XRamp Security Services Inc OU=www.xrampsecurity.com +# Subject: CN=XRamp Global Certification Authority O=XRamp Security Services Inc OU=www.xrampsecurity.com +# Label: "XRamp Global CA Root" +# Serial: 107108908803651509692980124233745014957 +# MD5 Fingerprint: a1:0b:44:b3:ca:10:d8:00:6e:9d:0f:d8:0f:92:0a:d1 +# SHA1 Fingerprint: b8:01:86:d1:eb:9c:86:a5:41:04:cf:30:54:f3:4c:52:b7:e5:58:c6 +# SHA256 Fingerprint: ce:cd:dc:90:50:99:d8:da:df:c5:b1:d2:09:b7:37:cb:e2:c1:8c:fb:2c:10:c0:ff:0b:cf:0d:32:86:fc:1a:a2 +-----BEGIN CERTIFICATE----- +MIIEMDCCAxigAwIBAgIQUJRs7Bjq1ZxN1ZfvdY+grTANBgkqhkiG9w0BAQUFADCB +gjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEk +MCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2VydmljZXMgSW5jMS0wKwYDVQQDEyRY +UmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQxMTAxMTcx +NDA0WhcNMzUwMTAxMDUzNzE5WjCBgjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3 +dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2Vy +dmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCYJB69FbS6 +38eMpSe2OAtp87ZOqCwuIR1cRN8hXX4jdP5efrRKt6atH67gBhbim1vZZ3RrXYCP +KZ2GG9mcDZhtdhAoWORlsH9KmHmf4MMxfoArtYzAQDsRhtDLooY2YKTVMIJt2W7Q +DxIEM5dfT2Fa8OT5kavnHTu86M/0ay00fOJIYRyO82FEzG+gSqmUsE3a56k0enI4 +qEHMPJQRfevIpoy3hsvKMzvZPTeL+3o+hiznc9cKV6xkmxnr9A8ECIqsAxcZZPRa +JSKNNCyy9mgdEm3Tih4U2sSPpuIjhdV6Db1q4Ons7Be7QhtnqiXtRYMh/MHJfNVi +PvryxS3T/dRlAgMBAAGjgZ8wgZwwEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0P +BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMZPoj0GY4QJnM5i5ASs +jVy16bYbMDYGA1UdHwQvMC0wK6ApoCeGJWh0dHA6Ly9jcmwueHJhbXBzZWN1cml0 +eS5jb20vWEdDQS5jcmwwEAYJKwYBBAGCNxUBBAMCAQEwDQYJKoZIhvcNAQEFBQAD +ggEBAJEVOQMBG2f7Shz5CmBbodpNl2L5JFMn14JkTpAuw0kbK5rc/Kh4ZzXxHfAR +vbdI4xD2Dd8/0sm2qlWkSLoC295ZLhVbO50WfUfXN+pfTXYSNrsf16GBBEYgoyxt +qZ4Bfj8pzgCT3/3JknOJiWSe5yvkHJEs0rnOfc5vMZnT5r7SHpDwCRR5XCOrTdLa +IR9NmXmd4c8nnxCbHIgNsIpkQTG4DmyQJKSbXHGPurt+HBvbaoAPIbzp26a3QPSy +i6mx5O+aGtA9aZnuqCij4Tyz8LIRnM98QObd50N9otg6tamN8jSZxNQQ4Qb9CYQQ +O+7ETPTsJ3xCwnR8gooJybQDJbw= +-----END CERTIFICATE----- + +# Issuer: O=The Go Daddy Group, Inc. OU=Go Daddy Class 2 Certification Authority +# Subject: O=The Go Daddy Group, Inc. OU=Go Daddy Class 2 Certification Authority +# Label: "Go Daddy Class 2 CA" +# Serial: 0 +# MD5 Fingerprint: 91:de:06:25:ab:da:fd:32:17:0c:bb:25:17:2a:84:67 +# SHA1 Fingerprint: 27:96:ba:e6:3f:18:01:e2:77:26:1b:a0:d7:77:70:02:8f:20:ee:e4 +# SHA256 Fingerprint: c3:84:6b:f2:4b:9e:93:ca:64:27:4c:0e:c6:7c:1e:cc:5e:02:4f:fc:ac:d2:d7:40:19:35:0e:81:fe:54:6a:e4 +-----BEGIN CERTIFICATE----- +MIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEh +MB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBE +YWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3 +MDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkGA1UEBhMCVVMxITAfBgNVBAoTGFRo +ZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28gRGFkZHkgQ2xhc3Mg +MiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQADggEN +ADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCA +PVYYYwhv2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6w +wdhFJ2+qN1j3hybX2C32qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXi +EqITLdiOr18SPaAIBQi2XKVlOARFmR6jYGB0xUGlcmIbYsUfb18aQr4CUWWoriMY +avx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmYvLEHZ6IVDd2gWMZEewo+ +YihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0OBBYEFNLE +sNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h +/t2oatTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5 +IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD +ggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wimPQoZ+YeAEW5p5JYXMP80kWNy +OO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKtI3lpjbi2Tc7P +TMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ +HmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mER +dEr/VxqHD3VILs9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5Cuf +ReYNnyicsbkqWletNw+vHX/bvZ8= +-----END CERTIFICATE----- + +# Issuer: O=Starfield Technologies, Inc. OU=Starfield Class 2 Certification Authority +# Subject: O=Starfield Technologies, Inc. OU=Starfield Class 2 Certification Authority +# Label: "Starfield Class 2 CA" +# Serial: 0 +# MD5 Fingerprint: 32:4a:4b:bb:c8:63:69:9b:be:74:9a:c6:dd:1d:46:24 +# SHA1 Fingerprint: ad:7e:1c:28:b0:64:ef:8f:60:03:40:20:14:c3:d0:e3:37:0e:b5:8a +# SHA256 Fingerprint: 14:65:fa:20:53:97:b8:76:fa:a6:f0:a9:95:8e:55:90:e4:0f:cc:7f:aa:4f:b7:c2:c8:67:75:21:fb:5f:b6:58 +-----BEGIN CERTIFICATE----- +MIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzEl +MCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMp +U3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQw +NjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBoMQswCQYDVQQGEwJVUzElMCMGA1UE +ChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZp +ZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqGSIb3 +DQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf +8MOh2tTYbitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN ++lq2cwQlZut3f+dZxkqZJRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0 +X9tDkYI22WY8sbi5gv2cOj4QyDvvBmVmepsZGD3/cVE8MC5fvj13c7JdBmzDI1aa +K4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSNF4Azbl5KXZnJHoe0nRrA +1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HFMIHCMB0G +A1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fR +zt0fhvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0 +YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBD +bGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8w +DQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGsafPzWdqbAYcaT1epoXkJKtv3 +L7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLMPUxA2IGvd56D +eruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl +xy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynp +VSJYACPq4xJDKVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEY +WQPJIrSPnNVeKtelttQKbfi3QBFGmh95DmK/D5fs4C8fF5Q= +-----END CERTIFICATE----- + +# Issuer: CN=StartCom Certification Authority O=StartCom Ltd. OU=Secure Digital Certificate Signing +# Subject: CN=StartCom Certification Authority O=StartCom Ltd. OU=Secure Digital Certificate Signing +# Label: "StartCom Certification Authority" +# Serial: 1 +# MD5 Fingerprint: 22:4d:8f:8a:fc:f7:35:c2:bb:57:34:90:7b:8b:22:16 +# SHA1 Fingerprint: 3e:2b:f7:f2:03:1b:96:f3:8c:e6:c4:d8:a8:5d:3e:2d:58:47:6a:0f +# SHA256 Fingerprint: c7:66:a9:be:f2:d4:07:1c:86:3a:31:aa:49:20:e8:13:b2:d1:98:60:8c:b7:b7:cf:e2:11:43:b8:36:df:09:ea +-----BEGIN CERTIFICATE----- +MIIHyTCCBbGgAwIBAgIBATANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJJTDEW +MBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwg +Q2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0NjM2WhcNMzYwOTE3MTk0NjM2WjB9 +MQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMi +U2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3Rh +cnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZk +pMyONvg45iPwbm2xPN1yo4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rf +OQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/C +Ji/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/deMotHweXMAEtcnn6RtYT +Kqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt2PZE4XNi +HzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMM +Av+Z6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w ++2OqqGwaVLRcJXrJosmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+ +Gkhpi8KWTRoSsmkXwQqQ1vp5Iki/untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3 +Zzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVcUjyJthkqcwEKDwOzEmDyei+B +26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT37uMdBNSSwID +AQABo4ICUjCCAk4wDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAa4wHQYDVR0OBBYE +FE4L7xqkQFulF2mHMMo0aEPQQa7yMGQGA1UdHwRdMFswLKAqoCiGJmh0dHA6Ly9j +ZXJ0LnN0YXJ0Y29tLm9yZy9zZnNjYS1jcmwuY3JsMCugKaAnhiVodHRwOi8vY3Js +LnN0YXJ0Y29tLm9yZy9zZnNjYS1jcmwuY3JsMIIBXQYDVR0gBIIBVDCCAVAwggFM +BgsrBgEEAYG1NwEBATCCATswLwYIKwYBBQUHAgEWI2h0dHA6Ly9jZXJ0LnN0YXJ0 +Y29tLm9yZy9wb2xpY3kucGRmMDUGCCsGAQUFBwIBFilodHRwOi8vY2VydC5zdGFy +dGNvbS5vcmcvaW50ZXJtZWRpYXRlLnBkZjCB0AYIKwYBBQUHAgIwgcMwJxYgU3Rh +cnQgQ29tbWVyY2lhbCAoU3RhcnRDb20pIEx0ZC4wAwIBARqBl0xpbWl0ZWQgTGlh +YmlsaXR5LCByZWFkIHRoZSBzZWN0aW9uICpMZWdhbCBMaW1pdGF0aW9ucyogb2Yg +dGhlIFN0YXJ0Q29tIENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFBvbGljeSBhdmFp +bGFibGUgYXQgaHR0cDovL2NlcnQuc3RhcnRjb20ub3JnL3BvbGljeS5wZGYwEQYJ +YIZIAYb4QgEBBAQDAgAHMDgGCWCGSAGG+EIBDQQrFilTdGFydENvbSBGcmVlIFNT +TCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTANBgkqhkiG9w0BAQUFAAOCAgEAFmyZ +9GYMNPXQhV59CuzaEE44HF7fpiUFS5Eyweg78T3dRAlbB0mKKctmArexmvclmAk8 +jhvh3TaHK0u7aNM5Zj2gJsfyOZEdUauCe37Vzlrk4gNXcGmXCPleWKYK34wGmkUW +FjgKXlf2Ysd6AgXmvB618p70qSmD+LIU424oh0TDkBreOKk8rENNZEXO3SipXPJz +ewT4F+irsfMuXGRuczE6Eri8sxHkfY+BUZo7jYn0TZNmezwD7dOaHZrzZVD1oNB1 +ny+v8OqCQ5j4aZyJecRDjkZy42Q2Eq/3JR44iZB3fsNrarnDy0RLrHiQi+fHLB5L +EUTINFInzQpdn4XBidUaePKVEFMy3YCEZnXZtWgo+2EuvoSoOMCZEoalHmdkrQYu +L6lwhceWD3yJZfWOQ1QOq92lgDmUYMA0yZZwLKMS9R9Ie70cfmu3nZD0Ijuu+Pwq +yvqCUqDvr0tVk+vBtfAii6w0TiYiBKGHLHVKt+V9E9e4DGTANtLJL4YSjCMJwRuC +O3NJo2pXh5Tl1njFmUNj403gdy3hZZlyaQQaRwnmDwFWJPsfvw55qVguucQJAX6V +um0ABj6y6koQOdjQK/W/7HW/lwLFCRsI3FU34oH7N4RDYiDK51ZLZer+bMEkkySh +NOsF/5oirpt9P/FlUQqmMGqz9IgcgA38corog14= +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Assured ID Root CA O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Assured ID Root CA O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Assured ID Root CA" +# Serial: 17154717934120587862167794914071425081 +# MD5 Fingerprint: 87:ce:0b:7b:2a:0e:49:00:e1:58:71:9b:37:a8:93:72 +# SHA1 Fingerprint: 05:63:b8:63:0d:62:d7:5a:bb:c8:ab:1e:4b:df:b5:a8:99:b2:4d:43 +# SHA256 Fingerprint: 3e:90:99:b5:01:5e:8f:48:6c:00:bc:ea:9d:11:1e:e7:21:fa:ba:35:5a:89:bc:f1:df:69:56:1e:3d:c6:32:5c +-----BEGIN CERTIFICATE----- +MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBl +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv +b3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl +cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7c +JpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYP +mDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+ +wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4 +VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1roV9Iq4/ +AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whfGHdPAgMB +AAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYun +pyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRC +dWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTf +fwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+fT8r87cm +NW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5QZ7dsvfPx +H2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe ++o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g== +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Global Root CA O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Global Root CA O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Global Root CA" +# Serial: 10944719598952040374951832963794454346 +# MD5 Fingerprint: 79:e4:a9:84:0d:7d:3a:96:d7:c0:4f:e2:43:4c:89:2e +# SHA1 Fingerprint: a8:98:5d:3a:65:e5:e5:c4:b2:d7:d6:6d:40:c6:dd:2f:b1:9c:54:36 +# SHA256 Fingerprint: 43:48:a0:e9:44:4c:78:cb:26:5e:05:8d:5e:89:44:b4:d8:4f:96:62:bd:26:db:25:7f:89:34:a4:43:c7:01:61 +-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD +QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB +CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 +nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt +43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P +T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 +gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR +TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw +DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr +hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg +06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF +PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls +YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk +CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert High Assurance EV Root CA O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert High Assurance EV Root CA O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert High Assurance EV Root CA" +# Serial: 3553400076410547919724730734378100087 +# MD5 Fingerprint: d4:74:de:57:5c:39:b2:d3:9c:85:83:c5:c0:65:49:8a +# SHA1 Fingerprint: 5f:b7:ee:06:33:e2:59:db:ad:0c:4c:9a:e6:d3:8f:1a:61:c7:dc:25 +# SHA256 Fingerprint: 74:31:e5:f4:c3:c1:ce:46:90:77:4f:0b:61:e0:54:40:88:3b:a9:a0:1e:d0:0b:a6:ab:d7:80:6e:d3:b1:18:cf +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j +ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL +MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 +LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug +RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm ++9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW +PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM +xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB +Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3 +hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg +EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA +FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec +nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z +eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF +hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2 +Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe +vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep ++OkuE6N36B9K +-----END CERTIFICATE----- + +# Issuer: CN=GeoTrust Primary Certification Authority O=GeoTrust Inc. +# Subject: CN=GeoTrust Primary Certification Authority O=GeoTrust Inc. +# Label: "GeoTrust Primary Certification Authority" +# Serial: 32798226551256963324313806436981982369 +# MD5 Fingerprint: 02:26:c3:01:5e:08:30:37:43:a9:d0:7d:cf:37:e6:bf +# SHA1 Fingerprint: 32:3c:11:8e:1b:f7:b8:b6:52:54:e2:e2:10:0d:d6:02:90:37:f0:96 +# SHA256 Fingerprint: 37:d5:10:06:c5:12:ea:ab:62:64:21:f1:ec:8c:92:01:3f:c5:f8:2a:e9:8e:e5:33:eb:46:19:b8:de:b4:d0:6c +-----BEGIN CERTIFICATE----- +MIIDfDCCAmSgAwIBAgIQGKy1av1pthU6Y2yv2vrEoTANBgkqhkiG9w0BAQUFADBY +MQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjExMC8GA1UEAxMo +R2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEx +MjcwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMFgxCzAJBgNVBAYTAlVTMRYwFAYDVQQK +Ew1HZW9UcnVzdCBJbmMuMTEwLwYDVQQDEyhHZW9UcnVzdCBQcmltYXJ5IENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAvrgVe//UfH1nrYNke8hCUy3f9oQIIGHWAVlqnEQRr+92/ZV+zmEwu3qDXwK9 +AWbK7hWNb6EwnL2hhZ6UOvNWiAAxz9juapYC2e0DjPt1befquFUWBRaa9OBesYjA +ZIVcFU2Ix7e64HXprQU9nceJSOC7KMgD4TCTZF5SwFlwIjVXiIrxlQqD17wxcwE0 +7e9GceBrAqg1cmuXm2bgyxx5X9gaBGgeRwLmnWDiNpcB3841kt++Z8dtd1k7j53W +kBWUvEI0EME5+bEnPn7WinXFsq+W06Lem+SYvn3h6YGttm/81w7a4DSwDRp35+MI +mO9Y+pyEtzavwt+s0vQQBnBxNQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4G +A1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQULNVQQZcVi/CPNmFbSvtr2ZnJM5IwDQYJ +KoZIhvcNAQEFBQADggEBAFpwfyzdtzRP9YZRqSa+S7iq8XEN3GHHoOo0Hnp3DwQ1 +6CePbJC/kRYkRj5KTs4rFtULUh38H2eiAkUxT87z+gOneZ1TatnaYzr4gNfTmeGl +4b7UVXGYNTq+k+qurUKykG/g/CFNNWMziUnWm07Kx+dOCQD32sfvmWKZd7aVIl6K +oKv0uHiYyjgZmclynnjNS6yvGaBzEi38wkG6gZHaFloxt/m0cYASSJlyc1pZU8Fj +UjPtp8nSOQJw+uCxQmYpqptR7TBUIhRf2asdweSU8Pj1K/fqynhG1riR/aYNKxoU +AT6A8EKglQdebc3MS6RFjasS6LPeWuWgfOgPIh1a6Vk= +-----END CERTIFICATE----- + +# Issuer: CN=thawte Primary Root CA O=thawte, Inc. OU=Certification Services Division/(c) 2006 thawte, Inc. - For authorized use only +# Subject: CN=thawte Primary Root CA O=thawte, Inc. OU=Certification Services Division/(c) 2006 thawte, Inc. - For authorized use only +# Label: "thawte Primary Root CA" +# Serial: 69529181992039203566298953787712940909 +# MD5 Fingerprint: 8c:ca:dc:0b:22:ce:f5:be:72:ac:41:1a:11:a8:d8:12 +# SHA1 Fingerprint: 91:c6:d6:ee:3e:8a:c8:63:84:e5:48:c2:99:29:5c:75:6c:81:7b:81 +# SHA256 Fingerprint: 8d:72:2f:81:a9:c1:13:c0:79:1d:f1:36:a2:96:6d:b2:6c:95:0a:97:1d:b4:6b:41:99:f4:ea:54:b7:8b:fb:9f +-----BEGIN CERTIFICATE----- +MIIEIDCCAwigAwIBAgIQNE7VVyDV7exJ9C/ON9srbTANBgkqhkiG9w0BAQUFADCB +qTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMf +Q2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIw +MDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxHzAdBgNV +BAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwHhcNMDYxMTE3MDAwMDAwWhcNMzYw +NzE2MjM1OTU5WjCBqTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5j +LjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYG +A1UECxMvKGMpIDIwMDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNl +IG9ubHkxHzAdBgNVBAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCsoPD7gFnUnMekz52hWXMJEEUMDSxuaPFs +W0hoSVk3/AszGcJ3f8wQLZU0HObrTQmnHNK4yZc2AreJ1CRfBsDMRJSUjQJib+ta +3RGNKJpchJAQeg29dGYvajig4tVUROsdB58Hum/u6f1OCyn1PoSgAfGcq/gcfomk +6KHYcWUNo1F77rzSImANuVud37r8UVsLr5iy6S7pBOhih94ryNdOwUxkHt3Ph1i6 +Sk/KaAcdHJ1KxtUvkcx8cXIcxcBn6zL9yZJclNqFwJu/U30rCfSMnZEfl2pSy94J +NqR32HuHUETVPm4pafs5SSYeCaWAe0At6+gnhcn+Yf1+5nyXHdWdAgMBAAGjQjBA +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBR7W0XP +r87Lev0xkhpqtvNG61dIUDANBgkqhkiG9w0BAQUFAAOCAQEAeRHAS7ORtvzw6WfU +DW5FvlXok9LOAz/t2iWwHVfLHjp2oEzsUHboZHIMpKnxuIvW1oeEuzLlQRHAd9mz +YJ3rG9XRbkREqaYB7FViHXe4XI5ISXycO1cRrK1zN44veFyQaEfZYGDm/Ac9IiAX +xPcW6cTYcvnIc3zfFi8VqT79aie2oetaupgf1eNNZAqdE8hhuvU5HIe6uL17In/2 +/qxAeeWsEG89jxt5dovEN7MhGITlNgDrYyCZuen+MwS7QcjBAvlEYyCegc5C09Y/ +LHbTY5xZ3Y+m4Q6gLkH3LpVHz7z9M/P2C2F+fpErgUfCJzDupxBdN49cOSvkBPB7 +jVaMaA== +-----END CERTIFICATE----- + +# Issuer: CN=VeriSign Class 3 Public Primary Certification Authority - G5 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2006 VeriSign, Inc. - For authorized use only +# Subject: CN=VeriSign Class 3 Public Primary Certification Authority - G5 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2006 VeriSign, Inc. - For authorized use only +# Label: "VeriSign Class 3 Public Primary Certification Authority - G5" +# Serial: 33037644167568058970164719475676101450 +# MD5 Fingerprint: cb:17:e4:31:67:3e:e2:09:fe:45:57:93:f3:0a:fa:1c +# SHA1 Fingerprint: 4e:b6:d5:78:49:9b:1c:cf:5f:58:1e:ad:56:be:3d:9b:67:44:a5:e5 +# SHA256 Fingerprint: 9a:cf:ab:7e:43:c8:d8:80:d0:6b:26:2a:94:de:ee:e4:b4:65:99:89:c3:d0:ca:f1:9b:af:64:05:e4:1a:b7:df +-----BEGIN CERTIFICATE----- +MIIE0zCCA7ugAwIBAgIQGNrRniZ96LtKIVjNzGs7SjANBgkqhkiG9w0BAQUFADCB +yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL +ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJp +U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxW +ZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5IC0gRzUwHhcNMDYxMTA4MDAwMDAwWhcNMzYwNzE2MjM1OTU5WjCByjEL +MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW +ZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJpU2ln +biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJp +U2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9y +aXR5IC0gRzUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvJAgIKXo1 +nmAMqudLO07cfLw8RRy7K+D+KQL5VwijZIUVJ/XxrcgxiV0i6CqqpkKzj/i5Vbex +t0uz/o9+B1fs70PbZmIVYc9gDaTY3vjgw2IIPVQT60nKWVSFJuUrjxuf6/WhkcIz +SdhDY2pSS9KP6HBRTdGJaXvHcPaz3BJ023tdS1bTlr8Vd6Gw9KIl8q8ckmcY5fQG +BO+QueQA5N06tRn/Arr0PO7gi+s3i+z016zy9vA9r911kTMZHRxAy3QkGSGT2RT+ +rCpSx4/VBEnkjWNHiDxpg8v+R70rfk/Fla4OndTRQ8Bnc+MUCH7lP59zuDMKz10/ +NIeWiu5T6CUVAgMBAAGjgbIwga8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8E +BAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJaW1hZ2UvZ2lmMCEwHzAH +BgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYjaHR0cDovL2xvZ28udmVy +aXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFH/TZafC3ey78DAJ80M5+gKv +MzEzMA0GCSqGSIb3DQEBBQUAA4IBAQCTJEowX2LP2BqYLz3q3JktvXf2pXkiOOzE +p6B4Eq1iDkVwZMXnl2YtmAl+X6/WzChl8gGqCBpH3vn5fJJaCGkgDdk+bW48DW7Y +5gaRQBi5+MHt39tBquCWIMnNZBU4gcmU7qKEKQsTb47bDN0lAtukixlE0kF6BWlK +WE9gyn6CagsCqiUXObXbf+eEZSqVir2G3l6BFoMtEMze/aiCKm0oHw0LxOXnGiYZ +4fQRbxC1lfznQgUy286dUV4otp6F01vvpX1FQHKOtw5rDgb7MzVIcbidJ4vEZV8N +hnacRHr2lVz2XTIIM6RUthg/aFzyQkqFOFSDX9HoLPKsEdao7WNq +-----END CERTIFICATE----- + +# Issuer: CN=COMODO Certification Authority O=COMODO CA Limited +# Subject: CN=COMODO Certification Authority O=COMODO CA Limited +# Label: "COMODO Certification Authority" +# Serial: 104350513648249232941998508985834464573 +# MD5 Fingerprint: 5c:48:dc:f7:42:72:ec:56:94:6d:1c:cc:71:35:80:75 +# SHA1 Fingerprint: 66:31:bf:9e:f7:4f:9e:b6:c9:d5:a6:0c:ba:6a:be:d1:f7:bd:ef:7b +# SHA256 Fingerprint: 0c:2c:d6:3d:f7:80:6f:a3:99:ed:e8:09:11:6b:57:5b:f8:79:89:f0:65:18:f9:80:8c:86:05:03:17:8b:af:66 +-----BEGIN CERTIFICATE----- +MIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCB +gTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNV +BAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEyMDEwMDAw +MDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3Jl +YXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01P +RE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3 +UcEbVASY06m/weaKXTuH+7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI +2GqGd0S7WWaXUF601CxwRM/aN5VCaTwwxHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8 +Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV4EajcNxo2f8ESIl33rXp ++2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA1KGzqSX+ +DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5O +nKVIrLsm9wIDAQABo4GOMIGLMB0GA1UdDgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW +/zAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zBJBgNVHR8EQjBAMD6g +PKA6hjhodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9u +QXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOCAQEAPpiem/Yb6dc5t3iuHXIY +SdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CPOGEIqB6BCsAv +IC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/ +RxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4 +zJVSk/BwJVmcIGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5dd +BA6+C4OmF4O5MBKgxTMVBbkN+8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IB +ZQ== +-----END CERTIFICATE----- + +# Issuer: CN=Network Solutions Certificate Authority O=Network Solutions L.L.C. +# Subject: CN=Network Solutions Certificate Authority O=Network Solutions L.L.C. +# Label: "Network Solutions Certificate Authority" +# Serial: 116697915152937497490437556386812487904 +# MD5 Fingerprint: d3:f3:a6:16:c0:fa:6b:1d:59:b1:2d:96:4d:0e:11:2e +# SHA1 Fingerprint: 74:f8:a3:c3:ef:e7:b3:90:06:4b:83:90:3c:21:64:60:20:e5:df:ce +# SHA256 Fingerprint: 15:f0:ba:00:a3:ac:7a:f3:ac:88:4c:07:2b:10:11:a0:77:bd:77:c0:97:f4:01:64:b2:f8:59:8a:bd:83:86:0c +-----BEGIN CERTIFICATE----- +MIID5jCCAs6gAwIBAgIQV8szb8JcFuZHFhfjkDFo4DANBgkqhkiG9w0BAQUFADBi +MQswCQYDVQQGEwJVUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMu +MTAwLgYDVQQDEydOZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3Jp +dHkwHhcNMDYxMjAxMDAwMDAwWhcNMjkxMjMxMjM1OTU5WjBiMQswCQYDVQQGEwJV +UzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMuMTAwLgYDVQQDEydO +ZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkvH6SMG3G2I4rC7xGzuAnlt7e+foS0zwz +c7MEL7xxjOWftiJgPl9dzgn/ggwbmlFQGiaJ3dVhXRncEg8tCqJDXRfQNJIg6nPP +OCwGJgl6cvf6UDL4wpPTaaIjzkGxzOTVHzbRijr4jGPiFFlp7Q3Tf2vouAPlT2rl +mGNpSAW+Lv8ztumXWWn4Zxmuk2GWRBXTcrA/vGp97Eh/jcOrqnErU2lBUzS1sLnF +BgrEsEX1QV1uiUV7PTsmjHTC5dLRfbIR1PtYMiKagMnc/Qzpf14Dl847ABSHJ3A4 +qY5usyd2mFHgBeMhqxrVhSI8KbWaFsWAqPS7azCPL0YCorEMIuDTAgMBAAGjgZcw +gZQwHQYDVR0OBBYEFCEwyfsA106Y2oeqKtCnLrFAMadMMA4GA1UdDwEB/wQEAwIB +BjAPBgNVHRMBAf8EBTADAQH/MFIGA1UdHwRLMEkwR6BFoEOGQWh0dHA6Ly9jcmwu +bmV0c29sc3NsLmNvbS9OZXR3b3JrU29sdXRpb25zQ2VydGlmaWNhdGVBdXRob3Jp +dHkuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQC7rkvnt1frf6ott3NHhWrB5KUd5Oc8 +6fRZZXe1eltajSU24HqXLjjAV2CDmAaDn7l2em5Q4LqILPxFzBiwmZVRDuwduIj/ +h1AcgsLj4DKAv6ALR8jDMe+ZZzKATxcheQxpXN5eNK4CtSbqUN9/GGUsyfJj4akH +/nxxH2szJGoeBfcFaMBqEssuXmHLrijTfsK0ZpEmXzwuJF/LWA/rKOyvEZbz3Htv +wKeI8lN3s2Berq4o2jUsbzRF0ybh3uxbTydrFny9RAQYgrOJeRcQcT16ohZO9QHN +pGxlaKFJdlxDydi8NmdspZS11My5vWo1ViHe2MPr+8ukYEywVaCge1ey +-----END CERTIFICATE----- + +# Issuer: CN=COMODO ECC Certification Authority O=COMODO CA Limited +# Subject: CN=COMODO ECC Certification Authority O=COMODO CA Limited +# Label: "COMODO ECC Certification Authority" +# Serial: 41578283867086692638256921589707938090 +# MD5 Fingerprint: 7c:62:ff:74:9d:31:53:5e:68:4a:d5:78:aa:1e:bf:23 +# SHA1 Fingerprint: 9f:74:4e:9f:2b:4d:ba:ec:0f:31:2c:50:b6:56:3b:8e:2d:93:c3:11 +# SHA256 Fingerprint: 17:93:92:7a:06:14:54:97:89:ad:ce:2f:8f:34:f7:f0:b6:6d:0f:3a:e3:a3:b8:4d:21:ec:15:db:ba:4f:ad:c7 +-----BEGIN CERTIFICATE----- +MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT +IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw +MDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy +ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N +T0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR +FtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J +cfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW +BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm +fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv +GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= +-----END CERTIFICATE----- + +# Issuer: CN=TC TrustCenter Class 2 CA II O=TC TrustCenter GmbH OU=TC TrustCenter Class 2 CA +# Subject: CN=TC TrustCenter Class 2 CA II O=TC TrustCenter GmbH OU=TC TrustCenter Class 2 CA +# Label: "TC TrustCenter Class 2 CA II" +# Serial: 941389028203453866782103406992443 +# MD5 Fingerprint: ce:78:33:5c:59:78:01:6e:18:ea:b9:36:a0:b9:2e:23 +# SHA1 Fingerprint: ae:50:83:ed:7c:f4:5c:bc:8f:61:c6:21:fe:68:5d:79:42:21:15:6e +# SHA256 Fingerprint: e6:b8:f8:76:64:85:f8:07:ae:7f:8d:ac:16:70:46:1f:07:c0:a1:3e:ef:3a:1f:f7:17:53:8d:7a:ba:d3:91:b4 +-----BEGIN CERTIFICATE----- +MIIEqjCCA5KgAwIBAgIOLmoAAQACH9dSISwRXDswDQYJKoZIhvcNAQEFBQAwdjEL +MAkGA1UEBhMCREUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxIjAgBgNV +BAsTGVRDIFRydXN0Q2VudGVyIENsYXNzIDIgQ0ExJTAjBgNVBAMTHFRDIFRydXN0 +Q2VudGVyIENsYXNzIDIgQ0EgSUkwHhcNMDYwMTEyMTQzODQzWhcNMjUxMjMxMjI1 +OTU5WjB2MQswCQYDVQQGEwJERTEcMBoGA1UEChMTVEMgVHJ1c3RDZW50ZXIgR21i +SDEiMCAGA1UECxMZVEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMiBDQTElMCMGA1UEAxMc +VEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMiBDQSBJSTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAKuAh5uO8MN8h9foJIIRszzdQ2Lu+MNF2ujhoF/RKrLqk2jf +tMjWQ+nEdVl//OEd+DFwIxuInie5e/060smp6RQvkL4DUsFJzfb95AhmC1eKokKg +uNV/aVyQMrKXDcpK3EY+AlWJU+MaWss2xgdW94zPEfRMuzBwBJWl9jmM/XOBCH2J +XjIeIqkiRUuwZi4wzJ9l/fzLganx4Duvo4bRierERXlQXa7pIXSSTYtZgo+U4+lK +8edJsBTj9WLL1XK9H7nSn6DNqPoByNkN39r8R52zyFTfSUrxIan+GE7uSNQZu+99 +5OKdy1u2bv/jzVrndIIFuoAlOMvkaZ6vQaoahPUCAwEAAaOCATQwggEwMA8GA1Ud +EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTjq1RMgKHbVkO3 +kUrL84J6E1wIqzCB7QYDVR0fBIHlMIHiMIHfoIHcoIHZhjVodHRwOi8vd3d3LnRy +dXN0Y2VudGVyLmRlL2NybC92Mi90Y19jbGFzc18yX2NhX0lJLmNybIaBn2xkYXA6 +Ly93d3cudHJ1c3RjZW50ZXIuZGUvQ049VEMlMjBUcnVzdENlbnRlciUyMENsYXNz +JTIwMiUyMENBJTIwSUksTz1UQyUyMFRydXN0Q2VudGVyJTIwR21iSCxPVT1yb290 +Y2VydHMsREM9dHJ1c3RjZW50ZXIsREM9ZGU/Y2VydGlmaWNhdGVSZXZvY2F0aW9u +TGlzdD9iYXNlPzANBgkqhkiG9w0BAQUFAAOCAQEAjNfffu4bgBCzg/XbEeprS6iS +GNn3Bzn1LL4GdXpoUxUc6krtXvwjshOg0wn/9vYua0Fxec3ibf2uWWuFHbhOIprt +ZjluS5TmVfwLG4t3wVMTZonZKNaL80VKY7f9ewthXbhtvsPcW3nS7Yblok2+XnR8 +au0WOB9/WIFaGusyiC2y8zl3gK9etmF1KdsjTYjKUCjLhdLTEKJZbtOTVAB6okaV +hgWcqRmY5TFyDADiZ9lA4CQze28suVyrZZ0srHbqNZn1l7kPJOzHdiEoZa5X6AeI +dUpWoNIFOqTmjZKILPPy4cHGYdtBxceb9w4aUUXCYWvcZCcXjFq32nQozZfkvQ== +-----END CERTIFICATE----- + +# Issuer: CN=TC TrustCenter Class 3 CA II O=TC TrustCenter GmbH OU=TC TrustCenter Class 3 CA +# Subject: CN=TC TrustCenter Class 3 CA II O=TC TrustCenter GmbH OU=TC TrustCenter Class 3 CA +# Label: "TC TrustCenter Class 3 CA II" +# Serial: 1506523511417715638772220530020799 +# MD5 Fingerprint: 56:5f:aa:80:61:12:17:f6:67:21:e6:2b:6d:61:56:8e +# SHA1 Fingerprint: 80:25:ef:f4:6e:70:c8:d4:72:24:65:84:fe:40:3b:8a:8d:6a:db:f5 +# SHA256 Fingerprint: 8d:a0:84:fc:f9:9c:e0:77:22:f8:9b:32:05:93:98:06:fa:5c:b8:11:e1:c8:13:f6:a1:08:c7:d3:36:b3:40:8e +-----BEGIN CERTIFICATE----- +MIIEqjCCA5KgAwIBAgIOSkcAAQAC5aBd1j8AUb8wDQYJKoZIhvcNAQEFBQAwdjEL +MAkGA1UEBhMCREUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxIjAgBgNV +BAsTGVRDIFRydXN0Q2VudGVyIENsYXNzIDMgQ0ExJTAjBgNVBAMTHFRDIFRydXN0 +Q2VudGVyIENsYXNzIDMgQ0EgSUkwHhcNMDYwMTEyMTQ0MTU3WhcNMjUxMjMxMjI1 +OTU5WjB2MQswCQYDVQQGEwJERTEcMBoGA1UEChMTVEMgVHJ1c3RDZW50ZXIgR21i +SDEiMCAGA1UECxMZVEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMyBDQTElMCMGA1UEAxMc +VEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMyBDQSBJSTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBALTgu1G7OVyLBMVMeRwjhjEQY0NVJz/GRcekPewJDRoeIMJW +Ht4bNwcwIi9v8Qbxq63WyKthoy9DxLCyLfzDlml7forkzMA5EpBCYMnMNWju2l+Q +Vl/NHE1bWEnrDgFPZPosPIlY2C8u4rBo6SI7dYnWRBpl8huXJh0obazovVkdKyT2 +1oQDZogkAHhg8fir/gKya/si+zXmFtGt9i4S5Po1auUZuV3bOx4a+9P/FRQI2Alq +ukWdFHlgfa9Aigdzs5OW03Q0jTo3Kd5c7PXuLjHCINy+8U9/I1LZW+Jk2ZyqBwi1 +Rb3R0DHBq1SfqdLDYmAD8bs5SpJKPQq5ncWg/jcCAwEAAaOCATQwggEwMA8GA1Ud +EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTUovyfs8PYA9NX +XAek0CSnwPIA1DCB7QYDVR0fBIHlMIHiMIHfoIHcoIHZhjVodHRwOi8vd3d3LnRy +dXN0Y2VudGVyLmRlL2NybC92Mi90Y19jbGFzc18zX2NhX0lJLmNybIaBn2xkYXA6 +Ly93d3cudHJ1c3RjZW50ZXIuZGUvQ049VEMlMjBUcnVzdENlbnRlciUyMENsYXNz +JTIwMyUyMENBJTIwSUksTz1UQyUyMFRydXN0Q2VudGVyJTIwR21iSCxPVT1yb290 +Y2VydHMsREM9dHJ1c3RjZW50ZXIsREM9ZGU/Y2VydGlmaWNhdGVSZXZvY2F0aW9u +TGlzdD9iYXNlPzANBgkqhkiG9w0BAQUFAAOCAQEANmDkcPcGIEPZIxpC8vijsrlN +irTzwppVMXzEO2eatN9NDoqTSheLG43KieHPOh6sHfGcMrSOWXaiQYUlN6AT0PV8 +TtXqluJucsG7Kv5sbviRmEb8yRtXW+rIGjs/sFGYPAfaLFkB2otE6OF0/ado3VS6 +g0bsyEa1+K+XwDsJHI/OcpY9M1ZwvJbL2NV9IJqDnxrcOfHFcqMRA/07QlIp2+gB +95tejNaNhk4Z+rwcvsUhpYeeeC422wlxo3I0+GzjBgnyXlal092Y+tTmBvTwtiBj +S+opvaqCZh77gaqnN60TGOaSw4HBM7uIHqHn4rS9MWwOUT1v+5ZWgOI2F9Hc5A== +-----END CERTIFICATE----- + +# Issuer: CN=TC TrustCenter Universal CA I O=TC TrustCenter GmbH OU=TC TrustCenter Universal CA +# Subject: CN=TC TrustCenter Universal CA I O=TC TrustCenter GmbH OU=TC TrustCenter Universal CA +# Label: "TC TrustCenter Universal CA I" +# Serial: 601024842042189035295619584734726 +# MD5 Fingerprint: 45:e1:a5:72:c5:a9:36:64:40:9e:f5:e4:58:84:67:8c +# SHA1 Fingerprint: 6b:2f:34:ad:89:58:be:62:fd:b0:6b:5c:ce:bb:9d:d9:4f:4e:39:f3 +# SHA256 Fingerprint: eb:f3:c0:2a:87:89:b1:fb:7d:51:19:95:d6:63:b7:29:06:d9:13:ce:0d:5e:10:56:8a:8a:77:e2:58:61:67:e7 +-----BEGIN CERTIFICATE----- +MIID3TCCAsWgAwIBAgIOHaIAAQAC7LdggHiNtgYwDQYJKoZIhvcNAQEFBQAweTEL +MAkGA1UEBhMCREUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxJDAiBgNV +BAsTG1RDIFRydXN0Q2VudGVyIFVuaXZlcnNhbCBDQTEmMCQGA1UEAxMdVEMgVHJ1 +c3RDZW50ZXIgVW5pdmVyc2FsIENBIEkwHhcNMDYwMzIyMTU1NDI4WhcNMjUxMjMx +MjI1OTU5WjB5MQswCQYDVQQGEwJERTEcMBoGA1UEChMTVEMgVHJ1c3RDZW50ZXIg +R21iSDEkMCIGA1UECxMbVEMgVHJ1c3RDZW50ZXIgVW5pdmVyc2FsIENBMSYwJAYD +VQQDEx1UQyBUcnVzdENlbnRlciBVbml2ZXJzYWwgQ0EgSTCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBAKR3I5ZEr5D0MacQ9CaHnPM42Q9e3s9B6DGtxnSR +JJZ4Hgmgm5qVSkr1YnwCqMqs+1oEdjneX/H5s7/zA1hV0qq34wQi0fiU2iIIAI3T +fCZdzHd55yx4Oagmcw6iXSVphU9VDprvxrlE4Vc93x9UIuVvZaozhDrzznq+VZeu +jRIPFDPiUHDDSYcTvFHe15gSWu86gzOSBnWLknwSaHtwag+1m7Z3W0hZneTvWq3z +wZ7U10VOylY0Ibw+F1tvdwxIAUMpsN0/lm7mlaoMwCC2/T42J5zjXM9OgdwZu5GQ +fezmlwQek8wiSdeXhrYTCjxDI3d+8NzmzSQfO4ObNDqDNOMCAwEAAaNjMGEwHwYD +VR0jBBgwFoAUkqR1LKSevoFE63n8isWVpesQdXMwDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFJKkdSyknr6BROt5/IrFlaXrEHVzMA0G +CSqGSIb3DQEBBQUAA4IBAQAo0uCG1eb4e/CX3CJrO5UUVg8RMKWaTzqwOuAGy2X1 +7caXJ/4l8lfmXpWMPmRgFVp/Lw0BxbFg/UU1z/CyvwbZ71q+s2IhtNerNXxTPqYn +8aEt2hojnczd7Dwtnic0XQ/CNnm8yUpiLe1r2X1BQ3y2qsrtYbE3ghUJGooWMNjs +ydZHcnhLEEYUjl8Or+zHL6sQ17bxbuyGssLoDZJz3KL0Dzq/YSMQiZxIQG5wALPT +ujdEWBF6AmqI8Dc08BnprNRlc/ZpjGSUOnmFKbAWKwyCPwacx/0QK54PLLae4xW/ +2TYcuiUaUj0a7CIMHOCkoj3w6DnPgcB77V0fb8XQC9eY +-----END CERTIFICATE----- + +# Issuer: CN=Cybertrust Global Root O=Cybertrust, Inc +# Subject: CN=Cybertrust Global Root O=Cybertrust, Inc +# Label: "Cybertrust Global Root" +# Serial: 4835703278459682877484360 +# MD5 Fingerprint: 72:e4:4a:87:e3:69:40:80:77:ea:bc:e3:f4:ff:f0:e1 +# SHA1 Fingerprint: 5f:43:e5:b1:bf:f8:78:8c:ac:1c:c7:ca:4a:9a:c6:22:2b:cc:34:c6 +# SHA256 Fingerprint: 96:0a:df:00:63:e9:63:56:75:0c:29:65:dd:0a:08:67:da:0b:9c:bd:6e:77:71:4a:ea:fb:23:49:ab:39:3d:a3 +-----BEGIN CERTIFICATE----- +MIIDoTCCAomgAwIBAgILBAAAAAABD4WqLUgwDQYJKoZIhvcNAQEFBQAwOzEYMBYG +A1UEChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2Jh +bCBSb290MB4XDTA2MTIxNTA4MDAwMFoXDTIxMTIxNTA4MDAwMFowOzEYMBYGA1UE +ChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2JhbCBS +b290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA+Mi8vRRQZhP/8NN5 +7CPytxrHjoXxEnOmGaoQ25yiZXRadz5RfVb23CO21O1fWLE3TdVJDm71aofW0ozS +J8bi/zafmGWgE07GKmSb1ZASzxQG9Dvj1Ci+6A74q05IlG2OlTEQXO2iLb3VOm2y +HLtgwEZLAfVJrn5GitB0jaEMAs7u/OePuGtm839EAL9mJRQr3RAwHQeWP032a7iP +t3sMpTjr3kfb1V05/Iin89cqdPHoWqI7n1C6poxFNcJQZZXcY4Lv3b93TZxiyWNz +FtApD0mpSPCzqrdsxacwOUBdrsTiXSZT8M4cIwhhqJQZugRiQOwfOHB3EgZxpzAY +XSUnpQIDAQABo4GlMIGiMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/ +MB0GA1UdDgQWBBS2CHsNesysIEyGVjJez6tuhS1wVzA/BgNVHR8EODA2MDSgMqAw +hi5odHRwOi8vd3d3Mi5wdWJsaWMtdHJ1c3QuY29tL2NybC9jdC9jdHJvb3QuY3Js +MB8GA1UdIwQYMBaAFLYIew16zKwgTIZWMl7Pq26FLXBXMA0GCSqGSIb3DQEBBQUA +A4IBAQBW7wojoFROlZfJ+InaRcHUowAl9B8Tq7ejhVhpwjCt2BWKLePJzYFa+HMj +Wqd8BfP9IjsO0QbE2zZMcwSO5bAi5MXzLqXZI+O4Tkogp24CJJ8iYGd7ix1yCcUx +XOl5n4BHPa2hCwcUPUf/A2kaDAtE52Mlp3+yybh2hO0j9n0Hq0V+09+zv+mKts2o +omcrUtW3ZfA5TGOgkXmTUg9U3YO7n9GPp1Nzw8v/MOx8BLjYRB+TX3EJIrduPuoc +A06dGiBh+4E37F78CkWr1+cXVdCg6mCbpvbjjFspwgZgFJ0tl0ypkxWdYcQBX0jW +WL1WMRJOEcgh4LMRkWXbtKaIOM5V +-----END CERTIFICATE----- + +# Issuer: CN=GeoTrust Primary Certification Authority - G3 O=GeoTrust Inc. OU=(c) 2008 GeoTrust Inc. - For authorized use only +# Subject: CN=GeoTrust Primary Certification Authority - G3 O=GeoTrust Inc. OU=(c) 2008 GeoTrust Inc. - For authorized use only +# Label: "GeoTrust Primary Certification Authority - G3" +# Serial: 28809105769928564313984085209975885599 +# MD5 Fingerprint: b5:e8:34:36:c9:10:44:58:48:70:6d:2e:83:d4:b8:05 +# SHA1 Fingerprint: 03:9e:ed:b8:0b:e7:a0:3c:69:53:89:3b:20:d2:d9:32:3a:4c:2a:fd +# SHA256 Fingerprint: b4:78:b8:12:25:0d:f8:78:63:5c:2a:a7:ec:7d:15:5e:aa:62:5e:e8:29:16:e2:cd:29:43:61:88:6c:d1:fb:d4 +-----BEGIN CERTIFICATE----- +MIID/jCCAuagAwIBAgIQFaxulBmyeUtB9iepwxgPHzANBgkqhkiG9w0BAQsFADCB +mDELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsT +MChjKSAyMDA4IEdlb1RydXN0IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25s +eTE2MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhv +cml0eSAtIEczMB4XDTA4MDQwMjAwMDAwMFoXDTM3MTIwMTIzNTk1OVowgZgxCzAJ +BgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAoYykg +MjAwOCBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0 +BgNVBAMTLUdlb1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg +LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANziXmJYHTNXOTIz ++uvLh4yn1ErdBojqZI4xmKU4kB6Yzy5jK/BGvESyiaHAKAxJcCGVn2TAppMSAmUm +hsalifD614SgcK9PGpc/BkTVyetyEH3kMSj7HGHmKAdEc5IiaacDiGydY8hS2pgn +5whMcD60yRLBxWeDXTPzAxHsatBT4tG6NmCUgLthY2xbF37fQJQeqw3CIShwiP/W +JmxsYAQlTlV+fe+/lEjetx3dcI0FX4ilm/LC7urRQEFtYjgdVgbFA0dRIBn8exAL +DmKudlW/X3e+PkkBUz2YJQN2JFodtNuJ6nnltrM7P7pMKEF/BqxqjsHQ9gUdfeZC +huOl1UcCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw +HQYDVR0OBBYEFMR5yo6hTgMdHNxr2zFblD4/MH8tMA0GCSqGSIb3DQEBCwUAA4IB +AQAtxRPPVoB7eni9n64smefv2t+UXglpp+duaIy9cr5HqQ6XErhK8WTTOd8lNNTB +zU6B8A8ExCSzNJbGpqow32hhc9f5joWJ7w5elShKKiePEI4ufIbEAp7aDHdlDkQN +kv39sxY2+hENHYwOB4lqKVb3cvTdFZx3NWZXqxNT2I7BQMXXExZacse3aQHEerGD +AWh9jUGhlBjBJVz88P6DAod8DQ3PLghcSkANPuyBYeYk28rgDi0Hsj5W3I31QYUH +SJsMC8tJP33st/3LjWeJGqvtux6jAAgIFyqCXDFdRootD4abdNlF+9RAsXqqaC2G +spki4cErx5z481+oghLrGREt +-----END CERTIFICATE----- + +# Issuer: CN=thawte Primary Root CA - G2 O=thawte, Inc. OU=(c) 2007 thawte, Inc. - For authorized use only +# Subject: CN=thawte Primary Root CA - G2 O=thawte, Inc. OU=(c) 2007 thawte, Inc. - For authorized use only +# Label: "thawte Primary Root CA - G2" +# Serial: 71758320672825410020661621085256472406 +# MD5 Fingerprint: 74:9d:ea:60:24:c4:fd:22:53:3e:cc:3a:72:d9:29:4f +# SHA1 Fingerprint: aa:db:bc:22:23:8f:c4:01:a1:27:bb:38:dd:f4:1d:db:08:9e:f0:12 +# SHA256 Fingerprint: a4:31:0d:50:af:18:a6:44:71:90:37:2a:86:af:af:8b:95:1f:fb:43:1d:83:7f:1e:56:88:b4:59:71:ed:15:57 +-----BEGIN CERTIFICATE----- +MIICiDCCAg2gAwIBAgIQNfwmXNmET8k9Jj1Xm67XVjAKBggqhkjOPQQDAzCBhDEL +MAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjE4MDYGA1UECxMvKGMp +IDIwMDcgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxJDAi +BgNVBAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EgLSBHMjAeFw0wNzExMDUwMDAw +MDBaFw0zODAxMTgyMzU5NTlaMIGEMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhh +d3RlLCBJbmMuMTgwNgYDVQQLEy8oYykgMjAwNyB0aGF3dGUsIEluYy4gLSBGb3Ig +YXV0aG9yaXplZCB1c2Ugb25seTEkMCIGA1UEAxMbdGhhd3RlIFByaW1hcnkgUm9v +dCBDQSAtIEcyMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEotWcgnuVnfFSeIf+iha/ +BebfowJPDQfGAFG6DAJSLSKkQjnE/o/qycG+1E3/n3qe4rF8mq2nhglzh9HnmuN6 +papu+7qzcMBniKI11KOasf2twu8x+qi58/sIxpHR+ymVo0IwQDAPBgNVHRMBAf8E +BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUmtgAMADna3+FGO6Lts6K +DPgR4bswCgYIKoZIzj0EAwMDaQAwZgIxAN344FdHW6fmCsO99YCKlzUNG4k8VIZ3 +KMqh9HneteY4sPBlcIx/AlTCv//YoT7ZzwIxAMSNlPzcU9LcnXgWHxUzI1NS41ox +XZ3Krr0TKUQNJ1uo52icEvdYPy5yAlejj6EULg== +-----END CERTIFICATE----- + +# Issuer: CN=thawte Primary Root CA - G3 O=thawte, Inc. OU=Certification Services Division/(c) 2008 thawte, Inc. - For authorized use only +# Subject: CN=thawte Primary Root CA - G3 O=thawte, Inc. OU=Certification Services Division/(c) 2008 thawte, Inc. - For authorized use only +# Label: "thawte Primary Root CA - G3" +# Serial: 127614157056681299805556476275995414779 +# MD5 Fingerprint: fb:1b:5d:43:8a:94:cd:44:c6:76:f2:43:4b:47:e7:31 +# SHA1 Fingerprint: f1:8b:53:8d:1b:e9:03:b6:a6:f0:56:43:5b:17:15:89:ca:f3:6b:f2 +# SHA256 Fingerprint: 4b:03:f4:58:07:ad:70:f2:1b:fc:2c:ae:71:c9:fd:e4:60:4c:06:4c:f5:ff:b6:86:ba:e5:db:aa:d7:fd:d3:4c +-----BEGIN CERTIFICATE----- +MIIEKjCCAxKgAwIBAgIQYAGXt0an6rS0mtZLL/eQ+zANBgkqhkiG9w0BAQsFADCB +rjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMf +Q2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIw +MDggdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxJDAiBgNV +BAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EgLSBHMzAeFw0wODA0MDIwMDAwMDBa +Fw0zNzEyMDEyMzU5NTlaMIGuMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhhd3Rl +LCBJbmMuMSgwJgYDVQQLEx9DZXJ0aWZpY2F0aW9uIFNlcnZpY2VzIERpdmlzaW9u +MTgwNgYDVQQLEy8oYykgMjAwOCB0aGF3dGUsIEluYy4gLSBGb3IgYXV0aG9yaXpl +ZCB1c2Ugb25seTEkMCIGA1UEAxMbdGhhd3RlIFByaW1hcnkgUm9vdCBDQSAtIEcz +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsr8nLPvb2FvdeHsbnndm +gcs+vHyu86YnmjSjaDFxODNi5PNxZnmxqWWjpYvVj2AtP0LMqmsywCPLLEHd5N/8 +YZzic7IilRFDGF/Eth9XbAoFWCLINkw6fKXRz4aviKdEAhN0cXMKQlkC+BsUa0Lf +b1+6a4KinVvnSr0eAXLbS3ToO39/fR8EtCab4LRarEc9VbjXsCZSKAExQGbY2SS9 +9irY7CFJXJv2eul/VTV+lmuNk5Mny5K76qxAwJ/C+IDPXfRa3M50hqY+bAtTyr2S +zhkGcuYMXDhpxwTWvGzOW/b3aJzcJRVIiKHpqfiYnODz1TEoYRFsZ5aNOZnLwkUk +OQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNV +HQ4EFgQUrWyqlGCc7eT/+j4KdCtjA/e2Wb8wDQYJKoZIhvcNAQELBQADggEBABpA +2JVlrAmSicY59BDlqQ5mU1143vokkbvnRFHfxhY0Cu9qRFHqKweKA3rD6z8KLFIW +oCtDuSWQP3CpMyVtRRooOyfPqsMpQhvfO0zAMzRbQYi/aytlryjvsvXDqmbOe1bu +t8jLZ8HJnBoYuMTDSQPxYA5QzUbF83d597YV4Djbxy8ooAw/dyZ02SUS2jHaGh7c +KUGRIjxpp7sC8rZcJwOJ9Abqm+RyguOhCcHpABnTPtRwa7pxpqpYrvS76Wy274fM +m7v/OeZWYdMKp8RcTGB7BXcmer/YB1IsYvdwY9k5vG8cwnncdimvzsUsZAReiDZu +MdRAGmI0Nj81Aa6sY6A= +-----END CERTIFICATE----- + +# Issuer: CN=GeoTrust Primary Certification Authority - G2 O=GeoTrust Inc. OU=(c) 2007 GeoTrust Inc. - For authorized use only +# Subject: CN=GeoTrust Primary Certification Authority - G2 O=GeoTrust Inc. OU=(c) 2007 GeoTrust Inc. - For authorized use only +# Label: "GeoTrust Primary Certification Authority - G2" +# Serial: 80682863203381065782177908751794619243 +# MD5 Fingerprint: 01:5e:d8:6b:bd:6f:3d:8e:a1:31:f8:12:e0:98:73:6a +# SHA1 Fingerprint: 8d:17:84:d5:37:f3:03:7d:ec:70:fe:57:8b:51:9a:99:e6:10:d7:b0 +# SHA256 Fingerprint: 5e:db:7a:c4:3b:82:a0:6a:87:61:e8:d7:be:49:79:eb:f2:61:1f:7d:d7:9b:f9:1c:1c:6b:56:6a:21:9e:d7:66 +-----BEGIN CERTIFICATE----- +MIICrjCCAjWgAwIBAgIQPLL0SAoA4v7rJDteYD7DazAKBggqhkjOPQQDAzCBmDEL +MAkGA1UEBhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsTMChj +KSAyMDA3IEdlb1RydXN0IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTE2 +MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0 +eSAtIEcyMB4XDTA3MTEwNTAwMDAwMFoXDTM4MDExODIzNTk1OVowgZgxCzAJBgNV +BAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAoYykgMjAw +NyBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0BgNV +BAMTLUdlb1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBH +MjB2MBAGByqGSM49AgEGBSuBBAAiA2IABBWx6P0DFUPlrOuHNxFi79KDNlJ9RVcL +So17VDs6bl8VAsBQps8lL33KSLjHUGMcKiEIfJo22Av+0SbFWDEwKCXzXV2juLal +tJLtbCyf691DiaI8S0iRHVDsJt/WYC69IaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBVfNVdRVfslsq0DafwBo/q+EVXVMAoG +CCqGSM49BAMDA2cAMGQCMGSWWaboCd6LuvpaiIjwH5HTRqjySkwCY/tsXzjbLkGT +qQ7mndwxHLKgpxgceeHHNgIwOlavmnRs9vuD4DPTCF+hnMJbn0bWtsuRBmOiBucz +rD6ogRLQy7rQkgu2npaqBA+K +-----END CERTIFICATE----- + +# Issuer: CN=VeriSign Universal Root Certification Authority O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2008 VeriSign, Inc. - For authorized use only +# Subject: CN=VeriSign Universal Root Certification Authority O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2008 VeriSign, Inc. - For authorized use only +# Label: "VeriSign Universal Root Certification Authority" +# Serial: 85209574734084581917763752644031726877 +# MD5 Fingerprint: 8e:ad:b5:01:aa:4d:81:e4:8c:1d:d1:e1:14:00:95:19 +# SHA1 Fingerprint: 36:79:ca:35:66:87:72:30:4d:30:a5:fb:87:3b:0f:a7:7b:b7:0d:54 +# SHA256 Fingerprint: 23:99:56:11:27:a5:71:25:de:8c:ef:ea:61:0d:df:2f:a0:78:b5:c8:06:7f:4e:82:82:90:bf:b8:60:e8:4b:3c +-----BEGIN CERTIFICATE----- +MIIEuTCCA6GgAwIBAgIQQBrEZCGzEyEDDrvkEhrFHTANBgkqhkiG9w0BAQsFADCB +vTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL +ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwOCBWZXJp +U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MTgwNgYDVQQDEy9W +ZXJpU2lnbiBVbml2ZXJzYWwgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe +Fw0wODA0MDIwMDAwMDBaFw0zNzEyMDEyMzU5NTlaMIG9MQswCQYDVQQGEwJVUzEX +MBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRydXN0 +IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAyMDA4IFZlcmlTaWduLCBJbmMuIC0gRm9y +IGF1dGhvcml6ZWQgdXNlIG9ubHkxODA2BgNVBAMTL1ZlcmlTaWduIFVuaXZlcnNh +bCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAx2E3XrEBNNti1xWb/1hajCMj1mCOkdeQmIN65lgZOIzF +9uVkhbSicfvtvbnazU0AtMgtc6XHaXGVHzk8skQHnOgO+k1KxCHfKWGPMiJhgsWH +H26MfF8WIFFE0XBPV+rjHOPMee5Y2A7Cs0WTwCznmhcrewA3ekEzeOEz4vMQGn+H +LL729fdC4uW/h2KJXwBL38Xd5HVEMkE6HnFuacsLdUYI0crSK5XQz/u5QGtkjFdN +/BMReYTtXlT2NJ8IAfMQJQYXStrxHXpma5hgZqTZ79IugvHw7wnqRMkVauIDbjPT +rJ9VAMf2CGqUuV/c4DPxhGD5WycRtPwW8rtWaoAljQIDAQABo4GyMIGvMA8GA1Ud +EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMG0GCCsGAQUFBwEMBGEwX6FdoFsw +WTBXMFUWCWltYWdlL2dpZjAhMB8wBwYFKw4DAhoEFI/l0xqGrI2Oa8PPgGrUSBgs +exkuMCUWI2h0dHA6Ly9sb2dvLnZlcmlzaWduLmNvbS92c2xvZ28uZ2lmMB0GA1Ud +DgQWBBS2d/ppSEefUxLVwuoHMnYH0ZcHGTANBgkqhkiG9w0BAQsFAAOCAQEASvj4 +sAPmLGd75JR3Y8xuTPl9Dg3cyLk1uXBPY/ok+myDjEedO2Pzmvl2MpWRsXe8rJq+ +seQxIcaBlVZaDrHC1LGmWazxY8u4TB1ZkErvkBYoH1quEPuBUDgMbMzxPcP1Y+Oz +4yHJJDnp/RVmRvQbEdBNc6N9Rvk97ahfYtTxP/jgdFcrGJ2BtMQo2pSXpXDrrB2+ +BxHw1dvd5Yzw1TKwg+ZX4o+/vqGqvz0dtdQ46tewXDpPaj+PwGZsY6rp2aQW9IHR +lRQOfc2VNNnSj3BzgXucfr2YYdhFh5iQxeuGMMY1v/D/w1WIg0vvBZIGcfK4mJO3 +7M2CYfE45k+XmCpajQ== +-----END CERTIFICATE----- + +# Issuer: CN=VeriSign Class 3 Public Primary Certification Authority - G4 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2007 VeriSign, Inc. - For authorized use only +# Subject: CN=VeriSign Class 3 Public Primary Certification Authority - G4 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2007 VeriSign, Inc. - For authorized use only +# Label: "VeriSign Class 3 Public Primary Certification Authority - G4" +# Serial: 63143484348153506665311985501458640051 +# MD5 Fingerprint: 3a:52:e1:e7:fd:6f:3a:e3:6f:f3:6f:99:1b:f9:22:41 +# SHA1 Fingerprint: 22:d5:d8:df:8f:02:31:d1:8d:f7:9d:b7:cf:8a:2d:64:c9:3f:6c:3a +# SHA256 Fingerprint: 69:dd:d7:ea:90:bb:57:c9:3e:13:5d:c8:5e:a6:fc:d5:48:0b:60:32:39:bd:c4:54:fc:75:8b:2a:26:cf:7f:79 +-----BEGIN CERTIFICATE----- +MIIDhDCCAwqgAwIBAgIQL4D+I4wOIg9IZxIokYesszAKBggqhkjOPQQDAzCByjEL +MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW +ZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2ln +biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJp +U2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9y +aXR5IC0gRzQwHhcNMDcxMTA1MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCByjELMAkG +A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJp +U2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2lnbiwg +SW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2ln +biBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5 +IC0gRzQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASnVnp8Utpkmw4tXNherJI9/gHm +GUo9FANL+mAnINmDiWn6VMaaGF5VKmTeBvaNSjutEDxlPZCIBIngMGGzrl0Bp3ve +fLK+ymVhAIau2o970ImtTR1ZmkGxvEeA3J5iw/mjgbIwga8wDwYDVR0TAQH/BAUw +AwEB/zAOBgNVHQ8BAf8EBAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJ +aW1hZ2UvZ2lmMCEwHzAHBgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYj +aHR0cDovL2xvZ28udmVyaXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFLMW +kf3upm7ktS5Jj4d4gYDs5bG1MAoGCCqGSM49BAMDA2gAMGUCMGYhDBgmYFo4e1ZC +4Kf8NoRRkSAsdk1DPcQdhCPQrNZ8NQbOzWm9kA3bbEhCHQ6qQgIxAJw9SDkjOVga +FRJZap7v1VmyHVIsmXHNxynfGyphe3HR3vPA5Q06Sqotp9iGKt0uEA== +-----END CERTIFICATE----- + +# Issuer: O=VeriSign, Inc. OU=Class 3 Public Primary Certification Authority +# Subject: O=VeriSign, Inc. OU=Class 3 Public Primary Certification Authority +# Label: "Verisign Class 3 Public Primary Certification Authority" +# Serial: 80507572722862485515306429940691309246 +# MD5 Fingerprint: ef:5a:f1:33:ef:f1:cd:bb:51:02:ee:12:14:4b:96:c4 +# SHA1 Fingerprint: a1:db:63:93:91:6f:17:e4:18:55:09:40:04:15:c7:02:40:b0:ae:6b +# SHA256 Fingerprint: a4:b6:b3:99:6f:c2:f3:06:b3:fd:86:81:bd:63:41:3d:8c:50:09:cc:4f:a3:29:c2:cc:f0:e2:fa:1b:14:03:05 +-----BEGIN CERTIFICATE----- +MIICPDCCAaUCEDyRMcsf9tAbDpq40ES/Er4wDQYJKoZIhvcNAQEFBQAwXzELMAkG +A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz +cyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2 +MDEyOTAwMDAwMFoXDTI4MDgwMjIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV +BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmlt +YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN +ADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhE +BarsAx94f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/is +I19wKTakyYbnsZogy1Olhec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0G +CSqGSIb3DQEBBQUAA4GBABByUqkFFBkyCEHwxWsKzH4PIRnN5GfcX6kb5sroc50i +2JhucwNhkcV8sEVAbkSdjbCxlnRhLQ2pRdKkkirWmnWXbj9T/UWZYB2oK0z5XqcJ +2HUw19JlYD1n1khVdWk/kfVIC0dpImmClr7JyDiGSnoscxlIaU5rfGW/D/xwzoiQ +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3 +# Label: "GlobalSign Root CA - R3" +# Serial: 4835703278459759426209954 +# MD5 Fingerprint: c5:df:b8:49:ca:05:13:55:ee:2d:ba:1a:c3:3e:b0:28 +# SHA1 Fingerprint: d6:9b:56:11:48:f0:1c:77:c5:45:78:c1:09:26:df:5b:85:69:76:ad +# SHA256 Fingerprint: cb:b5:22:d7:b7:f1:27:ad:6a:01:13:86:5b:df:1c:d4:10:2e:7d:07:59:af:63:5a:7c:f4:72:0d:c9:63:c5:3b +-----BEGIN CERTIFICATE----- +MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G +A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp +Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4 +MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG +A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8 +RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT +gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm +KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd +QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ +XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw +DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o +LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU +RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp +jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK +6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX +mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs +Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH +WD9f +-----END CERTIFICATE----- + +# Issuer: CN=TC TrustCenter Universal CA III O=TC TrustCenter GmbH OU=TC TrustCenter Universal CA +# Subject: CN=TC TrustCenter Universal CA III O=TC TrustCenter GmbH OU=TC TrustCenter Universal CA +# Label: "TC TrustCenter Universal CA III" +# Serial: 2010889993983507346460533407902964 +# MD5 Fingerprint: 9f:dd:db:ab:ff:8e:ff:45:21:5f:f0:6c:9d:8f:fe:2b +# SHA1 Fingerprint: 96:56:cd:7b:57:96:98:95:d0:e1:41:46:68:06:fb:b8:c6:11:06:87 +# SHA256 Fingerprint: 30:9b:4a:87:f6:ca:56:c9:31:69:aa:a9:9c:6d:98:88:54:d7:89:2b:d5:43:7e:2d:07:b2:9c:be:da:55:d3:5d +-----BEGIN CERTIFICATE----- +MIID4TCCAsmgAwIBAgIOYyUAAQACFI0zFQLkbPQwDQYJKoZIhvcNAQEFBQAwezEL +MAkGA1UEBhMCREUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxJDAiBgNV +BAsTG1RDIFRydXN0Q2VudGVyIFVuaXZlcnNhbCBDQTEoMCYGA1UEAxMfVEMgVHJ1 +c3RDZW50ZXIgVW5pdmVyc2FsIENBIElJSTAeFw0wOTA5MDkwODE1MjdaFw0yOTEy +MzEyMzU5NTlaMHsxCzAJBgNVBAYTAkRFMRwwGgYDVQQKExNUQyBUcnVzdENlbnRl +ciBHbWJIMSQwIgYDVQQLExtUQyBUcnVzdENlbnRlciBVbml2ZXJzYWwgQ0ExKDAm +BgNVBAMTH1RDIFRydXN0Q2VudGVyIFVuaXZlcnNhbCBDQSBJSUkwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDC2pxisLlxErALyBpXsq6DFJmzNEubkKLF +5+cvAqBNLaT6hdqbJYUtQCggbergvbFIgyIpRJ9Og+41URNzdNW88jBmlFPAQDYv +DIRlzg9uwliT6CwLOunBjvvya8o84pxOjuT5fdMnnxvVZ3iHLX8LR7PH6MlIfK8v +zArZQe+f/prhsq75U7Xl6UafYOPfjdN/+5Z+s7Vy+EutCHnNaYlAJ/Uqwa1D7KRT +yGG299J5KmcYdkhtWyUB0SbFt1dpIxVbYYqt8Bst2a9c8SaQaanVDED1M4BDj5yj +dipFtK+/fz6HP3bFzSreIMUWWMv5G/UPyw0RUmS40nZid4PxWJ//AgMBAAGjYzBh +MB8GA1UdIwQYMBaAFFbn4VslQ4Dg9ozhcbyO5YAvxEjiMA8GA1UdEwEB/wQFMAMB +Af8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRW5+FbJUOA4PaM4XG8juWAL8RI +4jANBgkqhkiG9w0BAQUFAAOCAQEAg8ev6n9NCjw5sWi+e22JLumzCecYV42Fmhfz +dkJQEw/HkG8zrcVJYCtsSVgZ1OK+t7+rSbyUyKu+KGwWaODIl0YgoGhnYIg5IFHY +aAERzqf2EQf27OysGh+yZm5WZ2B6dF7AbZc2rrUNXWZzwCUyRdhKBgePxLcHsU0G +DeGl6/R1yrqc0L2z0zIkTO5+4nYES0lT2PLpVDP85XEfPRRclkvxOvIAu2y0+pZV +CIgJwcyRGSmwIC3/yzikQOEXvnlhgP8HA4ZMTnsGnxGGjYnuJ8Tb4rwZjgvDwxPH +LQNjO9Po5KIqwoIIlBZU8O8fJ5AluA0OKBtHd0e9HKgl8ZS0Zg== +-----END CERTIFICATE----- + +# Issuer: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc. +# Subject: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc. +# Label: "Go Daddy Root Certificate Authority - G2" +# Serial: 0 +# MD5 Fingerprint: 80:3a:bc:22:c1:e6:fb:8d:9b:3b:27:4a:32:1b:9a:01 +# SHA1 Fingerprint: 47:be:ab:c9:22:ea:e8:0e:78:78:34:62:a7:9f:45:c2:54:fd:e6:8b +# SHA256 Fingerprint: 45:14:0b:32:47:eb:9c:c8:c5:b4:f0:d7:b5:30:91:f7:32:92:08:9e:6e:5a:63:e2:74:9d:d3:ac:a9:19:8e:da +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT +EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp +ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz +NTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH +EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE +AxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD +E6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH +/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy +DfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh +GkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR +tDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA +AaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX +WWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu +9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr +gIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo +2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO +LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI +4uJEvlz36hz1 +-----END CERTIFICATE----- + +# Issuer: CN=Starfield Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Subject: CN=Starfield Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Label: "Starfield Root Certificate Authority - G2" +# Serial: 0 +# MD5 Fingerprint: d6:39:81:c6:52:7e:96:69:fc:fc:ca:66:ed:05:f2:96 +# SHA1 Fingerprint: b5:1c:06:7c:ee:2b:0c:3d:f8:55:ab:2d:92:f4:fe:39:d4:e7:0f:0e +# SHA256 Fingerprint: 2c:e1:cb:0b:f9:d2:f9:e1:02:99:3f:be:21:51:52:c3:b2:dd:0c:ab:de:1c:68:e5:31:9b:83:91:54:db:b7:f5 +-----BEGIN CERTIFICATE----- +MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs +ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw +MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 +b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj +aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp +Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg +nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1 +HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N +Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN +dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0 +HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G +CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU +sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3 +4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg +8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K +pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1 +mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 +-----END CERTIFICATE----- + +# Issuer: CN=Starfield Services Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Subject: CN=Starfield Services Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Label: "Starfield Services Root Certificate Authority - G2" +# Serial: 0 +# MD5 Fingerprint: 17:35:74:af:7b:61:1c:eb:f4:f9:3c:e2:ee:40:f9:a2 +# SHA1 Fingerprint: 92:5a:8f:8d:2c:6d:04:e0:66:5f:59:6a:ff:22:d8:63:e8:25:6f:3f +# SHA256 Fingerprint: 56:8d:69:05:a2:c8:87:08:a4:b3:02:51:90:ed:cf:ed:b1:97:4a:60:6a:13:c6:e5:29:0f:cb:2a:e6:3e:da:b5 +-----BEGIN CERTIFICATE----- +MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVs +ZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5 +MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNVBAYTAlVTMRAwDgYD +VQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFy +ZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2Vy +dmljZXMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20p +OsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm2 +8xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4PahHQUw2eeBGg6345AWh1K +Ts9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLPLJGmpufe +hRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk +6mFBrMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAw +DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+q +AdcwKziIorhtSpzyEZGDMA0GCSqGSIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMI +bw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPPE95Dz+I0swSdHynVv/heyNXB +ve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTyxQGjhdByPq1z +qwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd +iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn +0q23KXB56jzaYyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCN +sSi6 +-----END CERTIFICATE----- + +# Issuer: CN=AffirmTrust Commercial O=AffirmTrust +# Subject: CN=AffirmTrust Commercial O=AffirmTrust +# Label: "AffirmTrust Commercial" +# Serial: 8608355977964138876 +# MD5 Fingerprint: 82:92:ba:5b:ef:cd:8a:6f:a6:3d:55:f9:84:f6:d6:b7 +# SHA1 Fingerprint: f9:b5:b6:32:45:5f:9c:be:ec:57:5f:80:dc:e9:6e:2c:c7:b2:78:b7 +# SHA256 Fingerprint: 03:76:ab:1d:54:c5:f9:80:3c:e4:b2:e2:01:a0:ee:7e:ef:7b:57:b6:36:e8:a9:3c:9b:8d:48:60:c9:6f:5f:a7 +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz +dCBDb21tZXJjaWFsMB4XDTEwMDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDEL +MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp +cm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6EqdbDuKP +Hx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yr +ba0F8PrVC8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPAL +MeIrJmqbTFeurCA+ukV6BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1 +yHp52UKqK39c/s4mT6NmgTWvRLpUHhwwMmWd5jyTXlBOeuM61G7MGvv50jeuJCqr +VwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNVHQ4EFgQUnZPGU4teyq8/ +nx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ +KoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYG +XUPGhi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNj +vbz4YYCanrHOQnDiqX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivt +Z8SOyUOyXGsViQK8YvxO8rUzqrJv0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9g +N53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0khsUlHRUe072o0EclNmsxZt9YC +nlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8= +-----END CERTIFICATE----- + +# Issuer: CN=AffirmTrust Networking O=AffirmTrust +# Subject: CN=AffirmTrust Networking O=AffirmTrust +# Label: "AffirmTrust Networking" +# Serial: 8957382827206547757 +# MD5 Fingerprint: 42:65:ca:be:01:9a:9a:4c:a9:8c:41:49:cd:c0:d5:7f +# SHA1 Fingerprint: 29:36:21:02:8b:20:ed:02:f5:66:c5:32:d1:d6:ed:90:9f:45:00:2f +# SHA256 Fingerprint: 0a:81:ec:5a:92:97:77:f1:45:90:4a:f3:8d:5d:50:9f:66:b5:e2:c5:8f:cd:b5:31:05:8b:0e:17:f3:f0:b4:1b +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz +dCBOZXR3b3JraW5nMB4XDTEwMDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDEL +MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp +cm1UcnVzdCBOZXR3b3JraW5nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWREZY9nZOIG41w3SfYvm4SEHi3y +YJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ/Ls6rnla1fTWcbua +kCNrmreIdIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXLviRmVSRL +QESxG9fhwoXA3hA/Pe24/PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp +6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gbh+0t+nvujArjqWaJGctB+d1ENmHP4ndG +yH329JKBNv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNVHQ4EFgQUBx/S55zawm6i +QLSwelAQUHTEyL0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ +KoZIhvcNAQEFBQADggEBAIlXshZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfO +tDIuUFUaNU52Q3Eg75N3ThVwLofDwR1t3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzu +QY0x2+c06lkh1QF612S4ZDnNye2v7UsDSKegmQGA3GWjNq5lWUhPgkvIZfFXHeVZ +Lgo/bNjR9eUJtGxUAArgFU2HdW23WJZa3W3SAKD0m0i+wzekujbgfIeFlxoVot4u +olu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9/ZFvgrG+CJPbFEfxojfHRZ48 +x3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s= +-----END CERTIFICATE----- + +# Issuer: CN=AffirmTrust Premium O=AffirmTrust +# Subject: CN=AffirmTrust Premium O=AffirmTrust +# Label: "AffirmTrust Premium" +# Serial: 7893706540734352110 +# MD5 Fingerprint: c4:5d:0e:48:b6:ac:28:30:4e:0a:bc:f9:38:16:87:57 +# SHA1 Fingerprint: d8:a6:33:2c:e0:03:6f:b1:85:f6:63:4f:7d:6a:06:65:26:32:28:27 +# SHA256 Fingerprint: 70:a7:3f:7f:37:6b:60:07:42:48:90:45:34:b1:14:82:d5:bf:0e:69:8e:cc:49:8d:f5:25:77:eb:f2:e9:3b:9a +-----BEGIN CERTIFICATE----- +MIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVz +dCBQcmVtaXVtMB4XDTEwMDEyOTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkG +A1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1U +cnVzdCBQcmVtaXVtMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxBLf +qV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64tb+eT2TZwamjPjlGjhVtnBKAQ +JG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/0qRY7iZNyaqoe5rZ ++jjeRFcV5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/K+k8rNrS +s8PhaJyJ+HoAVt70VZVs+7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5 +HMQxK9VfvFMSF5yZVylmd2EhMQcuJUmdGPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d7 +70O/Nbua2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5Rp9EixAqnOEhss/n/fauG +V+O61oV4d7pD6kh/9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NIS+LI+H+S +qHZGnEJlPqQewQcDWkYtuJfzt9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S +5u046uwBHjxIVkkJx0w3AJ6IDsBz4W9m6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4Ia +C1nEWTJ3s7xgaVY5/bQGeyzWZDbZvUjthB9+pSKPKrhC9IK31FOQeE4tGv2Bb0TX +OwF0lkLgAOIua+rF7nKsu7/+6qqo+Nz2snmKtmcCAwEAAaNCMEAwHQYDVR0OBBYE +FJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByvMiPIs0laUZx2 +KI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMg +Nt58D2kTiKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B +8OWycvpEgjNC6C1Y91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQ +MKSOyARiqcTtNd56l+0OOF6SL5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc +0jlzCFfemQa0W50QBuHCAKi4HEoCChTQwUHK+4w1IX2COPKpVJEZNZOUbWo6xbLQ +u4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFooC8k4gmVBtWVyuEklut89pMF +u+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5YwH2AG7hsj/oFgIxpH +YoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/qzWaVYa8 +GKa1qF60g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaO +RtGdFNrHF+QFlozEJLUbzxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6e +KeC2uAloGRwYQw== +-----END CERTIFICATE----- + +# Issuer: CN=AffirmTrust Premium ECC O=AffirmTrust +# Subject: CN=AffirmTrust Premium ECC O=AffirmTrust +# Label: "AffirmTrust Premium ECC" +# Serial: 8401224907861490260 +# MD5 Fingerprint: 64:b0:09:55:cf:b1:d5:99:e2:be:13:ab:a6:5d:ea:4d +# SHA1 Fingerprint: b8:23:6b:00:2f:1d:16:86:53:01:55:6c:11:a4:37:ca:eb:ff:c3:bb +# SHA256 Fingerprint: bd:71:fd:f6:da:97:e4:cf:62:d1:64:7a:dd:25:81:b0:7d:79:ad:f8:39:7e:b4:ec:ba:9c:5e:84:88:82:14:23 +-----BEGIN CERTIFICATE----- +MIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMC +VVMxFDASBgNVBAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQ +cmVtaXVtIEVDQzAeFw0xMDAxMjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJ +BgNVBAYTAlVTMRQwEgYDVQQKDAtBZmZpcm1UcnVzdDEgMB4GA1UEAwwXQWZmaXJt +VHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQNMF4bFZ0D +0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQUX+iOGasvLkjmrBhDeKzQN8O9 +ss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR4ptlKymjQjBAMB0G +A1UdDgQWBBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTADAQH/MA4G +A1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/Vs +aobgxCd05DhT1wV/GzTjxi+zygk8N53X57hG8f2h4nECMEJZh0PUUd+60wkyWs6I +flc9nF9Ca/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKMeQ== +-----END CERTIFICATE----- + +# Issuer: CN=StartCom Certification Authority O=StartCom Ltd. OU=Secure Digital Certificate Signing +# Subject: CN=StartCom Certification Authority O=StartCom Ltd. OU=Secure Digital Certificate Signing +# Label: "StartCom Certification Authority" +# Serial: 45 +# MD5 Fingerprint: c9:3b:0d:84:41:fc:a4:76:79:23:08:57:de:10:19:16 +# SHA1 Fingerprint: a3:f1:33:3f:e2:42:bf:cf:c5:d1:4e:8f:39:42:98:40:68:10:d1:a0 +# SHA256 Fingerprint: e1:78:90:ee:09:a3:fb:f4:f4:8b:9c:41:4a:17:d6:37:b7:a5:06:47:e9:bc:75:23:22:72:7f:cc:17:42:a9:11 +-----BEGIN CERTIFICATE----- +MIIHhzCCBW+gAwIBAgIBLTANBgkqhkiG9w0BAQsFADB9MQswCQYDVQQGEwJJTDEW +MBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwg +Q2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0NjM3WhcNMzYwOTE3MTk0NjM2WjB9 +MQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMi +U2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3Rh +cnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZk +pMyONvg45iPwbm2xPN1yo4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rf +OQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/C +Ji/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/deMotHweXMAEtcnn6RtYT +Kqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt2PZE4XNi +HzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMM +Av+Z6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w ++2OqqGwaVLRcJXrJosmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+ +Gkhpi8KWTRoSsmkXwQqQ1vp5Iki/untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3 +Zzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVcUjyJthkqcwEKDwOzEmDyei+B +26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT37uMdBNSSwID +AQABo4ICEDCCAgwwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD +VR0OBBYEFE4L7xqkQFulF2mHMMo0aEPQQa7yMB8GA1UdIwQYMBaAFE4L7xqkQFul +F2mHMMo0aEPQQa7yMIIBWgYDVR0gBIIBUTCCAU0wggFJBgsrBgEEAYG1NwEBATCC +ATgwLgYIKwYBBQUHAgEWImh0dHA6Ly93d3cuc3RhcnRzc2wuY29tL3BvbGljeS5w +ZGYwNAYIKwYBBQUHAgEWKGh0dHA6Ly93d3cuc3RhcnRzc2wuY29tL2ludGVybWVk +aWF0ZS5wZGYwgc8GCCsGAQUFBwICMIHCMCcWIFN0YXJ0IENvbW1lcmNpYWwgKFN0 +YXJ0Q29tKSBMdGQuMAMCAQEagZZMaW1pdGVkIExpYWJpbGl0eSwgcmVhZCB0aGUg +c2VjdGlvbiAqTGVnYWwgTGltaXRhdGlvbnMqIG9mIHRoZSBTdGFydENvbSBDZXJ0 +aWZpY2F0aW9uIEF1dGhvcml0eSBQb2xpY3kgYXZhaWxhYmxlIGF0IGh0dHA6Ly93 +d3cuc3RhcnRzc2wuY29tL3BvbGljeS5wZGYwEQYJYIZIAYb4QgEBBAQDAgAHMDgG +CWCGSAGG+EIBDQQrFilTdGFydENvbSBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1 +dGhvcml0eTANBgkqhkiG9w0BAQsFAAOCAgEAjo/n3JR5fPGFf59Jb2vKXfuM/gTF +wWLRfUKKvFO3lANmMD+x5wqnUCBVJX92ehQN6wQOQOY+2IirByeDqXWmN3PH/UvS +Ta0XQMhGvjt/UfzDtgUx3M2FIk5xt/JxXrAaxrqTi3iSSoX4eA+D/i+tLPfkpLst +0OcNOrg+zvZ49q5HJMqjNTbOx8aHmNrs++myziebiMMEofYLWWivydsQD032ZGNc +pRJvkrKTlMeIFw6Ttn5ii5B/q06f/ON1FE8qMt9bDeD1e5MNq6HPh+GlBEXoPBKl +CcWw0bdT82AUuoVpaiF8H3VhFyAXe2w7QSlc4axa0c2Mm+tgHRns9+Ww2vl5GKVF +P0lDV9LdJNUso/2RjSe15esUBppMeyG7Oq0wBhjA2MFrLH9ZXF2RsXAiV+uKa0hK +1Q8p7MZAwC+ITGgBF3f0JBlPvfrhsiAhS90a2Cl9qrjeVOwhVYBsHvUwyKMQ5bLm +KhQxw4UtjJixhlpPiVktucf3HMiKf8CdBUrmQk9io20ppB+Fq9vlgcitKj1MXVuE +JnHEhV5xJMqlG2zYYdMa4FTbzrqpMrUi9nNBCV24F10OD5mQ1kfabwo6YigUZ4LZ +8dCAWZvLMdibD4x3TrVoivJs9iQOLWxwxXPR3hTQcY+203sC9uO41Alua551hDnm +fyWl8kgAwKQB2j8= +-----END CERTIFICATE----- + +# Issuer: CN=StartCom Certification Authority G2 O=StartCom Ltd. +# Subject: CN=StartCom Certification Authority G2 O=StartCom Ltd. +# Label: "StartCom Certification Authority G2" +# Serial: 59 +# MD5 Fingerprint: 78:4b:fb:9e:64:82:0a:d3:b8:4c:62:f3:64:f2:90:64 +# SHA1 Fingerprint: 31:f1:fd:68:22:63:20:ee:c6:3b:3f:9d:ea:4a:3e:53:7c:7c:39:17 +# SHA256 Fingerprint: c7:ba:65:67:de:93:a7:98:ae:1f:aa:79:1e:71:2d:37:8f:ae:1f:93:c4:39:7f:ea:44:1b:b7:cb:e6:fd:59:95 +-----BEGIN CERTIFICATE----- +MIIFYzCCA0ugAwIBAgIBOzANBgkqhkiG9w0BAQsFADBTMQswCQYDVQQGEwJJTDEW +MBQGA1UEChMNU3RhcnRDb20gTHRkLjEsMCoGA1UEAxMjU3RhcnRDb20gQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkgRzIwHhcNMTAwMTAxMDEwMDAxWhcNMzkxMjMxMjM1 +OTAxWjBTMQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjEsMCoG +A1UEAxMjU3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgRzIwggIiMA0G +CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2iTZbB7cgNr2Cu+EWIAOVeq8Oo1XJ +JZlKxdBWQYeQTSFgpBSHO839sj60ZwNq7eEPS8CRhXBF4EKe3ikj1AENoBB5uNsD +vfOpL9HG4A/LnooUCri99lZi8cVytjIl2bLzvWXFDSxu1ZJvGIsAQRSCb0AgJnoo +D/Uefyf3lLE3PbfHkffiAez9lInhzG7TNtYKGXmu1zSCZf98Qru23QumNK9LYP5/ +Q0kGi4xDuFby2X8hQxfqp0iVAXV16iulQ5XqFYSdCI0mblWbq9zSOdIxHWDirMxW +RST1HFSr7obdljKF+ExP6JV2tgXdNiNnvP8V4so75qbsO+wmETRIjfaAKxojAuuK +HDp2KntWFhxyKrOq42ClAJ8Em+JvHhRYW6Vsi1g8w7pOOlz34ZYrPu8HvKTlXcxN +nw3h3Kq74W4a7I/htkxNeXJdFzULHdfBR9qWJODQcqhaX2YtENwvKhOuJv4KHBnM +0D4LnMgJLvlblnpHnOl68wVQdJVznjAJ85eCXuaPOQgeWeU1FEIT/wCc976qUM/i +UUjXuG+v+E5+M5iSFGI6dWPPe/regjupuznixL0sAA7IF6wT700ljtizkC+p2il9 +Ha90OrInwMEePnWjFqmveiJdnxMaz6eg6+OGCtP95paV1yPIN93EfKo2rJgaErHg +TuixO/XWb/Ew1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQE +AwIBBjAdBgNVHQ4EFgQUS8W0QGutHLOlHGVuRjaJhwUMDrYwDQYJKoZIhvcNAQEL +BQADggIBAHNXPyzVlTJ+N9uWkusZXn5T50HsEbZH77Xe7XRcxfGOSeD8bpkTzZ+K +2s06Ctg6Wgk/XzTQLwPSZh0avZyQN8gMjgdalEVGKua+etqhqaRpEpKwfTbURIfX +UfEpY9Z1zRbkJ4kd+MIySP3bmdCPX1R0zKxnNBFi2QwKN4fRoxdIjtIXHfbX/dtl +6/2o1PXWT6RbdejF0mCy2wl+JYt7ulKSnj7oxXehPOBKc2thz4bcQ///If4jXSRK +9dNtD2IEBVeC2m6kMyV5Sy5UGYvMLD0w6dEG/+gyRr61M3Z3qAFdlsHB1b6uJcDJ +HgoJIIihDsnzb02CVAAgp9KP5DlUFy6NHrgbuxu9mk47EDTcnIhT76IxW1hPkWLI +wpqazRVdOKnWvvgTtZ8SafJQYqz7Fzf07rh1Z2AQ+4NQ+US1dZxAF7L+/XldblhY +XzD8AK6vM8EOTmy6p6ahfzLbOOCxchcKK5HsamMm7YnUeMx0HgX4a/6ManY5Ka5l +IxKVCCIcl85bBu4M4ru8H0ST9tg4RQUh7eStqxK2A6RCLi3ECToDZ2mEmuFZkIoo +hdVddLHRDiBYmxOlsGOm7XtH/UVVMKTumtTm4ofvmMkyghEpIrwACjFeLQ/Ajulr +so8uBtjRkcfGEvRM/TAXw8HaOFvjqermobp573PYtlNXLfbQ4ddI +-----END CERTIFICATE----- diff --git a/appengine/dashdemo-cached/httplib2/iri2uri.py b/appengine/dashdemo-cached/httplib2/iri2uri.py new file mode 100644 index 0000000..d88c91f --- /dev/null +++ b/appengine/dashdemo-cached/httplib2/iri2uri.py @@ -0,0 +1,110 @@ +""" +iri2uri + +Converts an IRI to a URI. + +""" +__author__ = "Joe Gregorio (joe@bitworking.org)" +__copyright__ = "Copyright 2006, Joe Gregorio" +__contributors__ = [] +__version__ = "1.0.0" +__license__ = "MIT" +__history__ = """ +""" + +import urlparse + + +# Convert an IRI to a URI following the rules in RFC 3987 +# +# The characters we need to enocde and escape are defined in the spec: +# +# iprivate = %xE000-F8FF / %xF0000-FFFFD / %x100000-10FFFD +# ucschar = %xA0-D7FF / %xF900-FDCF / %xFDF0-FFEF +# / %x10000-1FFFD / %x20000-2FFFD / %x30000-3FFFD +# / %x40000-4FFFD / %x50000-5FFFD / %x60000-6FFFD +# / %x70000-7FFFD / %x80000-8FFFD / %x90000-9FFFD +# / %xA0000-AFFFD / %xB0000-BFFFD / %xC0000-CFFFD +# / %xD0000-DFFFD / %xE1000-EFFFD + +escape_range = [ + (0xA0, 0xD7FF), + (0xE000, 0xF8FF), + (0xF900, 0xFDCF), + (0xFDF0, 0xFFEF), + (0x10000, 0x1FFFD), + (0x20000, 0x2FFFD), + (0x30000, 0x3FFFD), + (0x40000, 0x4FFFD), + (0x50000, 0x5FFFD), + (0x60000, 0x6FFFD), + (0x70000, 0x7FFFD), + (0x80000, 0x8FFFD), + (0x90000, 0x9FFFD), + (0xA0000, 0xAFFFD), + (0xB0000, 0xBFFFD), + (0xC0000, 0xCFFFD), + (0xD0000, 0xDFFFD), + (0xE1000, 0xEFFFD), + (0xF0000, 0xFFFFD), + (0x100000, 0x10FFFD), +] + +def encode(c): + retval = c + i = ord(c) + for low, high in escape_range: + if i < low: + break + if i >= low and i <= high: + retval = "".join(["%%%2X" % ord(o) for o in c.encode('utf-8')]) + break + return retval + + +def iri2uri(uri): + """Convert an IRI to a URI. Note that IRIs must be + passed in a unicode strings. That is, do not utf-8 encode + the IRI before passing it into the function.""" + if isinstance(uri ,unicode): + (scheme, authority, path, query, fragment) = urlparse.urlsplit(uri) + authority = authority.encode('idna') + # For each character in 'ucschar' or 'iprivate' + # 1. encode as utf-8 + # 2. then %-encode each octet of that utf-8 + uri = urlparse.urlunsplit((scheme, authority, path, query, fragment)) + uri = "".join([encode(c) for c in uri]) + return uri + +if __name__ == "__main__": + import unittest + + class Test(unittest.TestCase): + + def test_uris(self): + """Test that URIs are invariant under the transformation.""" + invariant = [ + u"ftp://ftp.is.co.za/rfc/rfc1808.txt", + u"http://www.ietf.org/rfc/rfc2396.txt", + u"ldap://[2001:db8::7]/c=GB?objectClass?one", + u"mailto:John.Doe@example.com", + u"news:comp.infosystems.www.servers.unix", + u"tel:+1-816-555-1212", + u"telnet://192.0.2.16:80/", + u"urn:oasis:names:specification:docbook:dtd:xml:4.1.2" ] + for uri in invariant: + self.assertEqual(uri, iri2uri(uri)) + + def test_iri(self): + """ Test that the right type of escaping is done for each part of the URI.""" + self.assertEqual("http://xn--o3h.com/%E2%98%84", iri2uri(u"http://\N{COMET}.com/\N{COMET}")) + self.assertEqual("http://bitworking.org/?fred=%E2%98%84", iri2uri(u"http://bitworking.org/?fred=\N{COMET}")) + self.assertEqual("http://bitworking.org/#%E2%98%84", iri2uri(u"http://bitworking.org/#\N{COMET}")) + self.assertEqual("#%E2%98%84", iri2uri(u"#\N{COMET}")) + self.assertEqual("/fred?bar=%E2%98%9A#%E2%98%84", iri2uri(u"/fred?bar=\N{BLACK LEFT POINTING INDEX}#\N{COMET}")) + self.assertEqual("/fred?bar=%E2%98%9A#%E2%98%84", iri2uri(iri2uri(u"/fred?bar=\N{BLACK LEFT POINTING INDEX}#\N{COMET}"))) + self.assertNotEqual("/fred?bar=%E2%98%9A#%E2%98%84", iri2uri(u"/fred?bar=\N{BLACK LEFT POINTING INDEX}#\N{COMET}".encode('utf-8'))) + + unittest.main() + + diff --git a/appengine/dashdemo-cached/httplib2/socks.py b/appengine/dashdemo-cached/httplib2/socks.py new file mode 100644 index 0000000..0991f4c --- /dev/null +++ b/appengine/dashdemo-cached/httplib2/socks.py @@ -0,0 +1,438 @@ +"""SocksiPy - Python SOCKS module. +Version 1.00 + +Copyright 2006 Dan-Haim. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +3. Neither the name of Dan Haim nor the names of his contributors may be used + to endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY DAN HAIM "AS IS" AND ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +EVENT SHALL DAN HAIM OR HIS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMANGE. + + +This module provides a standard socket-like interface for Python +for tunneling connections through SOCKS proxies. + +""" + +""" + +Minor modifications made by Christopher Gilbert (http://motomastyle.com/) +for use in PyLoris (http://pyloris.sourceforge.net/) + +Minor modifications made by Mario Vilas (http://breakingcode.wordpress.com/) +mainly to merge bug fixes found in Sourceforge + +""" + +import base64 +import socket +import struct +import sys + +if getattr(socket, 'socket', None) is None: + raise ImportError('socket.socket missing, proxy support unusable') + +PROXY_TYPE_SOCKS4 = 1 +PROXY_TYPE_SOCKS5 = 2 +PROXY_TYPE_HTTP = 3 +PROXY_TYPE_HTTP_NO_TUNNEL = 4 + +_defaultproxy = None +_orgsocket = socket.socket + +class ProxyError(Exception): pass +class GeneralProxyError(ProxyError): pass +class Socks5AuthError(ProxyError): pass +class Socks5Error(ProxyError): pass +class Socks4Error(ProxyError): pass +class HTTPError(ProxyError): pass + +_generalerrors = ("success", + "invalid data", + "not connected", + "not available", + "bad proxy type", + "bad input") + +_socks5errors = ("succeeded", + "general SOCKS server failure", + "connection not allowed by ruleset", + "Network unreachable", + "Host unreachable", + "Connection refused", + "TTL expired", + "Command not supported", + "Address type not supported", + "Unknown error") + +_socks5autherrors = ("succeeded", + "authentication is required", + "all offered authentication methods were rejected", + "unknown username or invalid password", + "unknown error") + +_socks4errors = ("request granted", + "request rejected or failed", + "request rejected because SOCKS server cannot connect to identd on the client", + "request rejected because the client program and identd report different user-ids", + "unknown error") + +def setdefaultproxy(proxytype=None, addr=None, port=None, rdns=True, username=None, password=None): + """setdefaultproxy(proxytype, addr[, port[, rdns[, username[, password]]]]) + Sets a default proxy which all further socksocket objects will use, + unless explicitly changed. + """ + global _defaultproxy + _defaultproxy = (proxytype, addr, port, rdns, username, password) + +def wrapmodule(module): + """wrapmodule(module) + Attempts to replace a module's socket library with a SOCKS socket. Must set + a default proxy using setdefaultproxy(...) first. + This will only work on modules that import socket directly into the namespace; + most of the Python Standard Library falls into this category. + """ + if _defaultproxy != None: + module.socket.socket = socksocket + else: + raise GeneralProxyError((4, "no proxy specified")) + +class socksocket(socket.socket): + """socksocket([family[, type[, proto]]]) -> socket object + Open a SOCKS enabled socket. The parameters are the same as + those of the standard socket init. In order for SOCKS to work, + you must specify family=AF_INET, type=SOCK_STREAM and proto=0. + """ + + def __init__(self, family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0, _sock=None): + _orgsocket.__init__(self, family, type, proto, _sock) + if _defaultproxy != None: + self.__proxy = _defaultproxy + else: + self.__proxy = (None, None, None, None, None, None) + self.__proxysockname = None + self.__proxypeername = None + self.__httptunnel = True + + def __recvall(self, count): + """__recvall(count) -> data + Receive EXACTLY the number of bytes requested from the socket. + Blocks until the required number of bytes have been received. + """ + data = self.recv(count) + while len(data) < count: + d = self.recv(count-len(data)) + if not d: raise GeneralProxyError((0, "connection closed unexpectedly")) + data = data + d + return data + + def sendall(self, content, *args): + """ override socket.socket.sendall method to rewrite the header + for non-tunneling proxies if needed + """ + if not self.__httptunnel: + content = self.__rewriteproxy(content) + return super(socksocket, self).sendall(content, *args) + + def __rewriteproxy(self, header): + """ rewrite HTTP request headers to support non-tunneling proxies + (i.e. those which do not support the CONNECT method). + This only works for HTTP (not HTTPS) since HTTPS requires tunneling. + """ + host, endpt = None, None + hdrs = header.split("\r\n") + for hdr in hdrs: + if hdr.lower().startswith("host:"): + host = hdr + elif hdr.lower().startswith("get") or hdr.lower().startswith("post"): + endpt = hdr + if host and endpt: + hdrs.remove(host) + hdrs.remove(endpt) + host = host.split(" ")[1] + endpt = endpt.split(" ") + if (self.__proxy[4] != None and self.__proxy[5] != None): + hdrs.insert(0, self.__getauthheader()) + hdrs.insert(0, "Host: %s" % host) + hdrs.insert(0, "%s http://%s%s %s" % (endpt[0], host, endpt[1], endpt[2])) + return "\r\n".join(hdrs) + + def __getauthheader(self): + auth = self.__proxy[4] + ":" + self.__proxy[5] + return "Proxy-Authorization: Basic " + base64.b64encode(auth) + + def setproxy(self, proxytype=None, addr=None, port=None, rdns=True, username=None, password=None): + """setproxy(proxytype, addr[, port[, rdns[, username[, password]]]]) + Sets the proxy to be used. + proxytype - The type of the proxy to be used. Three types + are supported: PROXY_TYPE_SOCKS4 (including socks4a), + PROXY_TYPE_SOCKS5 and PROXY_TYPE_HTTP + addr - The address of the server (IP or DNS). + port - The port of the server. Defaults to 1080 for SOCKS + servers and 8080 for HTTP proxy servers. + rdns - Should DNS queries be preformed on the remote side + (rather than the local side). The default is True. + Note: This has no effect with SOCKS4 servers. + username - Username to authenticate with to the server. + The default is no authentication. + password - Password to authenticate with to the server. + Only relevant when username is also provided. + """ + self.__proxy = (proxytype, addr, port, rdns, username, password) + + def __negotiatesocks5(self, destaddr, destport): + """__negotiatesocks5(self,destaddr,destport) + Negotiates a connection through a SOCKS5 server. + """ + # First we'll send the authentication packages we support. + if (self.__proxy[4]!=None) and (self.__proxy[5]!=None): + # The username/password details were supplied to the + # setproxy method so we support the USERNAME/PASSWORD + # authentication (in addition to the standard none). + self.sendall(struct.pack('BBBB', 0x05, 0x02, 0x00, 0x02)) + else: + # No username/password were entered, therefore we + # only support connections with no authentication. + self.sendall(struct.pack('BBB', 0x05, 0x01, 0x00)) + # We'll receive the server's response to determine which + # method was selected + chosenauth = self.__recvall(2) + if chosenauth[0:1] != chr(0x05).encode(): + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + # Check the chosen authentication method + if chosenauth[1:2] == chr(0x00).encode(): + # No authentication is required + pass + elif chosenauth[1:2] == chr(0x02).encode(): + # Okay, we need to perform a basic username/password + # authentication. + self.sendall(chr(0x01).encode() + chr(len(self.__proxy[4])) + self.__proxy[4] + chr(len(self.__proxy[5])) + self.__proxy[5]) + authstat = self.__recvall(2) + if authstat[0:1] != chr(0x01).encode(): + # Bad response + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + if authstat[1:2] != chr(0x00).encode(): + # Authentication failed + self.close() + raise Socks5AuthError((3, _socks5autherrors[3])) + # Authentication succeeded + else: + # Reaching here is always bad + self.close() + if chosenauth[1] == chr(0xFF).encode(): + raise Socks5AuthError((2, _socks5autherrors[2])) + else: + raise GeneralProxyError((1, _generalerrors[1])) + # Now we can request the actual connection + req = struct.pack('BBB', 0x05, 0x01, 0x00) + # If the given destination address is an IP address, we'll + # use the IPv4 address request even if remote resolving was specified. + try: + ipaddr = socket.inet_aton(destaddr) + req = req + chr(0x01).encode() + ipaddr + except socket.error: + # Well it's not an IP number, so it's probably a DNS name. + if self.__proxy[3]: + # Resolve remotely + ipaddr = None + req = req + chr(0x03).encode() + chr(len(destaddr)).encode() + destaddr + else: + # Resolve locally + ipaddr = socket.inet_aton(socket.gethostbyname(destaddr)) + req = req + chr(0x01).encode() + ipaddr + req = req + struct.pack(">H", destport) + self.sendall(req) + # Get the response + resp = self.__recvall(4) + if resp[0:1] != chr(0x05).encode(): + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + elif resp[1:2] != chr(0x00).encode(): + # Connection failed + self.close() + if ord(resp[1:2])<=8: + raise Socks5Error((ord(resp[1:2]), _socks5errors[ord(resp[1:2])])) + else: + raise Socks5Error((9, _socks5errors[9])) + # Get the bound address/port + elif resp[3:4] == chr(0x01).encode(): + boundaddr = self.__recvall(4) + elif resp[3:4] == chr(0x03).encode(): + resp = resp + self.recv(1) + boundaddr = self.__recvall(ord(resp[4:5])) + else: + self.close() + raise GeneralProxyError((1,_generalerrors[1])) + boundport = struct.unpack(">H", self.__recvall(2))[0] + self.__proxysockname = (boundaddr, boundport) + if ipaddr != None: + self.__proxypeername = (socket.inet_ntoa(ipaddr), destport) + else: + self.__proxypeername = (destaddr, destport) + + def getproxysockname(self): + """getsockname() -> address info + Returns the bound IP address and port number at the proxy. + """ + return self.__proxysockname + + def getproxypeername(self): + """getproxypeername() -> address info + Returns the IP and port number of the proxy. + """ + return _orgsocket.getpeername(self) + + def getpeername(self): + """getpeername() -> address info + Returns the IP address and port number of the destination + machine (note: getproxypeername returns the proxy) + """ + return self.__proxypeername + + def __negotiatesocks4(self,destaddr,destport): + """__negotiatesocks4(self,destaddr,destport) + Negotiates a connection through a SOCKS4 server. + """ + # Check if the destination address provided is an IP address + rmtrslv = False + try: + ipaddr = socket.inet_aton(destaddr) + except socket.error: + # It's a DNS name. Check where it should be resolved. + if self.__proxy[3]: + ipaddr = struct.pack("BBBB", 0x00, 0x00, 0x00, 0x01) + rmtrslv = True + else: + ipaddr = socket.inet_aton(socket.gethostbyname(destaddr)) + # Construct the request packet + req = struct.pack(">BBH", 0x04, 0x01, destport) + ipaddr + # The username parameter is considered userid for SOCKS4 + if self.__proxy[4] != None: + req = req + self.__proxy[4] + req = req + chr(0x00).encode() + # DNS name if remote resolving is required + # NOTE: This is actually an extension to the SOCKS4 protocol + # called SOCKS4A and may not be supported in all cases. + if rmtrslv: + req = req + destaddr + chr(0x00).encode() + self.sendall(req) + # Get the response from the server + resp = self.__recvall(8) + if resp[0:1] != chr(0x00).encode(): + # Bad data + self.close() + raise GeneralProxyError((1,_generalerrors[1])) + if resp[1:2] != chr(0x5A).encode(): + # Server returned an error + self.close() + if ord(resp[1:2]) in (91, 92, 93): + self.close() + raise Socks4Error((ord(resp[1:2]), _socks4errors[ord(resp[1:2]) - 90])) + else: + raise Socks4Error((94, _socks4errors[4])) + # Get the bound address/port + self.__proxysockname = (socket.inet_ntoa(resp[4:]), struct.unpack(">H", resp[2:4])[0]) + if rmtrslv != None: + self.__proxypeername = (socket.inet_ntoa(ipaddr), destport) + else: + self.__proxypeername = (destaddr, destport) + + def __negotiatehttp(self, destaddr, destport): + """__negotiatehttp(self,destaddr,destport) + Negotiates a connection through an HTTP server. + """ + # If we need to resolve locally, we do this now + if not self.__proxy[3]: + addr = socket.gethostbyname(destaddr) + else: + addr = destaddr + headers = ["CONNECT ", addr, ":", str(destport), " HTTP/1.1\r\n"] + headers += ["Host: ", destaddr, "\r\n"] + if (self.__proxy[4] != None and self.__proxy[5] != None): + headers += [self.__getauthheader(), "\r\n"] + headers.append("\r\n") + self.sendall("".join(headers).encode()) + # We read the response until we get the string "\r\n\r\n" + resp = self.recv(1) + while resp.find("\r\n\r\n".encode()) == -1: + resp = resp + self.recv(1) + # We just need the first line to check if the connection + # was successful + statusline = resp.splitlines()[0].split(" ".encode(), 2) + if statusline[0] not in ("HTTP/1.0".encode(), "HTTP/1.1".encode()): + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + try: + statuscode = int(statusline[1]) + except ValueError: + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + if statuscode != 200: + self.close() + raise HTTPError((statuscode, statusline[2])) + self.__proxysockname = ("0.0.0.0", 0) + self.__proxypeername = (addr, destport) + + def connect(self, destpair): + """connect(self, despair) + Connects to the specified destination through a proxy. + destpar - A tuple of the IP/DNS address and the port number. + (identical to socket's connect). + To select the proxy server use setproxy(). + """ + # Do a minimal input check first + if (not type(destpair) in (list,tuple)) or (len(destpair) < 2) or (not isinstance(destpair[0], basestring)) or (type(destpair[1]) != int): + raise GeneralProxyError((5, _generalerrors[5])) + if self.__proxy[0] == PROXY_TYPE_SOCKS5: + if self.__proxy[2] != None: + portnum = self.__proxy[2] + else: + portnum = 1080 + _orgsocket.connect(self, (self.__proxy[1], portnum)) + self.__negotiatesocks5(destpair[0], destpair[1]) + elif self.__proxy[0] == PROXY_TYPE_SOCKS4: + if self.__proxy[2] != None: + portnum = self.__proxy[2] + else: + portnum = 1080 + _orgsocket.connect(self,(self.__proxy[1], portnum)) + self.__negotiatesocks4(destpair[0], destpair[1]) + elif self.__proxy[0] == PROXY_TYPE_HTTP: + if self.__proxy[2] != None: + portnum = self.__proxy[2] + else: + portnum = 8080 + _orgsocket.connect(self,(self.__proxy[1], portnum)) + self.__negotiatehttp(destpair[0], destpair[1]) + elif self.__proxy[0] == PROXY_TYPE_HTTP_NO_TUNNEL: + if self.__proxy[2] != None: + portnum = self.__proxy[2] + else: + portnum = 8080 + _orgsocket.connect(self,(self.__proxy[1],portnum)) + if destpair[1] == 443: + self.__negotiatehttp(destpair[0],destpair[1]) + else: + self.__httptunnel = False + elif self.__proxy[0] == None: + _orgsocket.connect(self, (destpair[0], destpair[1])) + else: + raise GeneralProxyError((4, _generalerrors[4])) diff --git a/appengine/dashdemo-cached/index.html b/appengine/dashdemo-cached/index.html new file mode 100644 index 0000000..6c893ff --- /dev/null +++ b/appengine/dashdemo-cached/index.html @@ -0,0 +1,38 @@ + + + hellodashboard + + + + +
+

A simple example of a graphical dashboard, backed by + Google's BigQuery and running in Python on App Engine.

+

Click on the map to highlight the value of each state, as + generated by this query:

+
 {{ query }} 
+ + diff --git a/appengine/dashdemo-cached/main.py b/appengine/dashdemo-cached/main.py new file mode 100644 index 0000000..675c307 --- /dev/null +++ b/appengine/dashdemo-cached/main.py @@ -0,0 +1,99 @@ +import webapp2 +import bqclient +import httplib2 +import logging +import os +from django.utils import simplejson as json +from google.appengine.ext.webapp.template import render +from oauth2client.appengine import oauth2decorator_from_clientsecrets +from google.appengine.api.memcache import Client + +# CLIENT_SECRETS, name of a file containing +# the OAuth 2.0 information for this application, +# including client_id and client_secret, which are found +# on the API Access tab on the Google APIs Console # +CLIENT_SECRETS = os.path.join(os.path.dirname(__file__), 'client_secrets.json') + +# Project ID for a project where you and your users +# are viewing members. This is where the bill will be sent. +# During the limited availability preview, there is no bill. +# Replace this value with the Client ID value from your project, +# the same numeric value you used in client_secrets.json +BILLING_PROJECT_ID = "475473128136" +DATA_PROJECT_ID = "publicdata" +DATASET = "samples" +TABLE = "natality" +QUERY = """ +select state, SUM(gestation_weeks) / COUNT(gestation_weeks) as weeks +from publicdata:samples.natality +where year > 1990 and year < 2005 and IS_EXPLICITLY_DEFINED(gestation_weeks) +group by state order by weeks +""" +decorator = oauth2decorator_from_clientsecrets(CLIENT_SECRETS, + 'https://www.googleapis.com/auth/bigquery') + +mem = Client() + +class InfoHandler(webapp2.RequestHandler): + @decorator.oauth_aware + def get(self): + self.response.out.write(decorator.authorize_url()) + +class MainHandler(webapp2.RequestHandler): + def _bq4geo(self, bqdata): + """geodata output for region maps must be in the format region, value. + Assume the BigQuery query output is in this format and get names from schema. + """ + columnNameGeo = bqdata["schema"]["fields"][0]["name"] + columnNameVal = bqdata["schema"]["fields"][1]["name"] + #logging.info("Column Names=%s, %s" % (columnNameGeo, columnNameVal)) + geodata = []; + geodata.append((columnNameGeo,columnNameVal)) + logging.info(geodata) + for row in bqdata["rows"]: + geodata.append(("US-"+row["f"][0]["v"],row["f"][1]["v"])) + Wlogging.info("FINAL GEODATA---") + #logging.info(geodata) + return json.dumps(geodata, ensure_ascii=True) + + @decorator.oauth_required + def _bq2geo(self ): + """geodata output for region maps must be in the format region, value. + Assume the BigQuery query output is in this format and get names from schema. + """ + http = decorator.http() + bq = bqclient.BigQueryClient(http, decorator) + bqdata = bq.Query(QUERY, BILLING_PROJECT_ID) + #logging.info(bqdata) + columnNameGeo = bqdata["schema"]["fields"][0]["name"] + columnNameVal = bqdata["schema"]["fields"][1]["name"] + #logging.info("Column Names=%s, %s" % (columnNameGeo, columnNameVal)) + geodata = {} + geodata["rows"] = []; + logging.info(geodata) + for row in bqdata["rows"]: + newrow = ({"c":[]}) + newrow["c"].append({"v": "US-"+row["f"][0]["v"]}) + newrow["c"].append({"v":row["f"][1]["v"]}) + geodata["rows"].append(newrow) + geodata["cols"] = ({"id":columnNameGeo,"label":columnNameGeo,"type":"string"}, + {"id":columnNameVal, "label":columnNameVal, "type":"number"}) + logging.info("FINAL GEODATA---") + logging.info(geodata) + return json.dumps(geodata, ensure_ascii=True) + + def get(self): + data = mem.get('natality') + if not(data): + values = self._bq2geo() + data = { 'data': values, + 'query': QUERY } + mem.set('natality', data) + template = os.path.join(os.path.dirname(__file__), 'index.html') + self.response.out.write(render(template, data)) + +application = webapp2.WSGIApplication([ + ('/', MainHandler), + ('/info', InfoHandler), + (decorator.callback_path, decorator.callback_handler()), +], debug=True) diff --git a/appengine/dashdemo-cached/oauth2client/__init__.py b/appengine/dashdemo-cached/oauth2client/__init__.py new file mode 100644 index 0000000..ac84748 --- /dev/null +++ b/appengine/dashdemo-cached/oauth2client/__init__.py @@ -0,0 +1,5 @@ +__version__ = "1.2" + +GOOGLE_AUTH_URI = 'https://accounts.google.com/o/oauth2/auth' +GOOGLE_REVOKE_URI = 'https://accounts.google.com/o/oauth2/revoke' +GOOGLE_TOKEN_URI = 'https://accounts.google.com/o/oauth2/token' diff --git a/appengine/dashdemo-cached/oauth2client/anyjson.py b/appengine/dashdemo-cached/oauth2client/anyjson.py new file mode 100644 index 0000000..ae21c33 --- /dev/null +++ b/appengine/dashdemo-cached/oauth2client/anyjson.py @@ -0,0 +1,32 @@ +# Copyright (C) 2010 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Utility module to import a JSON module + +Hides all the messy details of exactly where +we get a simplejson module from. +""" + +__author__ = 'jcgregorio@google.com (Joe Gregorio)' + + +try: # pragma: no cover + # Should work for Python2.6 and higher. + import json as simplejson +except ImportError: # pragma: no cover + try: + import simplejson + except ImportError: + # Try to import from django, should work on App Engine + from django.utils import simplejson diff --git a/appengine/dashdemo-cached/oauth2client/appengine.py b/appengine/dashdemo-cached/oauth2client/appengine.py new file mode 100644 index 0000000..b463d4a --- /dev/null +++ b/appengine/dashdemo-cached/oauth2client/appengine.py @@ -0,0 +1,975 @@ +# Copyright (C) 2010 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Utilities for Google App Engine + +Utilities for making it easier to use OAuth 2.0 on Google App Engine. +""" + +__author__ = 'jcgregorio@google.com (Joe Gregorio)' + +import base64 +import cgi +import httplib2 +import logging +import os +import pickle +import threading +import time + +from google.appengine.api import app_identity +from google.appengine.api import memcache +from google.appengine.api import users +from google.appengine.ext import db +from google.appengine.ext import webapp +from google.appengine.ext.webapp.util import login_required +from google.appengine.ext.webapp.util import run_wsgi_app +from oauth2client import GOOGLE_AUTH_URI +from oauth2client import GOOGLE_REVOKE_URI +from oauth2client import GOOGLE_TOKEN_URI +from oauth2client import clientsecrets +from oauth2client import util +from oauth2client import xsrfutil +from oauth2client.anyjson import simplejson +from oauth2client.client import AccessTokenRefreshError +from oauth2client.client import AssertionCredentials +from oauth2client.client import Credentials +from oauth2client.client import Flow +from oauth2client.client import OAuth2WebServerFlow +from oauth2client.client import Storage + +# TODO(dhermes): Resolve import issue. +# This is a temporary fix for a Google internal issue. +try: + from google.appengine.ext import ndb +except ImportError: + ndb = None + + +logger = logging.getLogger(__name__) + +OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns' + +XSRF_MEMCACHE_ID = 'xsrf_secret_key' + + +def _safe_html(s): + """Escape text to make it safe to display. + + Args: + s: string, The text to escape. + + Returns: + The escaped text as a string. + """ + return cgi.escape(s, quote=1).replace("'", ''') + + +class InvalidClientSecretsError(Exception): + """The client_secrets.json file is malformed or missing required fields.""" + + +class InvalidXsrfTokenError(Exception): + """The XSRF token is invalid or expired.""" + + +class SiteXsrfSecretKey(db.Model): + """Storage for the sites XSRF secret key. + + There will only be one instance stored of this model, the one used for the + site. + """ + secret = db.StringProperty() + +if ndb is not None: + class SiteXsrfSecretKeyNDB(ndb.Model): + """NDB Model for storage for the sites XSRF secret key. + + Since this model uses the same kind as SiteXsrfSecretKey, it can be used + interchangeably. This simply provides an NDB model for interacting with the + same data the DB model interacts with. + + There should only be one instance stored of this model, the one used for the + site. + """ + secret = ndb.StringProperty() + + @classmethod + def _get_kind(cls): + """Return the kind name for this class.""" + return 'SiteXsrfSecretKey' + + +def _generate_new_xsrf_secret_key(): + """Returns a random XSRF secret key. + """ + return os.urandom(16).encode("hex") + + +def xsrf_secret_key(): + """Return the secret key for use for XSRF protection. + + If the Site entity does not have a secret key, this method will also create + one and persist it. + + Returns: + The secret key. + """ + secret = memcache.get(XSRF_MEMCACHE_ID, namespace=OAUTH2CLIENT_NAMESPACE) + if not secret: + # Load the one and only instance of SiteXsrfSecretKey. + model = SiteXsrfSecretKey.get_or_insert(key_name='site') + if not model.secret: + model.secret = _generate_new_xsrf_secret_key() + model.put() + secret = model.secret + memcache.add(XSRF_MEMCACHE_ID, secret, namespace=OAUTH2CLIENT_NAMESPACE) + + return str(secret) + + +class AppAssertionCredentials(AssertionCredentials): + """Credentials object for App Engine Assertion Grants + + This object will allow an App Engine application to identify itself to Google + and other OAuth 2.0 servers that can verify assertions. It can be used for the + purpose of accessing data stored under an account assigned to the App Engine + application itself. + + This credential does not require a flow to instantiate because it represents + a two legged flow, and therefore has all of the required information to + generate and refresh its own access tokens. + """ + + @util.positional(2) + def __init__(self, scope, **kwargs): + """Constructor for AppAssertionCredentials + + Args: + scope: string or iterable of strings, scope(s) of the credentials being + requested. + kwargs: optional keyword args, including: + service_account_id: service account id of the application. If None or + unspecified, the default service account for the app is used. + """ + self.scope = util.scopes_to_string(scope) + self.service_account_id = kwargs.get('service_account_id', None) + + # Assertion type is no longer used, but still in the parent class signature. + super(AppAssertionCredentials, self).__init__(None) + + @classmethod + def from_json(cls, json): + data = simplejson.loads(json) + return AppAssertionCredentials(data['scope']) + + def _refresh(self, http_request): + """Refreshes the access_token. + + Since the underlying App Engine app_identity implementation does its own + caching we can skip all the storage hoops and just to a refresh using the + API. + + Args: + http_request: callable, a callable that matches the method signature of + httplib2.Http.request, used to make the refresh request. + + Raises: + AccessTokenRefreshError: When the refresh fails. + """ + try: + scopes = self.scope.split() + (token, _) = app_identity.get_access_token( + scopes, service_account_id=self.service_account_id) + except app_identity.Error, e: + raise AccessTokenRefreshError(str(e)) + self.access_token = token + + +class FlowProperty(db.Property): + """App Engine datastore Property for Flow. + + Utility property that allows easy storage and retrieval of an + oauth2client.Flow""" + + # Tell what the user type is. + data_type = Flow + + # For writing to datastore. + def get_value_for_datastore(self, model_instance): + flow = super(FlowProperty, + self).get_value_for_datastore(model_instance) + return db.Blob(pickle.dumps(flow)) + + # For reading from datastore. + def make_value_from_datastore(self, value): + if value is None: + return None + return pickle.loads(value) + + def validate(self, value): + if value is not None and not isinstance(value, Flow): + raise db.BadValueError('Property %s must be convertible ' + 'to a FlowThreeLegged instance (%s)' % + (self.name, value)) + return super(FlowProperty, self).validate(value) + + def empty(self, value): + return not value + + +if ndb is not None: + class FlowNDBProperty(ndb.PickleProperty): + """App Engine NDB datastore Property for Flow. + + Serves the same purpose as the DB FlowProperty, but for NDB models. Since + PickleProperty inherits from BlobProperty, the underlying representation of + the data in the datastore will be the same as in the DB case. + + Utility property that allows easy storage and retrieval of an + oauth2client.Flow + """ + + def _validate(self, value): + """Validates a value as a proper Flow object. + + Args: + value: A value to be set on the property. + + Raises: + TypeError if the value is not an instance of Flow. + """ + logger.info('validate: Got type %s', type(value)) + if value is not None and not isinstance(value, Flow): + raise TypeError('Property %s must be convertible to a flow ' + 'instance; received: %s.' % (self._name, value)) + + +class CredentialsProperty(db.Property): + """App Engine datastore Property for Credentials. + + Utility property that allows easy storage and retrieval of + oath2client.Credentials + """ + + # Tell what the user type is. + data_type = Credentials + + # For writing to datastore. + def get_value_for_datastore(self, model_instance): + logger.info("get: Got type " + str(type(model_instance))) + cred = super(CredentialsProperty, + self).get_value_for_datastore(model_instance) + if cred is None: + cred = '' + else: + cred = cred.to_json() + return db.Blob(cred) + + # For reading from datastore. + def make_value_from_datastore(self, value): + logger.info("make: Got type " + str(type(value))) + if value is None: + return None + if len(value) == 0: + return None + try: + credentials = Credentials.new_from_json(value) + except ValueError: + credentials = None + return credentials + + def validate(self, value): + value = super(CredentialsProperty, self).validate(value) + logger.info("validate: Got type " + str(type(value))) + if value is not None and not isinstance(value, Credentials): + raise db.BadValueError('Property %s must be convertible ' + 'to a Credentials instance (%s)' % + (self.name, value)) + #if value is not None and not isinstance(value, Credentials): + # return None + return value + + +if ndb is not None: + # TODO(dhermes): Turn this into a JsonProperty and overhaul the Credentials + # and subclass mechanics to use new_from_dict, to_dict, + # from_dict, etc. + class CredentialsNDBProperty(ndb.BlobProperty): + """App Engine NDB datastore Property for Credentials. + + Serves the same purpose as the DB CredentialsProperty, but for NDB models. + Since CredentialsProperty stores data as a blob and this inherits from + BlobProperty, the data in the datastore will be the same as in the DB case. + + Utility property that allows easy storage and retrieval of Credentials and + subclasses. + """ + def _validate(self, value): + """Validates a value as a proper credentials object. + + Args: + value: A value to be set on the property. + + Raises: + TypeError if the value is not an instance of Credentials. + """ + logger.info('validate: Got type %s', type(value)) + if value is not None and not isinstance(value, Credentials): + raise TypeError('Property %s must be convertible to a credentials ' + 'instance; received: %s.' % (self._name, value)) + + def _to_base_type(self, value): + """Converts our validated value to a JSON serialized string. + + Args: + value: A value to be set in the datastore. + + Returns: + A JSON serialized version of the credential, else '' if value is None. + """ + if value is None: + return '' + else: + return value.to_json() + + def _from_base_type(self, value): + """Converts our stored JSON string back to the desired type. + + Args: + value: A value from the datastore to be converted to the desired type. + + Returns: + A deserialized Credentials (or subclass) object, else None if the + value can't be parsed. + """ + if not value: + return None + try: + # Uses the from_json method of the implied class of value + credentials = Credentials.new_from_json(value) + except ValueError: + credentials = None + return credentials + + +class StorageByKeyName(Storage): + """Store and retrieve a credential to and from the App Engine datastore. + + This Storage helper presumes the Credentials have been stored as a + CredentialsProperty or CredentialsNDBProperty on a datastore model class, and + that entities are stored by key_name. + """ + + @util.positional(4) + def __init__(self, model, key_name, property_name, cache=None, user=None): + """Constructor for Storage. + + Args: + model: db.Model or ndb.Model, model class + key_name: string, key name for the entity that has the credentials + property_name: string, name of the property that is a CredentialsProperty + or CredentialsNDBProperty. + cache: memcache, a write-through cache to put in front of the datastore. + If the model you are using is an NDB model, using a cache will be + redundant since the model uses an instance cache and memcache for you. + user: users.User object, optional. Can be used to grab user ID as a + key_name if no key name is specified. + """ + if key_name is None: + if user is None: + raise ValueError('StorageByKeyName called with no key name or user.') + key_name = user.user_id() + + self._model = model + self._key_name = key_name + self._property_name = property_name + self._cache = cache + + def _is_ndb(self): + """Determine whether the model of the instance is an NDB model. + + Returns: + Boolean indicating whether or not the model is an NDB or DB model. + """ + # issubclass will fail if one of the arguments is not a class, only need + # worry about new-style classes since ndb and db models are new-style + if isinstance(self._model, type): + if ndb is not None and issubclass(self._model, ndb.Model): + return True + elif issubclass(self._model, db.Model): + return False + + raise TypeError('Model class not an NDB or DB model: %s.' % (self._model,)) + + def _get_entity(self): + """Retrieve entity from datastore. + + Uses a different model method for db or ndb models. + + Returns: + Instance of the model corresponding to the current storage object + and stored using the key name of the storage object. + """ + if self._is_ndb(): + return self._model.get_by_id(self._key_name) + else: + return self._model.get_by_key_name(self._key_name) + + def _delete_entity(self): + """Delete entity from datastore. + + Attempts to delete using the key_name stored on the object, whether or not + the given key is in the datastore. + """ + if self._is_ndb(): + ndb.Key(self._model, self._key_name).delete() + else: + entity_key = db.Key.from_path(self._model.kind(), self._key_name) + db.delete(entity_key) + + @db.non_transactional(allow_existing=True) + def locked_get(self): + """Retrieve Credential from datastore. + + Returns: + oauth2client.Credentials + """ + credentials = None + if self._cache: + json = self._cache.get(self._key_name) + if json: + credentials = Credentials.new_from_json(json) + if credentials is None: + entity = self._get_entity() + if entity is not None: + credentials = getattr(entity, self._property_name) + if self._cache: + self._cache.set(self._key_name, credentials.to_json()) + + if credentials and hasattr(credentials, 'set_store'): + credentials.set_store(self) + return credentials + + @db.non_transactional(allow_existing=True) + def locked_put(self, credentials): + """Write a Credentials to the datastore. + + Args: + credentials: Credentials, the credentials to store. + """ + entity = self._model.get_or_insert(self._key_name) + setattr(entity, self._property_name, credentials) + entity.put() + if self._cache: + self._cache.set(self._key_name, credentials.to_json()) + + @db.non_transactional(allow_existing=True) + def locked_delete(self): + """Delete Credential from datastore.""" + + if self._cache: + self._cache.delete(self._key_name) + + self._delete_entity() + + +class CredentialsModel(db.Model): + """Storage for OAuth 2.0 Credentials + + Storage of the model is keyed by the user.user_id(). + """ + credentials = CredentialsProperty() + + +if ndb is not None: + class CredentialsNDBModel(ndb.Model): + """NDB Model for storage of OAuth 2.0 Credentials + + Since this model uses the same kind as CredentialsModel and has a property + which can serialize and deserialize Credentials correctly, it can be used + interchangeably with a CredentialsModel to access, insert and delete the + same entities. This simply provides an NDB model for interacting with the + same data the DB model interacts with. + + Storage of the model is keyed by the user.user_id(). + """ + credentials = CredentialsNDBProperty() + + @classmethod + def _get_kind(cls): + """Return the kind name for this class.""" + return 'CredentialsModel' + + +def _build_state_value(request_handler, user): + """Composes the value for the 'state' parameter. + + Packs the current request URI and an XSRF token into an opaque string that + can be passed to the authentication server via the 'state' parameter. + + Args: + request_handler: webapp.RequestHandler, The request. + user: google.appengine.api.users.User, The current user. + + Returns: + The state value as a string. + """ + uri = request_handler.request.url + token = xsrfutil.generate_token(xsrf_secret_key(), user.user_id(), + action_id=str(uri)) + return uri + ':' + token + + +def _parse_state_value(state, user): + """Parse the value of the 'state' parameter. + + Parses the value and validates the XSRF token in the state parameter. + + Args: + state: string, The value of the state parameter. + user: google.appengine.api.users.User, The current user. + + Raises: + InvalidXsrfTokenError: if the XSRF token is invalid. + + Returns: + The redirect URI. + """ + uri, token = state.rsplit(':', 1) + if not xsrfutil.validate_token(xsrf_secret_key(), token, user.user_id(), + action_id=uri): + raise InvalidXsrfTokenError() + + return uri + + +class OAuth2Decorator(object): + """Utility for making OAuth 2.0 easier. + + Instantiate and then use with oauth_required or oauth_aware + as decorators on webapp.RequestHandler methods. + + Example: + + decorator = OAuth2Decorator( + client_id='837...ent.com', + client_secret='Qh...wwI', + scope='https://www.googleapis.com/auth/plus') + + + class MainHandler(webapp.RequestHandler): + + @decorator.oauth_required + def get(self): + http = decorator.http() + # http is authorized with the user's Credentials and can be used + # in API calls + + """ + + def set_credentials(self, credentials): + self._tls.credentials = credentials + + def get_credentials(self): + """A thread local Credentials object. + + Returns: + A client.Credentials object, or None if credentials hasn't been set in + this thread yet, which may happen when calling has_credentials inside + oauth_aware. + """ + return getattr(self._tls, 'credentials', None) + + credentials = property(get_credentials, set_credentials) + + def set_flow(self, flow): + self._tls.flow = flow + + def get_flow(self): + """A thread local Flow object. + + Returns: + A credentials.Flow object, or None if the flow hasn't been set in this + thread yet, which happens in _create_flow() since Flows are created + lazily. + """ + return getattr(self._tls, 'flow', None) + + flow = property(get_flow, set_flow) + + + @util.positional(4) + def __init__(self, client_id, client_secret, scope, + auth_uri=GOOGLE_AUTH_URI, + token_uri=GOOGLE_TOKEN_URI, + revoke_uri=GOOGLE_REVOKE_URI, + user_agent=None, + message=None, + callback_path='/oauth2callback', + token_response_param=None, + _storage_class=StorageByKeyName, + _credentials_class=CredentialsModel, + _credentials_property_name='credentials', + **kwargs): + + """Constructor for OAuth2Decorator + + Args: + client_id: string, client identifier. + client_secret: string client secret. + scope: string or iterable of strings, scope(s) of the credentials being + requested. + auth_uri: string, URI for authorization endpoint. For convenience + defaults to Google's endpoints but any OAuth 2.0 provider can be used. + token_uri: string, URI for token endpoint. For convenience + defaults to Google's endpoints but any OAuth 2.0 provider can be used. + revoke_uri: string, URI for revoke endpoint. For convenience + defaults to Google's endpoints but any OAuth 2.0 provider can be used. + user_agent: string, User agent of your application, default to None. + message: Message to display if there are problems with the OAuth 2.0 + configuration. The message may contain HTML and will be presented on the + web interface for any method that uses the decorator. + callback_path: string, The absolute path to use as the callback URI. Note + that this must match up with the URI given when registering the + application in the APIs Console. + token_response_param: string. If provided, the full JSON response + to the access token request will be encoded and included in this query + parameter in the callback URI. This is useful with providers (e.g. + wordpress.com) that include extra fields that the client may want. + _storage_class: "Protected" keyword argument not typically provided to + this constructor. A storage class to aid in storing a Credentials object + for a user in the datastore. Defaults to StorageByKeyName. + _credentials_class: "Protected" keyword argument not typically provided to + this constructor. A db or ndb Model class to hold credentials. Defaults + to CredentialsModel. + _credentials_property_name: "Protected" keyword argument not typically + provided to this constructor. A string indicating the name of the field + on the _credentials_class where a Credentials object will be stored. + Defaults to 'credentials'. + **kwargs: dict, Keyword arguments are be passed along as kwargs to the + OAuth2WebServerFlow constructor. + """ + self._tls = threading.local() + self.flow = None + self.credentials = None + self._client_id = client_id + self._client_secret = client_secret + self._scope = util.scopes_to_string(scope) + self._auth_uri = auth_uri + self._token_uri = token_uri + self._revoke_uri = revoke_uri + self._user_agent = user_agent + self._kwargs = kwargs + self._message = message + self._in_error = False + self._callback_path = callback_path + self._token_response_param = token_response_param + self._storage_class = _storage_class + self._credentials_class = _credentials_class + self._credentials_property_name = _credentials_property_name + + def _display_error_message(self, request_handler): + request_handler.response.out.write('') + request_handler.response.out.write(_safe_html(self._message)) + request_handler.response.out.write('') + + def oauth_required(self, method): + """Decorator that starts the OAuth 2.0 dance. + + Starts the OAuth dance for the logged in user if they haven't already + granted access for this application. + + Args: + method: callable, to be decorated method of a webapp.RequestHandler + instance. + """ + + def check_oauth(request_handler, *args, **kwargs): + if self._in_error: + self._display_error_message(request_handler) + return + + user = users.get_current_user() + # Don't use @login_decorator as this could be used in a POST request. + if not user: + request_handler.redirect(users.create_login_url( + request_handler.request.uri)) + return + + self._create_flow(request_handler) + + # Store the request URI in 'state' so we can use it later + self.flow.params['state'] = _build_state_value(request_handler, user) + self.credentials = self._storage_class( + self._credentials_class, None, + self._credentials_property_name, user=user).get() + + if not self.has_credentials(): + return request_handler.redirect(self.authorize_url()) + try: + resp = method(request_handler, *args, **kwargs) + except AccessTokenRefreshError: + return request_handler.redirect(self.authorize_url()) + finally: + self.credentials = None + return resp + + return check_oauth + + def _create_flow(self, request_handler): + """Create the Flow object. + + The Flow is calculated lazily since we don't know where this app is + running until it receives a request, at which point redirect_uri can be + calculated and then the Flow object can be constructed. + + Args: + request_handler: webapp.RequestHandler, the request handler. + """ + if self.flow is None: + redirect_uri = request_handler.request.relative_url( + self._callback_path) # Usually /oauth2callback + self.flow = OAuth2WebServerFlow(self._client_id, self._client_secret, + self._scope, redirect_uri=redirect_uri, + user_agent=self._user_agent, + auth_uri=self._auth_uri, + token_uri=self._token_uri, + revoke_uri=self._revoke_uri, + **self._kwargs) + + def oauth_aware(self, method): + """Decorator that sets up for OAuth 2.0 dance, but doesn't do it. + + Does all the setup for the OAuth dance, but doesn't initiate it. + This decorator is useful if you want to create a page that knows + whether or not the user has granted access to this application. + From within a method decorated with @oauth_aware the has_credentials() + and authorize_url() methods can be called. + + Args: + method: callable, to be decorated method of a webapp.RequestHandler + instance. + """ + + def setup_oauth(request_handler, *args, **kwargs): + if self._in_error: + self._display_error_message(request_handler) + return + + user = users.get_current_user() + # Don't use @login_decorator as this could be used in a POST request. + if not user: + request_handler.redirect(users.create_login_url( + request_handler.request.uri)) + return + + self._create_flow(request_handler) + + self.flow.params['state'] = _build_state_value(request_handler, user) + self.credentials = self._storage_class( + self._credentials_class, None, + self._credentials_property_name, user=user).get() + try: + resp = method(request_handler, *args, **kwargs) + finally: + self.credentials = None + return resp + return setup_oauth + + + def has_credentials(self): + """True if for the logged in user there are valid access Credentials. + + Must only be called from with a webapp.RequestHandler subclassed method + that had been decorated with either @oauth_required or @oauth_aware. + """ + return self.credentials is not None and not self.credentials.invalid + + def authorize_url(self): + """Returns the URL to start the OAuth dance. + + Must only be called from with a webapp.RequestHandler subclassed method + that had been decorated with either @oauth_required or @oauth_aware. + """ + url = self.flow.step1_get_authorize_url() + return str(url) + + def http(self, *args, **kwargs): + """Returns an authorized http instance. + + Must only be called from within an @oauth_required decorated method, or + from within an @oauth_aware decorated method where has_credentials() + returns True. + + Args: + args: Positional arguments passed to httplib2.Http constructor. + kwargs: Positional arguments passed to httplib2.Http constructor. + """ + return self.credentials.authorize(httplib2.Http(*args, **kwargs)) + + @property + def callback_path(self): + """The absolute path where the callback will occur. + + Note this is the absolute path, not the absolute URI, that will be + calculated by the decorator at runtime. See callback_handler() for how this + should be used. + + Returns: + The callback path as a string. + """ + return self._callback_path + + + def callback_handler(self): + """RequestHandler for the OAuth 2.0 redirect callback. + + Usage: + app = webapp.WSGIApplication([ + ('/index', MyIndexHandler), + ..., + (decorator.callback_path, decorator.callback_handler()) + ]) + + Returns: + A webapp.RequestHandler that handles the redirect back from the + server during the OAuth 2.0 dance. + """ + decorator = self + + class OAuth2Handler(webapp.RequestHandler): + """Handler for the redirect_uri of the OAuth 2.0 dance.""" + + @login_required + def get(self): + error = self.request.get('error') + if error: + errormsg = self.request.get('error_description', error) + self.response.out.write( + 'The authorization request failed: %s' % _safe_html(errormsg)) + else: + user = users.get_current_user() + decorator._create_flow(self) + credentials = decorator.flow.step2_exchange(self.request.params) + decorator._storage_class( + decorator._credentials_class, None, + decorator._credentials_property_name, user=user).put(credentials) + redirect_uri = _parse_state_value(str(self.request.get('state')), + user) + + if decorator._token_response_param and credentials.token_response: + resp_json = simplejson.dumps(credentials.token_response) + redirect_uri = util._add_query_parameter( + redirect_uri, decorator._token_response_param, resp_json) + + self.redirect(redirect_uri) + + return OAuth2Handler + + def callback_application(self): + """WSGI application for handling the OAuth 2.0 redirect callback. + + If you need finer grained control use `callback_handler` which returns just + the webapp.RequestHandler. + + Returns: + A webapp.WSGIApplication that handles the redirect back from the + server during the OAuth 2.0 dance. + """ + return webapp.WSGIApplication([ + (self.callback_path, self.callback_handler()) + ]) + + +class OAuth2DecoratorFromClientSecrets(OAuth2Decorator): + """An OAuth2Decorator that builds from a clientsecrets file. + + Uses a clientsecrets file as the source for all the information when + constructing an OAuth2Decorator. + + Example: + + decorator = OAuth2DecoratorFromClientSecrets( + os.path.join(os.path.dirname(__file__), 'client_secrets.json') + scope='https://www.googleapis.com/auth/plus') + + + class MainHandler(webapp.RequestHandler): + + @decorator.oauth_required + def get(self): + http = decorator.http() + # http is authorized with the user's Credentials and can be used + # in API calls + """ + + @util.positional(3) + def __init__(self, filename, scope, message=None, cache=None): + """Constructor + + Args: + filename: string, File name of client secrets. + scope: string or iterable of strings, scope(s) of the credentials being + requested. + message: string, A friendly string to display to the user if the + clientsecrets file is missing or invalid. The message may contain HTML + and will be presented on the web interface for any method that uses the + decorator. + cache: An optional cache service client that implements get() and set() + methods. See clientsecrets.loadfile() for details. + """ + client_type, client_info = clientsecrets.loadfile(filename, cache=cache) + if client_type not in [ + clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]: + raise InvalidClientSecretsError( + 'OAuth2Decorator doesn\'t support this OAuth 2.0 flow.') + constructor_kwargs = { + 'auth_uri': client_info['auth_uri'], + 'token_uri': client_info['token_uri'], + 'message': message, + } + revoke_uri = client_info.get('revoke_uri') + if revoke_uri is not None: + constructor_kwargs['revoke_uri'] = revoke_uri + super(OAuth2DecoratorFromClientSecrets, self).__init__( + client_info['client_id'], client_info['client_secret'], + scope, **constructor_kwargs) + if message is not None: + self._message = message + else: + self._message = 'Please configure your application for OAuth 2.0.' + + +@util.positional(2) +def oauth2decorator_from_clientsecrets(filename, scope, + message=None, cache=None): + """Creates an OAuth2Decorator populated from a clientsecrets file. + + Args: + filename: string, File name of client secrets. + scope: string or list of strings, scope(s) of the credentials being + requested. + message: string, A friendly string to display to the user if the + clientsecrets file is missing or invalid. The message may contain HTML and + will be presented on the web interface for any method that uses the + decorator. + cache: An optional cache service client that implements get() and set() + methods. See clientsecrets.loadfile() for details. + + Returns: An OAuth2Decorator + + """ + return OAuth2DecoratorFromClientSecrets(filename, scope, + message=message, cache=cache) diff --git a/appengine/dashdemo-cached/oauth2client/client.py b/appengine/dashdemo-cached/oauth2client/client.py new file mode 100644 index 0000000..99873e2 --- /dev/null +++ b/appengine/dashdemo-cached/oauth2client/client.py @@ -0,0 +1,1364 @@ +# Copyright (C) 2010 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""An OAuth 2.0 client. + +Tools for interacting with OAuth 2.0 protected resources. +""" + +__author__ = 'jcgregorio@google.com (Joe Gregorio)' + +import base64 +import clientsecrets +import copy +import datetime +import httplib2 +import logging +import os +import sys +import time +import urllib +import urlparse + +from oauth2client import GOOGLE_AUTH_URI +from oauth2client import GOOGLE_REVOKE_URI +from oauth2client import GOOGLE_TOKEN_URI +from oauth2client import util +from oauth2client.anyjson import simplejson + +HAS_OPENSSL = False +HAS_CRYPTO = False +try: + from oauth2client import crypt + HAS_CRYPTO = True + if crypt.OpenSSLVerifier is not None: + HAS_OPENSSL = True +except ImportError: + pass + +try: + from urlparse import parse_qsl +except ImportError: + from cgi import parse_qsl + +logger = logging.getLogger(__name__) + +# Expiry is stored in RFC3339 UTC format +EXPIRY_FORMAT = '%Y-%m-%dT%H:%M:%SZ' + +# Which certs to use to validate id_tokens received. +ID_TOKEN_VERIFICATON_CERTS = 'https://www.googleapis.com/oauth2/v1/certs' + +# Constant to use for the out of band OAuth 2.0 flow. +OOB_CALLBACK_URN = 'urn:ietf:wg:oauth:2.0:oob' + +# Google Data client libraries may need to set this to [401, 403]. +REFRESH_STATUS_CODES = [401] + + +class Error(Exception): + """Base error for this module.""" + + +class FlowExchangeError(Error): + """Error trying to exchange an authorization grant for an access token.""" + + +class AccessTokenRefreshError(Error): + """Error trying to refresh an expired access token.""" + + +class TokenRevokeError(Error): + """Error trying to revoke a token.""" + + +class UnknownClientSecretsFlowError(Error): + """The client secrets file called for an unknown type of OAuth 2.0 flow. """ + + +class AccessTokenCredentialsError(Error): + """Having only the access_token means no refresh is possible.""" + + +class VerifyJwtTokenError(Error): + """Could on retrieve certificates for validation.""" + + +class NonAsciiHeaderError(Error): + """Header names and values must be ASCII strings.""" + + +def _abstract(): + raise NotImplementedError('You need to override this function') + + +class MemoryCache(object): + """httplib2 Cache implementation which only caches locally.""" + + def __init__(self): + self.cache = {} + + def get(self, key): + return self.cache.get(key) + + def set(self, key, value): + self.cache[key] = value + + def delete(self, key): + self.cache.pop(key, None) + + +class Credentials(object): + """Base class for all Credentials objects. + + Subclasses must define an authorize() method that applies the credentials to + an HTTP transport. + + Subclasses must also specify a classmethod named 'from_json' that takes a JSON + string as input and returns an instaniated Credentials object. + """ + + NON_SERIALIZED_MEMBERS = ['store'] + + def authorize(self, http): + """Take an httplib2.Http instance (or equivalent) and authorizes it. + + Authorizes it for the set of credentials, usually by replacing + http.request() with a method that adds in the appropriate headers and then + delegates to the original Http.request() method. + + Args: + http: httplib2.Http, an http object to be used to make the refresh + request. + """ + _abstract() + + def refresh(self, http): + """Forces a refresh of the access_token. + + Args: + http: httplib2.Http, an http object to be used to make the refresh + request. + """ + _abstract() + + def revoke(self, http): + """Revokes a refresh_token and makes the credentials void. + + Args: + http: httplib2.Http, an http object to be used to make the revoke + request. + """ + _abstract() + + def apply(self, headers): + """Add the authorization to the headers. + + Args: + headers: dict, the headers to add the Authorization header to. + """ + _abstract() + + def _to_json(self, strip): + """Utility function that creates JSON repr. of a Credentials object. + + Args: + strip: array, An array of names of members to not include in the JSON. + + Returns: + string, a JSON representation of this instance, suitable to pass to + from_json(). + """ + t = type(self) + d = copy.copy(self.__dict__) + for member in strip: + if member in d: + del d[member] + if 'token_expiry' in d and isinstance(d['token_expiry'], datetime.datetime): + d['token_expiry'] = d['token_expiry'].strftime(EXPIRY_FORMAT) + # Add in information we will need later to reconsistitue this instance. + d['_class'] = t.__name__ + d['_module'] = t.__module__ + return simplejson.dumps(d) + + def to_json(self): + """Creating a JSON representation of an instance of Credentials. + + Returns: + string, a JSON representation of this instance, suitable to pass to + from_json(). + """ + return self._to_json(Credentials.NON_SERIALIZED_MEMBERS) + + @classmethod + def new_from_json(cls, s): + """Utility class method to instantiate a Credentials subclass from a JSON + representation produced by to_json(). + + Args: + s: string, JSON from to_json(). + + Returns: + An instance of the subclass of Credentials that was serialized with + to_json(). + """ + data = simplejson.loads(s) + # Find and call the right classmethod from_json() to restore the object. + module = data['_module'] + try: + m = __import__(module) + except ImportError: + # In case there's an object from the old package structure, update it + module = module.replace('.googleapiclient', '') + m = __import__(module) + + m = __import__(module, fromlist=module.split('.')[:-1]) + kls = getattr(m, data['_class']) + from_json = getattr(kls, 'from_json') + return from_json(s) + + @classmethod + def from_json(cls, s): + """Instantiate a Credentials object from a JSON description of it. + + The JSON should have been produced by calling .to_json() on the object. + + Args: + data: dict, A deserialized JSON object. + + Returns: + An instance of a Credentials subclass. + """ + return Credentials() + + +class Flow(object): + """Base class for all Flow objects.""" + pass + + +class Storage(object): + """Base class for all Storage objects. + + Store and retrieve a single credential. This class supports locking + such that multiple processes and threads can operate on a single + store. + """ + + def acquire_lock(self): + """Acquires any lock necessary to access this Storage. + + This lock is not reentrant. + """ + pass + + def release_lock(self): + """Release the Storage lock. + + Trying to release a lock that isn't held will result in a + RuntimeError. + """ + pass + + def locked_get(self): + """Retrieve credential. + + The Storage lock must be held when this is called. + + Returns: + oauth2client.client.Credentials + """ + _abstract() + + def locked_put(self, credentials): + """Write a credential. + + The Storage lock must be held when this is called. + + Args: + credentials: Credentials, the credentials to store. + """ + _abstract() + + def locked_delete(self): + """Delete a credential. + + The Storage lock must be held when this is called. + """ + _abstract() + + def get(self): + """Retrieve credential. + + The Storage lock must *not* be held when this is called. + + Returns: + oauth2client.client.Credentials + """ + self.acquire_lock() + try: + return self.locked_get() + finally: + self.release_lock() + + def put(self, credentials): + """Write a credential. + + The Storage lock must be held when this is called. + + Args: + credentials: Credentials, the credentials to store. + """ + self.acquire_lock() + try: + self.locked_put(credentials) + finally: + self.release_lock() + + def delete(self): + """Delete credential. + + Frees any resources associated with storing the credential. + The Storage lock must *not* be held when this is called. + + Returns: + None + """ + self.acquire_lock() + try: + return self.locked_delete() + finally: + self.release_lock() + + +def clean_headers(headers): + """Forces header keys and values to be strings, i.e not unicode. + + The httplib module just concats the header keys and values in a way that may + make the message header a unicode string, which, if it then tries to + contatenate to a binary request body may result in a unicode decode error. + + Args: + headers: dict, A dictionary of headers. + + Returns: + The same dictionary but with all the keys converted to strings. + """ + clean = {} + try: + for k, v in headers.iteritems(): + clean[str(k)] = str(v) + except UnicodeEncodeError: + raise NonAsciiHeaderError(k + ': ' + v) + return clean + + +def _update_query_params(uri, params): + """Updates a URI with new query parameters. + + Args: + uri: string, A valid URI, with potential existing query parameters. + params: dict, A dictionary of query parameters. + + Returns: + The same URI but with the new query parameters added. + """ + parts = list(urlparse.urlparse(uri)) + query_params = dict(parse_qsl(parts[4])) # 4 is the index of the query part + query_params.update(params) + parts[4] = urllib.urlencode(query_params) + return urlparse.urlunparse(parts) + + +class OAuth2Credentials(Credentials): + """Credentials object for OAuth 2.0. + + Credentials can be applied to an httplib2.Http object using the authorize() + method, which then adds the OAuth 2.0 access token to each request. + + OAuth2Credentials objects may be safely pickled and unpickled. + """ + + @util.positional(8) + def __init__(self, access_token, client_id, client_secret, refresh_token, + token_expiry, token_uri, user_agent, revoke_uri=None, + id_token=None, token_response=None): + """Create an instance of OAuth2Credentials. + + This constructor is not usually called by the user, instead + OAuth2Credentials objects are instantiated by the OAuth2WebServerFlow. + + Args: + access_token: string, access token. + client_id: string, client identifier. + client_secret: string, client secret. + refresh_token: string, refresh token. + token_expiry: datetime, when the access_token expires. + token_uri: string, URI of token endpoint. + user_agent: string, The HTTP User-Agent to provide for this application. + revoke_uri: string, URI for revoke endpoint. Defaults to None; a token + can't be revoked if this is None. + id_token: object, The identity of the resource owner. + token_response: dict, the decoded response to the token request. None + if a token hasn't been requested yet. Stored because some providers + (e.g. wordpress.com) include extra fields that clients may want. + + Notes: + store: callable, A callable that when passed a Credential + will store the credential back to where it came from. + This is needed to store the latest access_token if it + has expired and been refreshed. + """ + self.access_token = access_token + self.client_id = client_id + self.client_secret = client_secret + self.refresh_token = refresh_token + self.store = None + self.token_expiry = token_expiry + self.token_uri = token_uri + self.user_agent = user_agent + self.revoke_uri = revoke_uri + self.id_token = id_token + self.token_response = token_response + + # True if the credentials have been revoked or expired and can't be + # refreshed. + self.invalid = False + + def authorize(self, http): + """Authorize an httplib2.Http instance with these credentials. + + The modified http.request method will add authentication headers to each + request and will refresh access_tokens when a 401 is received on a + request. In addition the http.request method has a credentials property, + http.request.credentials, which is the Credentials object that authorized + it. + + Args: + http: An instance of httplib2.Http + or something that acts like it. + + Returns: + A modified instance of http that was passed in. + + Example: + + h = httplib2.Http() + h = credentials.authorize(h) + + You can't create a new OAuth subclass of httplib2.Authenication + because it never gets passed the absolute URI, which is needed for + signing. So instead we have to overload 'request' with a closure + that adds in the Authorization header and then calls the original + version of 'request()'. + """ + request_orig = http.request + + # The closure that will replace 'httplib2.Http.request'. + @util.positional(1) + def new_request(uri, method='GET', body=None, headers=None, + redirections=httplib2.DEFAULT_MAX_REDIRECTS, + connection_type=None): + if not self.access_token: + logger.info('Attempting refresh to obtain initial access_token') + self._refresh(request_orig) + + # Modify the request headers to add the appropriate + # Authorization header. + if headers is None: + headers = {} + self.apply(headers) + + if self.user_agent is not None: + if 'user-agent' in headers: + headers['user-agent'] = self.user_agent + ' ' + headers['user-agent'] + else: + headers['user-agent'] = self.user_agent + + resp, content = request_orig(uri, method, body, clean_headers(headers), + redirections, connection_type) + + if resp.status in REFRESH_STATUS_CODES: + logger.info('Refreshing due to a %s' % str(resp.status)) + self._refresh(request_orig) + self.apply(headers) + return request_orig(uri, method, body, clean_headers(headers), + redirections, connection_type) + else: + return (resp, content) + + # Replace the request method with our own closure. + http.request = new_request + + # Set credentials as a property of the request method. + setattr(http.request, 'credentials', self) + + return http + + def refresh(self, http): + """Forces a refresh of the access_token. + + Args: + http: httplib2.Http, an http object to be used to make the refresh + request. + """ + self._refresh(http.request) + + def revoke(self, http): + """Revokes a refresh_token and makes the credentials void. + + Args: + http: httplib2.Http, an http object to be used to make the revoke + request. + """ + self._revoke(http.request) + + def apply(self, headers): + """Add the authorization to the headers. + + Args: + headers: dict, the headers to add the Authorization header to. + """ + headers['Authorization'] = 'Bearer ' + self.access_token + + def to_json(self): + return self._to_json(Credentials.NON_SERIALIZED_MEMBERS) + + @classmethod + def from_json(cls, s): + """Instantiate a Credentials object from a JSON description of it. The JSON + should have been produced by calling .to_json() on the object. + + Args: + data: dict, A deserialized JSON object. + + Returns: + An instance of a Credentials subclass. + """ + data = simplejson.loads(s) + if 'token_expiry' in data and not isinstance(data['token_expiry'], + datetime.datetime): + try: + data['token_expiry'] = datetime.datetime.strptime( + data['token_expiry'], EXPIRY_FORMAT) + except: + data['token_expiry'] = None + retval = cls( + data['access_token'], + data['client_id'], + data['client_secret'], + data['refresh_token'], + data['token_expiry'], + data['token_uri'], + data['user_agent'], + revoke_uri=data.get('revoke_uri', None), + id_token=data.get('id_token', None), + token_response=data.get('token_response', None)) + retval.invalid = data['invalid'] + return retval + + @property + def access_token_expired(self): + """True if the credential is expired or invalid. + + If the token_expiry isn't set, we assume the token doesn't expire. + """ + if self.invalid: + return True + + if not self.token_expiry: + return False + + now = datetime.datetime.utcnow() + if now >= self.token_expiry: + logger.info('access_token is expired. Now: %s, token_expiry: %s', + now, self.token_expiry) + return True + return False + + def set_store(self, store): + """Set the Storage for the credential. + + Args: + store: Storage, an implementation of Stroage object. + This is needed to store the latest access_token if it + has expired and been refreshed. This implementation uses + locking to check for updates before updating the + access_token. + """ + self.store = store + + def _updateFromCredential(self, other): + """Update this Credential from another instance.""" + self.__dict__.update(other.__getstate__()) + + def __getstate__(self): + """Trim the state down to something that can be pickled.""" + d = copy.copy(self.__dict__) + del d['store'] + return d + + def __setstate__(self, state): + """Reconstitute the state of the object from being pickled.""" + self.__dict__.update(state) + self.store = None + + def _generate_refresh_request_body(self): + """Generate the body that will be used in the refresh request.""" + body = urllib.urlencode({ + 'grant_type': 'refresh_token', + 'client_id': self.client_id, + 'client_secret': self.client_secret, + 'refresh_token': self.refresh_token, + }) + return body + + def _generate_refresh_request_headers(self): + """Generate the headers that will be used in the refresh request.""" + headers = { + 'content-type': 'application/x-www-form-urlencoded', + } + + if self.user_agent is not None: + headers['user-agent'] = self.user_agent + + return headers + + def _refresh(self, http_request): + """Refreshes the access_token. + + This method first checks by reading the Storage object if available. + If a refresh is still needed, it holds the Storage lock until the + refresh is completed. + + Args: + http_request: callable, a callable that matches the method signature of + httplib2.Http.request, used to make the refresh request. + + Raises: + AccessTokenRefreshError: When the refresh fails. + """ + if not self.store: + self._do_refresh_request(http_request) + else: + self.store.acquire_lock() + try: + new_cred = self.store.locked_get() + if (new_cred and not new_cred.invalid and + new_cred.access_token != self.access_token): + logger.info('Updated access_token read from Storage') + self._updateFromCredential(new_cred) + else: + self._do_refresh_request(http_request) + finally: + self.store.release_lock() + + def _do_refresh_request(self, http_request): + """Refresh the access_token using the refresh_token. + + Args: + http_request: callable, a callable that matches the method signature of + httplib2.Http.request, used to make the refresh request. + + Raises: + AccessTokenRefreshError: When the refresh fails. + """ + body = self._generate_refresh_request_body() + headers = self._generate_refresh_request_headers() + + logger.info('Refreshing access_token') + resp, content = http_request( + self.token_uri, method='POST', body=body, headers=headers) + if resp.status == 200: + # TODO(jcgregorio) Raise an error if loads fails? + d = simplejson.loads(content) + self.token_response = d + self.access_token = d['access_token'] + self.refresh_token = d.get('refresh_token', self.refresh_token) + if 'expires_in' in d: + self.token_expiry = datetime.timedelta( + seconds=int(d['expires_in'])) + datetime.datetime.utcnow() + else: + self.token_expiry = None + if self.store: + self.store.locked_put(self) + else: + # An {'error':...} response body means the token is expired or revoked, + # so we flag the credentials as such. + logger.info('Failed to retrieve access token: %s' % content) + error_msg = 'Invalid response %s.' % resp['status'] + try: + d = simplejson.loads(content) + if 'error' in d: + error_msg = d['error'] + self.invalid = True + if self.store: + self.store.locked_put(self) + except StandardError: + pass + raise AccessTokenRefreshError(error_msg) + + def _revoke(self, http_request): + """Revokes the refresh_token and deletes the store if available. + + Args: + http_request: callable, a callable that matches the method signature of + httplib2.Http.request, used to make the revoke request. + """ + self._do_revoke(http_request, self.refresh_token) + + def _do_revoke(self, http_request, token): + """Revokes the credentials and deletes the store if available. + + Args: + http_request: callable, a callable that matches the method signature of + httplib2.Http.request, used to make the refresh request. + token: A string used as the token to be revoked. Can be either an + access_token or refresh_token. + + Raises: + TokenRevokeError: If the revoke request does not return with a 200 OK. + """ + logger.info('Revoking token') + query_params = {'token': token} + token_revoke_uri = _update_query_params(self.revoke_uri, query_params) + resp, content = http_request(token_revoke_uri) + if resp.status == 200: + self.invalid = True + else: + error_msg = 'Invalid response %s.' % resp.status + try: + d = simplejson.loads(content) + if 'error' in d: + error_msg = d['error'] + except StandardError: + pass + raise TokenRevokeError(error_msg) + + if self.store: + self.store.delete() + + +class AccessTokenCredentials(OAuth2Credentials): + """Credentials object for OAuth 2.0. + + Credentials can be applied to an httplib2.Http object using the + authorize() method, which then signs each request from that object + with the OAuth 2.0 access token. This set of credentials is for the + use case where you have acquired an OAuth 2.0 access_token from + another place such as a JavaScript client or another web + application, and wish to use it from Python. Because only the + access_token is present it can not be refreshed and will in time + expire. + + AccessTokenCredentials objects may be safely pickled and unpickled. + + Usage: + credentials = AccessTokenCredentials('', + 'my-user-agent/1.0') + http = httplib2.Http() + http = credentials.authorize(http) + + Exceptions: + AccessTokenCredentialsExpired: raised when the access_token expires or is + revoked. + """ + + def __init__(self, access_token, user_agent, revoke_uri=None): + """Create an instance of OAuth2Credentials + + This is one of the few types if Credentials that you should contrust, + Credentials objects are usually instantiated by a Flow. + + Args: + access_token: string, access token. + user_agent: string, The HTTP User-Agent to provide for this application. + revoke_uri: string, URI for revoke endpoint. Defaults to None; a token + can't be revoked if this is None. + """ + super(AccessTokenCredentials, self).__init__( + access_token, + None, + None, + None, + None, + None, + user_agent, + revoke_uri=revoke_uri) + + + @classmethod + def from_json(cls, s): + data = simplejson.loads(s) + retval = AccessTokenCredentials( + data['access_token'], + data['user_agent']) + return retval + + def _refresh(self, http_request): + raise AccessTokenCredentialsError( + 'The access_token is expired or invalid and can\'t be refreshed.') + + def _revoke(self, http_request): + """Revokes the access_token and deletes the store if available. + + Args: + http_request: callable, a callable that matches the method signature of + httplib2.Http.request, used to make the revoke request. + """ + self._do_revoke(http_request, self.access_token) + + +class AssertionCredentials(OAuth2Credentials): + """Abstract Credentials object used for OAuth 2.0 assertion grants. + + This credential does not require a flow to instantiate because it + represents a two legged flow, and therefore has all of the required + information to generate and refresh its own access tokens. It must + be subclassed to generate the appropriate assertion string. + + AssertionCredentials objects may be safely pickled and unpickled. + """ + + @util.positional(2) + def __init__(self, assertion_type, user_agent=None, + token_uri=GOOGLE_TOKEN_URI, + revoke_uri=GOOGLE_REVOKE_URI, + **unused_kwargs): + """Constructor for AssertionFlowCredentials. + + Args: + assertion_type: string, assertion type that will be declared to the auth + server + user_agent: string, The HTTP User-Agent to provide for this application. + token_uri: string, URI for token endpoint. For convenience + defaults to Google's endpoints but any OAuth 2.0 provider can be used. + revoke_uri: string, URI for revoke endpoint. + """ + super(AssertionCredentials, self).__init__( + None, + None, + None, + None, + None, + token_uri, + user_agent, + revoke_uri=revoke_uri) + self.assertion_type = assertion_type + + def _generate_refresh_request_body(self): + assertion = self._generate_assertion() + + body = urllib.urlencode({ + 'assertion': assertion, + 'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer', + }) + + return body + + def _generate_assertion(self): + """Generate the assertion string that will be used in the access token + request. + """ + _abstract() + + def _revoke(self, http_request): + """Revokes the access_token and deletes the store if available. + + Args: + http_request: callable, a callable that matches the method signature of + httplib2.Http.request, used to make the revoke request. + """ + self._do_revoke(http_request, self.access_token) + + +if HAS_CRYPTO: + # PyOpenSSL and PyCrypto are not prerequisites for oauth2client, so if it is + # missing then don't create the SignedJwtAssertionCredentials or the + # verify_id_token() method. + + class SignedJwtAssertionCredentials(AssertionCredentials): + """Credentials object used for OAuth 2.0 Signed JWT assertion grants. + + This credential does not require a flow to instantiate because it represents + a two legged flow, and therefore has all of the required information to + generate and refresh its own access tokens. + + SignedJwtAssertionCredentials requires either PyOpenSSL, or PyCrypto 2.6 or + later. For App Engine you may also consider using AppAssertionCredentials. + """ + + MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds + + @util.positional(4) + def __init__(self, + service_account_name, + private_key, + scope, + private_key_password='notasecret', + user_agent=None, + token_uri=GOOGLE_TOKEN_URI, + revoke_uri=GOOGLE_REVOKE_URI, + **kwargs): + """Constructor for SignedJwtAssertionCredentials. + + Args: + service_account_name: string, id for account, usually an email address. + private_key: string, private key in PKCS12 or PEM format. + scope: string or iterable of strings, scope(s) of the credentials being + requested. + private_key_password: string, password for private_key, unused if + private_key is in PEM format. + user_agent: string, HTTP User-Agent to provide for this application. + token_uri: string, URI for token endpoint. For convenience + defaults to Google's endpoints but any OAuth 2.0 provider can be used. + revoke_uri: string, URI for revoke endpoint. + kwargs: kwargs, Additional parameters to add to the JWT token, for + example sub=joe@xample.org.""" + + super(SignedJwtAssertionCredentials, self).__init__( + None, + user_agent=user_agent, + token_uri=token_uri, + revoke_uri=revoke_uri, + ) + + self.scope = util.scopes_to_string(scope) + + # Keep base64 encoded so it can be stored in JSON. + self.private_key = base64.b64encode(private_key) + + self.private_key_password = private_key_password + self.service_account_name = service_account_name + self.kwargs = kwargs + + @classmethod + def from_json(cls, s): + data = simplejson.loads(s) + retval = SignedJwtAssertionCredentials( + data['service_account_name'], + base64.b64decode(data['private_key']), + data['scope'], + private_key_password=data['private_key_password'], + user_agent=data['user_agent'], + token_uri=data['token_uri'], + **data['kwargs'] + ) + retval.invalid = data['invalid'] + retval.access_token = data['access_token'] + return retval + + def _generate_assertion(self): + """Generate the assertion that will be used in the request.""" + now = long(time.time()) + payload = { + 'aud': self.token_uri, + 'scope': self.scope, + 'iat': now, + 'exp': now + SignedJwtAssertionCredentials.MAX_TOKEN_LIFETIME_SECS, + 'iss': self.service_account_name + } + payload.update(self.kwargs) + logger.debug(str(payload)) + + private_key = base64.b64decode(self.private_key) + return crypt.make_signed_jwt(crypt.Signer.from_string( + private_key, self.private_key_password), payload) + + # Only used in verify_id_token(), which is always calling to the same URI + # for the certs. + _cached_http = httplib2.Http(MemoryCache()) + + @util.positional(2) + def verify_id_token(id_token, audience, http=None, + cert_uri=ID_TOKEN_VERIFICATON_CERTS): + """Verifies a signed JWT id_token. + + This function requires PyOpenSSL and because of that it does not work on + App Engine. + + Args: + id_token: string, A Signed JWT. + audience: string, The audience 'aud' that the token should be for. + http: httplib2.Http, instance to use to make the HTTP request. Callers + should supply an instance that has caching enabled. + cert_uri: string, URI of the certificates in JSON format to + verify the JWT against. + + Returns: + The deserialized JSON in the JWT. + + Raises: + oauth2client.crypt.AppIdentityError if the JWT fails to verify. + """ + if http is None: + http = _cached_http + + resp, content = http.request(cert_uri) + + if resp.status == 200: + certs = simplejson.loads(content) + return crypt.verify_signed_jwt_with_certs(id_token, certs, audience) + else: + raise VerifyJwtTokenError('Status code: %d' % resp.status) + + +def _urlsafe_b64decode(b64string): + # Guard against unicode strings, which base64 can't handle. + b64string = b64string.encode('ascii') + padded = b64string + '=' * (4 - len(b64string) % 4) + return base64.urlsafe_b64decode(padded) + + +def _extract_id_token(id_token): + """Extract the JSON payload from a JWT. + + Does the extraction w/o checking the signature. + + Args: + id_token: string, OAuth 2.0 id_token. + + Returns: + object, The deserialized JSON payload. + """ + segments = id_token.split('.') + + if (len(segments) != 3): + raise VerifyJwtTokenError( + 'Wrong number of segments in token: %s' % id_token) + + return simplejson.loads(_urlsafe_b64decode(segments[1])) + + +def _parse_exchange_token_response(content): + """Parses response of an exchange token request. + + Most providers return JSON but some (e.g. Facebook) return a + url-encoded string. + + Args: + content: The body of a response + + Returns: + Content as a dictionary object. Note that the dict could be empty, + i.e. {}. That basically indicates a failure. + """ + resp = {} + try: + resp = simplejson.loads(content) + except StandardError: + # different JSON libs raise different exceptions, + # so we just do a catch-all here + resp = dict(parse_qsl(content)) + + # some providers respond with 'expires', others with 'expires_in' + if resp and 'expires' in resp: + resp['expires_in'] = resp.pop('expires') + + return resp + + +@util.positional(4) +def credentials_from_code(client_id, client_secret, scope, code, + redirect_uri='postmessage', http=None, + user_agent=None, token_uri=GOOGLE_TOKEN_URI, + auth_uri=GOOGLE_AUTH_URI, + revoke_uri=GOOGLE_REVOKE_URI): + """Exchanges an authorization code for an OAuth2Credentials object. + + Args: + client_id: string, client identifier. + client_secret: string, client secret. + scope: string or iterable of strings, scope(s) to request. + code: string, An authroization code, most likely passed down from + the client + redirect_uri: string, this is generally set to 'postmessage' to match the + redirect_uri that the client specified + http: httplib2.Http, optional http instance to use to do the fetch + token_uri: string, URI for token endpoint. For convenience + defaults to Google's endpoints but any OAuth 2.0 provider can be used. + auth_uri: string, URI for authorization endpoint. For convenience + defaults to Google's endpoints but any OAuth 2.0 provider can be used. + revoke_uri: string, URI for revoke endpoint. For convenience + defaults to Google's endpoints but any OAuth 2.0 provider can be used. + + Returns: + An OAuth2Credentials object. + + Raises: + FlowExchangeError if the authorization code cannot be exchanged for an + access token + """ + flow = OAuth2WebServerFlow(client_id, client_secret, scope, + redirect_uri=redirect_uri, user_agent=user_agent, + auth_uri=auth_uri, token_uri=token_uri, + revoke_uri=revoke_uri) + + credentials = flow.step2_exchange(code, http=http) + return credentials + + +@util.positional(3) +def credentials_from_clientsecrets_and_code(filename, scope, code, + message = None, + redirect_uri='postmessage', + http=None, + cache=None): + """Returns OAuth2Credentials from a clientsecrets file and an auth code. + + Will create the right kind of Flow based on the contents of the clientsecrets + file or will raise InvalidClientSecretsError for unknown types of Flows. + + Args: + filename: string, File name of clientsecrets. + scope: string or iterable of strings, scope(s) to request. + code: string, An authorization code, most likely passed down from + the client + message: string, A friendly string to display to the user if the + clientsecrets file is missing or invalid. If message is provided then + sys.exit will be called in the case of an error. If message in not + provided then clientsecrets.InvalidClientSecretsError will be raised. + redirect_uri: string, this is generally set to 'postmessage' to match the + redirect_uri that the client specified + http: httplib2.Http, optional http instance to use to do the fetch + cache: An optional cache service client that implements get() and set() + methods. See clientsecrets.loadfile() for details. + + Returns: + An OAuth2Credentials object. + + Raises: + FlowExchangeError if the authorization code cannot be exchanged for an + access token + UnknownClientSecretsFlowError if the file describes an unknown kind of Flow. + clientsecrets.InvalidClientSecretsError if the clientsecrets file is + invalid. + """ + flow = flow_from_clientsecrets(filename, scope, message=message, cache=cache, + redirect_uri=redirect_uri) + credentials = flow.step2_exchange(code, http=http) + return credentials + + +class OAuth2WebServerFlow(Flow): + """Does the Web Server Flow for OAuth 2.0. + + OAuth2WebServerFlow objects may be safely pickled and unpickled. + """ + + @util.positional(4) + def __init__(self, client_id, client_secret, scope, + redirect_uri=None, + user_agent=None, + auth_uri=GOOGLE_AUTH_URI, + token_uri=GOOGLE_TOKEN_URI, + revoke_uri=GOOGLE_REVOKE_URI, + **kwargs): + """Constructor for OAuth2WebServerFlow. + + The kwargs argument is used to set extra query parameters on the + auth_uri. For example, the access_type and approval_prompt + query parameters can be set via kwargs. + + Args: + client_id: string, client identifier. + client_secret: string client secret. + scope: string or iterable of strings, scope(s) of the credentials being + requested. + redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for + a non-web-based application, or a URI that handles the callback from + the authorization server. + user_agent: string, HTTP User-Agent to provide for this application. + auth_uri: string, URI for authorization endpoint. For convenience + defaults to Google's endpoints but any OAuth 2.0 provider can be used. + token_uri: string, URI for token endpoint. For convenience + defaults to Google's endpoints but any OAuth 2.0 provider can be used. + revoke_uri: string, URI for revoke endpoint. For convenience + defaults to Google's endpoints but any OAuth 2.0 provider can be used. + **kwargs: dict, The keyword arguments are all optional and required + parameters for the OAuth calls. + """ + self.client_id = client_id + self.client_secret = client_secret + self.scope = util.scopes_to_string(scope) + self.redirect_uri = redirect_uri + self.user_agent = user_agent + self.auth_uri = auth_uri + self.token_uri = token_uri + self.revoke_uri = revoke_uri + self.params = { + 'access_type': 'offline', + 'response_type': 'code', + } + self.params.update(kwargs) + + @util.positional(1) + def step1_get_authorize_url(self, redirect_uri=None): + """Returns a URI to redirect to the provider. + + Args: + redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for + a non-web-based application, or a URI that handles the callback from + the authorization server. This parameter is deprecated, please move to + passing the redirect_uri in via the constructor. + + Returns: + A URI as a string to redirect the user to begin the authorization flow. + """ + if redirect_uri is not None: + logger.warning(('The redirect_uri parameter for' + 'OAuth2WebServerFlow.step1_get_authorize_url is deprecated. Please' + 'move to passing the redirect_uri in via the constructor.')) + self.redirect_uri = redirect_uri + + if self.redirect_uri is None: + raise ValueError('The value of redirect_uri must not be None.') + + query_params = { + 'client_id': self.client_id, + 'redirect_uri': self.redirect_uri, + 'scope': self.scope, + } + query_params.update(self.params) + return _update_query_params(self.auth_uri, query_params) + + @util.positional(2) + def step2_exchange(self, code, http=None): + """Exhanges a code for OAuth2Credentials. + + Args: + code: string or dict, either the code as a string, or a dictionary + of the query parameters to the redirect_uri, which contains + the code. + http: httplib2.Http, optional http instance to use to do the fetch + + Returns: + An OAuth2Credentials object that can be used to authorize requests. + + Raises: + FlowExchangeError if a problem occured exchanging the code for a + refresh_token. + """ + + if not (isinstance(code, str) or isinstance(code, unicode)): + if 'code' not in code: + if 'error' in code: + error_msg = code['error'] + else: + error_msg = 'No code was supplied in the query parameters.' + raise FlowExchangeError(error_msg) + else: + code = code['code'] + + body = urllib.urlencode({ + 'grant_type': 'authorization_code', + 'client_id': self.client_id, + 'client_secret': self.client_secret, + 'code': code, + 'redirect_uri': self.redirect_uri, + 'scope': self.scope, + }) + headers = { + 'content-type': 'application/x-www-form-urlencoded', + } + + if self.user_agent is not None: + headers['user-agent'] = self.user_agent + + if http is None: + http = httplib2.Http() + + resp, content = http.request(self.token_uri, method='POST', body=body, + headers=headers) + d = _parse_exchange_token_response(content) + if resp.status == 200 and 'access_token' in d: + access_token = d['access_token'] + refresh_token = d.get('refresh_token', None) + token_expiry = None + if 'expires_in' in d: + token_expiry = datetime.datetime.utcnow() + datetime.timedelta( + seconds=int(d['expires_in'])) + + if 'id_token' in d: + d['id_token'] = _extract_id_token(d['id_token']) + + logger.info('Successfully retrieved access token') + return OAuth2Credentials(access_token, self.client_id, + self.client_secret, refresh_token, token_expiry, + self.token_uri, self.user_agent, + revoke_uri=self.revoke_uri, + id_token=d.get('id_token', None), + token_response=d) + else: + logger.info('Failed to retrieve access token: %s' % content) + if 'error' in d: + # you never know what those providers got to say + error_msg = unicode(d['error']) + else: + error_msg = 'Invalid response: %s.' % str(resp.status) + raise FlowExchangeError(error_msg) + + +@util.positional(2) +def flow_from_clientsecrets(filename, scope, redirect_uri=None, + message=None, cache=None): + """Create a Flow from a clientsecrets file. + + Will create the right kind of Flow based on the contents of the clientsecrets + file or will raise InvalidClientSecretsError for unknown types of Flows. + + Args: + filename: string, File name of client secrets. + scope: string or iterable of strings, scope(s) to request. + redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for + a non-web-based application, or a URI that handles the callback from + the authorization server. + message: string, A friendly string to display to the user if the + clientsecrets file is missing or invalid. If message is provided then + sys.exit will be called in the case of an error. If message in not + provided then clientsecrets.InvalidClientSecretsError will be raised. + cache: An optional cache service client that implements get() and set() + methods. See clientsecrets.loadfile() for details. + + Returns: + A Flow object. + + Raises: + UnknownClientSecretsFlowError if the file describes an unknown kind of Flow. + clientsecrets.InvalidClientSecretsError if the clientsecrets file is + invalid. + """ + try: + client_type, client_info = clientsecrets.loadfile(filename, cache=cache) + if client_type in (clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED): + constructor_kwargs = { + 'redirect_uri': redirect_uri, + 'auth_uri': client_info['auth_uri'], + 'token_uri': client_info['token_uri'], + } + revoke_uri = client_info.get('revoke_uri') + if revoke_uri is not None: + constructor_kwargs['revoke_uri'] = revoke_uri + return OAuth2WebServerFlow( + client_info['client_id'], client_info['client_secret'], + scope, **constructor_kwargs) + + except clientsecrets.InvalidClientSecretsError: + if message: + sys.exit(message) + else: + raise + else: + raise UnknownClientSecretsFlowError( + 'This OAuth 2.0 flow is unsupported: %r' % client_type) diff --git a/appengine/dashdemo-cached/oauth2client/clientsecrets.py b/appengine/dashdemo-cached/oauth2client/clientsecrets.py new file mode 100644 index 0000000..ac99aae --- /dev/null +++ b/appengine/dashdemo-cached/oauth2client/clientsecrets.py @@ -0,0 +1,153 @@ +# Copyright (C) 2011 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Utilities for reading OAuth 2.0 client secret files. + +A client_secrets.json file contains all the information needed to interact with +an OAuth 2.0 protected service. +""" + +__author__ = 'jcgregorio@google.com (Joe Gregorio)' + + +from anyjson import simplejson + +# Properties that make a client_secrets.json file valid. +TYPE_WEB = 'web' +TYPE_INSTALLED = 'installed' + +VALID_CLIENT = { + TYPE_WEB: { + 'required': [ + 'client_id', + 'client_secret', + 'redirect_uris', + 'auth_uri', + 'token_uri', + ], + 'string': [ + 'client_id', + 'client_secret', + ], + }, + TYPE_INSTALLED: { + 'required': [ + 'client_id', + 'client_secret', + 'redirect_uris', + 'auth_uri', + 'token_uri', + ], + 'string': [ + 'client_id', + 'client_secret', + ], + }, +} + + +class Error(Exception): + """Base error for this module.""" + pass + + +class InvalidClientSecretsError(Error): + """Format of ClientSecrets file is invalid.""" + pass + + +def _validate_clientsecrets(obj): + if obj is None or len(obj) != 1: + raise InvalidClientSecretsError('Invalid file format.') + client_type = obj.keys()[0] + if client_type not in VALID_CLIENT.keys(): + raise InvalidClientSecretsError('Unknown client type: %s.' % client_type) + client_info = obj[client_type] + for prop_name in VALID_CLIENT[client_type]['required']: + if prop_name not in client_info: + raise InvalidClientSecretsError( + 'Missing property "%s" in a client type of "%s".' % (prop_name, + client_type)) + for prop_name in VALID_CLIENT[client_type]['string']: + if client_info[prop_name].startswith('[['): + raise InvalidClientSecretsError( + 'Property "%s" is not configured.' % prop_name) + return client_type, client_info + + +def load(fp): + obj = simplejson.load(fp) + return _validate_clientsecrets(obj) + + +def loads(s): + obj = simplejson.loads(s) + return _validate_clientsecrets(obj) + + +def _loadfile(filename): + try: + fp = file(filename, 'r') + try: + obj = simplejson.load(fp) + finally: + fp.close() + except IOError: + raise InvalidClientSecretsError('File not found: "%s"' % filename) + return _validate_clientsecrets(obj) + + +def loadfile(filename, cache=None): + """Loading of client_secrets JSON file, optionally backed by a cache. + + Typical cache storage would be App Engine memcache service, + but you can pass in any other cache client that implements + these methods: + - get(key, namespace=ns) + - set(key, value, namespace=ns) + + Usage: + # without caching + client_type, client_info = loadfile('secrets.json') + # using App Engine memcache service + from google.appengine.api import memcache + client_type, client_info = loadfile('secrets.json', cache=memcache) + + Args: + filename: string, Path to a client_secrets.json file on a filesystem. + cache: An optional cache service client that implements get() and set() + methods. If not specified, the file is always being loaded from + a filesystem. + + Raises: + InvalidClientSecretsError: In case of a validation error or some + I/O failure. Can happen only on cache miss. + + Returns: + (client_type, client_info) tuple, as _loadfile() normally would. + JSON contents is validated only during first load. Cache hits are not + validated. + """ + _SECRET_NAMESPACE = 'oauth2client:secrets#ns' + + if not cache: + return _loadfile(filename) + + obj = cache.get(filename, namespace=_SECRET_NAMESPACE) + if obj is None: + client_type, client_info = _loadfile(filename) + obj = {client_type: client_info} + cache.set(filename, obj, namespace=_SECRET_NAMESPACE) + + return obj.iteritems().next() diff --git a/appengine/dashdemo-cached/oauth2client/crypt.py b/appengine/dashdemo-cached/oauth2client/crypt.py new file mode 100644 index 0000000..d2d7a3b --- /dev/null +++ b/appengine/dashdemo-cached/oauth2client/crypt.py @@ -0,0 +1,396 @@ +#!/usr/bin/python2.4 +# -*- coding: utf-8 -*- +# +# Copyright (C) 2011 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import base64 +import hashlib +import logging +import time + +from anyjson import simplejson + + +CLOCK_SKEW_SECS = 300 # 5 minutes in seconds +AUTH_TOKEN_LIFETIME_SECS = 300 # 5 minutes in seconds +MAX_TOKEN_LIFETIME_SECS = 86400 # 1 day in seconds + + +logger = logging.getLogger(__name__) + + +class AppIdentityError(Exception): + pass + + +try: + from OpenSSL import crypto + + class OpenSSLVerifier(object): + """Verifies the signature on a message.""" + + def __init__(self, pubkey): + """Constructor. + + Args: + pubkey, OpenSSL.crypto.PKey, The public key to verify with. + """ + self._pubkey = pubkey + + def verify(self, message, signature): + """Verifies a message against a signature. + + Args: + message: string, The message to verify. + signature: string, The signature on the message. + + Returns: + True if message was signed by the private key associated with the public + key that this object was constructed with. + """ + try: + crypto.verify(self._pubkey, signature, message, 'sha256') + return True + except: + return False + + @staticmethod + def from_string(key_pem, is_x509_cert): + """Construct a Verified instance from a string. + + Args: + key_pem: string, public key in PEM format. + is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it is + expected to be an RSA key in PEM format. + + Returns: + Verifier instance. + + Raises: + OpenSSL.crypto.Error if the key_pem can't be parsed. + """ + if is_x509_cert: + pubkey = crypto.load_certificate(crypto.FILETYPE_PEM, key_pem) + else: + pubkey = crypto.load_privatekey(crypto.FILETYPE_PEM, key_pem) + return OpenSSLVerifier(pubkey) + + + class OpenSSLSigner(object): + """Signs messages with a private key.""" + + def __init__(self, pkey): + """Constructor. + + Args: + pkey, OpenSSL.crypto.PKey (or equiv), The private key to sign with. + """ + self._key = pkey + + def sign(self, message): + """Signs a message. + + Args: + message: string, Message to be signed. + + Returns: + string, The signature of the message for the given key. + """ + return crypto.sign(self._key, message, 'sha256') + + @staticmethod + def from_string(key, password='notasecret'): + """Construct a Signer instance from a string. + + Args: + key: string, private key in PKCS12 or PEM format. + password: string, password for the private key file. + + Returns: + Signer instance. + + Raises: + OpenSSL.crypto.Error if the key can't be parsed. + """ + parsed_pem_key = _parse_pem_key(key) + if parsed_pem_key: + pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, parsed_pem_key) + else: + pkey = crypto.load_pkcs12(key, password).get_privatekey() + return OpenSSLSigner(pkey) + +except ImportError: + OpenSSLVerifier = None + OpenSSLSigner = None + + +try: + from Crypto.PublicKey import RSA + from Crypto.Hash import SHA256 + from Crypto.Signature import PKCS1_v1_5 + + + class PyCryptoVerifier(object): + """Verifies the signature on a message.""" + + def __init__(self, pubkey): + """Constructor. + + Args: + pubkey, OpenSSL.crypto.PKey (or equiv), The public key to verify with. + """ + self._pubkey = pubkey + + def verify(self, message, signature): + """Verifies a message against a signature. + + Args: + message: string, The message to verify. + signature: string, The signature on the message. + + Returns: + True if message was signed by the private key associated with the public + key that this object was constructed with. + """ + try: + return PKCS1_v1_5.new(self._pubkey).verify( + SHA256.new(message), signature) + except: + return False + + @staticmethod + def from_string(key_pem, is_x509_cert): + """Construct a Verified instance from a string. + + Args: + key_pem: string, public key in PEM format. + is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it is + expected to be an RSA key in PEM format. + + Returns: + Verifier instance. + + Raises: + NotImplementedError if is_x509_cert is true. + """ + if is_x509_cert: + raise NotImplementedError( + 'X509 certs are not supported by the PyCrypto library. ' + 'Try using PyOpenSSL if native code is an option.') + else: + pubkey = RSA.importKey(key_pem) + return PyCryptoVerifier(pubkey) + + + class PyCryptoSigner(object): + """Signs messages with a private key.""" + + def __init__(self, pkey): + """Constructor. + + Args: + pkey, OpenSSL.crypto.PKey (or equiv), The private key to sign with. + """ + self._key = pkey + + def sign(self, message): + """Signs a message. + + Args: + message: string, Message to be signed. + + Returns: + string, The signature of the message for the given key. + """ + return PKCS1_v1_5.new(self._key).sign(SHA256.new(message)) + + @staticmethod + def from_string(key, password='notasecret'): + """Construct a Signer instance from a string. + + Args: + key: string, private key in PEM format. + password: string, password for private key file. Unused for PEM files. + + Returns: + Signer instance. + + Raises: + NotImplementedError if they key isn't in PEM format. + """ + parsed_pem_key = _parse_pem_key(key) + if parsed_pem_key: + pkey = RSA.importKey(parsed_pem_key) + else: + raise NotImplementedError( + 'PKCS12 format is not supported by the PyCrpto library. ' + 'Try converting to a "PEM" ' + '(openssl pkcs12 -in xxxxx.p12 -nodes -nocerts > privatekey.pem) ' + 'or using PyOpenSSL if native code is an option.') + return PyCryptoSigner(pkey) + +except ImportError: + PyCryptoVerifier = None + PyCryptoSigner = None + + +if OpenSSLSigner: + Signer = OpenSSLSigner + Verifier = OpenSSLVerifier +elif PyCryptoSigner: + Signer = PyCryptoSigner + Verifier = PyCryptoVerifier +else: + raise ImportError('No encryption library found. Please install either ' + 'PyOpenSSL, or PyCrypto 2.6 or later') + + +def _parse_pem_key(raw_key_input): + """Identify and extract PEM keys. + + Determines whether the given key is in the format of PEM key, and extracts + the relevant part of the key if it is. + + Args: + raw_key_input: The contents of a private key file (either PEM or PKCS12). + + Returns: + string, The actual key if the contents are from a PEM file, or else None. + """ + offset = raw_key_input.find('-----BEGIN ') + if offset != -1: + return raw_key_input[offset:] + else: + return None + +def _urlsafe_b64encode(raw_bytes): + return base64.urlsafe_b64encode(raw_bytes).rstrip('=') + + +def _urlsafe_b64decode(b64string): + # Guard against unicode strings, which base64 can't handle. + b64string = b64string.encode('ascii') + padded = b64string + '=' * (4 - len(b64string) % 4) + return base64.urlsafe_b64decode(padded) + + +def _json_encode(data): + return simplejson.dumps(data, separators = (',', ':')) + + +def make_signed_jwt(signer, payload): + """Make a signed JWT. + + See http://self-issued.info/docs/draft-jones-json-web-token.html. + + Args: + signer: crypt.Signer, Cryptographic signer. + payload: dict, Dictionary of data to convert to JSON and then sign. + + Returns: + string, The JWT for the payload. + """ + header = {'typ': 'JWT', 'alg': 'RS256'} + + segments = [ + _urlsafe_b64encode(_json_encode(header)), + _urlsafe_b64encode(_json_encode(payload)), + ] + signing_input = '.'.join(segments) + + signature = signer.sign(signing_input) + segments.append(_urlsafe_b64encode(signature)) + + logger.debug(str(segments)) + + return '.'.join(segments) + + +def verify_signed_jwt_with_certs(jwt, certs, audience): + """Verify a JWT against public certs. + + See http://self-issued.info/docs/draft-jones-json-web-token.html. + + Args: + jwt: string, A JWT. + certs: dict, Dictionary where values of public keys in PEM format. + audience: string, The audience, 'aud', that this JWT should contain. If + None then the JWT's 'aud' parameter is not verified. + + Returns: + dict, The deserialized JSON payload in the JWT. + + Raises: + AppIdentityError if any checks are failed. + """ + segments = jwt.split('.') + + if (len(segments) != 3): + raise AppIdentityError( + 'Wrong number of segments in token: %s' % jwt) + signed = '%s.%s' % (segments[0], segments[1]) + + signature = _urlsafe_b64decode(segments[2]) + + # Parse token. + json_body = _urlsafe_b64decode(segments[1]) + try: + parsed = simplejson.loads(json_body) + except: + raise AppIdentityError('Can\'t parse token: %s' % json_body) + + # Check signature. + verified = False + for (keyname, pem) in certs.items(): + verifier = Verifier.from_string(pem, True) + if (verifier.verify(signed, signature)): + verified = True + break + if not verified: + raise AppIdentityError('Invalid token signature: %s' % jwt) + + # Check creation timestamp. + iat = parsed.get('iat') + if iat is None: + raise AppIdentityError('No iat field in token: %s' % json_body) + earliest = iat - CLOCK_SKEW_SECS + + # Check expiration timestamp. + now = long(time.time()) + exp = parsed.get('exp') + if exp is None: + raise AppIdentityError('No exp field in token: %s' % json_body) + if exp >= now + MAX_TOKEN_LIFETIME_SECS: + raise AppIdentityError( + 'exp field too far in future: %s' % json_body) + latest = exp + CLOCK_SKEW_SECS + + if now < earliest: + raise AppIdentityError('Token used too early, %d < %d: %s' % + (now, earliest, json_body)) + if now > latest: + raise AppIdentityError('Token used too late, %d > %d: %s' % + (now, latest, json_body)) + + # Check audience. + if audience is not None: + aud = parsed.get('aud') + if aud is None: + raise AppIdentityError('No aud field in token: %s' % json_body) + if aud != audience: + raise AppIdentityError('Wrong recipient, %s != %s: %s' % + (aud, audience, json_body)) + + return parsed diff --git a/appengine/dashdemo-cached/oauth2client/django_orm.py b/appengine/dashdemo-cached/oauth2client/django_orm.py new file mode 100644 index 0000000..d54d20c --- /dev/null +++ b/appengine/dashdemo-cached/oauth2client/django_orm.py @@ -0,0 +1,134 @@ +# Copyright (C) 2010 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""OAuth 2.0 utilities for Django. + +Utilities for using OAuth 2.0 in conjunction with +the Django datastore. +""" + +__author__ = 'jcgregorio@google.com (Joe Gregorio)' + +import oauth2client +import base64 +import pickle + +from django.db import models +from oauth2client.client import Storage as BaseStorage + +class CredentialsField(models.Field): + + __metaclass__ = models.SubfieldBase + + def __init__(self, *args, **kwargs): + if 'null' not in kwargs: + kwargs['null'] = True + super(CredentialsField, self).__init__(*args, **kwargs) + + def get_internal_type(self): + return "TextField" + + def to_python(self, value): + if value is None: + return None + if isinstance(value, oauth2client.client.Credentials): + return value + return pickle.loads(base64.b64decode(value)) + + def get_db_prep_value(self, value, connection, prepared=False): + if value is None: + return None + return base64.b64encode(pickle.dumps(value)) + + +class FlowField(models.Field): + + __metaclass__ = models.SubfieldBase + + def __init__(self, *args, **kwargs): + if 'null' not in kwargs: + kwargs['null'] = True + super(FlowField, self).__init__(*args, **kwargs) + + def get_internal_type(self): + return "TextField" + + def to_python(self, value): + if value is None: + return None + if isinstance(value, oauth2client.client.Flow): + return value + return pickle.loads(base64.b64decode(value)) + + def get_db_prep_value(self, value, connection, prepared=False): + if value is None: + return None + return base64.b64encode(pickle.dumps(value)) + + +class Storage(BaseStorage): + """Store and retrieve a single credential to and from + the datastore. + + This Storage helper presumes the Credentials + have been stored as a CredenialsField + on a db model class. + """ + + def __init__(self, model_class, key_name, key_value, property_name): + """Constructor for Storage. + + Args: + model: db.Model, model class + key_name: string, key name for the entity that has the credentials + key_value: string, key value for the entity that has the credentials + property_name: string, name of the property that is an CredentialsProperty + """ + self.model_class = model_class + self.key_name = key_name + self.key_value = key_value + self.property_name = property_name + + def locked_get(self): + """Retrieve Credential from datastore. + + Returns: + oauth2client.Credentials + """ + credential = None + + query = {self.key_name: self.key_value} + entities = self.model_class.objects.filter(**query) + if len(entities) > 0: + credential = getattr(entities[0], self.property_name) + if credential and hasattr(credential, 'set_store'): + credential.set_store(self) + return credential + + def locked_put(self, credentials): + """Write a Credentials to the datastore. + + Args: + credentials: Credentials, the credentials to store. + """ + args = {self.key_name: self.key_value} + entity = self.model_class(**args) + setattr(entity, self.property_name, credentials) + entity.save() + + def locked_delete(self): + """Delete Credentials from the datastore.""" + + query = {self.key_name: self.key_value} + entities = self.model_class.objects.filter(**query).delete() diff --git a/appengine/dashdemo-cached/oauth2client/file.py b/appengine/dashdemo-cached/oauth2client/file.py new file mode 100644 index 0000000..1895f94 --- /dev/null +++ b/appengine/dashdemo-cached/oauth2client/file.py @@ -0,0 +1,124 @@ +# Copyright (C) 2010 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Utilities for OAuth. + +Utilities for making it easier to work with OAuth 2.0 +credentials. +""" + +__author__ = 'jcgregorio@google.com (Joe Gregorio)' + +import os +import stat +import threading + +from anyjson import simplejson +from client import Storage as BaseStorage +from client import Credentials + + +class CredentialsFileSymbolicLinkError(Exception): + """Credentials files must not be symbolic links.""" + + +class Storage(BaseStorage): + """Store and retrieve a single credential to and from a file.""" + + def __init__(self, filename): + self._filename = filename + self._lock = threading.Lock() + + def _validate_file(self): + if os.path.islink(self._filename): + raise CredentialsFileSymbolicLinkError( + 'File: %s is a symbolic link.' % self._filename) + + def acquire_lock(self): + """Acquires any lock necessary to access this Storage. + + This lock is not reentrant.""" + self._lock.acquire() + + def release_lock(self): + """Release the Storage lock. + + Trying to release a lock that isn't held will result in a + RuntimeError. + """ + self._lock.release() + + def locked_get(self): + """Retrieve Credential from file. + + Returns: + oauth2client.client.Credentials + + Raises: + CredentialsFileSymbolicLinkError if the file is a symbolic link. + """ + credentials = None + self._validate_file() + try: + f = open(self._filename, 'rb') + content = f.read() + f.close() + except IOError: + return credentials + + try: + credentials = Credentials.new_from_json(content) + credentials.set_store(self) + except ValueError: + pass + + return credentials + + def _create_file_if_needed(self): + """Create an empty file if necessary. + + This method will not initialize the file. Instead it implements a + simple version of "touch" to ensure the file has been created. + """ + if not os.path.exists(self._filename): + old_umask = os.umask(0177) + try: + open(self._filename, 'a+b').close() + finally: + os.umask(old_umask) + + def locked_put(self, credentials): + """Write Credentials to file. + + Args: + credentials: Credentials, the credentials to store. + + Raises: + CredentialsFileSymbolicLinkError if the file is a symbolic link. + """ + + self._create_file_if_needed() + self._validate_file() + f = open(self._filename, 'wb') + f.write(credentials.to_json()) + f.close() + + def locked_delete(self): + """Delete Credentials file. + + Args: + credentials: Credentials, the credentials to store. + """ + + os.unlink(self._filename) diff --git a/appengine/dashdemo-cached/oauth2client/gce.py b/appengine/dashdemo-cached/oauth2client/gce.py new file mode 100644 index 0000000..c7fd7c1 --- /dev/null +++ b/appengine/dashdemo-cached/oauth2client/gce.py @@ -0,0 +1,90 @@ +# Copyright (C) 2012 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Utilities for Google Compute Engine + +Utilities for making it easier to use OAuth 2.0 on Google Compute Engine. +""" + +__author__ = 'jcgregorio@google.com (Joe Gregorio)' + +import httplib2 +import logging +import uritemplate + +from oauth2client import util +from oauth2client.anyjson import simplejson +from oauth2client.client import AccessTokenRefreshError +from oauth2client.client import AssertionCredentials + +logger = logging.getLogger(__name__) + +# URI Template for the endpoint that returns access_tokens. +META = ('http://metadata.google.internal/0.1/meta-data/service-accounts/' + 'default/acquire{?scope}') + + +class AppAssertionCredentials(AssertionCredentials): + """Credentials object for Compute Engine Assertion Grants + + This object will allow a Compute Engine instance to identify itself to + Google and other OAuth 2.0 servers that can verify assertions. It can be used + for the purpose of accessing data stored under an account assigned to the + Compute Engine instance itself. + + This credential does not require a flow to instantiate because it represents + a two legged flow, and therefore has all of the required information to + generate and refresh its own access tokens. + """ + + @util.positional(2) + def __init__(self, scope, **kwargs): + """Constructor for AppAssertionCredentials + + Args: + scope: string or iterable of strings, scope(s) of the credentials being + requested. + """ + self.scope = util.scopes_to_string(scope) + + # Assertion type is no longer used, but still in the parent class signature. + super(AppAssertionCredentials, self).__init__(None) + + @classmethod + def from_json(cls, json): + data = simplejson.loads(json) + return AppAssertionCredentials(data['scope']) + + def _refresh(self, http_request): + """Refreshes the access_token. + + Skip all the storage hoops and just refresh using the API. + + Args: + http_request: callable, a callable that matches the method signature of + httplib2.Http.request, used to make the refresh request. + + Raises: + AccessTokenRefreshError: When the refresh fails. + """ + uri = uritemplate.expand(META, {'scope': self.scope}) + response, content = http_request(uri) + if response.status == 200: + try: + d = simplejson.loads(content) + except StandardError, e: + raise AccessTokenRefreshError(str(e)) + self.access_token = d['accessToken'] + else: + raise AccessTokenRefreshError(content) diff --git a/appengine/dashdemo-cached/oauth2client/keyring_storage.py b/appengine/dashdemo-cached/oauth2client/keyring_storage.py new file mode 100644 index 0000000..efe2949 --- /dev/null +++ b/appengine/dashdemo-cached/oauth2client/keyring_storage.py @@ -0,0 +1,109 @@ +# Copyright (C) 2012 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""A keyring based Storage. + +A Storage for Credentials that uses the keyring module. +""" + +__author__ = 'jcgregorio@google.com (Joe Gregorio)' + +import keyring +import threading + +from client import Storage as BaseStorage +from client import Credentials + + +class Storage(BaseStorage): + """Store and retrieve a single credential to and from the keyring. + + To use this module you must have the keyring module installed. See + . This is an optional module and is not + installed with oauth2client by default because it does not work on all the + platforms that oauth2client supports, such as Google App Engine. + + The keyring module is a cross-platform + library for access the keyring capabilities of the local system. The user will + be prompted for their keyring password when this module is used, and the + manner in which the user is prompted will vary per platform. + + Usage: + from oauth2client.keyring_storage import Storage + + s = Storage('name_of_application', 'user1') + credentials = s.get() + + """ + + def __init__(self, service_name, user_name): + """Constructor. + + Args: + service_name: string, The name of the service under which the credentials + are stored. + user_name: string, The name of the user to store credentials for. + """ + self._service_name = service_name + self._user_name = user_name + self._lock = threading.Lock() + + def acquire_lock(self): + """Acquires any lock necessary to access this Storage. + + This lock is not reentrant.""" + self._lock.acquire() + + def release_lock(self): + """Release the Storage lock. + + Trying to release a lock that isn't held will result in a + RuntimeError. + """ + self._lock.release() + + def locked_get(self): + """Retrieve Credential from file. + + Returns: + oauth2client.client.Credentials + """ + credentials = None + content = keyring.get_password(self._service_name, self._user_name) + + if content is not None: + try: + credentials = Credentials.new_from_json(content) + credentials.set_store(self) + except ValueError: + pass + + return credentials + + def locked_put(self, credentials): + """Write Credentials to file. + + Args: + credentials: Credentials, the credentials to store. + """ + keyring.set_password(self._service_name, self._user_name, + credentials.to_json()) + + def locked_delete(self): + """Delete Credentials file. + + Args: + credentials: Credentials, the credentials to store. + """ + keyring.set_password(self._service_name, self._user_name, '') diff --git a/appengine/dashdemo-cached/oauth2client/locked_file.py b/appengine/dashdemo-cached/oauth2client/locked_file.py new file mode 100644 index 0000000..31514dc --- /dev/null +++ b/appengine/dashdemo-cached/oauth2client/locked_file.py @@ -0,0 +1,373 @@ +# Copyright 2011 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Locked file interface that should work on Unix and Windows pythons. + +This module first tries to use fcntl locking to ensure serialized access +to a file, then falls back on a lock file if that is unavialable. + +Usage: + f = LockedFile('filename', 'r+b', 'rb') + f.open_and_lock() + if f.is_locked(): + print 'Acquired filename with r+b mode' + f.file_handle().write('locked data') + else: + print 'Aquired filename with rb mode' + f.unlock_and_close() +""" + +__author__ = 'cache@google.com (David T McWherter)' + +import errno +import logging +import os +import time + +from oauth2client import util + +logger = logging.getLogger(__name__) + + +class CredentialsFileSymbolicLinkError(Exception): + """Credentials files must not be symbolic links.""" + + +class AlreadyLockedException(Exception): + """Trying to lock a file that has already been locked by the LockedFile.""" + pass + + +def validate_file(filename): + if os.path.islink(filename): + raise CredentialsFileSymbolicLinkError( + 'File: %s is a symbolic link.' % filename) + +class _Opener(object): + """Base class for different locking primitives.""" + + def __init__(self, filename, mode, fallback_mode): + """Create an Opener. + + Args: + filename: string, The pathname of the file. + mode: string, The preferred mode to access the file with. + fallback_mode: string, The mode to use if locking fails. + """ + self._locked = False + self._filename = filename + self._mode = mode + self._fallback_mode = fallback_mode + self._fh = None + + def is_locked(self): + """Was the file locked.""" + return self._locked + + def file_handle(self): + """The file handle to the file. Valid only after opened.""" + return self._fh + + def filename(self): + """The filename that is being locked.""" + return self._filename + + def open_and_lock(self, timeout, delay): + """Open the file and lock it. + + Args: + timeout: float, How long to try to lock for. + delay: float, How long to wait between retries. + """ + pass + + def unlock_and_close(self): + """Unlock and close the file.""" + pass + + +class _PosixOpener(_Opener): + """Lock files using Posix advisory lock files.""" + + def open_and_lock(self, timeout, delay): + """Open the file and lock it. + + Tries to create a .lock file next to the file we're trying to open. + + Args: + timeout: float, How long to try to lock for. + delay: float, How long to wait between retries. + + Raises: + AlreadyLockedException: if the lock is already acquired. + IOError: if the open fails. + CredentialsFileSymbolicLinkError if the file is a symbolic link. + """ + if self._locked: + raise AlreadyLockedException('File %s is already locked' % + self._filename) + self._locked = False + + validate_file(self._filename) + try: + self._fh = open(self._filename, self._mode) + except IOError, e: + # If we can't access with _mode, try _fallback_mode and don't lock. + if e.errno == errno.EACCES: + self._fh = open(self._filename, self._fallback_mode) + return + + lock_filename = self._posix_lockfile(self._filename) + start_time = time.time() + while True: + try: + self._lock_fd = os.open(lock_filename, + os.O_CREAT|os.O_EXCL|os.O_RDWR) + self._locked = True + break + + except OSError, e: + if e.errno != errno.EEXIST: + raise + if (time.time() - start_time) >= timeout: + logger.warn('Could not acquire lock %s in %s seconds' % ( + lock_filename, timeout)) + # Close the file and open in fallback_mode. + if self._fh: + self._fh.close() + self._fh = open(self._filename, self._fallback_mode) + return + time.sleep(delay) + + def unlock_and_close(self): + """Unlock a file by removing the .lock file, and close the handle.""" + if self._locked: + lock_filename = self._posix_lockfile(self._filename) + os.close(self._lock_fd) + os.unlink(lock_filename) + self._locked = False + self._lock_fd = None + if self._fh: + self._fh.close() + + def _posix_lockfile(self, filename): + """The name of the lock file to use for posix locking.""" + return '%s.lock' % filename + + +try: + import fcntl + + class _FcntlOpener(_Opener): + """Open, lock, and unlock a file using fcntl.lockf.""" + + def open_and_lock(self, timeout, delay): + """Open the file and lock it. + + Args: + timeout: float, How long to try to lock for. + delay: float, How long to wait between retries + + Raises: + AlreadyLockedException: if the lock is already acquired. + IOError: if the open fails. + CredentialsFileSymbolicLinkError if the file is a symbolic link. + """ + if self._locked: + raise AlreadyLockedException('File %s is already locked' % + self._filename) + start_time = time.time() + + validate_file(self._filename) + try: + self._fh = open(self._filename, self._mode) + except IOError, e: + # If we can't access with _mode, try _fallback_mode and don't lock. + if e.errno == errno.EACCES: + self._fh = open(self._filename, self._fallback_mode) + return + + # We opened in _mode, try to lock the file. + while True: + try: + fcntl.lockf(self._fh.fileno(), fcntl.LOCK_EX) + self._locked = True + return + except IOError, e: + # If not retrying, then just pass on the error. + if timeout == 0: + raise e + if e.errno != errno.EACCES: + raise e + # We could not acquire the lock. Try again. + if (time.time() - start_time) >= timeout: + logger.warn('Could not lock %s in %s seconds' % ( + self._filename, timeout)) + if self._fh: + self._fh.close() + self._fh = open(self._filename, self._fallback_mode) + return + time.sleep(delay) + + def unlock_and_close(self): + """Close and unlock the file using the fcntl.lockf primitive.""" + if self._locked: + fcntl.lockf(self._fh.fileno(), fcntl.LOCK_UN) + self._locked = False + if self._fh: + self._fh.close() +except ImportError: + _FcntlOpener = None + + +try: + import pywintypes + import win32con + import win32file + + class _Win32Opener(_Opener): + """Open, lock, and unlock a file using windows primitives.""" + + # Error #33: + # 'The process cannot access the file because another process' + FILE_IN_USE_ERROR = 33 + + # Error #158: + # 'The segment is already unlocked.' + FILE_ALREADY_UNLOCKED_ERROR = 158 + + def open_and_lock(self, timeout, delay): + """Open the file and lock it. + + Args: + timeout: float, How long to try to lock for. + delay: float, How long to wait between retries + + Raises: + AlreadyLockedException: if the lock is already acquired. + IOError: if the open fails. + CredentialsFileSymbolicLinkError if the file is a symbolic link. + """ + if self._locked: + raise AlreadyLockedException('File %s is already locked' % + self._filename) + start_time = time.time() + + validate_file(self._filename) + try: + self._fh = open(self._filename, self._mode) + except IOError, e: + # If we can't access with _mode, try _fallback_mode and don't lock. + if e.errno == errno.EACCES: + self._fh = open(self._filename, self._fallback_mode) + return + + # We opened in _mode, try to lock the file. + while True: + try: + hfile = win32file._get_osfhandle(self._fh.fileno()) + win32file.LockFileEx( + hfile, + (win32con.LOCKFILE_FAIL_IMMEDIATELY| + win32con.LOCKFILE_EXCLUSIVE_LOCK), 0, -0x10000, + pywintypes.OVERLAPPED()) + self._locked = True + return + except pywintypes.error, e: + if timeout == 0: + raise e + + # If the error is not that the file is already in use, raise. + if e[0] != _Win32Opener.FILE_IN_USE_ERROR: + raise + + # We could not acquire the lock. Try again. + if (time.time() - start_time) >= timeout: + logger.warn('Could not lock %s in %s seconds' % ( + self._filename, timeout)) + if self._fh: + self._fh.close() + self._fh = open(self._filename, self._fallback_mode) + return + time.sleep(delay) + + def unlock_and_close(self): + """Close and unlock the file using the win32 primitive.""" + if self._locked: + try: + hfile = win32file._get_osfhandle(self._fh.fileno()) + win32file.UnlockFileEx(hfile, 0, -0x10000, pywintypes.OVERLAPPED()) + except pywintypes.error, e: + if e[0] != _Win32Opener.FILE_ALREADY_UNLOCKED_ERROR: + raise + self._locked = False + if self._fh: + self._fh.close() +except ImportError: + _Win32Opener = None + + +class LockedFile(object): + """Represent a file that has exclusive access.""" + + @util.positional(4) + def __init__(self, filename, mode, fallback_mode, use_native_locking=True): + """Construct a LockedFile. + + Args: + filename: string, The path of the file to open. + mode: string, The mode to try to open the file with. + fallback_mode: string, The mode to use if locking fails. + use_native_locking: bool, Whether or not fcntl/win32 locking is used. + """ + opener = None + if not opener and use_native_locking: + if _Win32Opener: + opener = _Win32Opener(filename, mode, fallback_mode) + if _FcntlOpener: + opener = _FcntlOpener(filename, mode, fallback_mode) + + if not opener: + opener = _PosixOpener(filename, mode, fallback_mode) + + self._opener = opener + + def filename(self): + """Return the filename we were constructed with.""" + return self._opener._filename + + def file_handle(self): + """Return the file_handle to the opened file.""" + return self._opener.file_handle() + + def is_locked(self): + """Return whether we successfully locked the file.""" + return self._opener.is_locked() + + def open_and_lock(self, timeout=0, delay=0.05): + """Open the file, trying to lock it. + + Args: + timeout: float, The number of seconds to try to acquire the lock. + delay: float, The number of seconds to wait between retry attempts. + + Raises: + AlreadyLockedException: if the lock is already acquired. + IOError: if the open fails. + """ + self._opener.open_and_lock(timeout, delay) + + def unlock_and_close(self): + """Unlock and close a file.""" + self._opener.unlock_and_close() diff --git a/appengine/dashdemo-cached/oauth2client/multistore_file.py b/appengine/dashdemo-cached/oauth2client/multistore_file.py new file mode 100644 index 0000000..ce7a519 --- /dev/null +++ b/appengine/dashdemo-cached/oauth2client/multistore_file.py @@ -0,0 +1,465 @@ +# Copyright 2011 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Multi-credential file store with lock support. + +This module implements a JSON credential store where multiple +credentials can be stored in one file. That file supports locking +both in a single process and across processes. + +The credential themselves are keyed off of: +* client_id +* user_agent +* scope + +The format of the stored data is like so: +{ + 'file_version': 1, + 'data': [ + { + 'key': { + 'clientId': '', + 'userAgent': '', + 'scope': '' + }, + 'credential': { + # JSON serialized Credentials. + } + } + ] +} +""" + +__author__ = 'jbeda@google.com (Joe Beda)' + +import base64 +import errno +import logging +import os +import threading + +from anyjson import simplejson +from oauth2client.client import Storage as BaseStorage +from oauth2client.client import Credentials +from oauth2client import util +from locked_file import LockedFile + +logger = logging.getLogger(__name__) + +# A dict from 'filename'->_MultiStore instances +_multistores = {} +_multistores_lock = threading.Lock() + + +class Error(Exception): + """Base error for this module.""" + pass + + +class NewerCredentialStoreError(Error): + """The credential store is a newer version that supported.""" + pass + + +@util.positional(4) +def get_credential_storage(filename, client_id, user_agent, scope, + warn_on_readonly=True): + """Get a Storage instance for a credential. + + Args: + filename: The JSON file storing a set of credentials + client_id: The client_id for the credential + user_agent: The user agent for the credential + scope: string or iterable of strings, Scope(s) being requested + warn_on_readonly: if True, log a warning if the store is readonly + + Returns: + An object derived from client.Storage for getting/setting the + credential. + """ + # Recreate the legacy key with these specific parameters + key = {'clientId': client_id, 'userAgent': user_agent, + 'scope': util.scopes_to_string(scope)} + return get_credential_storage_custom_key( + filename, key, warn_on_readonly=warn_on_readonly) + + +@util.positional(2) +def get_credential_storage_custom_string_key( + filename, key_string, warn_on_readonly=True): + """Get a Storage instance for a credential using a single string as a key. + + Allows you to provide a string as a custom key that will be used for + credential storage and retrieval. + + Args: + filename: The JSON file storing a set of credentials + key_string: A string to use as the key for storing this credential. + warn_on_readonly: if True, log a warning if the store is readonly + + Returns: + An object derived from client.Storage for getting/setting the + credential. + """ + # Create a key dictionary that can be used + key_dict = {'key': key_string} + return get_credential_storage_custom_key( + filename, key_dict, warn_on_readonly=warn_on_readonly) + + +@util.positional(2) +def get_credential_storage_custom_key( + filename, key_dict, warn_on_readonly=True): + """Get a Storage instance for a credential using a dictionary as a key. + + Allows you to provide a dictionary as a custom key that will be used for + credential storage and retrieval. + + Args: + filename: The JSON file storing a set of credentials + key_dict: A dictionary to use as the key for storing this credential. There + is no ordering of the keys in the dictionary. Logically equivalent + dictionaries will produce equivalent storage keys. + warn_on_readonly: if True, log a warning if the store is readonly + + Returns: + An object derived from client.Storage for getting/setting the + credential. + """ + multistore = _get_multistore(filename, warn_on_readonly=warn_on_readonly) + key = util.dict_to_tuple_key(key_dict) + return multistore._get_storage(key) + + +@util.positional(1) +def get_all_credential_keys(filename, warn_on_readonly=True): + """Gets all the registered credential keys in the given Multistore. + + Args: + filename: The JSON file storing a set of credentials + warn_on_readonly: if True, log a warning if the store is readonly + + Returns: + A list of the credential keys present in the file. They are returned as + dictionaries that can be passed into get_credential_storage_custom_key to + get the actual credentials. + """ + multistore = _get_multistore(filename, warn_on_readonly=warn_on_readonly) + multistore._lock() + try: + return multistore._get_all_credential_keys() + finally: + multistore._unlock() + + +@util.positional(1) +def _get_multistore(filename, warn_on_readonly=True): + """A helper method to initialize the multistore with proper locking. + + Args: + filename: The JSON file storing a set of credentials + warn_on_readonly: if True, log a warning if the store is readonly + + Returns: + A multistore object + """ + filename = os.path.expanduser(filename) + _multistores_lock.acquire() + try: + multistore = _multistores.setdefault( + filename, _MultiStore(filename, warn_on_readonly=warn_on_readonly)) + finally: + _multistores_lock.release() + return multistore + + +class _MultiStore(object): + """A file backed store for multiple credentials.""" + + @util.positional(2) + def __init__(self, filename, warn_on_readonly=True): + """Initialize the class. + + This will create the file if necessary. + """ + self._file = LockedFile(filename, 'r+b', 'rb') + self._thread_lock = threading.Lock() + self._read_only = False + self._warn_on_readonly = warn_on_readonly + + self._create_file_if_needed() + + # Cache of deserialized store. This is only valid after the + # _MultiStore is locked or _refresh_data_cache is called. This is + # of the form of: + # + # ((key, value), (key, value)...) -> OAuth2Credential + # + # If this is None, then the store hasn't been read yet. + self._data = None + + class _Storage(BaseStorage): + """A Storage object that knows how to read/write a single credential.""" + + def __init__(self, multistore, key): + self._multistore = multistore + self._key = key + + def acquire_lock(self): + """Acquires any lock necessary to access this Storage. + + This lock is not reentrant. + """ + self._multistore._lock() + + def release_lock(self): + """Release the Storage lock. + + Trying to release a lock that isn't held will result in a + RuntimeError. + """ + self._multistore._unlock() + + def locked_get(self): + """Retrieve credential. + + The Storage lock must be held when this is called. + + Returns: + oauth2client.client.Credentials + """ + credential = self._multistore._get_credential(self._key) + if credential: + credential.set_store(self) + return credential + + def locked_put(self, credentials): + """Write a credential. + + The Storage lock must be held when this is called. + + Args: + credentials: Credentials, the credentials to store. + """ + self._multistore._update_credential(self._key, credentials) + + def locked_delete(self): + """Delete a credential. + + The Storage lock must be held when this is called. + + Args: + credentials: Credentials, the credentials to store. + """ + self._multistore._delete_credential(self._key) + + def _create_file_if_needed(self): + """Create an empty file if necessary. + + This method will not initialize the file. Instead it implements a + simple version of "touch" to ensure the file has been created. + """ + if not os.path.exists(self._file.filename()): + old_umask = os.umask(0177) + try: + open(self._file.filename(), 'a+b').close() + finally: + os.umask(old_umask) + + def _lock(self): + """Lock the entire multistore.""" + self._thread_lock.acquire() + self._file.open_and_lock() + if not self._file.is_locked(): + self._read_only = True + if self._warn_on_readonly: + logger.warn('The credentials file (%s) is not writable. Opening in ' + 'read-only mode. Any refreshed credentials will only be ' + 'valid for this run.' % self._file.filename()) + if os.path.getsize(self._file.filename()) == 0: + logger.debug('Initializing empty multistore file') + # The multistore is empty so write out an empty file. + self._data = {} + self._write() + elif not self._read_only or self._data is None: + # Only refresh the data if we are read/write or we haven't + # cached the data yet. If we are readonly, we assume is isn't + # changing out from under us and that we only have to read it + # once. This prevents us from whacking any new access keys that + # we have cached in memory but were unable to write out. + self._refresh_data_cache() + + def _unlock(self): + """Release the lock on the multistore.""" + self._file.unlock_and_close() + self._thread_lock.release() + + def _locked_json_read(self): + """Get the raw content of the multistore file. + + The multistore must be locked when this is called. + + Returns: + The contents of the multistore decoded as JSON. + """ + assert self._thread_lock.locked() + self._file.file_handle().seek(0) + return simplejson.load(self._file.file_handle()) + + def _locked_json_write(self, data): + """Write a JSON serializable data structure to the multistore. + + The multistore must be locked when this is called. + + Args: + data: The data to be serialized and written. + """ + assert self._thread_lock.locked() + if self._read_only: + return + self._file.file_handle().seek(0) + simplejson.dump(data, self._file.file_handle(), sort_keys=True, indent=2) + self._file.file_handle().truncate() + + def _refresh_data_cache(self): + """Refresh the contents of the multistore. + + The multistore must be locked when this is called. + + Raises: + NewerCredentialStoreError: Raised when a newer client has written the + store. + """ + self._data = {} + try: + raw_data = self._locked_json_read() + except Exception: + logger.warn('Credential data store could not be loaded. ' + 'Will ignore and overwrite.') + return + + version = 0 + try: + version = raw_data['file_version'] + except Exception: + logger.warn('Missing version for credential data store. It may be ' + 'corrupt or an old version. Overwriting.') + if version > 1: + raise NewerCredentialStoreError( + 'Credential file has file_version of %d. ' + 'Only file_version of 1 is supported.' % version) + + credentials = [] + try: + credentials = raw_data['data'] + except (TypeError, KeyError): + pass + + for cred_entry in credentials: + try: + (key, credential) = self._decode_credential_from_json(cred_entry) + self._data[key] = credential + except: + # If something goes wrong loading a credential, just ignore it + logger.info('Error decoding credential, skipping', exc_info=True) + + def _decode_credential_from_json(self, cred_entry): + """Load a credential from our JSON serialization. + + Args: + cred_entry: A dict entry from the data member of our format + + Returns: + (key, cred) where the key is the key tuple and the cred is the + OAuth2Credential object. + """ + raw_key = cred_entry['key'] + key = util.dict_to_tuple_key(raw_key) + credential = None + credential = Credentials.new_from_json(simplejson.dumps(cred_entry['credential'])) + return (key, credential) + + def _write(self): + """Write the cached data back out. + + The multistore must be locked. + """ + raw_data = {'file_version': 1} + raw_creds = [] + raw_data['data'] = raw_creds + for (cred_key, cred) in self._data.items(): + raw_key = dict(cred_key) + raw_cred = simplejson.loads(cred.to_json()) + raw_creds.append({'key': raw_key, 'credential': raw_cred}) + self._locked_json_write(raw_data) + + def _get_all_credential_keys(self): + """Gets all the registered credential keys in the multistore. + + Returns: + A list of dictionaries corresponding to all the keys currently registered + """ + return [dict(key) for key in self._data.keys()] + + def _get_credential(self, key): + """Get a credential from the multistore. + + The multistore must be locked. + + Args: + key: The key used to retrieve the credential + + Returns: + The credential specified or None if not present + """ + return self._data.get(key, None) + + def _update_credential(self, key, cred): + """Update a credential and write the multistore. + + This must be called when the multistore is locked. + + Args: + key: The key used to retrieve the credential + cred: The OAuth2Credential to update/set + """ + self._data[key] = cred + self._write() + + def _delete_credential(self, key): + """Delete a credential and write the multistore. + + This must be called when the multistore is locked. + + Args: + key: The key used to retrieve the credential + """ + try: + del self._data[key] + except KeyError: + pass + self._write() + + def _get_storage(self, key): + """Get a Storage object to get/set a credential. + + This Storage is a 'view' into the multistore. + + Args: + key: The key used to retrieve the credential + + Returns: + A Storage object that can be used to get/set this cred + """ + return self._Storage(self, key) diff --git a/appengine/dashdemo-cached/oauth2client/old_run.py b/appengine/dashdemo-cached/oauth2client/old_run.py new file mode 100644 index 0000000..da23358 --- /dev/null +++ b/appengine/dashdemo-cached/oauth2client/old_run.py @@ -0,0 +1,160 @@ +# Copyright (C) 2013 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""This module holds the old run() function which is deprecated, the +tools.run_flow() function should be used in its place.""" + + +import logging +import socket +import sys +import webbrowser + +import gflags + +from oauth2client import client +from oauth2client import util +from tools import ClientRedirectHandler +from tools import ClientRedirectServer + + +FLAGS = gflags.FLAGS + +gflags.DEFINE_boolean('auth_local_webserver', True, + ('Run a local web server to handle redirects during ' + 'OAuth authorization.')) + +gflags.DEFINE_string('auth_host_name', 'localhost', + ('Host name to use when running a local web server to ' + 'handle redirects during OAuth authorization.')) + +gflags.DEFINE_multi_int('auth_host_port', [8080, 8090], + ('Port to use when running a local web server to ' + 'handle redirects during OAuth authorization.')) + + +@util.positional(2) +def run(flow, storage, http=None): + """Core code for a command-line application. + + The run() function is called from your application and runs through all + the steps to obtain credentials. It takes a Flow argument and attempts to + open an authorization server page in the user's default web browser. The + server asks the user to grant your application access to the user's data. + If the user grants access, the run() function returns new credentials. The + new credentials are also stored in the Storage argument, which updates the + file associated with the Storage object. + + It presumes it is run from a command-line application and supports the + following flags: + + --auth_host_name: Host name to use when running a local web server + to handle redirects during OAuth authorization. + (default: 'localhost') + + --auth_host_port: Port to use when running a local web server to handle + redirects during OAuth authorization.; + repeat this option to specify a list of values + (default: '[8080, 8090]') + (an integer) + + --[no]auth_local_webserver: Run a local web server to handle redirects + during OAuth authorization. + (default: 'true') + + Since it uses flags make sure to initialize the gflags module before + calling run(). + + Args: + flow: Flow, an OAuth 2.0 Flow to step through. + storage: Storage, a Storage to store the credential in. + http: An instance of httplib2.Http.request + or something that acts like it. + + Returns: + Credentials, the obtained credential. + """ + logging.warning('This function, oauth2client.tools.run(), and the use of ' + 'the gflags library are deprecated and will be removed in a future ' + 'version of the library.') + if FLAGS.auth_local_webserver: + success = False + port_number = 0 + for port in FLAGS.auth_host_port: + port_number = port + try: + httpd = ClientRedirectServer((FLAGS.auth_host_name, port), + ClientRedirectHandler) + except socket.error, e: + pass + else: + success = True + break + FLAGS.auth_local_webserver = success + if not success: + print 'Failed to start a local webserver listening on either port 8080' + print 'or port 9090. Please check your firewall settings and locally' + print 'running programs that may be blocking or using those ports.' + print + print 'Falling back to --noauth_local_webserver and continuing with', + print 'authorization.' + print + + if FLAGS.auth_local_webserver: + oauth_callback = 'http://%s:%s/' % (FLAGS.auth_host_name, port_number) + else: + oauth_callback = client.OOB_CALLBACK_URN + flow.redirect_uri = oauth_callback + authorize_url = flow.step1_get_authorize_url() + + if FLAGS.auth_local_webserver: + webbrowser.open(authorize_url, new=1, autoraise=True) + print 'Your browser has been opened to visit:' + print + print ' ' + authorize_url + print + print 'If your browser is on a different machine then exit and re-run' + print 'this application with the command-line parameter ' + print + print ' --noauth_local_webserver' + print + else: + print 'Go to the following link in your browser:' + print + print ' ' + authorize_url + print + + code = None + if FLAGS.auth_local_webserver: + httpd.handle_request() + if 'error' in httpd.query_params: + sys.exit('Authentication request was rejected.') + if 'code' in httpd.query_params: + code = httpd.query_params['code'] + else: + print 'Failed to find "code" in the query parameters of the redirect.' + sys.exit('Try running with --noauth_local_webserver.') + else: + code = raw_input('Enter verification code: ').strip() + + try: + credential = flow.step2_exchange(code, http=http) + except client.FlowExchangeError, e: + sys.exit('Authentication has failed: %s' % e) + + storage.put(credential) + credential.set_store(storage) + print 'Authentication successful.' + + return credential diff --git a/appengine/dashdemo-cached/oauth2client/tools.py b/appengine/dashdemo-cached/oauth2client/tools.py new file mode 100644 index 0000000..12c68bf --- /dev/null +++ b/appengine/dashdemo-cached/oauth2client/tools.py @@ -0,0 +1,243 @@ +# Copyright (C) 2013 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Command-line tools for authenticating via OAuth 2.0 + +Do the OAuth 2.0 Web Server dance for a command line application. Stores the +generated credentials in a common file that is used by other example apps in +the same directory. +""" + +__author__ = 'jcgregorio@google.com (Joe Gregorio)' +__all__ = ['argparser', 'run_flow', 'run', 'message_if_missing'] + + +import BaseHTTPServer +import argparse +import httplib2 +import logging +import os +import socket +import sys +import webbrowser + +from oauth2client import client +from oauth2client import file +from oauth2client import util + +try: + from urlparse import parse_qsl +except ImportError: + from cgi import parse_qsl + +_CLIENT_SECRETS_MESSAGE = """WARNING: Please configure OAuth 2.0 + +To make this sample run you will need to populate the client_secrets.json file +found at: + + %s + +with information from the APIs Console . + +""" + +# run_parser is an ArgumentParser that contains command-line options expected +# by tools.run(). Pass it in as part of the 'parents' argument to your own +# ArgumentParser. +argparser = argparse.ArgumentParser(add_help=False) +argparser.add_argument('--auth_host_name', default='localhost', + help='Hostname when running a local web server.') +argparser.add_argument('--noauth_local_webserver', action='store_true', + default=False, help='Do not run a local web server.') +argparser.add_argument('--auth_host_port', default=[8080, 8090], type=int, + nargs='*', help='Port web server should listen on.') +argparser.add_argument('--logging_level', default='ERROR', + choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', + 'CRITICAL'], + help='Set the logging level of detail.') + + +class ClientRedirectServer(BaseHTTPServer.HTTPServer): + """A server to handle OAuth 2.0 redirects back to localhost. + + Waits for a single request and parses the query parameters + into query_params and then stops serving. + """ + query_params = {} + + +class ClientRedirectHandler(BaseHTTPServer.BaseHTTPRequestHandler): + """A handler for OAuth 2.0 redirects back to localhost. + + Waits for a single request and parses the query parameters + into the servers query_params and then stops serving. + """ + + def do_GET(s): + """Handle a GET request. + + Parses the query parameters and prints a message + if the flow has completed. Note that we can't detect + if an error occurred. + """ + s.send_response(200) + s.send_header("Content-type", "text/html") + s.end_headers() + query = s.path.split('?', 1)[-1] + query = dict(parse_qsl(query)) + s.server.query_params = query + s.wfile.write("Authentication Status") + s.wfile.write("

The authentication flow has completed.

") + s.wfile.write("") + + def log_message(self, format, *args): + """Do not log messages to stdout while running as command line program.""" + pass + + +@util.positional(3) +def run_flow(flow, storage, flags, http=None): + """Core code for a command-line application. + + The run() function is called from your application and runs through all the + steps to obtain credentials. It takes a Flow argument and attempts to open an + authorization server page in the user's default web browser. The server asks + the user to grant your application access to the user's data. If the user + grants access, the run() function returns new credentials. The new credentials + are also stored in the Storage argument, which updates the file associated + with the Storage object. + + It presumes it is run from a command-line application and supports the + following flags: + + --auth_host_name: Host name to use when running a local web server + to handle redirects during OAuth authorization. + (default: 'localhost') + + --auth_host_port: Port to use when running a local web server to handle + redirects during OAuth authorization.; + repeat this option to specify a list of values + (default: '[8080, 8090]') + (an integer) + + --[no]auth_local_webserver: Run a local web server to handle redirects + during OAuth authorization. + (default: 'true') + + The tools module defines an ArgumentParser the already contains the flag + definitions that run() requires. You can pass that ArgumentParser to your + ArgumentParser constructor: + + parser = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + parents=[tools.run_parser]) + flags = parser.parse_args(argv) + + Args: + flow: Flow, an OAuth 2.0 Flow to step through. + storage: Storage, a Storage to store the credential in. + flags: argparse.ArgumentParser, the command-line flags. + http: An instance of httplib2.Http.request + or something that acts like it. + + Returns: + Credentials, the obtained credential. + """ + logging.getLogger().setLevel(getattr(logging, flags.logging_level)) + if not flags.noauth_local_webserver: + success = False + port_number = 0 + for port in flags.auth_host_port: + port_number = port + try: + httpd = ClientRedirectServer((flags.auth_host_name, port), + ClientRedirectHandler) + except socket.error, e: + pass + else: + success = True + break + flags.noauth_local_webserver = not success + if not success: + print 'Failed to start a local webserver listening on either port 8080' + print 'or port 9090. Please check your firewall settings and locally' + print 'running programs that may be blocking or using those ports.' + print + print 'Falling back to --noauth_local_webserver and continuing with', + print 'authorization.' + print + + if not flags.noauth_local_webserver: + oauth_callback = 'http://%s:%s/' % (flags.auth_host_name, port_number) + else: + oauth_callback = client.OOB_CALLBACK_URN + flow.redirect_uri = oauth_callback + authorize_url = flow.step1_get_authorize_url() + + if not flags.noauth_local_webserver: + webbrowser.open(authorize_url, new=1, autoraise=True) + print 'Your browser has been opened to visit:' + print + print ' ' + authorize_url + print + print 'If your browser is on a different machine then exit and re-run this' + print 'application with the command-line parameter ' + print + print ' --noauth_local_webserver' + print + else: + print 'Go to the following link in your browser:' + print + print ' ' + authorize_url + print + + code = None + if not flags.noauth_local_webserver: + httpd.handle_request() + if 'error' in httpd.query_params: + sys.exit('Authentication request was rejected.') + if 'code' in httpd.query_params: + code = httpd.query_params['code'] + else: + print 'Failed to find "code" in the query parameters of the redirect.' + sys.exit('Try running with --noauth_local_webserver.') + else: + code = raw_input('Enter verification code: ').strip() + + try: + credential = flow.step2_exchange(code, http=http) + except client.FlowExchangeError, e: + sys.exit('Authentication has failed: %s' % e) + + storage.put(credential) + credential.set_store(storage) + print 'Authentication successful.' + + return credential + + +def message_if_missing(filename): + """Helpful message to display if the CLIENT_SECRETS file is missing.""" + + return _CLIENT_SECRETS_MESSAGE % filename + +try: + from old_run import run + from old_run import FLAGS +except ImportError: + def run(*args, **kwargs): + raise NotImplementedError( + 'The gflags library must be installed to use tools.run(). ' + 'Please install gflags or preferrably switch to using ' + 'tools.run_flow().') diff --git a/appengine/dashdemo-cached/oauth2client/util.py b/appengine/dashdemo-cached/oauth2client/util.py new file mode 100644 index 0000000..90dff15 --- /dev/null +++ b/appengine/dashdemo-cached/oauth2client/util.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python +# +# Copyright 2010 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""Common utility library.""" + +__author__ = ['rafek@google.com (Rafe Kaplan)', + 'guido@google.com (Guido van Rossum)', +] +__all__ = [ + 'positional', + 'POSITIONAL_WARNING', + 'POSITIONAL_EXCEPTION', + 'POSITIONAL_IGNORE', +] + +import inspect +import logging +import types +import urllib +import urlparse + +try: + from urlparse import parse_qsl +except ImportError: + from cgi import parse_qsl + +logger = logging.getLogger(__name__) + +POSITIONAL_WARNING = 'WARNING' +POSITIONAL_EXCEPTION = 'EXCEPTION' +POSITIONAL_IGNORE = 'IGNORE' +POSITIONAL_SET = frozenset([POSITIONAL_WARNING, POSITIONAL_EXCEPTION, + POSITIONAL_IGNORE]) + +positional_parameters_enforcement = POSITIONAL_WARNING + +def positional(max_positional_args): + """A decorator to declare that only the first N arguments my be positional. + + This decorator makes it easy to support Python 3 style key-word only + parameters. For example, in Python 3 it is possible to write: + + def fn(pos1, *, kwonly1=None, kwonly1=None): + ... + + All named parameters after * must be a keyword: + + fn(10, 'kw1', 'kw2') # Raises exception. + fn(10, kwonly1='kw1') # Ok. + + Example: + To define a function like above, do: + + @positional(1) + def fn(pos1, kwonly1=None, kwonly2=None): + ... + + If no default value is provided to a keyword argument, it becomes a required + keyword argument: + + @positional(0) + def fn(required_kw): + ... + + This must be called with the keyword parameter: + + fn() # Raises exception. + fn(10) # Raises exception. + fn(required_kw=10) # Ok. + + When defining instance or class methods always remember to account for + 'self' and 'cls': + + class MyClass(object): + + @positional(2) + def my_method(self, pos1, kwonly1=None): + ... + + @classmethod + @positional(2) + def my_method(cls, pos1, kwonly1=None): + ... + + The positional decorator behavior is controlled by + util.positional_parameters_enforcement, which may be set to + POSITIONAL_EXCEPTION, POSITIONAL_WARNING or POSITIONAL_IGNORE to raise an + exception, log a warning, or do nothing, respectively, if a declaration is + violated. + + Args: + max_positional_arguments: Maximum number of positional arguments. All + parameters after the this index must be keyword only. + + Returns: + A decorator that prevents using arguments after max_positional_args from + being used as positional parameters. + + Raises: + TypeError if a key-word only argument is provided as a positional + parameter, but only if util.positional_parameters_enforcement is set to + POSITIONAL_EXCEPTION. + """ + def positional_decorator(wrapped): + def positional_wrapper(*args, **kwargs): + if len(args) > max_positional_args: + plural_s = '' + if max_positional_args != 1: + plural_s = 's' + message = '%s() takes at most %d positional argument%s (%d given)' % ( + wrapped.__name__, max_positional_args, plural_s, len(args)) + if positional_parameters_enforcement == POSITIONAL_EXCEPTION: + raise TypeError(message) + elif positional_parameters_enforcement == POSITIONAL_WARNING: + logger.warning(message) + else: # IGNORE + pass + return wrapped(*args, **kwargs) + return positional_wrapper + + if isinstance(max_positional_args, (int, long)): + return positional_decorator + else: + args, _, _, defaults = inspect.getargspec(max_positional_args) + return positional(len(args) - len(defaults))(max_positional_args) + + +def scopes_to_string(scopes): + """Converts scope value to a string. + + If scopes is a string then it is simply passed through. If scopes is an + iterable then a string is returned that is all the individual scopes + concatenated with spaces. + + Args: + scopes: string or iterable of strings, the scopes. + + Returns: + The scopes formatted as a single string. + """ + if isinstance(scopes, types.StringTypes): + return scopes + else: + return ' '.join(scopes) + + +def dict_to_tuple_key(dictionary): + """Converts a dictionary to a tuple that can be used as an immutable key. + + The resulting key is always sorted so that logically equivalent dictionaries + always produce an identical tuple for a key. + + Args: + dictionary: the dictionary to use as the key. + + Returns: + A tuple representing the dictionary in it's naturally sorted ordering. + """ + return tuple(sorted(dictionary.items())) + + +def _add_query_parameter(url, name, value): + """Adds a query parameter to a url. + + Replaces the current value if it already exists in the URL. + + Args: + url: string, url to add the query parameter to. + name: string, query parameter name. + value: string, query parameter value. + + Returns: + Updated query parameter. Does not update the url if value is None. + """ + if value is None: + return url + else: + parsed = list(urlparse.urlparse(url)) + q = dict(parse_qsl(parsed[4])) + q[name] = value + parsed[4] = urllib.urlencode(q) + return urlparse.urlunparse(parsed) diff --git a/appengine/dashdemo-cached/oauth2client/xsrfutil.py b/appengine/dashdemo-cached/oauth2client/xsrfutil.py new file mode 100644 index 0000000..7e1fe5c --- /dev/null +++ b/appengine/dashdemo-cached/oauth2client/xsrfutil.py @@ -0,0 +1,113 @@ +#!/usr/bin/python2.5 +# +# Copyright 2010 the Melange authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Helper methods for creating & verifying XSRF tokens.""" + +__authors__ = [ + '"Doug Coker" ', + '"Joe Gregorio" ', +] + + +import base64 +import hmac +import os # for urandom +import time + +from oauth2client import util + + +# Delimiter character +DELIMITER = ':' + +# 1 hour in seconds +DEFAULT_TIMEOUT_SECS = 1*60*60 + +@util.positional(2) +def generate_token(key, user_id, action_id="", when=None): + """Generates a URL-safe token for the given user, action, time tuple. + + Args: + key: secret key to use. + user_id: the user ID of the authenticated user. + action_id: a string identifier of the action they requested + authorization for. + when: the time in seconds since the epoch at which the user was + authorized for this action. If not set the current time is used. + + Returns: + A string XSRF protection token. + """ + when = when or int(time.time()) + digester = hmac.new(key) + digester.update(str(user_id)) + digester.update(DELIMITER) + digester.update(action_id) + digester.update(DELIMITER) + digester.update(str(when)) + digest = digester.digest() + + token = base64.urlsafe_b64encode('%s%s%d' % (digest, + DELIMITER, + when)) + return token + + +@util.positional(3) +def validate_token(key, token, user_id, action_id="", current_time=None): + """Validates that the given token authorizes the user for the action. + + Tokens are invalid if the time of issue is too old or if the token + does not match what generateToken outputs (i.e. the token was forged). + + Args: + key: secret key to use. + token: a string of the token generated by generateToken. + user_id: the user ID of the authenticated user. + action_id: a string identifier of the action they requested + authorization for. + + Returns: + A boolean - True if the user is authorized for the action, False + otherwise. + """ + if not token: + return False + try: + decoded = base64.urlsafe_b64decode(str(token)) + token_time = long(decoded.split(DELIMITER)[-1]) + except (TypeError, ValueError): + return False + if current_time is None: + current_time = time.time() + # If the token is too old it's not valid. + if current_time - token_time > DEFAULT_TIMEOUT_SECS: + return False + + # The given token should match the generated one with the same time. + expected_token = generate_token(key, user_id, action_id=action_id, + when=token_time) + if len(token) != len(expected_token): + return False + + # Perform constant time comparison to avoid timing attacks + different = 0 + for x, y in zip(token, expected_token): + different |= ord(x) ^ ord(y) + if different: + return False + + return True diff --git a/appengine/dashdemo-cached/oauth_utils.py b/appengine/dashdemo-cached/oauth_utils.py new file mode 100644 index 0000000..043c329 --- /dev/null +++ b/appengine/dashdemo-cached/oauth_utils.py @@ -0,0 +1,15 @@ +import os + +from oauth2client.appengine import oauth2decorator_from_clientsecrets + + +# CLIENT_SECRETS, name of a file containing the OAuth 2.0 information for this +# application, including client_id and client_secret, which are found +# on the API Access tab on the Google APIs +# Console +CLIENT_SECRETS = os.path.join(os.path.dirname(__file__), 'client_secrets.json') + + +decorator = oauth2decorator_from_clientsecrets( + CLIENT_SECRETS, + scope='https://www.googleapis.com/auth/bigquery') diff --git a/appengine/dashdemo-cached/uritemplate/__init__.py b/appengine/dashdemo-cached/uritemplate/__init__.py new file mode 100644 index 0000000..5d0ebce --- /dev/null +++ b/appengine/dashdemo-cached/uritemplate/__init__.py @@ -0,0 +1,147 @@ +# Early, and incomplete implementation of -04. +# +import re +import urllib + +RESERVED = ":/?#[]@!$&'()*+,;=" +OPERATOR = "+./;?|!@" +EXPLODE = "*+" +MODIFIER = ":^" +TEMPLATE = re.compile(r"{(?P[\+\./;\?|!@])?(?P[^}]+)}", re.UNICODE) +VAR = re.compile(r"^(?P[^=\+\*:\^]+)((?P[\+\*])|(?P[:\^]-?[0-9]+))?(=(?P.*))?$", re.UNICODE) + +def _tostring(varname, value, explode, operator, safe=""): + if type(value) == type([]): + if explode == "+": + return ",".join([varname + "." + urllib.quote(x, safe) for x in value]) + else: + return ",".join([urllib.quote(x, safe) for x in value]) + if type(value) == type({}): + keys = value.keys() + keys.sort() + if explode == "+": + return ",".join([varname + "." + urllib.quote(key, safe) + "," + urllib.quote(value[key], safe) for key in keys]) + else: + return ",".join([urllib.quote(key, safe) + "," + urllib.quote(value[key], safe) for key in keys]) + else: + return urllib.quote(value, safe) + + +def _tostring_path(varname, value, explode, operator, safe=""): + joiner = operator + if type(value) == type([]): + if explode == "+": + return joiner.join([varname + "." + urllib.quote(x, safe) for x in value]) + elif explode == "*": + return joiner.join([urllib.quote(x, safe) for x in value]) + else: + return ",".join([urllib.quote(x, safe) for x in value]) + elif type(value) == type({}): + keys = value.keys() + keys.sort() + if explode == "+": + return joiner.join([varname + "." + urllib.quote(key, safe) + joiner + urllib.quote(value[key], safe) for key in keys]) + elif explode == "*": + return joiner.join([urllib.quote(key, safe) + joiner + urllib.quote(value[key], safe) for key in keys]) + else: + return ",".join([urllib.quote(key, safe) + "," + urllib.quote(value[key], safe) for key in keys]) + else: + if value: + return urllib.quote(value, safe) + else: + return "" + +def _tostring_query(varname, value, explode, operator, safe=""): + joiner = operator + varprefix = "" + if operator == "?": + joiner = "&" + varprefix = varname + "=" + if type(value) == type([]): + if 0 == len(value): + return "" + if explode == "+": + return joiner.join([varname + "=" + urllib.quote(x, safe) for x in value]) + elif explode == "*": + return joiner.join([urllib.quote(x, safe) for x in value]) + else: + return varprefix + ",".join([urllib.quote(x, safe) for x in value]) + elif type(value) == type({}): + if 0 == len(value): + return "" + keys = value.keys() + keys.sort() + if explode == "+": + return joiner.join([varname + "." + urllib.quote(key, safe) + "=" + urllib.quote(value[key], safe) for key in keys]) + elif explode == "*": + return joiner.join([urllib.quote(key, safe) + "=" + urllib.quote(value[key], safe) for key in keys]) + else: + return varprefix + ",".join([urllib.quote(key, safe) + "," + urllib.quote(value[key], safe) for key in keys]) + else: + if value: + return varname + "=" + urllib.quote(value, safe) + else: + return varname + +TOSTRING = { + "" : _tostring, + "+": _tostring, + ";": _tostring_query, + "?": _tostring_query, + "/": _tostring_path, + ".": _tostring_path, + } + + +def expand(template, vars): + def _sub(match): + groupdict = match.groupdict() + operator = groupdict.get('operator') + if operator is None: + operator = '' + varlist = groupdict.get('varlist') + + safe = "@" + if operator == '+': + safe = RESERVED + varspecs = varlist.split(",") + varnames = [] + defaults = {} + for varspec in varspecs: + m = VAR.search(varspec) + groupdict = m.groupdict() + varname = groupdict.get('varname') + explode = groupdict.get('explode') + partial = groupdict.get('partial') + default = groupdict.get('default') + if default: + defaults[varname] = default + varnames.append((varname, explode, partial)) + + retval = [] + joiner = operator + prefix = operator + if operator == "+": + prefix = "" + joiner = "," + if operator == "?": + joiner = "&" + if operator == "": + joiner = "," + for varname, explode, partial in varnames: + if varname in vars: + value = vars[varname] + #if not value and (type(value) == type({}) or type(value) == type([])) and varname in defaults: + if not value and value != "" and varname in defaults: + value = defaults[varname] + elif varname in defaults: + value = defaults[varname] + else: + continue + retval.append(TOSTRING[operator](varname, value, explode, operator, safe=safe)) + if "".join(retval): + return prefix + joiner.join(retval) + else: + return "" + + return TEMPLATE.sub(_sub, template) diff --git a/appengine/dashdemo/app.yaml b/appengine/dashdemo/app.yaml new file mode 100644 index 0000000..b00c405 --- /dev/null +++ b/appengine/dashdemo/app.yaml @@ -0,0 +1,25 @@ +application: google.com:dashdemo +version: bq +runtime: python27 +api_version: 1 +threadsafe: no + +handlers: +- url: /favicon\.ico + static_files: favicon.ico + upload: favicon\.ico + +- url: .* + script: main.application + +- url: /stats.* + script: google.appengine.ext.appstats.ui.app + +libraries: +- name: django + version: latest +- name: webapp2 + version: latest + +builtins: +- appstats: on diff --git a/appengine/dashdemo/appengine_config.py b/appengine/dashdemo/appengine_config.py new file mode 100644 index 0000000..f7da1e3 --- /dev/null +++ b/appengine/dashdemo/appengine_config.py @@ -0,0 +1,6 @@ +def webapp_add_wsgi_middleware(app): + from google.appengine.ext.appstats import recording + app = recording.appstats_wsgi_middleware(app) + return app + +appstats_SHELL_OK = True diff --git a/appengine/dashdemo/bqclient.py b/appengine/dashdemo/bqclient.py new file mode 100644 index 0000000..41a123b --- /dev/null +++ b/appengine/dashdemo/bqclient.py @@ -0,0 +1,30 @@ +from googleapiclient.discovery import build + +class BigQueryClient(object): + def __init__(self, http, decorator): + """Creates the BigQuery client connection""" + self.service = build('bigquery', 'v2', http=http) + self.decorator = decorator + + def getTableData(self, project, dataset, table): + decorated = self.decorator.http() + return self.service.tables().get(projectId=project, datasetId=dataset, tableId=table).execute(decorated) + + def getLastModTime(self, project, dataset, table): + data = self.getTableData(project, dataset, table) + if data is not None and 'lastModifiedTime' in data: + return data['lastModifiedTime'] + else: + return None + + def Query(self, query, project, timeout_ms=10000): + query_config = { + 'query': query, + 'timeoutMs': timeout_ms + } + decorated = self.decorator.http() + result_json = (self.service.jobs() + .query(projectId=project, + body=query_config) + .execute(decorated)) + return result_json diff --git a/appengine/dashdemo/client_secrets.json b/appengine/dashdemo/client_secrets.json new file mode 100644 index 0000000..921b3ad --- /dev/null +++ b/appengine/dashdemo/client_secrets.json @@ -0,0 +1 @@ +{"web":{"auth_uri":"https://accounts.google.com/o/oauth2/auth","client_secret":"jFfoJcG7Or7jZpchUjxQ4xmz","token_uri":"https://accounts.google.com/o/oauth2/token","client_email":"475473128136-7hs4hg3onoqva4h0d26fh0ptdntra1g1@developer.gserviceaccount.com","redirect_uris":["https://dashdemo.googleplex.com/oauth2callback","http://localhost:8080/oauth2callback"],"client_x509_cert_url":"https://www.googleapis.com/robot/v1/metadata/x509/475473128136-7hs4hg3onoqva4h0d26fh0ptdntra1g1@developer.gserviceaccount.com","client_id":"475473128136-7hs4hg3onoqva4h0d26fh0ptdntra1g1.apps.googleusercontent.com","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","javascript_origins":["https://dashdemo.googleplex.com","http://localhost:8080"]}} \ No newline at end of file diff --git a/appengine/dashdemo/googleapiclient/__init__.py b/appengine/dashdemo/googleapiclient/__init__.py new file mode 100644 index 0000000..fe31691 --- /dev/null +++ b/appengine/dashdemo/googleapiclient/__init__.py @@ -0,0 +1,15 @@ +# Copyright (C) 2012 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "1.2" diff --git a/appengine/dashdemo/googleapiclient/channel.py b/appengine/dashdemo/googleapiclient/channel.py new file mode 100644 index 0000000..265273e --- /dev/null +++ b/appengine/dashdemo/googleapiclient/channel.py @@ -0,0 +1,285 @@ +"""Channel notifications support. + +Classes and functions to support channel subscriptions and notifications +on those channels. + +Notes: + - This code is based on experimental APIs and is subject to change. + - Notification does not do deduplication of notification ids, that's up to + the receiver. + - Storing the Channel between calls is up to the caller. + + +Example setting up a channel: + + # Create a new channel that gets notifications via webhook. + channel = new_webhook_channel("https://example.com/my_web_hook") + + # Store the channel, keyed by 'channel.id'. Store it before calling the + # watch method because notifications may start arriving before the watch + # method returns. + ... + + resp = service.objects().watchAll( + bucket="some_bucket_id", body=channel.body()).execute() + channel.update(resp) + + # Store the channel, keyed by 'channel.id'. Store it after being updated + # since the resource_id value will now be correct, and that's needed to + # stop a subscription. + ... + + +An example Webhook implementation using webapp2. Note that webapp2 puts +headers in a case insensitive dictionary, as headers aren't guaranteed to +always be upper case. + + id = self.request.headers[X_GOOG_CHANNEL_ID] + + # Retrieve the channel by id. + channel = ... + + # Parse notification from the headers, including validating the id. + n = notification_from_headers(channel, self.request.headers) + + # Do app specific stuff with the notification here. + if n.resource_state == 'sync': + # Code to handle sync state. + elif n.resource_state == 'exists': + # Code to handle the exists state. + elif n.resource_state == 'not_exists': + # Code to handle the not exists state. + + +Example of unsubscribing. + + service.channels().stop(channel.body()) +""" + +import datetime +import uuid + +from googleapiclient import errors +from oauth2client import util + + +# The unix time epoch starts at midnight 1970. +EPOCH = datetime.datetime.utcfromtimestamp(0) + +# Map the names of the parameters in the JSON channel description to +# the parameter names we use in the Channel class. +CHANNEL_PARAMS = { + 'address': 'address', + 'id': 'id', + 'expiration': 'expiration', + 'params': 'params', + 'resourceId': 'resource_id', + 'resourceUri': 'resource_uri', + 'type': 'type', + 'token': 'token', + } + +X_GOOG_CHANNEL_ID = 'X-GOOG-CHANNEL-ID' +X_GOOG_MESSAGE_NUMBER = 'X-GOOG-MESSAGE-NUMBER' +X_GOOG_RESOURCE_STATE = 'X-GOOG-RESOURCE-STATE' +X_GOOG_RESOURCE_URI = 'X-GOOG-RESOURCE-URI' +X_GOOG_RESOURCE_ID = 'X-GOOG-RESOURCE-ID' + + +def _upper_header_keys(headers): + new_headers = {} + for k, v in headers.iteritems(): + new_headers[k.upper()] = v + return new_headers + + +class Notification(object): + """A Notification from a Channel. + + Notifications are not usually constructed directly, but are returned + from functions like notification_from_headers(). + + Attributes: + message_number: int, The unique id number of this notification. + state: str, The state of the resource being monitored. + uri: str, The address of the resource being monitored. + resource_id: str, The unique identifier of the version of the resource at + this event. + """ + @util.positional(5) + def __init__(self, message_number, state, resource_uri, resource_id): + """Notification constructor. + + Args: + message_number: int, The unique id number of this notification. + state: str, The state of the resource being monitored. Can be one + of "exists", "not_exists", or "sync". + resource_uri: str, The address of the resource being monitored. + resource_id: str, The identifier of the watched resource. + """ + self.message_number = message_number + self.state = state + self.resource_uri = resource_uri + self.resource_id = resource_id + + +class Channel(object): + """A Channel for notifications. + + Usually not constructed directly, instead it is returned from helper + functions like new_webhook_channel(). + + Attributes: + type: str, The type of delivery mechanism used by this channel. For + example, 'web_hook'. + id: str, A UUID for the channel. + token: str, An arbitrary string associated with the channel that + is delivered to the target address with each event delivered + over this channel. + address: str, The address of the receiving entity where events are + delivered. Specific to the channel type. + expiration: int, The time, in milliseconds from the epoch, when this + channel will expire. + params: dict, A dictionary of string to string, with additional parameters + controlling delivery channel behavior. + resource_id: str, An opaque id that identifies the resource that is + being watched. Stable across different API versions. + resource_uri: str, The canonicalized ID of the watched resource. + """ + + @util.positional(5) + def __init__(self, type, id, token, address, expiration=None, + params=None, resource_id="", resource_uri=""): + """Create a new Channel. + + In user code, this Channel constructor will not typically be called + manually since there are functions for creating channels for each specific + type with a more customized set of arguments to pass. + + Args: + type: str, The type of delivery mechanism used by this channel. For + example, 'web_hook'. + id: str, A UUID for the channel. + token: str, An arbitrary string associated with the channel that + is delivered to the target address with each event delivered + over this channel. + address: str, The address of the receiving entity where events are + delivered. Specific to the channel type. + expiration: int, The time, in milliseconds from the epoch, when this + channel will expire. + params: dict, A dictionary of string to string, with additional parameters + controlling delivery channel behavior. + resource_id: str, An opaque id that identifies the resource that is + being watched. Stable across different API versions. + resource_uri: str, The canonicalized ID of the watched resource. + """ + self.type = type + self.id = id + self.token = token + self.address = address + self.expiration = expiration + self.params = params + self.resource_id = resource_id + self.resource_uri = resource_uri + + def body(self): + """Build a body from the Channel. + + Constructs a dictionary that's appropriate for passing into watch() + methods as the value of body argument. + + Returns: + A dictionary representation of the channel. + """ + result = { + 'id': self.id, + 'token': self.token, + 'type': self.type, + 'address': self.address + } + if self.params: + result['params'] = self.params + if self.resource_id: + result['resourceId'] = self.resource_id + if self.resource_uri: + result['resourceUri'] = self.resource_uri + if self.expiration: + result['expiration'] = self.expiration + + return result + + def update(self, resp): + """Update a channel with information from the response of watch(). + + When a request is sent to watch() a resource, the response returned + from the watch() request is a dictionary with updated channel information, + such as the resource_id, which is needed when stopping a subscription. + + Args: + resp: dict, The response from a watch() method. + """ + for json_name, param_name in CHANNEL_PARAMS.iteritems(): + value = resp.get(json_name) + if value is not None: + setattr(self, param_name, value) + + +def notification_from_headers(channel, headers): + """Parse a notification from the webhook request headers, validate + the notification, and return a Notification object. + + Args: + channel: Channel, The channel that the notification is associated with. + headers: dict, A dictionary like object that contains the request headers + from the webhook HTTP request. + + Returns: + A Notification object. + + Raises: + errors.InvalidNotificationError if the notification is invalid. + ValueError if the X-GOOG-MESSAGE-NUMBER can't be converted to an int. + """ + headers = _upper_header_keys(headers) + channel_id = headers[X_GOOG_CHANNEL_ID] + if channel.id != channel_id: + raise errors.InvalidNotificationError( + 'Channel id mismatch: %s != %s' % (channel.id, channel_id)) + else: + message_number = int(headers[X_GOOG_MESSAGE_NUMBER]) + state = headers[X_GOOG_RESOURCE_STATE] + resource_uri = headers[X_GOOG_RESOURCE_URI] + resource_id = headers[X_GOOG_RESOURCE_ID] + return Notification(message_number, state, resource_uri, resource_id) + + +@util.positional(2) +def new_webhook_channel(url, token=None, expiration=None, params=None): + """Create a new webhook Channel. + + Args: + url: str, URL to post notifications to. + token: str, An arbitrary string associated with the channel that + is delivered to the target address with each notification delivered + over this channel. + expiration: datetime.datetime, A time in the future when the channel + should expire. Can also be None if the subscription should use the + default expiration. Note that different services may have different + limits on how long a subscription lasts. Check the response from the + watch() method to see the value the service has set for an expiration + time. + params: dict, Extra parameters to pass on channel creation. Currently + not used for webhook channels. + """ + expiration_ms = 0 + if expiration: + delta = expiration - EPOCH + expiration_ms = delta.microseconds/1000 + ( + delta.seconds + delta.days*24*3600)*1000 + if expiration_ms < 0: + expiration_ms = 0 + + return Channel('web_hook', str(uuid.uuid4()), + token, url, expiration=expiration_ms, + params=params) + diff --git a/appengine/dashdemo/googleapiclient/discovery.py b/appengine/dashdemo/googleapiclient/discovery.py new file mode 100644 index 0000000..1ed9921 --- /dev/null +++ b/appengine/dashdemo/googleapiclient/discovery.py @@ -0,0 +1,959 @@ +# Copyright (C) 2010 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Client for discovery based APIs. + +A client library for Google's discovery based APIs. +""" + +__author__ = 'jcgregorio@google.com (Joe Gregorio)' +__all__ = [ + 'build', + 'build_from_document', + 'fix_method_name', + 'key2param', + ] + + +# Standard library imports +import copy +from email.mime.multipart import MIMEMultipart +from email.mime.nonmultipart import MIMENonMultipart +import keyword +import logging +import mimetypes +import os +import re +import urllib +import urlparse + +try: + from urlparse import parse_qsl +except ImportError: + from cgi import parse_qsl + +# Third-party imports +import httplib2 +import mimeparse +import uritemplate + +# Local imports +from googleapiclient.errors import HttpError +from googleapiclient.errors import InvalidJsonError +from googleapiclient.errors import MediaUploadSizeError +from googleapiclient.errors import UnacceptableMimeTypeError +from googleapiclient.errors import UnknownApiNameOrVersion +from googleapiclient.errors import UnknownFileType +from googleapiclient.http import HttpRequest +from googleapiclient.http import MediaFileUpload +from googleapiclient.http import MediaUpload +from googleapiclient.model import JsonModel +from googleapiclient.model import MediaModel +from googleapiclient.model import RawModel +from googleapiclient.schema import Schemas +from oauth2client.anyjson import simplejson +from oauth2client.util import _add_query_parameter +from oauth2client.util import positional + + +# The client library requires a version of httplib2 that supports RETRIES. +httplib2.RETRIES = 1 + +logger = logging.getLogger(__name__) + +URITEMPLATE = re.compile('{[^}]*}') +VARNAME = re.compile('[a-zA-Z0-9_-]+') +DISCOVERY_URI = ('https://www.googleapis.com/discovery/v1/apis/' + '{api}/{apiVersion}/rest') +DEFAULT_METHOD_DOC = 'A description of how to use this function' +HTTP_PAYLOAD_METHODS = frozenset(['PUT', 'POST', 'PATCH']) +_MEDIA_SIZE_BIT_SHIFTS = {'KB': 10, 'MB': 20, 'GB': 30, 'TB': 40} +BODY_PARAMETER_DEFAULT_VALUE = { + 'description': 'The request body.', + 'type': 'object', + 'required': True, +} +MEDIA_BODY_PARAMETER_DEFAULT_VALUE = { + 'description': ('The filename of the media request body, or an instance ' + 'of a MediaUpload object.'), + 'type': 'string', + 'required': False, +} + +# Parameters accepted by the stack, but not visible via discovery. +# TODO(dhermes): Remove 'userip' in 'v2'. +STACK_QUERY_PARAMETERS = frozenset(['trace', 'pp', 'userip', 'strict']) +STACK_QUERY_PARAMETER_DEFAULT_VALUE = {'type': 'string', 'location': 'query'} + +# Library-specific reserved words beyond Python keywords. +RESERVED_WORDS = frozenset(['body']) + + +def fix_method_name(name): + """Fix method names to avoid reserved word conflicts. + + Args: + name: string, method name. + + Returns: + The name with a '_' prefixed if the name is a reserved word. + """ + if keyword.iskeyword(name) or name in RESERVED_WORDS: + return name + '_' + else: + return name + + +def key2param(key): + """Converts key names into parameter names. + + For example, converting "max-results" -> "max_results" + + Args: + key: string, the method key name. + + Returns: + A safe method name based on the key name. + """ + result = [] + key = list(key) + if not key[0].isalpha(): + result.append('x') + for c in key: + if c.isalnum(): + result.append(c) + else: + result.append('_') + + return ''.join(result) + + +@positional(2) +def build(serviceName, + version, + http=None, + discoveryServiceUrl=DISCOVERY_URI, + developerKey=None, + model=None, + requestBuilder=HttpRequest): + """Construct a Resource for interacting with an API. + + Construct a Resource object for interacting with an API. The serviceName and + version are the names from the Discovery service. + + Args: + serviceName: string, name of the service. + version: string, the version of the service. + http: httplib2.Http, An instance of httplib2.Http or something that acts + like it that HTTP requests will be made through. + discoveryServiceUrl: string, a URI Template that points to the location of + the discovery service. It should have two parameters {api} and + {apiVersion} that when filled in produce an absolute URI to the discovery + document for that service. + developerKey: string, key obtained from + https://code.google.com/apis/console. + model: googleapiclient.Model, converts to and from the wire format. + requestBuilder: googleapiclient.http.HttpRequest, encapsulator for an HTTP + request. + + Returns: + A Resource object with methods for interacting with the service. + """ + params = { + 'api': serviceName, + 'apiVersion': version + } + + if http is None: + http = httplib2.Http() + + requested_url = uritemplate.expand(discoveryServiceUrl, params) + + # REMOTE_ADDR is defined by the CGI spec [RFC3875] as the environment + # variable that contains the network address of the client sending the + # request. If it exists then add that to the request for the discovery + # document to avoid exceeding the quota on discovery requests. + if 'REMOTE_ADDR' in os.environ: + requested_url = _add_query_parameter(requested_url, 'userIp', + os.environ['REMOTE_ADDR']) + logger.info('URL being requested: %s' % requested_url) + + resp, content = http.request(requested_url) + + if resp.status == 404: + raise UnknownApiNameOrVersion("name: %s version: %s" % (serviceName, + version)) + if resp.status >= 400: + raise HttpError(resp, content, uri=requested_url) + + try: + service = simplejson.loads(content) + except ValueError, e: + logger.error('Failed to parse as JSON: ' + content) + raise InvalidJsonError() + + return build_from_document(content, base=discoveryServiceUrl, http=http, + developerKey=developerKey, model=model, requestBuilder=requestBuilder) + + +@positional(1) +def build_from_document( + service, + base=None, + future=None, + http=None, + developerKey=None, + model=None, + requestBuilder=HttpRequest): + """Create a Resource for interacting with an API. + + Same as `build()`, but constructs the Resource object from a discovery + document that is it given, as opposed to retrieving one over HTTP. + + Args: + service: string or object, the JSON discovery document describing the API. + The value passed in may either be the JSON string or the deserialized + JSON. + base: string, base URI for all HTTP requests, usually the discovery URI. + This parameter is no longer used as rootUrl and servicePath are included + within the discovery document. (deprecated) + future: string, discovery document with future capabilities (deprecated). + http: httplib2.Http, An instance of httplib2.Http or something that acts + like it that HTTP requests will be made through. + developerKey: string, Key for controlling API usage, generated + from the API Console. + model: Model class instance that serializes and de-serializes requests and + responses. + requestBuilder: Takes an http request and packages it up to be executed. + + Returns: + A Resource object with methods for interacting with the service. + """ + + # future is no longer used. + future = {} + + if isinstance(service, basestring): + service = simplejson.loads(service) + base = urlparse.urljoin(service['rootUrl'], service['servicePath']) + schema = Schemas(service) + + if model is None: + features = service.get('features', []) + model = JsonModel('dataWrapper' in features) + return Resource(http=http, baseUrl=base, model=model, + developerKey=developerKey, requestBuilder=requestBuilder, + resourceDesc=service, rootDesc=service, schema=schema) + + +def _cast(value, schema_type): + """Convert value to a string based on JSON Schema type. + + See http://tools.ietf.org/html/draft-zyp-json-schema-03 for more details on + JSON Schema. + + Args: + value: any, the value to convert + schema_type: string, the type that value should be interpreted as + + Returns: + A string representation of 'value' based on the schema_type. + """ + if schema_type == 'string': + if type(value) == type('') or type(value) == type(u''): + return value + else: + return str(value) + elif schema_type == 'integer': + return str(int(value)) + elif schema_type == 'number': + return str(float(value)) + elif schema_type == 'boolean': + return str(bool(value)).lower() + else: + if type(value) == type('') or type(value) == type(u''): + return value + else: + return str(value) + + +def _media_size_to_long(maxSize): + """Convert a string media size, such as 10GB or 3TB into an integer. + + Args: + maxSize: string, size as a string, such as 2MB or 7GB. + + Returns: + The size as an integer value. + """ + if len(maxSize) < 2: + return 0L + units = maxSize[-2:].upper() + bit_shift = _MEDIA_SIZE_BIT_SHIFTS.get(units) + if bit_shift is not None: + return long(maxSize[:-2]) << bit_shift + else: + return long(maxSize) + + +def _media_path_url_from_info(root_desc, path_url): + """Creates an absolute media path URL. + + Constructed using the API root URI and service path from the discovery + document and the relative path for the API method. + + Args: + root_desc: Dictionary; the entire original deserialized discovery document. + path_url: String; the relative URL for the API method. Relative to the API + root, which is specified in the discovery document. + + Returns: + String; the absolute URI for media upload for the API method. + """ + return '%(root)supload/%(service_path)s%(path)s' % { + 'root': root_desc['rootUrl'], + 'service_path': root_desc['servicePath'], + 'path': path_url, + } + + +def _fix_up_parameters(method_desc, root_desc, http_method): + """Updates parameters of an API method with values specific to this library. + + Specifically, adds whatever global parameters are specified by the API to the + parameters for the individual method. Also adds parameters which don't + appear in the discovery document, but are available to all discovery based + APIs (these are listed in STACK_QUERY_PARAMETERS). + + SIDE EFFECTS: This updates the parameters dictionary object in the method + description. + + Args: + method_desc: Dictionary with metadata describing an API method. Value comes + from the dictionary of methods stored in the 'methods' key in the + deserialized discovery document. + root_desc: Dictionary; the entire original deserialized discovery document. + http_method: String; the HTTP method used to call the API method described + in method_desc. + + Returns: + The updated Dictionary stored in the 'parameters' key of the method + description dictionary. + """ + parameters = method_desc.setdefault('parameters', {}) + + # Add in the parameters common to all methods. + for name, description in root_desc.get('parameters', {}).iteritems(): + parameters[name] = description + + # Add in undocumented query parameters. + for name in STACK_QUERY_PARAMETERS: + parameters[name] = STACK_QUERY_PARAMETER_DEFAULT_VALUE.copy() + + # Add 'body' (our own reserved word) to parameters if the method supports + # a request payload. + if http_method in HTTP_PAYLOAD_METHODS and 'request' in method_desc: + body = BODY_PARAMETER_DEFAULT_VALUE.copy() + body.update(method_desc['request']) + parameters['body'] = body + + return parameters + + +def _fix_up_media_upload(method_desc, root_desc, path_url, parameters): + """Updates parameters of API by adding 'media_body' if supported by method. + + SIDE EFFECTS: If the method supports media upload and has a required body, + sets body to be optional (required=False) instead. Also, if there is a + 'mediaUpload' in the method description, adds 'media_upload' key to + parameters. + + Args: + method_desc: Dictionary with metadata describing an API method. Value comes + from the dictionary of methods stored in the 'methods' key in the + deserialized discovery document. + root_desc: Dictionary; the entire original deserialized discovery document. + path_url: String; the relative URL for the API method. Relative to the API + root, which is specified in the discovery document. + parameters: A dictionary describing method parameters for method described + in method_desc. + + Returns: + Triple (accept, max_size, media_path_url) where: + - accept is a list of strings representing what content types are + accepted for media upload. Defaults to empty list if not in the + discovery document. + - max_size is a long representing the max size in bytes allowed for a + media upload. Defaults to 0L if not in the discovery document. + - media_path_url is a String; the absolute URI for media upload for the + API method. Constructed using the API root URI and service path from + the discovery document and the relative path for the API method. If + media upload is not supported, this is None. + """ + media_upload = method_desc.get('mediaUpload', {}) + accept = media_upload.get('accept', []) + max_size = _media_size_to_long(media_upload.get('maxSize', '')) + media_path_url = None + + if media_upload: + media_path_url = _media_path_url_from_info(root_desc, path_url) + parameters['media_body'] = MEDIA_BODY_PARAMETER_DEFAULT_VALUE.copy() + if 'body' in parameters: + parameters['body']['required'] = False + + return accept, max_size, media_path_url + + +def _fix_up_method_description(method_desc, root_desc): + """Updates a method description in a discovery document. + + SIDE EFFECTS: Changes the parameters dictionary in the method description with + extra parameters which are used locally. + + Args: + method_desc: Dictionary with metadata describing an API method. Value comes + from the dictionary of methods stored in the 'methods' key in the + deserialized discovery document. + root_desc: Dictionary; the entire original deserialized discovery document. + + Returns: + Tuple (path_url, http_method, method_id, accept, max_size, media_path_url) + where: + - path_url is a String; the relative URL for the API method. Relative to + the API root, which is specified in the discovery document. + - http_method is a String; the HTTP method used to call the API method + described in the method description. + - method_id is a String; the name of the RPC method associated with the + API method, and is in the method description in the 'id' key. + - accept is a list of strings representing what content types are + accepted for media upload. Defaults to empty list if not in the + discovery document. + - max_size is a long representing the max size in bytes allowed for a + media upload. Defaults to 0L if not in the discovery document. + - media_path_url is a String; the absolute URI for media upload for the + API method. Constructed using the API root URI and service path from + the discovery document and the relative path for the API method. If + media upload is not supported, this is None. + """ + path_url = method_desc['path'] + http_method = method_desc['httpMethod'] + method_id = method_desc['id'] + + parameters = _fix_up_parameters(method_desc, root_desc, http_method) + # Order is important. `_fix_up_media_upload` needs `method_desc` to have a + # 'parameters' key and needs to know if there is a 'body' parameter because it + # also sets a 'media_body' parameter. + accept, max_size, media_path_url = _fix_up_media_upload( + method_desc, root_desc, path_url, parameters) + + return path_url, http_method, method_id, accept, max_size, media_path_url + + +# TODO(dhermes): Convert this class to ResourceMethod and make it callable +class ResourceMethodParameters(object): + """Represents the parameters associated with a method. + + Attributes: + argmap: Map from method parameter name (string) to query parameter name + (string). + required_params: List of required parameters (represented by parameter + name as string). + repeated_params: List of repeated parameters (represented by parameter + name as string). + pattern_params: Map from method parameter name (string) to regular + expression (as a string). If the pattern is set for a parameter, the + value for that parameter must match the regular expression. + query_params: List of parameters (represented by parameter name as string) + that will be used in the query string. + path_params: Set of parameters (represented by parameter name as string) + that will be used in the base URL path. + param_types: Map from method parameter name (string) to parameter type. Type + can be any valid JSON schema type; valid values are 'any', 'array', + 'boolean', 'integer', 'number', 'object', or 'string'. Reference: + http://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.1 + enum_params: Map from method parameter name (string) to list of strings, + where each list of strings is the list of acceptable enum values. + """ + + def __init__(self, method_desc): + """Constructor for ResourceMethodParameters. + + Sets default values and defers to set_parameters to populate. + + Args: + method_desc: Dictionary with metadata describing an API method. Value + comes from the dictionary of methods stored in the 'methods' key in + the deserialized discovery document. + """ + self.argmap = {} + self.required_params = [] + self.repeated_params = [] + self.pattern_params = {} + self.query_params = [] + # TODO(dhermes): Change path_params to a list if the extra URITEMPLATE + # parsing is gotten rid of. + self.path_params = set() + self.param_types = {} + self.enum_params = {} + + self.set_parameters(method_desc) + + def set_parameters(self, method_desc): + """Populates maps and lists based on method description. + + Iterates through each parameter for the method and parses the values from + the parameter dictionary. + + Args: + method_desc: Dictionary with metadata describing an API method. Value + comes from the dictionary of methods stored in the 'methods' key in + the deserialized discovery document. + """ + for arg, desc in method_desc.get('parameters', {}).iteritems(): + param = key2param(arg) + self.argmap[param] = arg + + if desc.get('pattern'): + self.pattern_params[param] = desc['pattern'] + if desc.get('enum'): + self.enum_params[param] = desc['enum'] + if desc.get('required'): + self.required_params.append(param) + if desc.get('repeated'): + self.repeated_params.append(param) + if desc.get('location') == 'query': + self.query_params.append(param) + if desc.get('location') == 'path': + self.path_params.add(param) + self.param_types[param] = desc.get('type', 'string') + + # TODO(dhermes): Determine if this is still necessary. Discovery based APIs + # should have all path parameters already marked with + # 'location: path'. + for match in URITEMPLATE.finditer(method_desc['path']): + for namematch in VARNAME.finditer(match.group(0)): + name = key2param(namematch.group(0)) + self.path_params.add(name) + if name in self.query_params: + self.query_params.remove(name) + + +def createMethod(methodName, methodDesc, rootDesc, schema): + """Creates a method for attaching to a Resource. + + Args: + methodName: string, name of the method to use. + methodDesc: object, fragment of deserialized discovery document that + describes the method. + rootDesc: object, the entire deserialized discovery document. + schema: object, mapping of schema names to schema descriptions. + """ + methodName = fix_method_name(methodName) + (pathUrl, httpMethod, methodId, accept, + maxSize, mediaPathUrl) = _fix_up_method_description(methodDesc, rootDesc) + + parameters = ResourceMethodParameters(methodDesc) + + def method(self, **kwargs): + # Don't bother with doc string, it will be over-written by createMethod. + + for name in kwargs.iterkeys(): + if name not in parameters.argmap: + raise TypeError('Got an unexpected keyword argument "%s"' % name) + + # Remove args that have a value of None. + keys = kwargs.keys() + for name in keys: + if kwargs[name] is None: + del kwargs[name] + + for name in parameters.required_params: + if name not in kwargs: + raise TypeError('Missing required parameter "%s"' % name) + + for name, regex in parameters.pattern_params.iteritems(): + if name in kwargs: + if isinstance(kwargs[name], basestring): + pvalues = [kwargs[name]] + else: + pvalues = kwargs[name] + for pvalue in pvalues: + if re.match(regex, pvalue) is None: + raise TypeError( + 'Parameter "%s" value "%s" does not match the pattern "%s"' % + (name, pvalue, regex)) + + for name, enums in parameters.enum_params.iteritems(): + if name in kwargs: + # We need to handle the case of a repeated enum + # name differently, since we want to handle both + # arg='value' and arg=['value1', 'value2'] + if (name in parameters.repeated_params and + not isinstance(kwargs[name], basestring)): + values = kwargs[name] + else: + values = [kwargs[name]] + for value in values: + if value not in enums: + raise TypeError( + 'Parameter "%s" value "%s" is not an allowed value in "%s"' % + (name, value, str(enums))) + + actual_query_params = {} + actual_path_params = {} + for key, value in kwargs.iteritems(): + to_type = parameters.param_types.get(key, 'string') + # For repeated parameters we cast each member of the list. + if key in parameters.repeated_params and type(value) == type([]): + cast_value = [_cast(x, to_type) for x in value] + else: + cast_value = _cast(value, to_type) + if key in parameters.query_params: + actual_query_params[parameters.argmap[key]] = cast_value + if key in parameters.path_params: + actual_path_params[parameters.argmap[key]] = cast_value + body_value = kwargs.get('body', None) + media_filename = kwargs.get('media_body', None) + + if self._developerKey: + actual_query_params['key'] = self._developerKey + + model = self._model + if methodName.endswith('_media'): + model = MediaModel() + elif 'response' not in methodDesc: + model = RawModel() + + headers = {} + headers, params, query, body = model.request(headers, + actual_path_params, actual_query_params, body_value) + + expanded_url = uritemplate.expand(pathUrl, params) + url = urlparse.urljoin(self._baseUrl, expanded_url + query) + + resumable = None + multipart_boundary = '' + + if media_filename: + # Ensure we end up with a valid MediaUpload object. + if isinstance(media_filename, basestring): + (media_mime_type, encoding) = mimetypes.guess_type(media_filename) + if media_mime_type is None: + raise UnknownFileType(media_filename) + if not mimeparse.best_match([media_mime_type], ','.join(accept)): + raise UnacceptableMimeTypeError(media_mime_type) + media_upload = MediaFileUpload(media_filename, + mimetype=media_mime_type) + elif isinstance(media_filename, MediaUpload): + media_upload = media_filename + else: + raise TypeError('media_filename must be str or MediaUpload.') + + # Check the maxSize + if maxSize > 0 and media_upload.size() > maxSize: + raise MediaUploadSizeError("Media larger than: %s" % maxSize) + + # Use the media path uri for media uploads + expanded_url = uritemplate.expand(mediaPathUrl, params) + url = urlparse.urljoin(self._baseUrl, expanded_url + query) + if media_upload.resumable(): + url = _add_query_parameter(url, 'uploadType', 'resumable') + + if media_upload.resumable(): + # This is all we need to do for resumable, if the body exists it gets + # sent in the first request, otherwise an empty body is sent. + resumable = media_upload + else: + # A non-resumable upload + if body is None: + # This is a simple media upload + headers['content-type'] = media_upload.mimetype() + body = media_upload.getbytes(0, media_upload.size()) + url = _add_query_parameter(url, 'uploadType', 'media') + else: + # This is a multipart/related upload. + msgRoot = MIMEMultipart('related') + # msgRoot should not write out it's own headers + setattr(msgRoot, '_write_headers', lambda self: None) + + # attach the body as one part + msg = MIMENonMultipart(*headers['content-type'].split('/')) + msg.set_payload(body) + msgRoot.attach(msg) + + # attach the media as the second part + msg = MIMENonMultipart(*media_upload.mimetype().split('/')) + msg['Content-Transfer-Encoding'] = 'binary' + + payload = media_upload.getbytes(0, media_upload.size()) + msg.set_payload(payload) + msgRoot.attach(msg) + body = msgRoot.as_string() + + multipart_boundary = msgRoot.get_boundary() + headers['content-type'] = ('multipart/related; ' + 'boundary="%s"') % multipart_boundary + url = _add_query_parameter(url, 'uploadType', 'multipart') + + logger.info('URL being requested: %s' % url) + return self._requestBuilder(self._http, + model.response, + url, + method=httpMethod, + body=body, + headers=headers, + methodId=methodId, + resumable=resumable) + + docs = [methodDesc.get('description', DEFAULT_METHOD_DOC), '\n\n'] + if len(parameters.argmap) > 0: + docs.append('Args:\n') + + # Skip undocumented params and params common to all methods. + skip_parameters = rootDesc.get('parameters', {}).keys() + skip_parameters.extend(STACK_QUERY_PARAMETERS) + + all_args = parameters.argmap.keys() + args_ordered = [key2param(s) for s in methodDesc.get('parameterOrder', [])] + + # Move body to the front of the line. + if 'body' in all_args: + args_ordered.append('body') + + for name in all_args: + if name not in args_ordered: + args_ordered.append(name) + + for arg in args_ordered: + if arg in skip_parameters: + continue + + repeated = '' + if arg in parameters.repeated_params: + repeated = ' (repeated)' + required = '' + if arg in parameters.required_params: + required = ' (required)' + paramdesc = methodDesc['parameters'][parameters.argmap[arg]] + paramdoc = paramdesc.get('description', 'A parameter') + if '$ref' in paramdesc: + docs.append( + (' %s: object, %s%s%s\n The object takes the' + ' form of:\n\n%s\n\n') % (arg, paramdoc, required, repeated, + schema.prettyPrintByName(paramdesc['$ref']))) + else: + paramtype = paramdesc.get('type', 'string') + docs.append(' %s: %s, %s%s%s\n' % (arg, paramtype, paramdoc, required, + repeated)) + enum = paramdesc.get('enum', []) + enumDesc = paramdesc.get('enumDescriptions', []) + if enum and enumDesc: + docs.append(' Allowed values\n') + for (name, desc) in zip(enum, enumDesc): + docs.append(' %s - %s\n' % (name, desc)) + if 'response' in methodDesc: + if methodName.endswith('_media'): + docs.append('\nReturns:\n The media object as a string.\n\n ') + else: + docs.append('\nReturns:\n An object of the form:\n\n ') + docs.append(schema.prettyPrintSchema(methodDesc['response'])) + + setattr(method, '__doc__', ''.join(docs)) + return (methodName, method) + + +def createNextMethod(methodName): + """Creates any _next methods for attaching to a Resource. + + The _next methods allow for easy iteration through list() responses. + + Args: + methodName: string, name of the method to use. + """ + methodName = fix_method_name(methodName) + + def methodNext(self, previous_request, previous_response): + """Retrieves the next page of results. + +Args: + previous_request: The request for the previous page. (required) + previous_response: The response from the request for the previous page. (required) + +Returns: + A request object that you can call 'execute()' on to request the next + page. Returns None if there are no more items in the collection. + """ + # Retrieve nextPageToken from previous_response + # Use as pageToken in previous_request to create new request. + + if 'nextPageToken' not in previous_response: + return None + + request = copy.copy(previous_request) + + pageToken = previous_response['nextPageToken'] + parsed = list(urlparse.urlparse(request.uri)) + q = parse_qsl(parsed[4]) + + # Find and remove old 'pageToken' value from URI + newq = [(key, value) for (key, value) in q if key != 'pageToken'] + newq.append(('pageToken', pageToken)) + parsed[4] = urllib.urlencode(newq) + uri = urlparse.urlunparse(parsed) + + request.uri = uri + + logger.info('URL being requested: %s' % uri) + + return request + + return (methodName, methodNext) + + +class Resource(object): + """A class for interacting with a resource.""" + + def __init__(self, http, baseUrl, model, requestBuilder, developerKey, + resourceDesc, rootDesc, schema): + """Build a Resource from the API description. + + Args: + http: httplib2.Http, Object to make http requests with. + baseUrl: string, base URL for the API. All requests are relative to this + URI. + model: googleapiclient.Model, converts to and from the wire format. + requestBuilder: class or callable that instantiates an + googleapiclient.HttpRequest object. + developerKey: string, key obtained from + https://code.google.com/apis/console + resourceDesc: object, section of deserialized discovery document that + describes a resource. Note that the top level discovery document + is considered a resource. + rootDesc: object, the entire deserialized discovery document. + schema: object, mapping of schema names to schema descriptions. + """ + self._dynamic_attrs = [] + + self._http = http + self._baseUrl = baseUrl + self._model = model + self._developerKey = developerKey + self._requestBuilder = requestBuilder + self._resourceDesc = resourceDesc + self._rootDesc = rootDesc + self._schema = schema + + self._set_service_methods() + + def _set_dynamic_attr(self, attr_name, value): + """Sets an instance attribute and tracks it in a list of dynamic attributes. + + Args: + attr_name: string; The name of the attribute to be set + value: The value being set on the object and tracked in the dynamic cache. + """ + self._dynamic_attrs.append(attr_name) + self.__dict__[attr_name] = value + + def __getstate__(self): + """Trim the state down to something that can be pickled. + + Uses the fact that the instance variable _dynamic_attrs holds attrs that + will be wiped and restored on pickle serialization. + """ + state_dict = copy.copy(self.__dict__) + for dynamic_attr in self._dynamic_attrs: + del state_dict[dynamic_attr] + del state_dict['_dynamic_attrs'] + return state_dict + + def __setstate__(self, state): + """Reconstitute the state of the object from being pickled. + + Uses the fact that the instance variable _dynamic_attrs holds attrs that + will be wiped and restored on pickle serialization. + """ + self.__dict__.update(state) + self._dynamic_attrs = [] + self._set_service_methods() + + def _set_service_methods(self): + self._add_basic_methods(self._resourceDesc, self._rootDesc, self._schema) + self._add_nested_resources(self._resourceDesc, self._rootDesc, self._schema) + self._add_next_methods(self._resourceDesc, self._schema) + + def _add_basic_methods(self, resourceDesc, rootDesc, schema): + # Add basic methods to Resource + if 'methods' in resourceDesc: + for methodName, methodDesc in resourceDesc['methods'].iteritems(): + fixedMethodName, method = createMethod( + methodName, methodDesc, rootDesc, schema) + self._set_dynamic_attr(fixedMethodName, + method.__get__(self, self.__class__)) + # Add in _media methods. The functionality of the attached method will + # change when it sees that the method name ends in _media. + if methodDesc.get('supportsMediaDownload', False): + fixedMethodName, method = createMethod( + methodName + '_media', methodDesc, rootDesc, schema) + self._set_dynamic_attr(fixedMethodName, + method.__get__(self, self.__class__)) + + def _add_nested_resources(self, resourceDesc, rootDesc, schema): + # Add in nested resources + if 'resources' in resourceDesc: + + def createResourceMethod(methodName, methodDesc): + """Create a method on the Resource to access a nested Resource. + + Args: + methodName: string, name of the method to use. + methodDesc: object, fragment of deserialized discovery document that + describes the method. + """ + methodName = fix_method_name(methodName) + + def methodResource(self): + return Resource(http=self._http, baseUrl=self._baseUrl, + model=self._model, developerKey=self._developerKey, + requestBuilder=self._requestBuilder, + resourceDesc=methodDesc, rootDesc=rootDesc, + schema=schema) + + setattr(methodResource, '__doc__', 'A collection resource.') + setattr(methodResource, '__is_resource__', True) + + return (methodName, methodResource) + + for methodName, methodDesc in resourceDesc['resources'].iteritems(): + fixedMethodName, method = createResourceMethod(methodName, methodDesc) + self._set_dynamic_attr(fixedMethodName, + method.__get__(self, self.__class__)) + + def _add_next_methods(self, resourceDesc, schema): + # Add _next() methods + # Look for response bodies in schema that contain nextPageToken, and methods + # that take a pageToken parameter. + if 'methods' in resourceDesc: + for methodName, methodDesc in resourceDesc['methods'].iteritems(): + if 'response' in methodDesc: + responseSchema = methodDesc['response'] + if '$ref' in responseSchema: + responseSchema = schema.get(responseSchema['$ref']) + hasNextPageToken = 'nextPageToken' in responseSchema.get('properties', + {}) + hasPageToken = 'pageToken' in methodDesc.get('parameters', {}) + if hasNextPageToken and hasPageToken: + fixedMethodName, method = createNextMethod(methodName + '_next') + self._set_dynamic_attr(fixedMethodName, + method.__get__(self, self.__class__)) diff --git a/appengine/dashdemo/googleapiclient/errors.py b/appengine/dashdemo/googleapiclient/errors.py new file mode 100644 index 0000000..ef2b161 --- /dev/null +++ b/appengine/dashdemo/googleapiclient/errors.py @@ -0,0 +1,140 @@ +#!/usr/bin/python2.4 +# +# Copyright (C) 2010 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Errors for the library. + +All exceptions defined by the library +should be defined in this file. +""" + +__author__ = 'jcgregorio@google.com (Joe Gregorio)' + + +from oauth2client import util +from oauth2client.anyjson import simplejson + + +class Error(Exception): + """Base error for this module.""" + pass + + +class HttpError(Error): + """HTTP data was invalid or unexpected.""" + + @util.positional(3) + def __init__(self, resp, content, uri=None): + self.resp = resp + self.content = content + self.uri = uri + + def _get_reason(self): + """Calculate the reason for the error from the response content.""" + reason = self.resp.reason + try: + data = simplejson.loads(self.content) + reason = data['error']['message'] + except (ValueError, KeyError): + pass + if reason is None: + reason = '' + return reason + + def __repr__(self): + if self.uri: + return '' % ( + self.resp.status, self.uri, self._get_reason().strip()) + else: + return '' % (self.resp.status, self._get_reason()) + + __str__ = __repr__ + + +class InvalidJsonError(Error): + """The JSON returned could not be parsed.""" + pass + + +class UnknownFileType(Error): + """File type unknown or unexpected.""" + pass + + +class UnknownLinkType(Error): + """Link type unknown or unexpected.""" + pass + + +class UnknownApiNameOrVersion(Error): + """No API with that name and version exists.""" + pass + + +class UnacceptableMimeTypeError(Error): + """That is an unacceptable mimetype for this operation.""" + pass + + +class MediaUploadSizeError(Error): + """Media is larger than the method can accept.""" + pass + + +class ResumableUploadError(HttpError): + """Error occured during resumable upload.""" + pass + + +class InvalidChunkSizeError(Error): + """The given chunksize is not valid.""" + pass + +class InvalidNotificationError(Error): + """The channel Notification is invalid.""" + pass + +class BatchError(HttpError): + """Error occured during batch operations.""" + + @util.positional(2) + def __init__(self, reason, resp=None, content=None): + self.resp = resp + self.content = content + self.reason = reason + + def __repr__(self): + return '' % (self.resp.status, self.reason) + + __str__ = __repr__ + + +class UnexpectedMethodError(Error): + """Exception raised by RequestMockBuilder on unexpected calls.""" + + @util.positional(1) + def __init__(self, methodId=None): + """Constructor for an UnexpectedMethodError.""" + super(UnexpectedMethodError, self).__init__( + 'Received unexpected call %s' % methodId) + + +class UnexpectedBodyError(Error): + """Exception raised by RequestMockBuilder on unexpected bodies.""" + + def __init__(self, expected, provided): + """Constructor for an UnexpectedMethodError.""" + super(UnexpectedBodyError, self).__init__( + 'Expected: [%s] - Provided: [%s]' % (expected, provided)) diff --git a/appengine/dashdemo/googleapiclient/http.py b/appengine/dashdemo/googleapiclient/http.py new file mode 100644 index 0000000..2b65348 --- /dev/null +++ b/appengine/dashdemo/googleapiclient/http.py @@ -0,0 +1,1609 @@ +# Copyright (C) 2012 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Classes to encapsulate a single HTTP request. + +The classes implement a command pattern, with every +object supporting an execute() method that does the +actuall HTTP request. +""" + +__author__ = 'jcgregorio@google.com (Joe Gregorio)' + +import StringIO +import base64 +import copy +import gzip +import httplib2 +import logging +import mimeparse +import mimetypes +import os +import random +import sys +import time +import urllib +import urlparse +import uuid + +from email.generator import Generator +from email.mime.multipart import MIMEMultipart +from email.mime.nonmultipart import MIMENonMultipart +from email.parser import FeedParser +from errors import BatchError +from errors import HttpError +from errors import InvalidChunkSizeError +from errors import ResumableUploadError +from errors import UnexpectedBodyError +from errors import UnexpectedMethodError +from model import JsonModel +from oauth2client import util +from oauth2client.anyjson import simplejson + + +DEFAULT_CHUNK_SIZE = 512*1024 + +MAX_URI_LENGTH = 2048 + + +class MediaUploadProgress(object): + """Status of a resumable upload.""" + + def __init__(self, resumable_progress, total_size): + """Constructor. + + Args: + resumable_progress: int, bytes sent so far. + total_size: int, total bytes in complete upload, or None if the total + upload size isn't known ahead of time. + """ + self.resumable_progress = resumable_progress + self.total_size = total_size + + def progress(self): + """Percent of upload completed, as a float. + + Returns: + the percentage complete as a float, returning 0.0 if the total size of + the upload is unknown. + """ + if self.total_size is not None: + return float(self.resumable_progress) / float(self.total_size) + else: + return 0.0 + + +class MediaDownloadProgress(object): + """Status of a resumable download.""" + + def __init__(self, resumable_progress, total_size): + """Constructor. + + Args: + resumable_progress: int, bytes received so far. + total_size: int, total bytes in complete download. + """ + self.resumable_progress = resumable_progress + self.total_size = total_size + + def progress(self): + """Percent of download completed, as a float. + + Returns: + the percentage complete as a float, returning 0.0 if the total size of + the download is unknown. + """ + if self.total_size is not None: + return float(self.resumable_progress) / float(self.total_size) + else: + return 0.0 + + +class MediaUpload(object): + """Describes a media object to upload. + + Base class that defines the interface of MediaUpload subclasses. + + Note that subclasses of MediaUpload may allow you to control the chunksize + when uploading a media object. It is important to keep the size of the chunk + as large as possible to keep the upload efficient. Other factors may influence + the size of the chunk you use, particularly if you are working in an + environment where individual HTTP requests may have a hardcoded time limit, + such as under certain classes of requests under Google App Engine. + + Streams are io.Base compatible objects that support seek(). Some MediaUpload + subclasses support using streams directly to upload data. Support for + streaming may be indicated by a MediaUpload sub-class and if appropriate for a + platform that stream will be used for uploading the media object. The support + for streaming is indicated by has_stream() returning True. The stream() method + should return an io.Base object that supports seek(). On platforms where the + underlying httplib module supports streaming, for example Python 2.6 and + later, the stream will be passed into the http library which will result in + less memory being used and possibly faster uploads. + + If you need to upload media that can't be uploaded using any of the existing + MediaUpload sub-class then you can sub-class MediaUpload for your particular + needs. + """ + + def chunksize(self): + """Chunk size for resumable uploads. + + Returns: + Chunk size in bytes. + """ + raise NotImplementedError() + + def mimetype(self): + """Mime type of the body. + + Returns: + Mime type. + """ + return 'application/octet-stream' + + def size(self): + """Size of upload. + + Returns: + Size of the body, or None of the size is unknown. + """ + return None + + def resumable(self): + """Whether this upload is resumable. + + Returns: + True if resumable upload or False. + """ + return False + + def getbytes(self, begin, end): + """Get bytes from the media. + + Args: + begin: int, offset from beginning of file. + length: int, number of bytes to read, starting at begin. + + Returns: + A string of bytes read. May be shorter than length if EOF was reached + first. + """ + raise NotImplementedError() + + def has_stream(self): + """Does the underlying upload support a streaming interface. + + Streaming means it is an io.IOBase subclass that supports seek, i.e. + seekable() returns True. + + Returns: + True if the call to stream() will return an instance of a seekable io.Base + subclass. + """ + return False + + def stream(self): + """A stream interface to the data being uploaded. + + Returns: + The returned value is an io.IOBase subclass that supports seek, i.e. + seekable() returns True. + """ + raise NotImplementedError() + + @util.positional(1) + def _to_json(self, strip=None): + """Utility function for creating a JSON representation of a MediaUpload. + + Args: + strip: array, An array of names of members to not include in the JSON. + + Returns: + string, a JSON representation of this instance, suitable to pass to + from_json(). + """ + t = type(self) + d = copy.copy(self.__dict__) + if strip is not None: + for member in strip: + del d[member] + d['_class'] = t.__name__ + d['_module'] = t.__module__ + return simplejson.dumps(d) + + def to_json(self): + """Create a JSON representation of an instance of MediaUpload. + + Returns: + string, a JSON representation of this instance, suitable to pass to + from_json(). + """ + return self._to_json() + + @classmethod + def new_from_json(cls, s): + """Utility class method to instantiate a MediaUpload subclass from a JSON + representation produced by to_json(). + + Args: + s: string, JSON from to_json(). + + Returns: + An instance of the subclass of MediaUpload that was serialized with + to_json(). + """ + data = simplejson.loads(s) + # Find and call the right classmethod from_json() to restore the object. + module = data['_module'] + m = __import__(module, fromlist=module.split('.')[:-1]) + kls = getattr(m, data['_class']) + from_json = getattr(kls, 'from_json') + return from_json(s) + + +class MediaIoBaseUpload(MediaUpload): + """A MediaUpload for a io.Base objects. + + Note that the Python file object is compatible with io.Base and can be used + with this class also. + + fh = io.BytesIO('...Some data to upload...') + media = MediaIoBaseUpload(fh, mimetype='image/png', + chunksize=1024*1024, resumable=True) + farm.animals().insert( + id='cow', + name='cow.png', + media_body=media).execute() + + Depending on the platform you are working on, you may pass -1 as the + chunksize, which indicates that the entire file should be uploaded in a single + request. If the underlying platform supports streams, such as Python 2.6 or + later, then this can be very efficient as it avoids multiple connections, and + also avoids loading the entire file into memory before sending it. Note that + Google App Engine has a 5MB limit on request size, so you should never set + your chunksize larger than 5MB, or to -1. + """ + + @util.positional(3) + def __init__(self, fd, mimetype, chunksize=DEFAULT_CHUNK_SIZE, + resumable=False): + """Constructor. + + Args: + fd: io.Base or file object, The source of the bytes to upload. MUST be + opened in blocking mode, do not use streams opened in non-blocking mode. + The given stream must be seekable, that is, it must be able to call + seek() on fd. + mimetype: string, Mime-type of the file. + chunksize: int, File will be uploaded in chunks of this many bytes. Only + used if resumable=True. Pass in a value of -1 if the file is to be + uploaded as a single chunk. Note that Google App Engine has a 5MB limit + on request size, so you should never set your chunksize larger than 5MB, + or to -1. + resumable: bool, True if this is a resumable upload. False means upload + in a single request. + """ + super(MediaIoBaseUpload, self).__init__() + self._fd = fd + self._mimetype = mimetype + if not (chunksize == -1 or chunksize > 0): + raise InvalidChunkSizeError() + self._chunksize = chunksize + self._resumable = resumable + + self._fd.seek(0, os.SEEK_END) + self._size = self._fd.tell() + + def chunksize(self): + """Chunk size for resumable uploads. + + Returns: + Chunk size in bytes. + """ + return self._chunksize + + def mimetype(self): + """Mime type of the body. + + Returns: + Mime type. + """ + return self._mimetype + + def size(self): + """Size of upload. + + Returns: + Size of the body, or None of the size is unknown. + """ + return self._size + + def resumable(self): + """Whether this upload is resumable. + + Returns: + True if resumable upload or False. + """ + return self._resumable + + def getbytes(self, begin, length): + """Get bytes from the media. + + Args: + begin: int, offset from beginning of file. + length: int, number of bytes to read, starting at begin. + + Returns: + A string of bytes read. May be shorted than length if EOF was reached + first. + """ + self._fd.seek(begin) + return self._fd.read(length) + + def has_stream(self): + """Does the underlying upload support a streaming interface. + + Streaming means it is an io.IOBase subclass that supports seek, i.e. + seekable() returns True. + + Returns: + True if the call to stream() will return an instance of a seekable io.Base + subclass. + """ + return True + + def stream(self): + """A stream interface to the data being uploaded. + + Returns: + The returned value is an io.IOBase subclass that supports seek, i.e. + seekable() returns True. + """ + return self._fd + + def to_json(self): + """This upload type is not serializable.""" + raise NotImplementedError('MediaIoBaseUpload is not serializable.') + + +class MediaFileUpload(MediaIoBaseUpload): + """A MediaUpload for a file. + + Construct a MediaFileUpload and pass as the media_body parameter of the + method. For example, if we had a service that allowed uploading images: + + + media = MediaFileUpload('cow.png', mimetype='image/png', + chunksize=1024*1024, resumable=True) + farm.animals().insert( + id='cow', + name='cow.png', + media_body=media).execute() + + Depending on the platform you are working on, you may pass -1 as the + chunksize, which indicates that the entire file should be uploaded in a single + request. If the underlying platform supports streams, such as Python 2.6 or + later, then this can be very efficient as it avoids multiple connections, and + also avoids loading the entire file into memory before sending it. Note that + Google App Engine has a 5MB limit on request size, so you should never set + your chunksize larger than 5MB, or to -1. + """ + + @util.positional(2) + def __init__(self, filename, mimetype=None, chunksize=DEFAULT_CHUNK_SIZE, + resumable=False): + """Constructor. + + Args: + filename: string, Name of the file. + mimetype: string, Mime-type of the file. If None then a mime-type will be + guessed from the file extension. + chunksize: int, File will be uploaded in chunks of this many bytes. Only + used if resumable=True. Pass in a value of -1 if the file is to be + uploaded in a single chunk. Note that Google App Engine has a 5MB limit + on request size, so you should never set your chunksize larger than 5MB, + or to -1. + resumable: bool, True if this is a resumable upload. False means upload + in a single request. + """ + self._filename = filename + fd = open(self._filename, 'rb') + if mimetype is None: + (mimetype, encoding) = mimetypes.guess_type(filename) + super(MediaFileUpload, self).__init__(fd, mimetype, chunksize=chunksize, + resumable=resumable) + + def to_json(self): + """Creating a JSON representation of an instance of MediaFileUpload. + + Returns: + string, a JSON representation of this instance, suitable to pass to + from_json(). + """ + return self._to_json(strip=['_fd']) + + @staticmethod + def from_json(s): + d = simplejson.loads(s) + return MediaFileUpload(d['_filename'], mimetype=d['_mimetype'], + chunksize=d['_chunksize'], resumable=d['_resumable']) + + +class MediaInMemoryUpload(MediaIoBaseUpload): + """MediaUpload for a chunk of bytes. + + DEPRECATED: Use MediaIoBaseUpload with either io.TextIOBase or StringIO for + the stream. + """ + + @util.positional(2) + def __init__(self, body, mimetype='application/octet-stream', + chunksize=DEFAULT_CHUNK_SIZE, resumable=False): + """Create a new MediaInMemoryUpload. + + DEPRECATED: Use MediaIoBaseUpload with either io.TextIOBase or StringIO for + the stream. + + Args: + body: string, Bytes of body content. + mimetype: string, Mime-type of the file or default of + 'application/octet-stream'. + chunksize: int, File will be uploaded in chunks of this many bytes. Only + used if resumable=True. + resumable: bool, True if this is a resumable upload. False means upload + in a single request. + """ + fd = StringIO.StringIO(body) + super(MediaInMemoryUpload, self).__init__(fd, mimetype, chunksize=chunksize, + resumable=resumable) + + +class MediaIoBaseDownload(object): + """"Download media resources. + + Note that the Python file object is compatible with io.Base and can be used + with this class also. + + + Example: + request = farms.animals().get_media(id='cow') + fh = io.FileIO('cow.png', mode='wb') + downloader = MediaIoBaseDownload(fh, request, chunksize=1024*1024) + + done = False + while done is False: + status, done = downloader.next_chunk() + if status: + print "Download %d%%." % int(status.progress() * 100) + print "Download Complete!" + """ + + @util.positional(3) + def __init__(self, fd, request, chunksize=DEFAULT_CHUNK_SIZE): + """Constructor. + + Args: + fd: io.Base or file object, The stream in which to write the downloaded + bytes. + request: googleapiclient.http.HttpRequest, the media request to perform in + chunks. + chunksize: int, File will be downloaded in chunks of this many bytes. + """ + self._fd = fd + self._request = request + self._uri = request.uri + self._chunksize = chunksize + self._progress = 0 + self._total_size = None + self._done = False + + # Stubs for testing. + self._sleep = time.sleep + self._rand = random.random + + @util.positional(1) + def next_chunk(self, num_retries=0): + """Get the next chunk of the download. + + Args: + num_retries: Integer, number of times to retry 500's with randomized + exponential backoff. If all retries fail, the raised HttpError + represents the last request. If zero (default), we attempt the + request only once. + + Returns: + (status, done): (MediaDownloadStatus, boolean) + The value of 'done' will be True when the media has been fully + downloaded. + + Raises: + googleapiclient.errors.HttpError if the response was not a 2xx. + httplib2.HttpLib2Error if a transport error has occured. + """ + headers = { + 'range': 'bytes=%d-%d' % ( + self._progress, self._progress + self._chunksize) + } + http = self._request.http + + for retry_num in xrange(num_retries + 1): + if retry_num > 0: + self._sleep(self._rand() * 2**retry_num) + logging.warning( + 'Retry #%d for media download: GET %s, following status: %d' + % (retry_num, self._uri, resp.status)) + + resp, content = http.request(self._uri, headers=headers) + if resp.status < 500: + break + + if resp.status in [200, 206]: + if 'content-location' in resp and resp['content-location'] != self._uri: + self._uri = resp['content-location'] + self._progress += len(content) + self._fd.write(content) + + if 'content-range' in resp: + content_range = resp['content-range'] + length = content_range.rsplit('/', 1)[1] + self._total_size = int(length) + + if self._progress == self._total_size: + self._done = True + return MediaDownloadProgress(self._progress, self._total_size), self._done + else: + raise HttpError(resp, content, uri=self._uri) + + +class _StreamSlice(object): + """Truncated stream. + + Takes a stream and presents a stream that is a slice of the original stream. + This is used when uploading media in chunks. In later versions of Python a + stream can be passed to httplib in place of the string of data to send. The + problem is that httplib just blindly reads to the end of the stream. This + wrapper presents a virtual stream that only reads to the end of the chunk. + """ + + def __init__(self, stream, begin, chunksize): + """Constructor. + + Args: + stream: (io.Base, file object), the stream to wrap. + begin: int, the seek position the chunk begins at. + chunksize: int, the size of the chunk. + """ + self._stream = stream + self._begin = begin + self._chunksize = chunksize + self._stream.seek(begin) + + def read(self, n=-1): + """Read n bytes. + + Args: + n, int, the number of bytes to read. + + Returns: + A string of length 'n', or less if EOF is reached. + """ + # The data left available to read sits in [cur, end) + cur = self._stream.tell() + end = self._begin + self._chunksize + if n == -1 or cur + n > end: + n = end - cur + return self._stream.read(n) + + +class HttpRequest(object): + """Encapsulates a single HTTP request.""" + + @util.positional(4) + def __init__(self, http, postproc, uri, + method='GET', + body=None, + headers=None, + methodId=None, + resumable=None): + """Constructor for an HttpRequest. + + Args: + http: httplib2.Http, the transport object to use to make a request + postproc: callable, called on the HTTP response and content to transform + it into a data object before returning, or raising an exception + on an error. + uri: string, the absolute URI to send the request to + method: string, the HTTP method to use + body: string, the request body of the HTTP request, + headers: dict, the HTTP request headers + methodId: string, a unique identifier for the API method being called. + resumable: MediaUpload, None if this is not a resumbale request. + """ + self.uri = uri + self.method = method + self.body = body + self.headers = headers or {} + self.methodId = methodId + self.http = http + self.postproc = postproc + self.resumable = resumable + self.response_callbacks = [] + self._in_error_state = False + + # Pull the multipart boundary out of the content-type header. + major, minor, params = mimeparse.parse_mime_type( + headers.get('content-type', 'application/json')) + + # The size of the non-media part of the request. + self.body_size = len(self.body or '') + + # The resumable URI to send chunks to. + self.resumable_uri = None + + # The bytes that have been uploaded. + self.resumable_progress = 0 + + # Stubs for testing. + self._rand = random.random + self._sleep = time.sleep + + @util.positional(1) + def execute(self, http=None, num_retries=0): + """Execute the request. + + Args: + http: httplib2.Http, an http object to be used in place of the + one the HttpRequest request object was constructed with. + num_retries: Integer, number of times to retry 500's with randomized + exponential backoff. If all retries fail, the raised HttpError + represents the last request. If zero (default), we attempt the + request only once. + + Returns: + A deserialized object model of the response body as determined + by the postproc. + + Raises: + googleapiclient.errors.HttpError if the response was not a 2xx. + httplib2.HttpLib2Error if a transport error has occured. + """ + if http is None: + http = self.http + + if self.resumable: + body = None + while body is None: + _, body = self.next_chunk(http=http, num_retries=num_retries) + return body + + # Non-resumable case. + + if 'content-length' not in self.headers: + self.headers['content-length'] = str(self.body_size) + # If the request URI is too long then turn it into a POST request. + if len(self.uri) > MAX_URI_LENGTH and self.method == 'GET': + self.method = 'POST' + self.headers['x-http-method-override'] = 'GET' + self.headers['content-type'] = 'application/x-www-form-urlencoded' + parsed = urlparse.urlparse(self.uri) + self.uri = urlparse.urlunparse( + (parsed.scheme, parsed.netloc, parsed.path, parsed.params, None, + None) + ) + self.body = parsed.query + self.headers['content-length'] = str(len(self.body)) + + # Handle retries for server-side errors. + for retry_num in xrange(num_retries + 1): + if retry_num > 0: + self._sleep(self._rand() * 2**retry_num) + logging.warning('Retry #%d for request: %s %s, following status: %d' + % (retry_num, self.method, self.uri, resp.status)) + + resp, content = http.request(str(self.uri), method=str(self.method), + body=self.body, headers=self.headers) + if resp.status < 500: + break + + for callback in self.response_callbacks: + callback(resp) + if resp.status >= 300: + raise HttpError(resp, content, uri=self.uri) + return self.postproc(resp, content) + + @util.positional(2) + def add_response_callback(self, cb): + """add_response_headers_callback + + Args: + cb: Callback to be called on receiving the response headers, of signature: + + def cb(resp): + # Where resp is an instance of httplib2.Response + """ + self.response_callbacks.append(cb) + + @util.positional(1) + def next_chunk(self, http=None, num_retries=0): + """Execute the next step of a resumable upload. + + Can only be used if the method being executed supports media uploads and + the MediaUpload object passed in was flagged as using resumable upload. + + Example: + + media = MediaFileUpload('cow.png', mimetype='image/png', + chunksize=1000, resumable=True) + request = farm.animals().insert( + id='cow', + name='cow.png', + media_body=media) + + response = None + while response is None: + status, response = request.next_chunk() + if status: + print "Upload %d%% complete." % int(status.progress() * 100) + + + Args: + http: httplib2.Http, an http object to be used in place of the + one the HttpRequest request object was constructed with. + num_retries: Integer, number of times to retry 500's with randomized + exponential backoff. If all retries fail, the raised HttpError + represents the last request. If zero (default), we attempt the + request only once. + + Returns: + (status, body): (ResumableMediaStatus, object) + The body will be None until the resumable media is fully uploaded. + + Raises: + googleapiclient.errors.HttpError if the response was not a 2xx. + httplib2.HttpLib2Error if a transport error has occured. + """ + if http is None: + http = self.http + + if self.resumable.size() is None: + size = '*' + else: + size = str(self.resumable.size()) + + if self.resumable_uri is None: + start_headers = copy.copy(self.headers) + start_headers['X-Upload-Content-Type'] = self.resumable.mimetype() + if size != '*': + start_headers['X-Upload-Content-Length'] = size + start_headers['content-length'] = str(self.body_size) + + for retry_num in xrange(num_retries + 1): + if retry_num > 0: + self._sleep(self._rand() * 2**retry_num) + logging.warning( + 'Retry #%d for resumable URI request: %s %s, following status: %d' + % (retry_num, self.method, self.uri, resp.status)) + + resp, content = http.request(self.uri, method=self.method, + body=self.body, + headers=start_headers) + if resp.status < 500: + break + + if resp.status == 200 and 'location' in resp: + self.resumable_uri = resp['location'] + else: + raise ResumableUploadError(resp, content) + elif self._in_error_state: + # If we are in an error state then query the server for current state of + # the upload by sending an empty PUT and reading the 'range' header in + # the response. + headers = { + 'Content-Range': 'bytes */%s' % size, + 'content-length': '0' + } + resp, content = http.request(self.resumable_uri, 'PUT', + headers=headers) + status, body = self._process_response(resp, content) + if body: + # The upload was complete. + return (status, body) + + # The httplib.request method can take streams for the body parameter, but + # only in Python 2.6 or later. If a stream is available under those + # conditions then use it as the body argument. + if self.resumable.has_stream() and sys.version_info[1] >= 6: + data = self.resumable.stream() + if self.resumable.chunksize() == -1: + data.seek(self.resumable_progress) + chunk_end = self.resumable.size() - self.resumable_progress - 1 + else: + # Doing chunking with a stream, so wrap a slice of the stream. + data = _StreamSlice(data, self.resumable_progress, + self.resumable.chunksize()) + chunk_end = min( + self.resumable_progress + self.resumable.chunksize() - 1, + self.resumable.size() - 1) + else: + data = self.resumable.getbytes( + self.resumable_progress, self.resumable.chunksize()) + + # A short read implies that we are at EOF, so finish the upload. + if len(data) < self.resumable.chunksize(): + size = str(self.resumable_progress + len(data)) + + chunk_end = self.resumable_progress + len(data) - 1 + + headers = { + 'Content-Range': 'bytes %d-%d/%s' % ( + self.resumable_progress, chunk_end, size), + # Must set the content-length header here because httplib can't + # calculate the size when working with _StreamSlice. + 'Content-Length': str(chunk_end - self.resumable_progress + 1) + } + + for retry_num in xrange(num_retries + 1): + if retry_num > 0: + self._sleep(self._rand() * 2**retry_num) + logging.warning( + 'Retry #%d for media upload: %s %s, following status: %d' + % (retry_num, self.method, self.uri, resp.status)) + + try: + resp, content = http.request(self.resumable_uri, method='PUT', + body=data, + headers=headers) + except: + self._in_error_state = True + raise + if resp.status < 500: + break + + return self._process_response(resp, content) + + def _process_response(self, resp, content): + """Process the response from a single chunk upload. + + Args: + resp: httplib2.Response, the response object. + content: string, the content of the response. + + Returns: + (status, body): (ResumableMediaStatus, object) + The body will be None until the resumable media is fully uploaded. + + Raises: + googleapiclient.errors.HttpError if the response was not a 2xx or a 308. + """ + if resp.status in [200, 201]: + self._in_error_state = False + return None, self.postproc(resp, content) + elif resp.status == 308: + self._in_error_state = False + # A "308 Resume Incomplete" indicates we are not done. + self.resumable_progress = int(resp['range'].split('-')[1]) + 1 + if 'location' in resp: + self.resumable_uri = resp['location'] + else: + self._in_error_state = True + raise HttpError(resp, content, uri=self.uri) + + return (MediaUploadProgress(self.resumable_progress, self.resumable.size()), + None) + + def to_json(self): + """Returns a JSON representation of the HttpRequest.""" + d = copy.copy(self.__dict__) + if d['resumable'] is not None: + d['resumable'] = self.resumable.to_json() + del d['http'] + del d['postproc'] + del d['_sleep'] + del d['_rand'] + + return simplejson.dumps(d) + + @staticmethod + def from_json(s, http, postproc): + """Returns an HttpRequest populated with info from a JSON object.""" + d = simplejson.loads(s) + if d['resumable'] is not None: + d['resumable'] = MediaUpload.new_from_json(d['resumable']) + return HttpRequest( + http, + postproc, + uri=d['uri'], + method=d['method'], + body=d['body'], + headers=d['headers'], + methodId=d['methodId'], + resumable=d['resumable']) + + +class BatchHttpRequest(object): + """Batches multiple HttpRequest objects into a single HTTP request. + + Example: + from googleapiclient.http import BatchHttpRequest + + def list_animals(request_id, response, exception): + \"\"\"Do something with the animals list response.\"\"\" + if exception is not None: + # Do something with the exception. + pass + else: + # Do something with the response. + pass + + def list_farmers(request_id, response, exception): + \"\"\"Do something with the farmers list response.\"\"\" + if exception is not None: + # Do something with the exception. + pass + else: + # Do something with the response. + pass + + service = build('farm', 'v2') + + batch = BatchHttpRequest() + + batch.add(service.animals().list(), list_animals) + batch.add(service.farmers().list(), list_farmers) + batch.execute(http=http) + """ + + @util.positional(1) + def __init__(self, callback=None, batch_uri=None): + """Constructor for a BatchHttpRequest. + + Args: + callback: callable, A callback to be called for each response, of the + form callback(id, response, exception). The first parameter is the + request id, and the second is the deserialized response object. The + third is an googleapiclient.errors.HttpError exception object if an HTTP error + occurred while processing the request, or None if no error occurred. + batch_uri: string, URI to send batch requests to. + """ + if batch_uri is None: + batch_uri = 'https://www.googleapis.com/batch' + self._batch_uri = batch_uri + + # Global callback to be called for each individual response in the batch. + self._callback = callback + + # A map from id to request. + self._requests = {} + + # A map from id to callback. + self._callbacks = {} + + # List of request ids, in the order in which they were added. + self._order = [] + + # The last auto generated id. + self._last_auto_id = 0 + + # Unique ID on which to base the Content-ID headers. + self._base_id = None + + # A map from request id to (httplib2.Response, content) response pairs + self._responses = {} + + # A map of id(Credentials) that have been refreshed. + self._refreshed_credentials = {} + + def _refresh_and_apply_credentials(self, request, http): + """Refresh the credentials and apply to the request. + + Args: + request: HttpRequest, the request. + http: httplib2.Http, the global http object for the batch. + """ + # For the credentials to refresh, but only once per refresh_token + # If there is no http per the request then refresh the http passed in + # via execute() + creds = None + if request.http is not None and hasattr(request.http.request, + 'credentials'): + creds = request.http.request.credentials + elif http is not None and hasattr(http.request, 'credentials'): + creds = http.request.credentials + if creds is not None: + if id(creds) not in self._refreshed_credentials: + creds.refresh(http) + self._refreshed_credentials[id(creds)] = 1 + + # Only apply the credentials if we are using the http object passed in, + # otherwise apply() will get called during _serialize_request(). + if request.http is None or not hasattr(request.http.request, + 'credentials'): + creds.apply(request.headers) + + def _id_to_header(self, id_): + """Convert an id to a Content-ID header value. + + Args: + id_: string, identifier of individual request. + + Returns: + A Content-ID header with the id_ encoded into it. A UUID is prepended to + the value because Content-ID headers are supposed to be universally + unique. + """ + if self._base_id is None: + self._base_id = uuid.uuid4() + + return '<%s+%s>' % (self._base_id, urllib.quote(id_)) + + def _header_to_id(self, header): + """Convert a Content-ID header value to an id. + + Presumes the Content-ID header conforms to the format that _id_to_header() + returns. + + Args: + header: string, Content-ID header value. + + Returns: + The extracted id value. + + Raises: + BatchError if the header is not in the expected format. + """ + if header[0] != '<' or header[-1] != '>': + raise BatchError("Invalid value for Content-ID: %s" % header) + if '+' not in header: + raise BatchError("Invalid value for Content-ID: %s" % header) + base, id_ = header[1:-1].rsplit('+', 1) + + return urllib.unquote(id_) + + def _serialize_request(self, request): + """Convert an HttpRequest object into a string. + + Args: + request: HttpRequest, the request to serialize. + + Returns: + The request as a string in application/http format. + """ + # Construct status line + parsed = urlparse.urlparse(request.uri) + request_line = urlparse.urlunparse( + (None, None, parsed.path, parsed.params, parsed.query, None) + ) + status_line = request.method + ' ' + request_line + ' HTTP/1.1\n' + major, minor = request.headers.get('content-type', 'application/json').split('/') + msg = MIMENonMultipart(major, minor) + headers = request.headers.copy() + + if request.http is not None and hasattr(request.http.request, + 'credentials'): + request.http.request.credentials.apply(headers) + + # MIMENonMultipart adds its own Content-Type header. + if 'content-type' in headers: + del headers['content-type'] + + for key, value in headers.iteritems(): + msg[key] = value + msg['Host'] = parsed.netloc + msg.set_unixfrom(None) + + if request.body is not None: + msg.set_payload(request.body) + msg['content-length'] = str(len(request.body)) + + # Serialize the mime message. + fp = StringIO.StringIO() + # maxheaderlen=0 means don't line wrap headers. + g = Generator(fp, maxheaderlen=0) + g.flatten(msg, unixfrom=False) + body = fp.getvalue() + + # Strip off the \n\n that the MIME lib tacks onto the end of the payload. + if request.body is None: + body = body[:-2] + + return status_line.encode('utf-8') + body + + def _deserialize_response(self, payload): + """Convert string into httplib2 response and content. + + Args: + payload: string, headers and body as a string. + + Returns: + A pair (resp, content), such as would be returned from httplib2.request. + """ + # Strip off the status line + status_line, payload = payload.split('\n', 1) + protocol, status, reason = status_line.split(' ', 2) + + # Parse the rest of the response + parser = FeedParser() + parser.feed(payload) + msg = parser.close() + msg['status'] = status + + # Create httplib2.Response from the parsed headers. + resp = httplib2.Response(msg) + resp.reason = reason + resp.version = int(protocol.split('/', 1)[1].replace('.', '')) + + content = payload.split('\r\n\r\n', 1)[1] + + return resp, content + + def _new_id(self): + """Create a new id. + + Auto incrementing number that avoids conflicts with ids already used. + + Returns: + string, a new unique id. + """ + self._last_auto_id += 1 + while str(self._last_auto_id) in self._requests: + self._last_auto_id += 1 + return str(self._last_auto_id) + + @util.positional(2) + def add(self, request, callback=None, request_id=None): + """Add a new request. + + Every callback added will be paired with a unique id, the request_id. That + unique id will be passed back to the callback when the response comes back + from the server. The default behavior is to have the library generate it's + own unique id. If the caller passes in a request_id then they must ensure + uniqueness for each request_id, and if they are not an exception is + raised. Callers should either supply all request_ids or nevery supply a + request id, to avoid such an error. + + Args: + request: HttpRequest, Request to add to the batch. + callback: callable, A callback to be called for this response, of the + form callback(id, response, exception). The first parameter is the + request id, and the second is the deserialized response object. The + third is an googleapiclient.errors.HttpError exception object if an HTTP error + occurred while processing the request, or None if no errors occurred. + request_id: string, A unique id for the request. The id will be passed to + the callback with the response. + + Returns: + None + + Raises: + BatchError if a media request is added to a batch. + KeyError is the request_id is not unique. + """ + if request_id is None: + request_id = self._new_id() + if request.resumable is not None: + raise BatchError("Media requests cannot be used in a batch request.") + if request_id in self._requests: + raise KeyError("A request with this ID already exists: %s" % request_id) + self._requests[request_id] = request + self._callbacks[request_id] = callback + self._order.append(request_id) + + def _execute(self, http, order, requests): + """Serialize batch request, send to server, process response. + + Args: + http: httplib2.Http, an http object to be used to make the request with. + order: list, list of request ids in the order they were added to the + batch. + request: list, list of request objects to send. + + Raises: + httplib2.HttpLib2Error if a transport error has occured. + googleapiclient.errors.BatchError if the response is the wrong format. + """ + message = MIMEMultipart('mixed') + # Message should not write out it's own headers. + setattr(message, '_write_headers', lambda self: None) + + # Add all the individual requests. + for request_id in order: + request = requests[request_id] + + msg = MIMENonMultipart('application', 'http') + msg['Content-Transfer-Encoding'] = 'binary' + msg['Content-ID'] = self._id_to_header(request_id) + + body = self._serialize_request(request) + msg.set_payload(body) + message.attach(msg) + + body = message.as_string() + + headers = {} + headers['content-type'] = ('multipart/mixed; ' + 'boundary="%s"') % message.get_boundary() + + resp, content = http.request(self._batch_uri, method='POST', body=body, + headers=headers) + + if resp.status >= 300: + raise HttpError(resp, content, uri=self._batch_uri) + + # Now break out the individual responses and store each one. + boundary, _ = content.split(None, 1) + + # Prepend with a content-type header so FeedParser can handle it. + header = 'content-type: %s\r\n\r\n' % resp['content-type'] + for_parser = header + content + + parser = FeedParser() + parser.feed(for_parser) + mime_response = parser.close() + + if not mime_response.is_multipart(): + raise BatchError("Response not in multipart/mixed format.", resp=resp, + content=content) + + for part in mime_response.get_payload(): + request_id = self._header_to_id(part['Content-ID']) + response, content = self._deserialize_response(part.get_payload()) + self._responses[request_id] = (response, content) + + @util.positional(1) + def execute(self, http=None): + """Execute all the requests as a single batched HTTP request. + + Args: + http: httplib2.Http, an http object to be used in place of the one the + HttpRequest request object was constructed with. If one isn't supplied + then use a http object from the requests in this batch. + + Returns: + None + + Raises: + httplib2.HttpLib2Error if a transport error has occured. + googleapiclient.errors.BatchError if the response is the wrong format. + """ + + # If http is not supplied use the first valid one given in the requests. + if http is None: + for request_id in self._order: + request = self._requests[request_id] + if request is not None: + http = request.http + break + + if http is None: + raise ValueError("Missing a valid http object.") + + self._execute(http, self._order, self._requests) + + # Loop over all the requests and check for 401s. For each 401 request the + # credentials should be refreshed and then sent again in a separate batch. + redo_requests = {} + redo_order = [] + + for request_id in self._order: + resp, content = self._responses[request_id] + if resp['status'] == '401': + redo_order.append(request_id) + request = self._requests[request_id] + self._refresh_and_apply_credentials(request, http) + redo_requests[request_id] = request + + if redo_requests: + self._execute(http, redo_order, redo_requests) + + # Now process all callbacks that are erroring, and raise an exception for + # ones that return a non-2xx response? Or add extra parameter to callback + # that contains an HttpError? + + for request_id in self._order: + resp, content = self._responses[request_id] + + request = self._requests[request_id] + callback = self._callbacks[request_id] + + response = None + exception = None + try: + if resp.status >= 300: + raise HttpError(resp, content, uri=request.uri) + response = request.postproc(resp, content) + except HttpError, e: + exception = e + + if callback is not None: + callback(request_id, response, exception) + if self._callback is not None: + self._callback(request_id, response, exception) + + +class HttpRequestMock(object): + """Mock of HttpRequest. + + Do not construct directly, instead use RequestMockBuilder. + """ + + def __init__(self, resp, content, postproc): + """Constructor for HttpRequestMock + + Args: + resp: httplib2.Response, the response to emulate coming from the request + content: string, the response body + postproc: callable, the post processing function usually supplied by + the model class. See model.JsonModel.response() as an example. + """ + self.resp = resp + self.content = content + self.postproc = postproc + if resp is None: + self.resp = httplib2.Response({'status': 200, 'reason': 'OK'}) + if 'reason' in self.resp: + self.resp.reason = self.resp['reason'] + + def execute(self, http=None): + """Execute the request. + + Same behavior as HttpRequest.execute(), but the response is + mocked and not really from an HTTP request/response. + """ + return self.postproc(self.resp, self.content) + + +class RequestMockBuilder(object): + """A simple mock of HttpRequest + + Pass in a dictionary to the constructor that maps request methodIds to + tuples of (httplib2.Response, content, opt_expected_body) that should be + returned when that method is called. None may also be passed in for the + httplib2.Response, in which case a 200 OK response will be generated. + If an opt_expected_body (str or dict) is provided, it will be compared to + the body and UnexpectedBodyError will be raised on inequality. + + Example: + response = '{"data": {"id": "tag:google.c...' + requestBuilder = RequestMockBuilder( + { + 'plus.activities.get': (None, response), + } + ) + googleapiclient.discovery.build("plus", "v1", requestBuilder=requestBuilder) + + Methods that you do not supply a response for will return a + 200 OK with an empty string as the response content or raise an excpetion + if check_unexpected is set to True. The methodId is taken from the rpcName + in the discovery document. + + For more details see the project wiki. + """ + + def __init__(self, responses, check_unexpected=False): + """Constructor for RequestMockBuilder + + The constructed object should be a callable object + that can replace the class HttpResponse. + + responses - A dictionary that maps methodIds into tuples + of (httplib2.Response, content). The methodId + comes from the 'rpcName' field in the discovery + document. + check_unexpected - A boolean setting whether or not UnexpectedMethodError + should be raised on unsupplied method. + """ + self.responses = responses + self.check_unexpected = check_unexpected + + def __call__(self, http, postproc, uri, method='GET', body=None, + headers=None, methodId=None, resumable=None): + """Implements the callable interface that discovery.build() expects + of requestBuilder, which is to build an object compatible with + HttpRequest.execute(). See that method for the description of the + parameters and the expected response. + """ + if methodId in self.responses: + response = self.responses[methodId] + resp, content = response[:2] + if len(response) > 2: + # Test the body against the supplied expected_body. + expected_body = response[2] + if bool(expected_body) != bool(body): + # Not expecting a body and provided one + # or expecting a body and not provided one. + raise UnexpectedBodyError(expected_body, body) + if isinstance(expected_body, str): + expected_body = simplejson.loads(expected_body) + body = simplejson.loads(body) + if body != expected_body: + raise UnexpectedBodyError(expected_body, body) + return HttpRequestMock(resp, content, postproc) + elif self.check_unexpected: + raise UnexpectedMethodError(methodId=methodId) + else: + model = JsonModel(False) + return HttpRequestMock(None, '{}', model.response) + + +class HttpMock(object): + """Mock of httplib2.Http""" + + def __init__(self, filename=None, headers=None): + """ + Args: + filename: string, absolute filename to read response from + headers: dict, header to return with response + """ + if headers is None: + headers = {'status': '200 OK'} + if filename: + f = file(filename, 'r') + self.data = f.read() + f.close() + else: + self.data = None + self.response_headers = headers + self.headers = None + self.uri = None + self.method = None + self.body = None + self.headers = None + + + def request(self, uri, + method='GET', + body=None, + headers=None, + redirections=1, + connection_type=None): + self.uri = uri + self.method = method + self.body = body + self.headers = headers + return httplib2.Response(self.response_headers), self.data + + +class HttpMockSequence(object): + """Mock of httplib2.Http + + Mocks a sequence of calls to request returning different responses for each + call. Create an instance initialized with the desired response headers + and content and then use as if an httplib2.Http instance. + + http = HttpMockSequence([ + ({'status': '401'}, ''), + ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'), + ({'status': '200'}, 'echo_request_headers'), + ]) + resp, content = http.request("http://examples.com") + + There are special values you can pass in for content to trigger + behavours that are helpful in testing. + + 'echo_request_headers' means return the request headers in the response body + 'echo_request_headers_as_json' means return the request headers in + the response body + 'echo_request_body' means return the request body in the response body + 'echo_request_uri' means return the request uri in the response body + """ + + def __init__(self, iterable): + """ + Args: + iterable: iterable, a sequence of pairs of (headers, body) + """ + self._iterable = iterable + self.follow_redirects = True + + def request(self, uri, + method='GET', + body=None, + headers=None, + redirections=1, + connection_type=None): + resp, content = self._iterable.pop(0) + if content == 'echo_request_headers': + content = headers + elif content == 'echo_request_headers_as_json': + content = simplejson.dumps(headers) + elif content == 'echo_request_body': + if hasattr(body, 'read'): + content = body.read() + else: + content = body + elif content == 'echo_request_uri': + content = uri + return httplib2.Response(resp), content + + +def set_user_agent(http, user_agent): + """Set the user-agent on every request. + + Args: + http - An instance of httplib2.Http + or something that acts like it. + user_agent: string, the value for the user-agent header. + + Returns: + A modified instance of http that was passed in. + + Example: + + h = httplib2.Http() + h = set_user_agent(h, "my-app-name/6.0") + + Most of the time the user-agent will be set doing auth, this is for the rare + cases where you are accessing an unauthenticated endpoint. + """ + request_orig = http.request + + # The closure that will replace 'httplib2.Http.request'. + def new_request(uri, method='GET', body=None, headers=None, + redirections=httplib2.DEFAULT_MAX_REDIRECTS, + connection_type=None): + """Modify the request headers to add the user-agent.""" + if headers is None: + headers = {} + if 'user-agent' in headers: + headers['user-agent'] = user_agent + ' ' + headers['user-agent'] + else: + headers['user-agent'] = user_agent + resp, content = request_orig(uri, method, body, headers, + redirections, connection_type) + return resp, content + + http.request = new_request + return http + + +def tunnel_patch(http): + """Tunnel PATCH requests over POST. + Args: + http - An instance of httplib2.Http + or something that acts like it. + + Returns: + A modified instance of http that was passed in. + + Example: + + h = httplib2.Http() + h = tunnel_patch(h, "my-app-name/6.0") + + Useful if you are running on a platform that doesn't support PATCH. + Apply this last if you are using OAuth 1.0, as changing the method + will result in a different signature. + """ + request_orig = http.request + + # The closure that will replace 'httplib2.Http.request'. + def new_request(uri, method='GET', body=None, headers=None, + redirections=httplib2.DEFAULT_MAX_REDIRECTS, + connection_type=None): + """Modify the request headers to add the user-agent.""" + if headers is None: + headers = {} + if method == 'PATCH': + if 'oauth_token' in headers.get('authorization', ''): + logging.warning( + 'OAuth 1.0 request made with Credentials after tunnel_patch.') + headers['x-http-method-override'] = "PATCH" + method = 'POST' + resp, content = request_orig(uri, method, body, headers, + redirections, connection_type) + return resp, content + + http.request = new_request + return http diff --git a/appengine/dashdemo/googleapiclient/mimeparse.py b/appengine/dashdemo/googleapiclient/mimeparse.py new file mode 100644 index 0000000..cbb9d07 --- /dev/null +++ b/appengine/dashdemo/googleapiclient/mimeparse.py @@ -0,0 +1,172 @@ +# Copyright (C) 2007 Joe Gregorio +# +# Licensed under the MIT License + +"""MIME-Type Parser + +This module provides basic functions for handling mime-types. It can handle +matching mime-types against a list of media-ranges. See section 14.1 of the +HTTP specification [RFC 2616] for a complete explanation. + + http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1 + +Contents: + - parse_mime_type(): Parses a mime-type into its component parts. + - parse_media_range(): Media-ranges are mime-types with wild-cards and a 'q' + quality parameter. + - quality(): Determines the quality ('q') of a mime-type when + compared against a list of media-ranges. + - quality_parsed(): Just like quality() except the second parameter must be + pre-parsed. + - best_match(): Choose the mime-type with the highest quality ('q') + from a list of candidates. +""" + +__version__ = '0.1.3' +__author__ = 'Joe Gregorio' +__email__ = 'joe@bitworking.org' +__license__ = 'MIT License' +__credits__ = '' + + +def parse_mime_type(mime_type): + """Parses a mime-type into its component parts. + + Carves up a mime-type and returns a tuple of the (type, subtype, params) + where 'params' is a dictionary of all the parameters for the media range. + For example, the media range 'application/xhtml;q=0.5' would get parsed + into: + + ('application', 'xhtml', {'q', '0.5'}) + """ + parts = mime_type.split(';') + params = dict([tuple([s.strip() for s in param.split('=', 1)])\ + for param in parts[1:] + ]) + full_type = parts[0].strip() + # Java URLConnection class sends an Accept header that includes a + # single '*'. Turn it into a legal wildcard. + if full_type == '*': + full_type = '*/*' + (type, subtype) = full_type.split('/') + + return (type.strip(), subtype.strip(), params) + + +def parse_media_range(range): + """Parse a media-range into its component parts. + + Carves up a media range and returns a tuple of the (type, subtype, + params) where 'params' is a dictionary of all the parameters for the media + range. For example, the media range 'application/*;q=0.5' would get parsed + into: + + ('application', '*', {'q', '0.5'}) + + In addition this function also guarantees that there is a value for 'q' + in the params dictionary, filling it in with a proper default if + necessary. + """ + (type, subtype, params) = parse_mime_type(range) + if not params.has_key('q') or not params['q'] or \ + not float(params['q']) or float(params['q']) > 1\ + or float(params['q']) < 0: + params['q'] = '1' + + return (type, subtype, params) + + +def fitness_and_quality_parsed(mime_type, parsed_ranges): + """Find the best match for a mime-type amongst parsed media-ranges. + + Find the best match for a given mime-type against a list of media_ranges + that have already been parsed by parse_media_range(). Returns a tuple of + the fitness value and the value of the 'q' quality parameter of the best + match, or (-1, 0) if no match was found. Just as for quality_parsed(), + 'parsed_ranges' must be a list of parsed media ranges. + """ + best_fitness = -1 + best_fit_q = 0 + (target_type, target_subtype, target_params) =\ + parse_media_range(mime_type) + for (type, subtype, params) in parsed_ranges: + type_match = (type == target_type or\ + type == '*' or\ + target_type == '*') + subtype_match = (subtype == target_subtype or\ + subtype == '*' or\ + target_subtype == '*') + if type_match and subtype_match: + param_matches = reduce(lambda x, y: x + y, [1 for (key, value) in \ + target_params.iteritems() if key != 'q' and \ + params.has_key(key) and value == params[key]], 0) + fitness = (type == target_type) and 100 or 0 + fitness += (subtype == target_subtype) and 10 or 0 + fitness += param_matches + if fitness > best_fitness: + best_fitness = fitness + best_fit_q = params['q'] + + return best_fitness, float(best_fit_q) + + +def quality_parsed(mime_type, parsed_ranges): + """Find the best match for a mime-type amongst parsed media-ranges. + + Find the best match for a given mime-type against a list of media_ranges + that have already been parsed by parse_media_range(). Returns the 'q' + quality parameter of the best match, 0 if no match was found. This function + bahaves the same as quality() except that 'parsed_ranges' must be a list of + parsed media ranges. + """ + + return fitness_and_quality_parsed(mime_type, parsed_ranges)[1] + + +def quality(mime_type, ranges): + """Return the quality ('q') of a mime-type against a list of media-ranges. + + Returns the quality 'q' of a mime-type when compared against the + media-ranges in ranges. For example: + + >>> quality('text/html','text/*;q=0.3, text/html;q=0.7, + text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5') + 0.7 + + """ + parsed_ranges = [parse_media_range(r) for r in ranges.split(',')] + + return quality_parsed(mime_type, parsed_ranges) + + +def best_match(supported, header): + """Return mime-type with the highest quality ('q') from list of candidates. + + Takes a list of supported mime-types and finds the best match for all the + media-ranges listed in header. The value of header must be a string that + conforms to the format of the HTTP Accept: header. The value of 'supported' + is a list of mime-types. The list of supported mime-types should be sorted + in order of increasing desirability, in case of a situation where there is + a tie. + + >>> best_match(['application/xbel+xml', 'text/xml'], + 'text/*;q=0.5,*/*; q=0.1') + 'text/xml' + """ + split_header = _filter_blank(header.split(',')) + parsed_header = [parse_media_range(r) for r in split_header] + weighted_matches = [] + pos = 0 + for mime_type in supported: + weighted_matches.append((fitness_and_quality_parsed(mime_type, + parsed_header), pos, mime_type)) + pos += 1 + weighted_matches.sort() + + return weighted_matches[-1][0][1] and weighted_matches[-1][2] or '' + + +def _filter_blank(i): + for s in i: + if s.strip(): + yield s diff --git a/appengine/dashdemo/googleapiclient/model.py b/appengine/dashdemo/googleapiclient/model.py new file mode 100644 index 0000000..566a233 --- /dev/null +++ b/appengine/dashdemo/googleapiclient/model.py @@ -0,0 +1,383 @@ +#!/usr/bin/python2.4 +# +# Copyright (C) 2010 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Model objects for requests and responses. + +Each API may support one or more serializations, such +as JSON, Atom, etc. The model classes are responsible +for converting between the wire format and the Python +object representation. +""" + +__author__ = 'jcgregorio@google.com (Joe Gregorio)' + +import logging +import urllib + +from googleapiclient import __version__ +from errors import HttpError +from oauth2client.anyjson import simplejson + + +dump_request_response = False + + +def _abstract(): + raise NotImplementedError('You need to override this function') + + +class Model(object): + """Model base class. + + All Model classes should implement this interface. + The Model serializes and de-serializes between a wire + format such as JSON and a Python object representation. + """ + + def request(self, headers, path_params, query_params, body_value): + """Updates outgoing requests with a serialized body. + + Args: + headers: dict, request headers + path_params: dict, parameters that appear in the request path + query_params: dict, parameters that appear in the query + body_value: object, the request body as a Python object, which must be + serializable. + Returns: + A tuple of (headers, path_params, query, body) + + headers: dict, request headers + path_params: dict, parameters that appear in the request path + query: string, query part of the request URI + body: string, the body serialized in the desired wire format. + """ + _abstract() + + def response(self, resp, content): + """Convert the response wire format into a Python object. + + Args: + resp: httplib2.Response, the HTTP response headers and status + content: string, the body of the HTTP response + + Returns: + The body de-serialized as a Python object. + + Raises: + googleapiclient.errors.HttpError if a non 2xx response is received. + """ + _abstract() + + +class BaseModel(Model): + """Base model class. + + Subclasses should provide implementations for the "serialize" and + "deserialize" methods, as well as values for the following class attributes. + + Attributes: + accept: The value to use for the HTTP Accept header. + content_type: The value to use for the HTTP Content-type header. + no_content_response: The value to return when deserializing a 204 "No + Content" response. + alt_param: The value to supply as the "alt" query parameter for requests. + """ + + accept = None + content_type = None + no_content_response = None + alt_param = None + + def _log_request(self, headers, path_params, query, body): + """Logs debugging information about the request if requested.""" + if dump_request_response: + logging.info('--request-start--') + logging.info('-headers-start-') + for h, v in headers.iteritems(): + logging.info('%s: %s', h, v) + logging.info('-headers-end-') + logging.info('-path-parameters-start-') + for h, v in path_params.iteritems(): + logging.info('%s: %s', h, v) + logging.info('-path-parameters-end-') + logging.info('body: %s', body) + logging.info('query: %s', query) + logging.info('--request-end--') + + def request(self, headers, path_params, query_params, body_value): + """Updates outgoing requests with a serialized body. + + Args: + headers: dict, request headers + path_params: dict, parameters that appear in the request path + query_params: dict, parameters that appear in the query + body_value: object, the request body as a Python object, which must be + serializable by simplejson. + Returns: + A tuple of (headers, path_params, query, body) + + headers: dict, request headers + path_params: dict, parameters that appear in the request path + query: string, query part of the request URI + body: string, the body serialized as JSON + """ + query = self._build_query(query_params) + headers['accept'] = self.accept + headers['accept-encoding'] = 'gzip, deflate' + if 'user-agent' in headers: + headers['user-agent'] += ' ' + else: + headers['user-agent'] = '' + headers['user-agent'] += 'google-api-python-client/%s (gzip)' % __version__ + + if body_value is not None: + headers['content-type'] = self.content_type + body_value = self.serialize(body_value) + self._log_request(headers, path_params, query, body_value) + return (headers, path_params, query, body_value) + + def _build_query(self, params): + """Builds a query string. + + Args: + params: dict, the query parameters + + Returns: + The query parameters properly encoded into an HTTP URI query string. + """ + if self.alt_param is not None: + params.update({'alt': self.alt_param}) + astuples = [] + for key, value in params.iteritems(): + if type(value) == type([]): + for x in value: + x = x.encode('utf-8') + astuples.append((key, x)) + else: + if getattr(value, 'encode', False) and callable(value.encode): + value = value.encode('utf-8') + astuples.append((key, value)) + return '?' + urllib.urlencode(astuples) + + def _log_response(self, resp, content): + """Logs debugging information about the response if requested.""" + if dump_request_response: + logging.info('--response-start--') + for h, v in resp.iteritems(): + logging.info('%s: %s', h, v) + if content: + logging.info(content) + logging.info('--response-end--') + + def response(self, resp, content): + """Convert the response wire format into a Python object. + + Args: + resp: httplib2.Response, the HTTP response headers and status + content: string, the body of the HTTP response + + Returns: + The body de-serialized as a Python object. + + Raises: + googleapiclient.errors.HttpError if a non 2xx response is received. + """ + self._log_response(resp, content) + # Error handling is TBD, for example, do we retry + # for some operation/error combinations? + if resp.status < 300: + if resp.status == 204: + # A 204: No Content response should be treated differently + # to all the other success states + return self.no_content_response + return self.deserialize(content) + else: + logging.debug('Content from bad request was: %s' % content) + raise HttpError(resp, content) + + def serialize(self, body_value): + """Perform the actual Python object serialization. + + Args: + body_value: object, the request body as a Python object. + + Returns: + string, the body in serialized form. + """ + _abstract() + + def deserialize(self, content): + """Perform the actual deserialization from response string to Python + object. + + Args: + content: string, the body of the HTTP response + + Returns: + The body de-serialized as a Python object. + """ + _abstract() + + +class JsonModel(BaseModel): + """Model class for JSON. + + Serializes and de-serializes between JSON and the Python + object representation of HTTP request and response bodies. + """ + accept = 'application/json' + content_type = 'application/json' + alt_param = 'json' + + def __init__(self, data_wrapper=False): + """Construct a JsonModel. + + Args: + data_wrapper: boolean, wrap requests and responses in a data wrapper + """ + self._data_wrapper = data_wrapper + + def serialize(self, body_value): + if (isinstance(body_value, dict) and 'data' not in body_value and + self._data_wrapper): + body_value = {'data': body_value} + return simplejson.dumps(body_value) + + def deserialize(self, content): + content = content.decode('utf-8') + body = simplejson.loads(content) + if self._data_wrapper and isinstance(body, dict) and 'data' in body: + body = body['data'] + return body + + @property + def no_content_response(self): + return {} + + +class RawModel(JsonModel): + """Model class for requests that don't return JSON. + + Serializes and de-serializes between JSON and the Python + object representation of HTTP request, and returns the raw bytes + of the response body. + """ + accept = '*/*' + content_type = 'application/json' + alt_param = None + + def deserialize(self, content): + return content + + @property + def no_content_response(self): + return '' + + +class MediaModel(JsonModel): + """Model class for requests that return Media. + + Serializes and de-serializes between JSON and the Python + object representation of HTTP request, and returns the raw bytes + of the response body. + """ + accept = '*/*' + content_type = 'application/json' + alt_param = 'media' + + def deserialize(self, content): + return content + + @property + def no_content_response(self): + return '' + + +class ProtocolBufferModel(BaseModel): + """Model class for protocol buffers. + + Serializes and de-serializes the binary protocol buffer sent in the HTTP + request and response bodies. + """ + accept = 'application/x-protobuf' + content_type = 'application/x-protobuf' + alt_param = 'proto' + + def __init__(self, protocol_buffer): + """Constructs a ProtocolBufferModel. + + The serialzed protocol buffer returned in an HTTP response will be + de-serialized using the given protocol buffer class. + + Args: + protocol_buffer: The protocol buffer class used to de-serialize a + response from the API. + """ + self._protocol_buffer = protocol_buffer + + def serialize(self, body_value): + return body_value.SerializeToString() + + def deserialize(self, content): + return self._protocol_buffer.FromString(content) + + @property + def no_content_response(self): + return self._protocol_buffer() + + +def makepatch(original, modified): + """Create a patch object. + + Some methods support PATCH, an efficient way to send updates to a resource. + This method allows the easy construction of patch bodies by looking at the + differences between a resource before and after it was modified. + + Args: + original: object, the original deserialized resource + modified: object, the modified deserialized resource + Returns: + An object that contains only the changes from original to modified, in a + form suitable to pass to a PATCH method. + + Example usage: + item = service.activities().get(postid=postid, userid=userid).execute() + original = copy.deepcopy(item) + item['object']['content'] = 'This is updated.' + service.activities.patch(postid=postid, userid=userid, + body=makepatch(original, item)).execute() + """ + patch = {} + for key, original_value in original.iteritems(): + modified_value = modified.get(key, None) + if modified_value is None: + # Use None to signal that the element is deleted + patch[key] = None + elif original_value != modified_value: + if type(original_value) == type({}): + # Recursively descend objects + patch[key] = makepatch(original_value, modified_value) + else: + # In the case of simple types or arrays we just replace + patch[key] = modified_value + else: + # Don't add anything to patch if there's no change + pass + for key in modified: + if key not in original: + patch[key] = modified[key] + + return patch diff --git a/appengine/dashdemo/googleapiclient/sample_tools.py b/appengine/dashdemo/googleapiclient/sample_tools.py new file mode 100644 index 0000000..09f9057 --- /dev/null +++ b/appengine/dashdemo/googleapiclient/sample_tools.py @@ -0,0 +1,93 @@ +# Copyright (C) 2013 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Utilities for making samples. + +Consolidates a lot of code commonly repeated in sample applications. +""" + +__author__ = 'jcgregorio@google.com (Joe Gregorio)' +__all__ = ['init'] + + +import argparse +import httplib2 +import os + +from googleapiclient import discovery +from oauth2client import client +from oauth2client import file +from oauth2client import tools + + +def init(argv, name, version, doc, filename, scope=None, parents=[]): + """A common initialization routine for samples. + + Many of the sample applications do the same initialization, which has now + been consolidated into this function. This function uses common idioms found + in almost all the samples, i.e. for an API with name 'apiname', the + credentials are stored in a file named apiname.dat, and the + client_secrets.json file is stored in the same directory as the application + main file. + + Args: + argv: list of string, the command-line parameters of the application. + name: string, name of the API. + version: string, version of the API. + doc: string, description of the application. Usually set to __doc__. + file: string, filename of the application. Usually set to __file__. + parents: list of argparse.ArgumentParser, additional command-line flags. + scope: string, The OAuth scope used. + + Returns: + A tuple of (service, flags), where service is the service object and flags + is the parsed command-line flags. + """ + if scope is None: + scope = 'https://www.googleapis.com/auth/' + name + + # Parser command-line arguments. + parent_parsers = [tools.argparser] + parent_parsers.extend(parents) + parser = argparse.ArgumentParser( + description=doc, + formatter_class=argparse.RawDescriptionHelpFormatter, + parents=parent_parsers) + flags = parser.parse_args(argv[1:]) + + # Name of a file containing the OAuth 2.0 information for this + # application, including client_id and client_secret, which are found + # on the API Access tab on the Google APIs + # Console . + client_secrets = os.path.join(os.path.dirname(filename), + 'client_secrets.json') + + # Set up a Flow object to be used if we need to authenticate. + flow = client.flow_from_clientsecrets(client_secrets, + scope=scope, + message=tools.message_if_missing(client_secrets)) + + # Prepare credentials, and authorize HTTP object with them. + # If the credentials don't exist or are invalid run through the native client + # flow. The Storage object will ensure that if successful the good + # credentials will get written back to a file. + storage = file.Storage(name + '.dat') + credentials = storage.get() + if credentials is None or credentials.invalid: + credentials = tools.run_flow(flow, storage, flags) + http = credentials.authorize(http = httplib2.Http()) + + # Construct a service object via the discovery service. + service = discovery.build(name, version, http=http) + return (service, flags) diff --git a/appengine/dashdemo/googleapiclient/schema.py b/appengine/dashdemo/googleapiclient/schema.py new file mode 100644 index 0000000..d076a86 --- /dev/null +++ b/appengine/dashdemo/googleapiclient/schema.py @@ -0,0 +1,312 @@ +# Copyright (C) 2010 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Schema processing for discovery based APIs + +Schemas holds an APIs discovery schemas. It can return those schema as +deserialized JSON objects, or pretty print them as prototype objects that +conform to the schema. + +For example, given the schema: + + schema = \"\"\"{ + "Foo": { + "type": "object", + "properties": { + "etag": { + "type": "string", + "description": "ETag of the collection." + }, + "kind": { + "type": "string", + "description": "Type of the collection ('calendar#acl').", + "default": "calendar#acl" + }, + "nextPageToken": { + "type": "string", + "description": "Token used to access the next + page of this result. Omitted if no further results are available." + } + } + } + }\"\"\" + + s = Schemas(schema) + print s.prettyPrintByName('Foo') + + Produces the following output: + + { + "nextPageToken": "A String", # Token used to access the + # next page of this result. Omitted if no further results are available. + "kind": "A String", # Type of the collection ('calendar#acl'). + "etag": "A String", # ETag of the collection. + }, + +The constructor takes a discovery document in which to look up named schema. +""" + +# TODO(jcgregorio) support format, enum, minimum, maximum + +__author__ = 'jcgregorio@google.com (Joe Gregorio)' + +import copy + +from oauth2client import util +from oauth2client.anyjson import simplejson + + +class Schemas(object): + """Schemas for an API.""" + + def __init__(self, discovery): + """Constructor. + + Args: + discovery: object, Deserialized discovery document from which we pull + out the named schema. + """ + self.schemas = discovery.get('schemas', {}) + + # Cache of pretty printed schemas. + self.pretty = {} + + @util.positional(2) + def _prettyPrintByName(self, name, seen=None, dent=0): + """Get pretty printed object prototype from the schema name. + + Args: + name: string, Name of schema in the discovery document. + seen: list of string, Names of schema already seen. Used to handle + recursive definitions. + + Returns: + string, A string that contains a prototype object with + comments that conforms to the given schema. + """ + if seen is None: + seen = [] + + if name in seen: + # Do not fall into an infinite loop over recursive definitions. + return '# Object with schema name: %s' % name + seen.append(name) + + if name not in self.pretty: + self.pretty[name] = _SchemaToStruct(self.schemas[name], + seen, dent=dent).to_str(self._prettyPrintByName) + + seen.pop() + + return self.pretty[name] + + def prettyPrintByName(self, name): + """Get pretty printed object prototype from the schema name. + + Args: + name: string, Name of schema in the discovery document. + + Returns: + string, A string that contains a prototype object with + comments that conforms to the given schema. + """ + # Return with trailing comma and newline removed. + return self._prettyPrintByName(name, seen=[], dent=1)[:-2] + + @util.positional(2) + def _prettyPrintSchema(self, schema, seen=None, dent=0): + """Get pretty printed object prototype of schema. + + Args: + schema: object, Parsed JSON schema. + seen: list of string, Names of schema already seen. Used to handle + recursive definitions. + + Returns: + string, A string that contains a prototype object with + comments that conforms to the given schema. + """ + if seen is None: + seen = [] + + return _SchemaToStruct(schema, seen, dent=dent).to_str(self._prettyPrintByName) + + def prettyPrintSchema(self, schema): + """Get pretty printed object prototype of schema. + + Args: + schema: object, Parsed JSON schema. + + Returns: + string, A string that contains a prototype object with + comments that conforms to the given schema. + """ + # Return with trailing comma and newline removed. + return self._prettyPrintSchema(schema, dent=1)[:-2] + + def get(self, name): + """Get deserialized JSON schema from the schema name. + + Args: + name: string, Schema name. + """ + return self.schemas[name] + + +class _SchemaToStruct(object): + """Convert schema to a prototype object.""" + + @util.positional(3) + def __init__(self, schema, seen, dent=0): + """Constructor. + + Args: + schema: object, Parsed JSON schema. + seen: list, List of names of schema already seen while parsing. Used to + handle recursive definitions. + dent: int, Initial indentation depth. + """ + # The result of this parsing kept as list of strings. + self.value = [] + + # The final value of the parsing. + self.string = None + + # The parsed JSON schema. + self.schema = schema + + # Indentation level. + self.dent = dent + + # Method that when called returns a prototype object for the schema with + # the given name. + self.from_cache = None + + # List of names of schema already seen while parsing. + self.seen = seen + + def emit(self, text): + """Add text as a line to the output. + + Args: + text: string, Text to output. + """ + self.value.extend([" " * self.dent, text, '\n']) + + def emitBegin(self, text): + """Add text to the output, but with no line terminator. + + Args: + text: string, Text to output. + """ + self.value.extend([" " * self.dent, text]) + + def emitEnd(self, text, comment): + """Add text and comment to the output with line terminator. + + Args: + text: string, Text to output. + comment: string, Python comment. + """ + if comment: + divider = '\n' + ' ' * (self.dent + 2) + '# ' + lines = comment.splitlines() + lines = [x.rstrip() for x in lines] + comment = divider.join(lines) + self.value.extend([text, ' # ', comment, '\n']) + else: + self.value.extend([text, '\n']) + + def indent(self): + """Increase indentation level.""" + self.dent += 1 + + def undent(self): + """Decrease indentation level.""" + self.dent -= 1 + + def _to_str_impl(self, schema): + """Prototype object based on the schema, in Python code with comments. + + Args: + schema: object, Parsed JSON schema file. + + Returns: + Prototype object based on the schema, in Python code with comments. + """ + stype = schema.get('type') + if stype == 'object': + self.emitEnd('{', schema.get('description', '')) + self.indent() + if 'properties' in schema: + for pname, pschema in schema.get('properties', {}).iteritems(): + self.emitBegin('"%s": ' % pname) + self._to_str_impl(pschema) + elif 'additionalProperties' in schema: + self.emitBegin('"a_key": ') + self._to_str_impl(schema['additionalProperties']) + self.undent() + self.emit('},') + elif '$ref' in schema: + schemaName = schema['$ref'] + description = schema.get('description', '') + s = self.from_cache(schemaName, seen=self.seen) + parts = s.splitlines() + self.emitEnd(parts[0], description) + for line in parts[1:]: + self.emit(line.rstrip()) + elif stype == 'boolean': + value = schema.get('default', 'True or False') + self.emitEnd('%s,' % str(value), schema.get('description', '')) + elif stype == 'string': + value = schema.get('default', 'A String') + self.emitEnd('"%s",' % str(value), schema.get('description', '')) + elif stype == 'integer': + value = schema.get('default', '42') + self.emitEnd('%s,' % str(value), schema.get('description', '')) + elif stype == 'number': + value = schema.get('default', '3.14') + self.emitEnd('%s,' % str(value), schema.get('description', '')) + elif stype == 'null': + self.emitEnd('None,', schema.get('description', '')) + elif stype == 'any': + self.emitEnd('"",', schema.get('description', '')) + elif stype == 'array': + self.emitEnd('[', schema.get('description')) + self.indent() + self.emitBegin('') + self._to_str_impl(schema['items']) + self.undent() + self.emit('],') + else: + self.emit('Unknown type! %s' % stype) + self.emitEnd('', '') + + self.string = ''.join(self.value) + return self.string + + def to_str(self, from_cache): + """Prototype object based on the schema, in Python code with comments. + + Args: + from_cache: callable(name, seen), Callable that retrieves an object + prototype for a schema with the given name. Seen is a list of schema + names already seen as we recursively descend the schema definition. + + Returns: + Prototype object based on the schema, in Python code with comments. + The lines of the code will all be properly indented. + """ + self.from_cache = from_cache + return self._to_str_impl(self.schema) diff --git a/appengine/dashdemo/httplib2/__init__.py b/appengine/dashdemo/httplib2/__init__.py new file mode 100644 index 0000000..d1212b5 --- /dev/null +++ b/appengine/dashdemo/httplib2/__init__.py @@ -0,0 +1,1680 @@ +from __future__ import generators +""" +httplib2 + +A caching http interface that supports ETags and gzip +to conserve bandwidth. + +Requires Python 2.3 or later + +Changelog: +2007-08-18, Rick: Modified so it's able to use a socks proxy if needed. + +""" + +__author__ = "Joe Gregorio (joe@bitworking.org)" +__copyright__ = "Copyright 2006, Joe Gregorio" +__contributors__ = ["Thomas Broyer (t.broyer@ltgt.net)", + "James Antill", + "Xavier Verges Farrero", + "Jonathan Feinberg", + "Blair Zajac", + "Sam Ruby", + "Louis Nyffenegger"] +__license__ = "MIT" +__version__ = "0.9" + +import re +import sys +import email +import email.Utils +import email.Message +import email.FeedParser +import StringIO +import gzip +import zlib +import httplib +import urlparse +import urllib +import base64 +import os +import copy +import calendar +import time +import random +import errno +try: + from hashlib import sha1 as _sha, md5 as _md5 +except ImportError: + # prior to Python 2.5, these were separate modules + import sha + import md5 + _sha = sha.new + _md5 = md5.new +import hmac +from gettext import gettext as _ +import socket + +try: + from httplib2 import socks +except ImportError: + try: + import socks + except (ImportError, AttributeError): + socks = None + +# Build the appropriate socket wrapper for ssl +try: + import ssl # python 2.6 + ssl_SSLError = ssl.SSLError + def _ssl_wrap_socket(sock, key_file, cert_file, + disable_validation, ca_certs): + if disable_validation: + cert_reqs = ssl.CERT_NONE + else: + cert_reqs = ssl.CERT_REQUIRED + # We should be specifying SSL version 3 or TLS v1, but the ssl module + # doesn't expose the necessary knobs. So we need to go with the default + # of SSLv23. + return ssl.wrap_socket(sock, keyfile=key_file, certfile=cert_file, + cert_reqs=cert_reqs, ca_certs=ca_certs) +except (AttributeError, ImportError): + ssl_SSLError = None + def _ssl_wrap_socket(sock, key_file, cert_file, + disable_validation, ca_certs): + if not disable_validation: + raise CertificateValidationUnsupported( + "SSL certificate validation is not supported without " + "the ssl module installed. To avoid this error, install " + "the ssl module, or explicity disable validation.") + ssl_sock = socket.ssl(sock, key_file, cert_file) + return httplib.FakeSocket(sock, ssl_sock) + + +if sys.version_info >= (2,3): + from iri2uri import iri2uri +else: + def iri2uri(uri): + return uri + +def has_timeout(timeout): # python 2.6 + if hasattr(socket, '_GLOBAL_DEFAULT_TIMEOUT'): + return (timeout is not None and timeout is not socket._GLOBAL_DEFAULT_TIMEOUT) + return (timeout is not None) + +__all__ = [ + 'Http', 'Response', 'ProxyInfo', 'HttpLib2Error', 'RedirectMissingLocation', + 'RedirectLimit', 'FailedToDecompressContent', + 'UnimplementedDigestAuthOptionError', + 'UnimplementedHmacDigestAuthOptionError', + 'debuglevel', 'ProxiesUnavailableError'] + + +# The httplib debug level, set to a non-zero value to get debug output +debuglevel = 0 + +# A request will be tried 'RETRIES' times if it fails at the socket/connection level. +RETRIES = 2 + +# Python 2.3 support +if sys.version_info < (2,4): + def sorted(seq): + seq.sort() + return seq + +# Python 2.3 support +def HTTPResponse__getheaders(self): + """Return list of (header, value) tuples.""" + if self.msg is None: + raise httplib.ResponseNotReady() + return self.msg.items() + +if not hasattr(httplib.HTTPResponse, 'getheaders'): + httplib.HTTPResponse.getheaders = HTTPResponse__getheaders + +# All exceptions raised here derive from HttpLib2Error +class HttpLib2Error(Exception): pass + +# Some exceptions can be caught and optionally +# be turned back into responses. +class HttpLib2ErrorWithResponse(HttpLib2Error): + def __init__(self, desc, response, content): + self.response = response + self.content = content + HttpLib2Error.__init__(self, desc) + +class RedirectMissingLocation(HttpLib2ErrorWithResponse): pass +class RedirectLimit(HttpLib2ErrorWithResponse): pass +class FailedToDecompressContent(HttpLib2ErrorWithResponse): pass +class UnimplementedDigestAuthOptionError(HttpLib2ErrorWithResponse): pass +class UnimplementedHmacDigestAuthOptionError(HttpLib2ErrorWithResponse): pass + +class MalformedHeader(HttpLib2Error): pass +class RelativeURIError(HttpLib2Error): pass +class ServerNotFoundError(HttpLib2Error): pass +class ProxiesUnavailableError(HttpLib2Error): pass +class CertificateValidationUnsupported(HttpLib2Error): pass +class SSLHandshakeError(HttpLib2Error): pass +class NotSupportedOnThisPlatform(HttpLib2Error): pass +class CertificateHostnameMismatch(SSLHandshakeError): + def __init__(self, desc, host, cert): + HttpLib2Error.__init__(self, desc) + self.host = host + self.cert = cert + +# Open Items: +# ----------- +# Proxy support + +# Are we removing the cached content too soon on PUT (only delete on 200 Maybe?) + +# Pluggable cache storage (supports storing the cache in +# flat files by default. We need a plug-in architecture +# that can support Berkeley DB and Squid) + +# == Known Issues == +# Does not handle a resource that uses conneg and Last-Modified but no ETag as a cache validator. +# Does not handle Cache-Control: max-stale +# Does not use Age: headers when calculating cache freshness. + + +# The number of redirections to follow before giving up. +# Note that only GET redirects are automatically followed. +# Will also honor 301 requests by saving that info and never +# requesting that URI again. +DEFAULT_MAX_REDIRECTS = 5 + +try: + # Users can optionally provide a module that tells us where the CA_CERTS + # are located. + import ca_certs_locater + CA_CERTS = ca_certs_locater.get() +except ImportError: + # Default CA certificates file bundled with httplib2. + CA_CERTS = os.path.join( + os.path.dirname(os.path.abspath(__file__ )), "cacerts.txt") + +# Which headers are hop-by-hop headers by default +HOP_BY_HOP = ['connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailers', 'transfer-encoding', 'upgrade'] + +def _get_end2end_headers(response): + hopbyhop = list(HOP_BY_HOP) + hopbyhop.extend([x.strip() for x in response.get('connection', '').split(',')]) + return [header for header in response.keys() if header not in hopbyhop] + +URI = re.compile(r"^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?") + +def parse_uri(uri): + """Parses a URI using the regex given in Appendix B of RFC 3986. + + (scheme, authority, path, query, fragment) = parse_uri(uri) + """ + groups = URI.match(uri).groups() + return (groups[1], groups[3], groups[4], groups[6], groups[8]) + +def urlnorm(uri): + (scheme, authority, path, query, fragment) = parse_uri(uri) + if not scheme or not authority: + raise RelativeURIError("Only absolute URIs are allowed. uri = %s" % uri) + authority = authority.lower() + scheme = scheme.lower() + if not path: + path = "/" + # Could do syntax based normalization of the URI before + # computing the digest. See Section 6.2.2 of Std 66. + request_uri = query and "?".join([path, query]) or path + scheme = scheme.lower() + defrag_uri = scheme + "://" + authority + request_uri + return scheme, authority, request_uri, defrag_uri + + +# Cache filename construction (original borrowed from Venus http://intertwingly.net/code/venus/) +re_url_scheme = re.compile(r'^\w+://') +re_slash = re.compile(r'[?/:|]+') + +def safename(filename): + """Return a filename suitable for the cache. + + Strips dangerous and common characters to create a filename we + can use to store the cache in. + """ + + try: + if re_url_scheme.match(filename): + if isinstance(filename,str): + filename = filename.decode('utf-8') + filename = filename.encode('idna') + else: + filename = filename.encode('idna') + except UnicodeError: + pass + if isinstance(filename,unicode): + filename=filename.encode('utf-8') + filemd5 = _md5(filename).hexdigest() + filename = re_url_scheme.sub("", filename) + filename = re_slash.sub(",", filename) + + # limit length of filename + if len(filename)>200: + filename=filename[:200] + return ",".join((filename, filemd5)) + +NORMALIZE_SPACE = re.compile(r'(?:\r\n)?[ \t]+') +def _normalize_headers(headers): + return dict([ (key.lower(), NORMALIZE_SPACE.sub(value, ' ').strip()) for (key, value) in headers.iteritems()]) + +def _parse_cache_control(headers): + retval = {} + if headers.has_key('cache-control'): + parts = headers['cache-control'].split(',') + parts_with_args = [tuple([x.strip().lower() for x in part.split("=", 1)]) for part in parts if -1 != part.find("=")] + parts_wo_args = [(name.strip().lower(), 1) for name in parts if -1 == name.find("=")] + retval = dict(parts_with_args + parts_wo_args) + return retval + +# Whether to use a strict mode to parse WWW-Authenticate headers +# Might lead to bad results in case of ill-formed header value, +# so disabled by default, falling back to relaxed parsing. +# Set to true to turn on, usefull for testing servers. +USE_WWW_AUTH_STRICT_PARSING = 0 + +# In regex below: +# [^\0-\x1f\x7f-\xff()<>@,;:\\\"/[\]?={} \t]+ matches a "token" as defined by HTTP +# "(?:[^\0-\x08\x0A-\x1f\x7f-\xff\\\"]|\\[\0-\x7f])*?" matches a "quoted-string" as defined by HTTP, when LWS have already been replaced by a single space +# Actually, as an auth-param value can be either a token or a quoted-string, they are combined in a single pattern which matches both: +# \"?((?<=\")(?:[^\0-\x1f\x7f-\xff\\\"]|\\[\0-\x7f])*?(?=\")|(?@,;:\\\"/[\]?={} \t]+(?!\"))\"? +WWW_AUTH_STRICT = re.compile(r"^(?:\s*(?:,\s*)?([^\0-\x1f\x7f-\xff()<>@,;:\\\"/[\]?={} \t]+)\s*=\s*\"?((?<=\")(?:[^\0-\x08\x0A-\x1f\x7f-\xff\\\"]|\\[\0-\x7f])*?(?=\")|(?@,;:\\\"/[\]?={} \t]+(?!\"))\"?)(.*)$") +WWW_AUTH_RELAXED = re.compile(r"^(?:\s*(?:,\s*)?([^ \t\r\n=]+)\s*=\s*\"?((?<=\")(?:[^\\\"]|\\.)*?(?=\")|(? current_age: + retval = "FRESH" + return retval + +def _decompressContent(response, new_content): + content = new_content + try: + encoding = response.get('content-encoding', None) + if encoding in ['gzip', 'deflate']: + if encoding == 'gzip': + content = gzip.GzipFile(fileobj=StringIO.StringIO(new_content)).read() + if encoding == 'deflate': + content = zlib.decompress(content) + response['content-length'] = str(len(content)) + # Record the historical presence of the encoding in a way the won't interfere. + response['-content-encoding'] = response['content-encoding'] + del response['content-encoding'] + except IOError: + content = "" + raise FailedToDecompressContent(_("Content purported to be compressed with %s but failed to decompress.") % response.get('content-encoding'), response, content) + return content + +def _updateCache(request_headers, response_headers, content, cache, cachekey): + if cachekey: + cc = _parse_cache_control(request_headers) + cc_response = _parse_cache_control(response_headers) + if cc.has_key('no-store') or cc_response.has_key('no-store'): + cache.delete(cachekey) + else: + info = email.Message.Message() + for key, value in response_headers.iteritems(): + if key not in ['status','content-encoding','transfer-encoding']: + info[key] = value + + # Add annotations to the cache to indicate what headers + # are variant for this request. + vary = response_headers.get('vary', None) + if vary: + vary_headers = vary.lower().replace(' ', '').split(',') + for header in vary_headers: + key = '-varied-%s' % header + try: + info[key] = request_headers[header] + except KeyError: + pass + + status = response_headers.status + if status == 304: + status = 200 + + status_header = 'status: %d\r\n' % status + + header_str = info.as_string() + + header_str = re.sub("\r(?!\n)|(? 0: + service = "cl" + # No point in guessing Base or Spreadsheet + #elif request_uri.find("spreadsheets") > 0: + # service = "wise" + + auth = dict(Email=credentials[0], Passwd=credentials[1], service=service, source=headers['user-agent']) + resp, content = self.http.request("https://www.google.com/accounts/ClientLogin", method="POST", body=urlencode(auth), headers={'Content-Type': 'application/x-www-form-urlencoded'}) + lines = content.split('\n') + d = dict([tuple(line.split("=", 1)) for line in lines if line]) + if resp.status == 403: + self.Auth = "" + else: + self.Auth = d['Auth'] + + def request(self, method, request_uri, headers, content): + """Modify the request headers to add the appropriate + Authorization header.""" + headers['authorization'] = 'GoogleLogin Auth=' + self.Auth + + +AUTH_SCHEME_CLASSES = { + "basic": BasicAuthentication, + "wsse": WsseAuthentication, + "digest": DigestAuthentication, + "hmacdigest": HmacDigestAuthentication, + "googlelogin": GoogleLoginAuthentication +} + +AUTH_SCHEME_ORDER = ["hmacdigest", "googlelogin", "digest", "wsse", "basic"] + +class FileCache(object): + """Uses a local directory as a store for cached files. + Not really safe to use if multiple threads or processes are going to + be running on the same cache. + """ + def __init__(self, cache, safe=safename): # use safe=lambda x: md5.new(x).hexdigest() for the old behavior + self.cache = cache + self.safe = safe + if not os.path.exists(cache): + os.makedirs(self.cache) + + def get(self, key): + retval = None + cacheFullPath = os.path.join(self.cache, self.safe(key)) + try: + f = file(cacheFullPath, "rb") + retval = f.read() + f.close() + except IOError: + pass + return retval + + def set(self, key, value): + cacheFullPath = os.path.join(self.cache, self.safe(key)) + f = file(cacheFullPath, "wb") + f.write(value) + f.close() + + def delete(self, key): + cacheFullPath = os.path.join(self.cache, self.safe(key)) + if os.path.exists(cacheFullPath): + os.remove(cacheFullPath) + +class Credentials(object): + def __init__(self): + self.credentials = [] + + def add(self, name, password, domain=""): + self.credentials.append((domain.lower(), name, password)) + + def clear(self): + self.credentials = [] + + def iter(self, domain): + for (cdomain, name, password) in self.credentials: + if cdomain == "" or domain == cdomain: + yield (name, password) + +class KeyCerts(Credentials): + """Identical to Credentials except that + name/password are mapped to key/cert.""" + pass + +class AllHosts(object): + pass + +class ProxyInfo(object): + """Collect information required to use a proxy.""" + bypass_hosts = () + + def __init__(self, proxy_type, proxy_host, proxy_port, + proxy_rdns=None, proxy_user=None, proxy_pass=None): + """The parameter proxy_type must be set to one of socks.PROXY_TYPE_XXX + constants. For example: + + p = ProxyInfo(proxy_type=socks.PROXY_TYPE_HTTP, + proxy_host='localhost', proxy_port=8000) + """ + self.proxy_type = proxy_type + self.proxy_host = proxy_host + self.proxy_port = proxy_port + self.proxy_rdns = proxy_rdns + self.proxy_user = proxy_user + self.proxy_pass = proxy_pass + + def astuple(self): + return (self.proxy_type, self.proxy_host, self.proxy_port, + self.proxy_rdns, self.proxy_user, self.proxy_pass) + + def isgood(self): + return (self.proxy_host != None) and (self.proxy_port != None) + + def applies_to(self, hostname): + return not self.bypass_host(hostname) + + def bypass_host(self, hostname): + """Has this host been excluded from the proxy config""" + if self.bypass_hosts is AllHosts: + return True + + bypass = False + for domain in self.bypass_hosts: + if hostname.endswith(domain): + bypass = True + + return bypass + + +def proxy_info_from_environment(method='http'): + """ + Read proxy info from the environment variables. + """ + if method not in ['http', 'https']: + return + + env_var = method + '_proxy' + url = os.environ.get(env_var, os.environ.get(env_var.upper())) + if not url: + return + pi = proxy_info_from_url(url, method) + + no_proxy = os.environ.get('no_proxy', os.environ.get('NO_PROXY', '')) + bypass_hosts = [] + if no_proxy: + bypass_hosts = no_proxy.split(',') + # special case, no_proxy=* means all hosts bypassed + if no_proxy == '*': + bypass_hosts = AllHosts + + pi.bypass_hosts = bypass_hosts + return pi + +def proxy_info_from_url(url, method='http'): + """ + Construct a ProxyInfo from a URL (such as http_proxy env var) + """ + url = urlparse.urlparse(url) + username = None + password = None + port = None + if '@' in url[1]: + ident, host_port = url[1].split('@', 1) + if ':' in ident: + username, password = ident.split(':', 1) + else: + password = ident + else: + host_port = url[1] + if ':' in host_port: + host, port = host_port.split(':', 1) + else: + host = host_port + + if port: + port = int(port) + else: + port = dict(https=443, http=80)[method] + + proxy_type = 3 # socks.PROXY_TYPE_HTTP + return ProxyInfo( + proxy_type = proxy_type, + proxy_host = host, + proxy_port = port, + proxy_user = username or None, + proxy_pass = password or None, + ) + + +class HTTPConnectionWithTimeout(httplib.HTTPConnection): + """ + HTTPConnection subclass that supports timeouts + + All timeouts are in seconds. If None is passed for timeout then + Python's default timeout for sockets will be used. See for example + the docs of socket.setdefaulttimeout(): + http://docs.python.org/library/socket.html#socket.setdefaulttimeout + """ + + def __init__(self, host, port=None, strict=None, timeout=None, proxy_info=None): + httplib.HTTPConnection.__init__(self, host, port, strict) + self.timeout = timeout + self.proxy_info = proxy_info + + def connect(self): + """Connect to the host and port specified in __init__.""" + # Mostly verbatim from httplib.py. + if self.proxy_info and socks is None: + raise ProxiesUnavailableError( + 'Proxy support missing but proxy use was requested!') + msg = "getaddrinfo returns an empty list" + if self.proxy_info and self.proxy_info.isgood(): + use_proxy = True + proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass = self.proxy_info.astuple() + else: + use_proxy = False + if use_proxy and proxy_rdns: + host = proxy_host + port = proxy_port + else: + host = self.host + port = self.port + + for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM): + af, socktype, proto, canonname, sa = res + try: + if use_proxy: + self.sock = socks.socksocket(af, socktype, proto) + self.sock.setproxy(proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass) + else: + self.sock = socket.socket(af, socktype, proto) + self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + # Different from httplib: support timeouts. + if has_timeout(self.timeout): + self.sock.settimeout(self.timeout) + # End of difference from httplib. + if self.debuglevel > 0: + print "connect: (%s, %s) ************" % (self.host, self.port) + if use_proxy: + print "proxy: %s ************" % str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass)) + + self.sock.connect((self.host, self.port) + sa[2:]) + except socket.error, msg: + if self.debuglevel > 0: + print "connect fail: (%s, %s)" % (self.host, self.port) + if use_proxy: + print "proxy: %s" % str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass)) + if self.sock: + self.sock.close() + self.sock = None + continue + break + if not self.sock: + raise socket.error, msg + +class HTTPSConnectionWithTimeout(httplib.HTTPSConnection): + """ + This class allows communication via SSL. + + All timeouts are in seconds. If None is passed for timeout then + Python's default timeout for sockets will be used. See for example + the docs of socket.setdefaulttimeout(): + http://docs.python.org/library/socket.html#socket.setdefaulttimeout + """ + def __init__(self, host, port=None, key_file=None, cert_file=None, + strict=None, timeout=None, proxy_info=None, + ca_certs=None, disable_ssl_certificate_validation=False): + httplib.HTTPSConnection.__init__(self, host, port=port, + key_file=key_file, + cert_file=cert_file, strict=strict) + self.timeout = timeout + self.proxy_info = proxy_info + if ca_certs is None: + ca_certs = CA_CERTS + self.ca_certs = ca_certs + self.disable_ssl_certificate_validation = \ + disable_ssl_certificate_validation + + # The following two methods were adapted from https_wrapper.py, released + # with the Google Appengine SDK at + # http://googleappengine.googlecode.com/svn-history/r136/trunk/python/google/appengine/tools/https_wrapper.py + # under the following license: + # + # Copyright 2007 Google Inc. + # + # Licensed under the Apache License, Version 2.0 (the "License"); + # you may not use this file except in compliance with the License. + # You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License is distributed on an "AS IS" BASIS, + # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + # See the License for the specific language governing permissions and + # limitations under the License. + # + + def _GetValidHostsForCert(self, cert): + """Returns a list of valid host globs for an SSL certificate. + + Args: + cert: A dictionary representing an SSL certificate. + Returns: + list: A list of valid host globs. + """ + if 'subjectAltName' in cert: + return [x[1] for x in cert['subjectAltName'] + if x[0].lower() == 'dns'] + else: + return [x[0][1] for x in cert['subject'] + if x[0][0].lower() == 'commonname'] + + def _ValidateCertificateHostname(self, cert, hostname): + """Validates that a given hostname is valid for an SSL certificate. + + Args: + cert: A dictionary representing an SSL certificate. + hostname: The hostname to test. + Returns: + bool: Whether or not the hostname is valid for this certificate. + """ + hosts = self._GetValidHostsForCert(cert) + for host in hosts: + host_re = host.replace('.', '\.').replace('*', '[^.]*') + if re.search('^%s$' % (host_re,), hostname, re.I): + return True + return False + + def connect(self): + "Connect to a host on a given (SSL) port." + + msg = "getaddrinfo returns an empty list" + if self.proxy_info and self.proxy_info.isgood(): + use_proxy = True + proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass = self.proxy_info.astuple() + else: + use_proxy = False + if use_proxy and proxy_rdns: + host = proxy_host + port = proxy_port + else: + host = self.host + port = self.port + + address_info = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM) + for family, socktype, proto, canonname, sockaddr in address_info: + try: + if use_proxy: + sock = socks.socksocket(family, socktype, proto) + + sock.setproxy(proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass) + else: + sock = socket.socket(family, socktype, proto) + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + + if has_timeout(self.timeout): + sock.settimeout(self.timeout) + sock.connect((self.host, self.port)) + self.sock =_ssl_wrap_socket( + sock, self.key_file, self.cert_file, + self.disable_ssl_certificate_validation, self.ca_certs) + if self.debuglevel > 0: + print "connect: (%s, %s)" % (self.host, self.port) + if use_proxy: + print "proxy: %s" % str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass)) + if not self.disable_ssl_certificate_validation: + cert = self.sock.getpeercert() + hostname = self.host.split(':', 0)[0] + if not self._ValidateCertificateHostname(cert, hostname): + raise CertificateHostnameMismatch( + 'Server presented certificate that does not match ' + 'host %s: %s' % (hostname, cert), hostname, cert) + except ssl_SSLError, e: + if sock: + sock.close() + if self.sock: + self.sock.close() + self.sock = None + # Unfortunately the ssl module doesn't seem to provide any way + # to get at more detailed error information, in particular + # whether the error is due to certificate validation or + # something else (such as SSL protocol mismatch). + if e.errno == ssl.SSL_ERROR_SSL: + raise SSLHandshakeError(e) + else: + raise + except (socket.timeout, socket.gaierror): + raise + except socket.error, msg: + if self.debuglevel > 0: + print "connect fail: (%s, %s)" % (self.host, self.port) + if use_proxy: + print "proxy: %s" % str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass)) + if self.sock: + self.sock.close() + self.sock = None + continue + break + if not self.sock: + raise socket.error, msg + +SCHEME_TO_CONNECTION = { + 'http': HTTPConnectionWithTimeout, + 'https': HTTPSConnectionWithTimeout +} + +# Use a different connection object for Google App Engine +try: + try: + from google.appengine.api import apiproxy_stub_map + if apiproxy_stub_map.apiproxy.GetStub('urlfetch') is None: + raise ImportError # Bail out; we're not actually running on App Engine. + from google.appengine.api.urlfetch import fetch + from google.appengine.api.urlfetch import InvalidURLError + except (ImportError, AttributeError): + from google3.apphosting.api import apiproxy_stub_map + if apiproxy_stub_map.apiproxy.GetStub('urlfetch') is None: + raise ImportError # Bail out; we're not actually running on App Engine. + from google3.apphosting.api.urlfetch import fetch + from google3.apphosting.api.urlfetch import InvalidURLError + + def _new_fixed_fetch(validate_certificate): + def fixed_fetch(url, payload=None, method="GET", headers={}, + allow_truncated=False, follow_redirects=True, + deadline=None): + if deadline is None: + deadline = socket.getdefaulttimeout() or 5 + return fetch(url, payload=payload, method=method, headers=headers, + allow_truncated=allow_truncated, + follow_redirects=follow_redirects, deadline=deadline, + validate_certificate=validate_certificate) + return fixed_fetch + + class AppEngineHttpConnection(httplib.HTTPConnection): + """Use httplib on App Engine, but compensate for its weirdness. + + The parameters key_file, cert_file, proxy_info, ca_certs, and + disable_ssl_certificate_validation are all dropped on the ground. + """ + def __init__(self, host, port=None, key_file=None, cert_file=None, + strict=None, timeout=None, proxy_info=None, ca_certs=None, + disable_ssl_certificate_validation=False): + httplib.HTTPConnection.__init__(self, host, port=port, + strict=strict, timeout=timeout) + + class AppEngineHttpsConnection(httplib.HTTPSConnection): + """Same as AppEngineHttpConnection, but for HTTPS URIs.""" + def __init__(self, host, port=None, key_file=None, cert_file=None, + strict=None, timeout=None, proxy_info=None, ca_certs=None, + disable_ssl_certificate_validation=False): + httplib.HTTPSConnection.__init__(self, host, port=port, + key_file=key_file, + cert_file=cert_file, strict=strict, + timeout=timeout) + self._fetch = _new_fixed_fetch( + not disable_ssl_certificate_validation) + + # Update the connection classes to use the Googel App Engine specific ones. + SCHEME_TO_CONNECTION = { + 'http': AppEngineHttpConnection, + 'https': AppEngineHttpsConnection + } +except (ImportError, AttributeError): + pass + + +class Http(object): + """An HTTP client that handles: + + - all methods + - caching + - ETags + - compression, + - HTTPS + - Basic + - Digest + - WSSE + + and more. + """ + def __init__(self, cache=None, timeout=None, + proxy_info=proxy_info_from_environment, + ca_certs=None, disable_ssl_certificate_validation=False): + """If 'cache' is a string then it is used as a directory name for + a disk cache. Otherwise it must be an object that supports the + same interface as FileCache. + + All timeouts are in seconds. If None is passed for timeout + then Python's default timeout for sockets will be used. See + for example the docs of socket.setdefaulttimeout(): + http://docs.python.org/library/socket.html#socket.setdefaulttimeout + + `proxy_info` may be: + - a callable that takes the http scheme ('http' or 'https') and + returns a ProxyInfo instance per request. By default, uses + proxy_nfo_from_environment. + - a ProxyInfo instance (static proxy config). + - None (proxy disabled). + + ca_certs is the path of a file containing root CA certificates for SSL + server certificate validation. By default, a CA cert file bundled with + httplib2 is used. + + If disable_ssl_certificate_validation is true, SSL cert validation will + not be performed. + """ + self.proxy_info = proxy_info + self.ca_certs = ca_certs + self.disable_ssl_certificate_validation = \ + disable_ssl_certificate_validation + + # Map domain name to an httplib connection + self.connections = {} + # The location of the cache, for now a directory + # where cached responses are held. + if cache and isinstance(cache, basestring): + self.cache = FileCache(cache) + else: + self.cache = cache + + # Name/password + self.credentials = Credentials() + + # Key/cert + self.certificates = KeyCerts() + + # authorization objects + self.authorizations = [] + + # If set to False then no redirects are followed, even safe ones. + self.follow_redirects = True + + # Which HTTP methods do we apply optimistic concurrency to, i.e. + # which methods get an "if-match:" etag header added to them. + self.optimistic_concurrency_methods = ["PUT", "PATCH"] + + # If 'follow_redirects' is True, and this is set to True then + # all redirecs are followed, including unsafe ones. + self.follow_all_redirects = False + + self.ignore_etag = False + + self.force_exception_to_status_code = False + + self.timeout = timeout + + # Keep Authorization: headers on a redirect. + self.forward_authorization_headers = False + + def __getstate__(self): + state_dict = copy.copy(self.__dict__) + # In case request is augmented by some foreign object such as + # credentials which handle auth + if 'request' in state_dict: + del state_dict['request'] + if 'connections' in state_dict: + del state_dict['connections'] + return state_dict + + def __setstate__(self, state): + self.__dict__.update(state) + self.connections = {} + + def _auth_from_challenge(self, host, request_uri, headers, response, content): + """A generator that creates Authorization objects + that can be applied to requests. + """ + challenges = _parse_www_authenticate(response, 'www-authenticate') + for cred in self.credentials.iter(host): + for scheme in AUTH_SCHEME_ORDER: + if challenges.has_key(scheme): + yield AUTH_SCHEME_CLASSES[scheme](cred, host, request_uri, headers, response, content, self) + + def add_credentials(self, name, password, domain=""): + """Add a name and password that will be used + any time a request requires authentication.""" + self.credentials.add(name, password, domain) + + def add_certificate(self, key, cert, domain): + """Add a key and cert that will be used + any time a request requires authentication.""" + self.certificates.add(key, cert, domain) + + def clear_credentials(self): + """Remove all the names and passwords + that are used for authentication""" + self.credentials.clear() + self.authorizations = [] + + def _conn_request(self, conn, request_uri, method, body, headers): + i = 0 + seen_bad_status_line = False + while i < RETRIES: + i += 1 + try: + if hasattr(conn, 'sock') and conn.sock is None: + conn.connect() + conn.request(method, request_uri, body, headers) + except socket.timeout: + raise + except socket.gaierror: + conn.close() + raise ServerNotFoundError("Unable to find the server at %s" % conn.host) + except ssl_SSLError: + conn.close() + raise + except socket.error, e: + err = 0 + if hasattr(e, 'args'): + err = getattr(e, 'args')[0] + else: + err = e.errno + if err == errno.ECONNREFUSED: # Connection refused + raise + except httplib.HTTPException: + # Just because the server closed the connection doesn't apparently mean + # that the server didn't send a response. + if hasattr(conn, 'sock') and conn.sock is None: + if i < RETRIES-1: + conn.close() + conn.connect() + continue + else: + conn.close() + raise + if i < RETRIES-1: + conn.close() + conn.connect() + continue + try: + response = conn.getresponse() + except httplib.BadStatusLine: + # If we get a BadStatusLine on the first try then that means + # the connection just went stale, so retry regardless of the + # number of RETRIES set. + if not seen_bad_status_line and i == 1: + i = 0 + seen_bad_status_line = True + conn.close() + conn.connect() + continue + else: + conn.close() + raise + except (socket.error, httplib.HTTPException): + if i < RETRIES-1: + conn.close() + conn.connect() + continue + else: + conn.close() + raise + else: + content = "" + if method == "HEAD": + conn.close() + else: + content = response.read() + response = Response(response) + if method != "HEAD": + content = _decompressContent(response, content) + break + return (response, content) + + + def _request(self, conn, host, absolute_uri, request_uri, method, body, headers, redirections, cachekey): + """Do the actual request using the connection object + and also follow one level of redirects if necessary""" + + auths = [(auth.depth(request_uri), auth) for auth in self.authorizations if auth.inscope(host, request_uri)] + auth = auths and sorted(auths)[0][1] or None + if auth: + auth.request(method, request_uri, headers, body) + + (response, content) = self._conn_request(conn, request_uri, method, body, headers) + + if auth: + if auth.response(response, body): + auth.request(method, request_uri, headers, body) + (response, content) = self._conn_request(conn, request_uri, method, body, headers ) + response._stale_digest = 1 + + if response.status == 401: + for authorization in self._auth_from_challenge(host, request_uri, headers, response, content): + authorization.request(method, request_uri, headers, body) + (response, content) = self._conn_request(conn, request_uri, method, body, headers, ) + if response.status != 401: + self.authorizations.append(authorization) + authorization.response(response, body) + break + + if (self.follow_all_redirects or (method in ["GET", "HEAD"]) or response.status == 303): + if self.follow_redirects and response.status in [300, 301, 302, 303, 307]: + # Pick out the location header and basically start from the beginning + # remembering first to strip the ETag header and decrement our 'depth' + if redirections: + if not response.has_key('location') and response.status != 300: + raise RedirectMissingLocation( _("Redirected but the response is missing a Location: header."), response, content) + # Fix-up relative redirects (which violate an RFC 2616 MUST) + if response.has_key('location'): + location = response['location'] + (scheme, authority, path, query, fragment) = parse_uri(location) + if authority == None: + response['location'] = urlparse.urljoin(absolute_uri, location) + if response.status == 301 and method in ["GET", "HEAD"]: + response['-x-permanent-redirect-url'] = response['location'] + if not response.has_key('content-location'): + response['content-location'] = absolute_uri + _updateCache(headers, response, content, self.cache, cachekey) + if headers.has_key('if-none-match'): + del headers['if-none-match'] + if headers.has_key('if-modified-since'): + del headers['if-modified-since'] + if 'authorization' in headers and not self.forward_authorization_headers: + del headers['authorization'] + if response.has_key('location'): + location = response['location'] + old_response = copy.deepcopy(response) + if not old_response.has_key('content-location'): + old_response['content-location'] = absolute_uri + redirect_method = method + if response.status in [302, 303]: + redirect_method = "GET" + body = None + (response, content) = self.request( + location, method=redirect_method, + body=body, headers=headers, + redirections=redirections - 1) + response.previous = old_response + else: + raise RedirectLimit("Redirected more times than rediection_limit allows.", response, content) + elif response.status in [200, 203] and method in ["GET", "HEAD"]: + # Don't cache 206's since we aren't going to handle byte range requests + if not response.has_key('content-location'): + response['content-location'] = absolute_uri + _updateCache(headers, response, content, self.cache, cachekey) + + return (response, content) + + def _normalize_headers(self, headers): + return _normalize_headers(headers) + +# Need to catch and rebrand some exceptions +# Then need to optionally turn all exceptions into status codes +# including all socket.* and httplib.* exceptions. + + + def request(self, uri, method="GET", body=None, headers=None, redirections=DEFAULT_MAX_REDIRECTS, connection_type=None): + """ Performs a single HTTP request. + + The 'uri' is the URI of the HTTP resource and can begin with either + 'http' or 'https'. The value of 'uri' must be an absolute URI. + + The 'method' is the HTTP method to perform, such as GET, POST, DELETE, + etc. There is no restriction on the methods allowed. + + The 'body' is the entity body to be sent with the request. It is a + string object. + + Any extra headers that are to be sent with the request should be + provided in the 'headers' dictionary. + + The maximum number of redirect to follow before raising an + exception is 'redirections. The default is 5. + + The return value is a tuple of (response, content), the first + being and instance of the 'Response' class, the second being + a string that contains the response entity body. + """ + try: + if headers is None: + headers = {} + else: + headers = self._normalize_headers(headers) + + if not headers.has_key('user-agent'): + headers['user-agent'] = "Python-httplib2/%s (gzip)" % __version__ + + uri = iri2uri(uri) + + (scheme, authority, request_uri, defrag_uri) = urlnorm(uri) + domain_port = authority.split(":")[0:2] + if len(domain_port) == 2 and domain_port[1] == '443' and scheme == 'http': + scheme = 'https' + authority = domain_port[0] + + proxy_info = self._get_proxy_info(scheme, authority) + + conn_key = scheme+":"+authority + if conn_key in self.connections: + conn = self.connections[conn_key] + else: + if not connection_type: + connection_type = SCHEME_TO_CONNECTION[scheme] + certs = list(self.certificates.iter(authority)) + if scheme == 'https': + if certs: + conn = self.connections[conn_key] = connection_type( + authority, key_file=certs[0][0], + cert_file=certs[0][1], timeout=self.timeout, + proxy_info=proxy_info, + ca_certs=self.ca_certs, + disable_ssl_certificate_validation= + self.disable_ssl_certificate_validation) + else: + conn = self.connections[conn_key] = connection_type( + authority, timeout=self.timeout, + proxy_info=proxy_info, + ca_certs=self.ca_certs, + disable_ssl_certificate_validation= + self.disable_ssl_certificate_validation) + else: + conn = self.connections[conn_key] = connection_type( + authority, timeout=self.timeout, + proxy_info=proxy_info) + conn.set_debuglevel(debuglevel) + + if 'range' not in headers and 'accept-encoding' not in headers: + headers['accept-encoding'] = 'gzip, deflate' + + info = email.Message.Message() + cached_value = None + if self.cache: + cachekey = defrag_uri + cached_value = self.cache.get(cachekey) + if cached_value: + # info = email.message_from_string(cached_value) + # + # Need to replace the line above with the kludge below + # to fix the non-existent bug not fixed in this + # bug report: http://mail.python.org/pipermail/python-bugs-list/2005-September/030289.html + try: + info, content = cached_value.split('\r\n\r\n', 1) + feedparser = email.FeedParser.FeedParser() + feedparser.feed(info) + info = feedparser.close() + feedparser._parse = None + except (IndexError, ValueError): + self.cache.delete(cachekey) + cachekey = None + cached_value = None + else: + cachekey = None + + if method in self.optimistic_concurrency_methods and self.cache and info.has_key('etag') and not self.ignore_etag and 'if-match' not in headers: + # http://www.w3.org/1999/04/Editing/ + headers['if-match'] = info['etag'] + + if method not in ["GET", "HEAD"] and self.cache and cachekey: + # RFC 2616 Section 13.10 + self.cache.delete(cachekey) + + # Check the vary header in the cache to see if this request + # matches what varies in the cache. + if method in ['GET', 'HEAD'] and 'vary' in info: + vary = info['vary'] + vary_headers = vary.lower().replace(' ', '').split(',') + for header in vary_headers: + key = '-varied-%s' % header + value = info[key] + if headers.get(header, None) != value: + cached_value = None + break + + if cached_value and method in ["GET", "HEAD"] and self.cache and 'range' not in headers: + if info.has_key('-x-permanent-redirect-url'): + # Should cached permanent redirects be counted in our redirection count? For now, yes. + if redirections <= 0: + raise RedirectLimit("Redirected more times than rediection_limit allows.", {}, "") + (response, new_content) = self.request( + info['-x-permanent-redirect-url'], method='GET', + headers=headers, redirections=redirections - 1) + response.previous = Response(info) + response.previous.fromcache = True + else: + # Determine our course of action: + # Is the cached entry fresh or stale? + # Has the client requested a non-cached response? + # + # There seems to be three possible answers: + # 1. [FRESH] Return the cache entry w/o doing a GET + # 2. [STALE] Do the GET (but add in cache validators if available) + # 3. [TRANSPARENT] Do a GET w/o any cache validators (Cache-Control: no-cache) on the request + entry_disposition = _entry_disposition(info, headers) + + if entry_disposition == "FRESH": + if not cached_value: + info['status'] = '504' + content = "" + response = Response(info) + if cached_value: + response.fromcache = True + return (response, content) + + if entry_disposition == "STALE": + if info.has_key('etag') and not self.ignore_etag and not 'if-none-match' in headers: + headers['if-none-match'] = info['etag'] + if info.has_key('last-modified') and not 'last-modified' in headers: + headers['if-modified-since'] = info['last-modified'] + elif entry_disposition == "TRANSPARENT": + pass + + (response, new_content) = self._request(conn, authority, uri, request_uri, method, body, headers, redirections, cachekey) + + if response.status == 304 and method == "GET": + # Rewrite the cache entry with the new end-to-end headers + # Take all headers that are in response + # and overwrite their values in info. + # unless they are hop-by-hop, or are listed in the connection header. + + for key in _get_end2end_headers(response): + info[key] = response[key] + merged_response = Response(info) + if hasattr(response, "_stale_digest"): + merged_response._stale_digest = response._stale_digest + _updateCache(headers, merged_response, content, self.cache, cachekey) + response = merged_response + response.status = 200 + response.fromcache = True + + elif response.status == 200: + content = new_content + else: + self.cache.delete(cachekey) + content = new_content + else: + cc = _parse_cache_control(headers) + if cc.has_key('only-if-cached'): + info['status'] = '504' + response = Response(info) + content = "" + else: + (response, content) = self._request(conn, authority, uri, request_uri, method, body, headers, redirections, cachekey) + except Exception, e: + if self.force_exception_to_status_code: + if isinstance(e, HttpLib2ErrorWithResponse): + response = e.response + content = e.content + response.status = 500 + response.reason = str(e) + elif isinstance(e, socket.timeout): + content = "Request Timeout" + response = Response({ + "content-type": "text/plain", + "status": "408", + "content-length": len(content) + }) + response.reason = "Request Timeout" + else: + content = str(e) + response = Response({ + "content-type": "text/plain", + "status": "400", + "content-length": len(content) + }) + response.reason = "Bad Request" + else: + raise + + + return (response, content) + + def _get_proxy_info(self, scheme, authority): + """Return a ProxyInfo instance (or None) based on the scheme + and authority. + """ + hostname, port = urllib.splitport(authority) + proxy_info = self.proxy_info + if callable(proxy_info): + proxy_info = proxy_info(scheme) + + if (hasattr(proxy_info, 'applies_to') + and not proxy_info.applies_to(hostname)): + proxy_info = None + return proxy_info + + +class Response(dict): + """An object more like email.Message than httplib.HTTPResponse.""" + + """Is this response from our local cache""" + fromcache = False + + """HTTP protocol version used by server. 10 for HTTP/1.0, 11 for HTTP/1.1. """ + version = 11 + + "Status code returned by server. " + status = 200 + + """Reason phrase returned by server.""" + reason = "Ok" + + previous = None + + def __init__(self, info): + # info is either an email.Message or + # an httplib.HTTPResponse object. + if isinstance(info, httplib.HTTPResponse): + for key, value in info.getheaders(): + self[key.lower()] = value + self.status = info.status + self['status'] = str(self.status) + self.reason = info.reason + self.version = info.version + elif isinstance(info, email.Message.Message): + for key, value in info.items(): + self[key.lower()] = value + self.status = int(self['status']) + else: + for key, value in info.iteritems(): + self[key.lower()] = value + self.status = int(self.get('status', self.status)) + self.reason = self.get('reason', self.reason) + + + def __getattr__(self, name): + if name == 'dict': + return self + else: + raise AttributeError, name diff --git a/appengine/dashdemo/httplib2/cacerts.txt b/appengine/dashdemo/httplib2/cacerts.txt new file mode 100644 index 0000000..70990f1 --- /dev/null +++ b/appengine/dashdemo/httplib2/cacerts.txt @@ -0,0 +1,2183 @@ +# Issuer: CN=GTE CyberTrust Global Root O=GTE Corporation OU=GTE CyberTrust Solutions, Inc. +# Subject: CN=GTE CyberTrust Global Root O=GTE Corporation OU=GTE CyberTrust Solutions, Inc. +# Label: "GTE CyberTrust Global Root" +# Serial: 421 +# MD5 Fingerprint: ca:3d:d3:68:f1:03:5c:d0:32:fa:b8:2b:59:e8:5a:db +# SHA1 Fingerprint: 97:81:79:50:d8:1c:96:70:cc:34:d8:09:cf:79:44:31:36:7e:f4:74 +# SHA256 Fingerprint: a5:31:25:18:8d:21:10:aa:96:4b:02:c7:b7:c6:da:32:03:17:08:94:e5:fb:71:ff:fb:66:67:d5:e6:81:0a:36 +-----BEGIN CERTIFICATE----- +MIICWjCCAcMCAgGlMA0GCSqGSIb3DQEBBAUAMHUxCzAJBgNVBAYTAlVTMRgwFgYD +VQQKEw9HVEUgQ29ycG9yYXRpb24xJzAlBgNVBAsTHkdURSBDeWJlclRydXN0IFNv +bHV0aW9ucywgSW5jLjEjMCEGA1UEAxMaR1RFIEN5YmVyVHJ1c3QgR2xvYmFsIFJv +b3QwHhcNOTgwODEzMDAyOTAwWhcNMTgwODEzMjM1OTAwWjB1MQswCQYDVQQGEwJV +UzEYMBYGA1UEChMPR1RFIENvcnBvcmF0aW9uMScwJQYDVQQLEx5HVEUgQ3liZXJU +cnVzdCBTb2x1dGlvbnMsIEluYy4xIzAhBgNVBAMTGkdURSBDeWJlclRydXN0IEds +b2JhbCBSb290MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCVD6C28FCc6HrH +iM3dFw4usJTQGz0O9pTAipTHBsiQl8i4ZBp6fmw8U+E3KHNgf7KXUwefU/ltWJTS +r41tiGeA5u2ylc9yMcqlHHK6XALnZELn+aks1joNrI1CqiQBOeacPwGFVw1Yh0X4 +04Wqk2kmhXBIgD8SFcd5tB8FLztimQIDAQABMA0GCSqGSIb3DQEBBAUAA4GBAG3r +GwnpXtlR22ciYaQqPEh346B8pt5zohQDhT37qw4wxYMWM4ETCJ57NE7fQMh017l9 +3PR2VX2bY1QY6fDq81yx2YtCHrnAlU66+tXifPVoYb+O7AWXX1uw16OFNMQkpw0P +lZPvy5TYnh+dXIVtx6quTx8itc2VrbqnzPmrC3p/ +-----END CERTIFICATE----- + +# Issuer: CN=Thawte Server CA O=Thawte Consulting cc OU=Certification Services Division +# Subject: CN=Thawte Server CA O=Thawte Consulting cc OU=Certification Services Division +# Label: "Thawte Server CA" +# Serial: 1 +# MD5 Fingerprint: c5:70:c4:a2:ed:53:78:0c:c8:10:53:81:64:cb:d0:1d +# SHA1 Fingerprint: 23:e5:94:94:51:95:f2:41:48:03:b4:d5:64:d2:a3:a3:f5:d8:8b:8c +# SHA256 Fingerprint: b4:41:0b:73:e2:e6:ea:ca:47:fb:c4:2f:8f:a4:01:8a:f4:38:1d:c5:4c:fa:a8:44:50:46:1e:ed:09:45:4d:e9 +-----BEGIN CERTIFICATE----- +MIIDEzCCAnygAwIBAgIBATANBgkqhkiG9w0BAQQFADCBxDELMAkGA1UEBhMCWkEx +FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYD +VQQKExRUaGF3dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlv +biBTZXJ2aWNlcyBEaXZpc2lvbjEZMBcGA1UEAxMQVGhhd3RlIFNlcnZlciBDQTEm +MCQGCSqGSIb3DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5jb20wHhcNOTYwODAx +MDAwMDAwWhcNMjAxMjMxMjM1OTU5WjCBxDELMAkGA1UEBhMCWkExFTATBgNVBAgT +DFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYDVQQKExRUaGF3 +dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNl +cyBEaXZpc2lvbjEZMBcGA1UEAxMQVGhhd3RlIFNlcnZlciBDQTEmMCQGCSqGSIb3 +DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQAD +gY0AMIGJAoGBANOkUG7I/1Zr5s9dtuoMaHVHoqrC2oQl/Kj0R1HahbUgdJSGHg91 +yekIYfUGbTBuFRkC6VLAYttNmZ7iagxEOM3+vuNkCXDF/rFrKbYvScg71CcEJRCX +L+eQbcAoQpnXTEPew/UhbVSfXcNY4cDk2VuwuNy0e982OsK1ZiIS1ocNAgMBAAGj +EzARMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEEBQADgYEAB/pMaVz7lcxG +7oWDTSEwjsrZqG9JGubaUeNgcGyEYRGhGshIPllDfU+VPaGLtwtimHp1it2ITk6e +QNuozDJ0uW8NxuOzRAvZim+aKZuZGCg70eNAKJpaPNW15yAbi8qkq43pUdniTCxZ +qdq5snUb9kLy78fyGPmJvKP/iiMucEc= +-----END CERTIFICATE----- + +# Issuer: CN=Thawte Premium Server CA O=Thawte Consulting cc OU=Certification Services Division +# Subject: CN=Thawte Premium Server CA O=Thawte Consulting cc OU=Certification Services Division +# Label: "Thawte Premium Server CA" +# Serial: 1 +# MD5 Fingerprint: 06:9f:69:79:16:66:90:02:1b:8c:8c:a2:c3:07:6f:3a +# SHA1 Fingerprint: 62:7f:8d:78:27:65:63:99:d2:7d:7f:90:44:c9:fe:b3:f3:3e:fa:9a +# SHA256 Fingerprint: ab:70:36:36:5c:71:54:aa:29:c2:c2:9f:5d:41:91:16:3b:16:2a:22:25:01:13:57:d5:6d:07:ff:a7:bc:1f:72 +-----BEGIN CERTIFICATE----- +MIIDJzCCApCgAwIBAgIBATANBgkqhkiG9w0BAQQFADCBzjELMAkGA1UEBhMCWkEx +FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYD +VQQKExRUaGF3dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlv +biBTZXJ2aWNlcyBEaXZpc2lvbjEhMB8GA1UEAxMYVGhhd3RlIFByZW1pdW0gU2Vy +dmVyIENBMSgwJgYJKoZIhvcNAQkBFhlwcmVtaXVtLXNlcnZlckB0aGF3dGUuY29t +MB4XDTk2MDgwMTAwMDAwMFoXDTIwMTIzMTIzNTk1OVowgc4xCzAJBgNVBAYTAlpB +MRUwEwYDVQQIEwxXZXN0ZXJuIENhcGUxEjAQBgNVBAcTCUNhcGUgVG93bjEdMBsG +A1UEChMUVGhhd3RlIENvbnN1bHRpbmcgY2MxKDAmBgNVBAsTH0NlcnRpZmljYXRp +b24gU2VydmljZXMgRGl2aXNpb24xITAfBgNVBAMTGFRoYXd0ZSBQcmVtaXVtIFNl +cnZlciBDQTEoMCYGCSqGSIb3DQEJARYZcHJlbWl1bS1zZXJ2ZXJAdGhhd3RlLmNv +bTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA0jY2aovXwlue2oFBYo847kkE +VdbQ7xwblRZH7xhINTpS9CtqBo87L+pW46+GjZ4X9560ZXUCTe/LCaIhUdib0GfQ +ug2SBhRz1JPLlyoAnFxODLz6FVL88kRu2hFKbgifLy3j+ao6hnO2RlNYyIkFvYMR +uHM/qgeN9EJN50CdHDcCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG +9w0BAQQFAAOBgQAmSCwWwlj66BZ0DKqqX1Q/8tfJeGBeXm43YyJ3Nn6yF8Q0ufUI +hfzJATj/Tb7yFkJD57taRvvBxhEf8UqwKEbJw8RCfbz6q1lu1bdRiBHjpIUZa4JM +pAwSremkrj/xw0llmozFyD4lt5SZu5IycQfwhl7tUCemDaYj+bvLpgcUQg== +-----END CERTIFICATE----- + +# Issuer: O=Equifax OU=Equifax Secure Certificate Authority +# Subject: O=Equifax OU=Equifax Secure Certificate Authority +# Label: "Equifax Secure CA" +# Serial: 903804111 +# MD5 Fingerprint: 67:cb:9d:c0:13:24:8a:82:9b:b2:17:1e:d1:1b:ec:d4 +# SHA1 Fingerprint: d2:32:09:ad:23:d3:14:23:21:74:e4:0d:7f:9d:62:13:97:86:63:3a +# SHA256 Fingerprint: 08:29:7a:40:47:db:a2:36:80:c7:31:db:6e:31:76:53:ca:78:48:e1:be:bd:3a:0b:01:79:a7:07:f9:2c:f1:78 +-----BEGIN CERTIFICATE----- +MIIDIDCCAomgAwIBAgIENd70zzANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJV +UzEQMA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2Vy +dGlmaWNhdGUgQXV0aG9yaXR5MB4XDTk4MDgyMjE2NDE1MVoXDTE4MDgyMjE2NDE1 +MVowTjELMAkGA1UEBhMCVVMxEDAOBgNVBAoTB0VxdWlmYXgxLTArBgNVBAsTJEVx +dWlmYXggU2VjdXJlIENlcnRpZmljYXRlIEF1dGhvcml0eTCBnzANBgkqhkiG9w0B +AQEFAAOBjQAwgYkCgYEAwV2xWGcIYu6gmi0fCG2RFGiYCh7+2gRvE4RiIcPRfM6f +BeC4AfBONOziipUEZKzxa1NfBbPLZ4C/QgKO/t0BCezhABRP/PvwDN1Dulsr4R+A +cJkVV5MW8Q+XarfCaCMczE1ZMKxRHjuvK9buY0V7xdlfUNLjUA86iOe/FP3gx7kC +AwEAAaOCAQkwggEFMHAGA1UdHwRpMGcwZaBjoGGkXzBdMQswCQYDVQQGEwJVUzEQ +MA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2VydGlm +aWNhdGUgQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMBoGA1UdEAQTMBGBDzIwMTgw +ODIyMTY0MTUxWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUSOZo+SvSspXXR9gj +IBBPM5iQn9QwHQYDVR0OBBYEFEjmaPkr0rKV10fYIyAQTzOYkJ/UMAwGA1UdEwQF +MAMBAf8wGgYJKoZIhvZ9B0EABA0wCxsFVjMuMGMDAgbAMA0GCSqGSIb3DQEBBQUA +A4GBAFjOKer89961zgK5F7WF0bnj4JXMJTENAKaSbn+2kmOeUJXRmm/kEd5jhW6Y +7qj/WsjTVbJmcVfewCHrPSqnI0kBBIZCe/zuf6IWUrVnZ9NA2zsmWLIodz2uFHdh +1voqZiegDfqnc1zqcPGUIWVEX/r87yloqaKHee9570+sB3c4 +-----END CERTIFICATE----- + +# Issuer: O=VeriSign, Inc. OU=Class 3 Public Primary Certification Authority +# Subject: O=VeriSign, Inc. OU=Class 3 Public Primary Certification Authority +# Label: "Verisign Class 3 Public Primary Certification Authority" +# Serial: 149843929435818692848040365716851702463 +# MD5 Fingerprint: 10:fc:63:5d:f6:26:3e:0d:f3:25:be:5f:79:cd:67:67 +# SHA1 Fingerprint: 74:2c:31:92:e6:07:e4:24:eb:45:49:54:2b:e1:bb:c5:3e:61:74:e2 +# SHA256 Fingerprint: e7:68:56:34:ef:ac:f6:9a:ce:93:9a:6b:25:5b:7b:4f:ab:ef:42:93:5b:50:a2:65:ac:b5:cb:60:27:e4:4e:70 +-----BEGIN CERTIFICATE----- +MIICPDCCAaUCEHC65B0Q2Sk0tjjKewPMur8wDQYJKoZIhvcNAQECBQAwXzELMAkG +A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz +cyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2 +MDEyOTAwMDAwMFoXDTI4MDgwMTIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV +BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmlt +YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN +ADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhE +BarsAx94f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/is +I19wKTakyYbnsZogy1Olhec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0G +CSqGSIb3DQEBAgUAA4GBALtMEivPLCYATxQT3ab7/AoRhIzzKBxnki98tsX63/Do +lbwdj2wsqFHMc9ikwFPwTtYmwHYBV4GSXiHx0bH/59AhWM1pF+NEHJwZRDmJXNyc +AA9WjQKZ7aKQRUzkuxCkPfAyAw7xzvjoyVGM5mKf5p/AfbdynMk2OmufTqj/ZA1k +-----END CERTIFICATE----- + +# Issuer: O=VeriSign, Inc. OU=Class 3 Public Primary Certification Authority - G2/(c) 1998 VeriSign, Inc. - For authorized use only/VeriSign Trust Network +# Subject: O=VeriSign, Inc. OU=Class 3 Public Primary Certification Authority - G2/(c) 1998 VeriSign, Inc. - For authorized use only/VeriSign Trust Network +# Label: "Verisign Class 3 Public Primary Certification Authority - G2" +# Serial: 167285380242319648451154478808036881606 +# MD5 Fingerprint: a2:33:9b:4c:74:78:73:d4:6c:e7:c1:f3:8d:cb:5c:e9 +# SHA1 Fingerprint: 85:37:1c:a6:e5:50:14:3d:ce:28:03:47:1b:de:3a:09:e8:f8:77:0f +# SHA256 Fingerprint: 83:ce:3c:12:29:68:8a:59:3d:48:5f:81:97:3c:0f:91:95:43:1e:da:37:cc:5e:36:43:0e:79:c7:a8:88:63:8b +-----BEGIN CERTIFICATE----- +MIIDAjCCAmsCEH3Z/gfPqB63EHln+6eJNMYwDQYJKoZIhvcNAQEFBQAwgcExCzAJ +BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xh +c3MgMyBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcy +MTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3Jp +emVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMB4X +DTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVTMRcw +FQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMyBQdWJsaWMg +UHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEo +YykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5 +MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEB +AQUAA4GNADCBiQKBgQDMXtERXVxp0KvTuWpMmR9ZmDCOFoUgRm1HP9SFIIThbbP4 +pO0M8RcPO/mn+SXXwc+EY/J8Y8+iR/LGWzOOZEAEaMGAuWQcRXfH2G71lSk8UOg0 +13gfqLptQ5GVj0VXXn7F+8qkBOvqlzdUMG+7AUcyM83cV5tkaWH4mx0ciU9cZwID +AQABMA0GCSqGSIb3DQEBBQUAA4GBAFFNzb5cy5gZnBWyATl4Lk0PZ3BwmcYQWpSk +U01UbSuvDV1Ai2TT1+7eVmGSX6bEHRBhNtMsJzzoKQm5EWR0zLVznxxIqbxhAe7i +F6YM40AIOw7n60RzKprxaZLvcRTDOaxxp5EJb+RxBrO6WVcmeQD2+A2iMzAo1KpY +oJ2daZH9 +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign Root CA O=GlobalSign nv-sa OU=Root CA +# Subject: CN=GlobalSign Root CA O=GlobalSign nv-sa OU=Root CA +# Label: "GlobalSign Root CA" +# Serial: 4835703278459707669005204 +# MD5 Fingerprint: 3e:45:52:15:09:51:92:e1:b7:5d:37:9f:b1:87:29:8a +# SHA1 Fingerprint: b1:bc:96:8b:d4:f4:9d:62:2a:a8:9a:81:f2:15:01:52:a4:1d:82:9c +# SHA256 Fingerprint: eb:d4:10:40:e4:bb:3e:c7:42:c9:e3:81:d3:1e:f2:a4:1a:48:b6:68:5c:96:e7:ce:f3:c1:df:6c:d4:33:1c:99 +-----BEGIN CERTIFICATE----- +MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG +A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv +b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw +MDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i +YWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT +aWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ +jc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp +xy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp +1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG +snUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ +U26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8 +9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B +AQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz +yj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE +38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP +AbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad +DKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME +HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A== +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R2 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R2 +# Label: "GlobalSign Root CA - R2" +# Serial: 4835703278459682885658125 +# MD5 Fingerprint: 94:14:77:7e:3e:5e:fd:8f:30:bd:41:b0:cf:e7:d0:30 +# SHA1 Fingerprint: 75:e0:ab:b6:13:85:12:27:1c:04:f8:5f:dd:de:38:e4:b7:24:2e:fe +# SHA256 Fingerprint: ca:42:dd:41:74:5f:d0:b8:1e:b9:02:36:2c:f9:d8:bf:71:9d:a1:bd:1b:1e:fc:94:6f:5b:4c:99:f4:2c:1b:9e +-----BEGIN CERTIFICATE----- +MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4G +A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNp +Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1 +MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEG +A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6ErPL +v4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8 +eoLrvozps6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklq +tTleiDTsvHgMCJiEbKjNS7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzd +C9XZzPnqJworc5HGnRusyMvo4KD0L5CLTfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pa +zq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6CygPCm48CAwEAAaOBnDCB +mTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm+IH +V2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5n +bG9iYWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG +3lm0mi3f3BmGLjANBgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4Gs +J0/WwbgcQ3izDJr86iw8bmEbTUsp9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO +291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavS +ot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG79G+dwfCMNYxd +AfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7 +TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg== +-----END CERTIFICATE----- + +# Issuer: CN=http://www.valicert.com/ O=ValiCert, Inc. OU=ValiCert Class 1 Policy Validation Authority +# Subject: CN=http://www.valicert.com/ O=ValiCert, Inc. OU=ValiCert Class 1 Policy Validation Authority +# Label: "ValiCert Class 1 VA" +# Serial: 1 +# MD5 Fingerprint: 65:58:ab:15:ad:57:6c:1e:a8:a7:b5:69:ac:bf:ff:eb +# SHA1 Fingerprint: e5:df:74:3c:b6:01:c4:9b:98:43:dc:ab:8c:e8:6a:81:10:9f:e4:8e +# SHA256 Fingerprint: f4:c1:49:55:1a:30:13:a3:5b:c7:bf:fe:17:a7:f3:44:9b:c1:ab:5b:5a:0a:e7:4b:06:c2:3b:90:00:4c:01:04 +-----BEGIN CERTIFICATE----- +MIIC5zCCAlACAQEwDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0 +IFZhbGlkYXRpb24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAz +BgNVBAsTLFZhbGlDZXJ0IENsYXNzIDEgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9y +aXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG +9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTk5MDYyNTIyMjM0OFoXDTE5MDYy +NTIyMjM0OFowgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRpb24gTmV0d29y +azEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENs +YXNzIDEgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRw +Oi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNl +cnQuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDYWYJ6ibiWuqYvaG9Y +LqdUHAZu9OqNSLwxlBfw8068srg1knaw0KWlAdcAAxIiGQj4/xEjm84H9b9pGib+ +TunRf50sQB1ZaG6m+FiwnRqP0z/x3BkGgagO4DrdyFNFCQbmD3DD+kCmDuJWBQ8Y +TfwggtFzVXSNdnKgHZ0dwN0/cQIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAFBoPUn0 +LBwGlN+VYH+Wexf+T3GtZMjdd9LvWVXoP+iOBSoh8gfStadS/pyxtuJbdxdA6nLW +I8sogTLDAHkY7FkXicnGah5xyf23dKUlRWnFSKsZ4UWKJWsZ7uW7EvV/96aNUcPw +nXS3qT6gpf+2SQMT2iLM7XGCK5nPOrf1LXLI +-----END CERTIFICATE----- + +# Issuer: CN=http://www.valicert.com/ O=ValiCert, Inc. OU=ValiCert Class 2 Policy Validation Authority +# Subject: CN=http://www.valicert.com/ O=ValiCert, Inc. OU=ValiCert Class 2 Policy Validation Authority +# Label: "ValiCert Class 2 VA" +# Serial: 1 +# MD5 Fingerprint: a9:23:75:9b:ba:49:36:6e:31:c2:db:f2:e7:66:ba:87 +# SHA1 Fingerprint: 31:7a:2a:d0:7f:2b:33:5e:f5:a1:c3:4e:4b:57:e8:b7:d8:f1:fc:a6 +# SHA256 Fingerprint: 58:d0:17:27:9c:d4:dc:63:ab:dd:b1:96:a6:c9:90:6c:30:c4:e0:87:83:ea:e8:c1:60:99:54:d6:93:55:59:6b +-----BEGIN CERTIFICATE----- +MIIC5zCCAlACAQEwDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0 +IFZhbGlkYXRpb24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAz +BgNVBAsTLFZhbGlDZXJ0IENsYXNzIDIgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9y +aXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG +9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTk5MDYyNjAwMTk1NFoXDTE5MDYy +NjAwMTk1NFowgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRpb24gTmV0d29y +azEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENs +YXNzIDIgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRw +Oi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNl +cnQuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDOOnHK5avIWZJV16vY +dA757tn2VUdZZUcOBVXc65g2PFxTXdMwzzjsvUGJ7SVCCSRrCl6zfN1SLUzm1NZ9 +WlmpZdRJEy0kTRxQb7XBhVQ7/nHk01xC+YDgkRoKWzk2Z/M/VXwbP7RfZHM047QS +v4dk+NoS/zcnwbNDu+97bi5p9wIDAQABMA0GCSqGSIb3DQEBBQUAA4GBADt/UG9v +UJSZSWI4OB9L+KXIPqeCgfYrx+jFzug6EILLGACOTb2oWH+heQC1u+mNr0HZDzTu +IYEZoDJJKPTEjlbVUjP9UNV+mWwD5MlM/Mtsq2azSiGM5bUMMj4QssxsodyamEwC +W/POuZ6lcg5Ktz885hZo+L7tdEy8W9ViH0Pd +-----END CERTIFICATE----- + +# Issuer: CN=http://www.valicert.com/ O=ValiCert, Inc. OU=ValiCert Class 3 Policy Validation Authority +# Subject: CN=http://www.valicert.com/ O=ValiCert, Inc. OU=ValiCert Class 3 Policy Validation Authority +# Label: "RSA Root Certificate 1" +# Serial: 1 +# MD5 Fingerprint: a2:6f:53:b7:ee:40:db:4a:68:e7:fa:18:d9:10:4b:72 +# SHA1 Fingerprint: 69:bd:8c:f4:9c:d3:00:fb:59:2e:17:93:ca:55:6a:f3:ec:aa:35:fb +# SHA256 Fingerprint: bc:23:f9:8a:31:3c:b9:2d:e3:bb:fc:3a:5a:9f:44:61:ac:39:49:4c:4a:e1:5a:9e:9d:f1:31:e9:9b:73:01:9a +-----BEGIN CERTIFICATE----- +MIIC5zCCAlACAQEwDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0 +IFZhbGlkYXRpb24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAz +BgNVBAsTLFZhbGlDZXJ0IENsYXNzIDMgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9y +aXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG +9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTk5MDYyNjAwMjIzM1oXDTE5MDYy +NjAwMjIzM1owgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRpb24gTmV0d29y +azEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENs +YXNzIDMgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRw +Oi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNl +cnQuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDjmFGWHOjVsQaBalfD +cnWTq8+epvzzFlLWLU2fNUSoLgRNB0mKOCn1dzfnt6td3zZxFJmP3MKS8edgkpfs +2Ejcv8ECIMYkpChMMFp2bbFc893enhBxoYjHW5tBbcqwuI4V7q0zK89HBFx1cQqY +JJgpp0lZpd34t0NiYfPT4tBVPwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAFa7AliE +Zwgs3x/be0kz9dNnnfS0ChCzycUs4pJqcXgn8nCDQtM+z6lU9PHYkhaM0QTLS6vJ +n0WuPIqpsHEzXcjFV9+vqDWzf4mH6eglkrh/hXqu1rweN1gqZ8mRzyqBPu3GOd/A +PhmcGcwTTYJBtYze4D1gCCAPRX5ron+jjBXu +-----END CERTIFICATE----- + +# Issuer: CN=VeriSign Class 3 Public Primary Certification Authority - G3 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 1999 VeriSign, Inc. - For authorized use only +# Subject: CN=VeriSign Class 3 Public Primary Certification Authority - G3 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 1999 VeriSign, Inc. - For authorized use only +# Label: "Verisign Class 3 Public Primary Certification Authority - G3" +# Serial: 206684696279472310254277870180966723415 +# MD5 Fingerprint: cd:68:b6:a7:c7:c4:ce:75:e0:1d:4f:57:44:61:92:09 +# SHA1 Fingerprint: 13:2d:0d:45:53:4b:69:97:cd:b2:d5:c3:39:e2:55:76:60:9b:5c:c6 +# SHA256 Fingerprint: eb:04:cf:5e:b1:f3:9a:fa:76:2f:2b:b1:20:f2:96:cb:a5:20:c1:b9:7d:b1:58:95:65:b8:1c:b9:a1:7b:72:44 +-----BEGIN CERTIFICATE----- +MIIEGjCCAwICEQCbfgZJoz5iudXukEhxKe9XMA0GCSqGSIb3DQEBBQUAMIHKMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl +cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu +LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT +aWduIENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD +VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT +aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ +bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu +IENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg +LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMu6nFL8eB8aHm8b +N3O9+MlrlBIwT/A2R/XQkQr1F8ilYcEWQE37imGQ5XYgwREGfassbqb1EUGO+i2t +KmFZpGcmTNDovFJbcCAEWNF6yaRpvIMXZK0Fi7zQWM6NjPXr8EJJC52XJ2cybuGu +kxUccLwgTS8Y3pKI6GyFVxEa6X7jJhFUokWWVYPKMIno3Nij7SqAP395ZVc+FSBm +CC+Vk7+qRy+oRpfwEuL+wgorUeZ25rdGt+INpsyow0xZVYnm6FNcHOqd8GIWC6fJ +Xwzw3sJ2zq/3avL6QaaiMxTJ5Xpj055iN9WFZZ4O5lMkdBteHRJTW8cs54NJOxWu +imi5V5cCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAERSWwauSCPc/L8my/uRan2Te +2yFPhpk0djZX3dAVL8WtfxUfN2JzPtTnX84XA9s1+ivbrmAJXx5fj267Cz3qWhMe +DGBvtcC1IyIuBwvLqXTLR7sdwdela8wv0kL9Sd2nic9TutoAWii/gt/4uhMdUIaC +/Y4wjylGsB49Ndo4YhYYSq3mtlFs3q9i6wHQHiT+eo8SGhJouPtmmRQURVyu565p +F4ErWjfJXir0xuKhXFSbplQAz/DxwceYMBo7Nhbbo27q/a2ywtrvAkcTisDxszGt +TxzhT5yvDwyd93gN2PQ1VoDat20Xj50egWTh/sVFuq1ruQp6Tk9LhO5L8X3dEQ== +-----END CERTIFICATE----- + +# Issuer: CN=VeriSign Class 4 Public Primary Certification Authority - G3 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 1999 VeriSign, Inc. - For authorized use only +# Subject: CN=VeriSign Class 4 Public Primary Certification Authority - G3 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 1999 VeriSign, Inc. - For authorized use only +# Label: "Verisign Class 4 Public Primary Certification Authority - G3" +# Serial: 314531972711909413743075096039378935511 +# MD5 Fingerprint: db:c8:f2:27:2e:b1:ea:6a:29:23:5d:fe:56:3e:33:df +# SHA1 Fingerprint: c8:ec:8c:87:92:69:cb:4b:ab:39:e9:8d:7e:57:67:f3:14:95:73:9d +# SHA256 Fingerprint: e3:89:36:0d:0f:db:ae:b3:d2:50:58:4b:47:30:31:4e:22:2f:39:c1:56:a0:20:14:4e:8d:96:05:61:79:15:06 +-----BEGIN CERTIFICATE----- +MIIEGjCCAwICEQDsoKeLbnVqAc/EfMwvlF7XMA0GCSqGSIb3DQEBBQUAMIHKMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl +cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu +LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT +aWduIENsYXNzIDQgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD +VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT +aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ +bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu +IENsYXNzIDQgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg +LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK3LpRFpxlmr8Y+1 +GQ9Wzsy1HyDkniYlS+BzZYlZ3tCD5PUPtbut8XzoIfzk6AzufEUiGXaStBO3IFsJ ++mGuqPKljYXCKtbeZjbSmwL0qJJgfJxptI8kHtCGUvYynEFYHiK9zUVilQhu0Gbd +U6LM8BDcVHOLBKFGMzNcF0C5nk3T875Vg+ixiY5afJqWIpA7iCXy0lOIAgwLePLm +NxdLMEYH5IBtptiWLugs+BGzOA1mppvqySNb247i8xOOGlktqgLw7KSHZtzBP/XY +ufTsgsbSPZUd5cBPhMnZo0QoBmrXRazwa2rvTl/4EYIeOGM0ZlDUPpNz+jDDZq3/ +ky2X7wMCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAj/ola09b5KROJ1WrIhVZPMq1 +CtRK26vdoV9TxaBXOcLORyu+OshWv8LZJxA6sQU8wHcxuzrTBXttmhwwjIDLk5Mq +g6sFUYICABFna/OIYUdfA5PVWw3g8dShMjWFsjrbsIKr0csKvE+MW8VLADsfKoKm +fjaF3H48ZwC15DtS4KjrXRX5xm3wrR0OhbepmnMUWluPQSjA1egtTaRezarZ7c7c +2NU8Qh0XwRJdRTjDOPP8hS6DRkiy1yBfkjaP53kPmF6Z6PDQpLv1U70qzlmwr25/ +bLvSHgCwIe34QWKCudiyxLtGUPMxxY8BqHTr9Xgn2uf3ZkPznoM+IKrDNWCRzg== +-----END CERTIFICATE----- + +# Issuer: CN=Entrust.net Secure Server Certification Authority O=Entrust.net OU=www.entrust.net/CPS incorp. by ref. (limits liab.)/(c) 1999 Entrust.net Limited +# Subject: CN=Entrust.net Secure Server Certification Authority O=Entrust.net OU=www.entrust.net/CPS incorp. by ref. (limits liab.)/(c) 1999 Entrust.net Limited +# Label: "Entrust.net Secure Server CA" +# Serial: 927650371 +# MD5 Fingerprint: df:f2:80:73:cc:f1:e6:61:73:fc:f5:42:e9:c5:7c:ee +# SHA1 Fingerprint: 99:a6:9b:e6:1a:fe:88:6b:4d:2b:82:00:7c:b8:54:fc:31:7e:15:39 +# SHA256 Fingerprint: 62:f2:40:27:8c:56:4c:4d:d8:bf:7d:9d:4f:6f:36:6e:a8:94:d2:2f:5f:34:d9:89:a9:83:ac:ec:2f:ff:ed:50 +-----BEGIN CERTIFICATE----- +MIIE2DCCBEGgAwIBAgIEN0rSQzANBgkqhkiG9w0BAQUFADCBwzELMAkGA1UEBhMC +VVMxFDASBgNVBAoTC0VudHJ1c3QubmV0MTswOQYDVQQLEzJ3d3cuZW50cnVzdC5u +ZXQvQ1BTIGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxpYWIuKTElMCMGA1UECxMc +KGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDE6MDgGA1UEAxMxRW50cnVzdC5u +ZXQgU2VjdXJlIFNlcnZlciBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw05OTA1 +MjUxNjA5NDBaFw0xOTA1MjUxNjM5NDBaMIHDMQswCQYDVQQGEwJVUzEUMBIGA1UE +ChMLRW50cnVzdC5uZXQxOzA5BgNVBAsTMnd3dy5lbnRydXN0Lm5ldC9DUFMgaW5j +b3JwLiBieSByZWYuIChsaW1pdHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMTk5OSBF +bnRydXN0Lm5ldCBMaW1pdGVkMTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUg +U2VydmVyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGdMA0GCSqGSIb3DQEBAQUA +A4GLADCBhwKBgQDNKIM0VBuJ8w+vN5Ex/68xYMmo6LIQaO2f55M28Qpku0f1BBc/ +I0dNxScZgSYMVHINiC3ZH5oSn7yzcdOAGT9HZnuMNSjSuQrfJNqc1lB5gXpa0zf3 +wkrYKZImZNHkmGw6AIr1NJtl+O3jEP/9uElY3KDegjlrgbEWGWG5VLbmQwIBA6OC +AdcwggHTMBEGCWCGSAGG+EIBAQQEAwIABzCCARkGA1UdHwSCARAwggEMMIHeoIHb +oIHYpIHVMIHSMQswCQYDVQQGEwJVUzEUMBIGA1UEChMLRW50cnVzdC5uZXQxOzA5 +BgNVBAsTMnd3dy5lbnRydXN0Lm5ldC9DUFMgaW5jb3JwLiBieSByZWYuIChsaW1p +dHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMTk5OSBFbnRydXN0Lm5ldCBMaW1pdGVk +MTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUgU2VydmVyIENlcnRpZmljYXRp +b24gQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMCmgJ6AlhiNodHRwOi8vd3d3LmVu +dHJ1c3QubmV0L0NSTC9uZXQxLmNybDArBgNVHRAEJDAigA8xOTk5MDUyNTE2MDk0 +MFqBDzIwMTkwNTI1MTYwOTQwWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAU8Bdi +E1U9s/8KAGv7UISX8+1i0BowHQYDVR0OBBYEFPAXYhNVPbP/CgBr+1CEl/PtYtAa +MAwGA1UdEwQFMAMBAf8wGQYJKoZIhvZ9B0EABAwwChsEVjQuMAMCBJAwDQYJKoZI +hvcNAQEFBQADgYEAkNwwAvpkdMKnCqV8IY00F6j7Rw7/JXyNEwr75Ji174z4xRAN +95K+8cPV1ZVqBLssziY2ZcgxxufuP+NXdYR6Ee9GTxj005i7qIcyunL2POI9n9cd +2cNgQ4xYDiKWL2KjLB+6rQXvqzJ4h6BUcxm1XAX5Uj5tLUUL9wqT6u0G+bI= +-----END CERTIFICATE----- + +# Issuer: CN=Entrust.net Certification Authority (2048) O=Entrust.net OU=www.entrust.net/CPS_2048 incorp. by ref. (limits liab.)/(c) 1999 Entrust.net Limited +# Subject: CN=Entrust.net Certification Authority (2048) O=Entrust.net OU=www.entrust.net/CPS_2048 incorp. by ref. (limits liab.)/(c) 1999 Entrust.net Limited +# Label: "Entrust.net Premium 2048 Secure Server CA" +# Serial: 946059622 +# MD5 Fingerprint: ba:21:ea:20:d6:dd:db:8f:c1:57:8b:40:ad:a1:fc:fc +# SHA1 Fingerprint: 80:1d:62:d0:7b:44:9d:5c:5c:03:5c:98:ea:61:fa:44:3c:2a:58:fe +# SHA256 Fingerprint: d1:c3:39:ea:27:84:eb:87:0f:93:4f:c5:63:4e:4a:a9:ad:55:05:01:64:01:f2:64:65:d3:7a:57:46:63:35:9f +-----BEGIN CERTIFICATE----- +MIIEXDCCA0SgAwIBAgIEOGO5ZjANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChML +RW50cnVzdC5uZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBp +bmNvcnAuIGJ5IHJlZi4gKGxpbWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5 +IEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNVBAMTKkVudHJ1c3QubmV0IENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5ICgyMDQ4KTAeFw05OTEyMjQxNzUwNTFaFw0xOTEy +MjQxODIwNTFaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3d3d3 +LmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxp +YWIuKTElMCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEG +A1UEAxMqRW50cnVzdC5uZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKDIwNDgp +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArU1LqRKGsuqjIAcVFmQq +K0vRvwtKTY7tgHalZ7d4QMBzQshowNtTK91euHaYNZOLGp18EzoOH1u3Hs/lJBQe +sYGpjX24zGtLA/ECDNyrpUAkAH90lKGdCCmziAv1h3edVc3kw37XamSrhRSGlVuX +MlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZy/Gn7xxGWC4LeksyZB2ZnuU4q941mVT +XTzWnLLPKQP5L6RQstRIzgUyVYr9smRMDuSYB3Xbf9+5CFVghTAp+XtIpGmG4zU/ +HoZdenoVve8AjhUiVBcAkCaTvA5JaJG/+EfTnZVCwQ5N328mz8MYIWJmQ3DW1cAH +4QIDAQABo3QwcjARBglghkgBhvhCAQEEBAMCAAcwHwYDVR0jBBgwFoAUVeSB0RGA +vtiJuQijMfmhJAkWuXAwHQYDVR0OBBYEFFXkgdERgL7YibkIozH5oSQJFrlwMB0G +CSqGSIb2fQdBAAQQMA4bCFY1LjA6NC4wAwIEkDANBgkqhkiG9w0BAQUFAAOCAQEA +WUesIYSKF8mciVMeuoCFGsY8Tj6xnLZ8xpJdGGQC49MGCBFhfGPjK50xA3B20qMo +oPS7mmNz7W3lKtvtFKkrxjYR0CvrB4ul2p5cGZ1WEvVUKcgF7bISKo30Axv/55IQ +h7A6tcOdBTcSo8f0FbnVpDkWm1M6I5HxqIKiaohowXkCIryqptau37AUX7iH0N18 +f3v/rxzP5tsHrV7bhZ3QKw0z2wTR5klAEyt2+z7pnIkPFc4YsIV4IU9rTw76NmfN +B/L/CNDi3tm/Kq+4h4YhPATKt5Rof8886ZjXOP/swNlQ8C5LWK5Gb9Auw2DaclVy +vUxFnmG6v4SBkgPR0ml8xQ== +-----END CERTIFICATE----- + +# Issuer: CN=Baltimore CyberTrust Root O=Baltimore OU=CyberTrust +# Subject: CN=Baltimore CyberTrust Root O=Baltimore OU=CyberTrust +# Label: "Baltimore CyberTrust Root" +# Serial: 33554617 +# MD5 Fingerprint: ac:b6:94:a5:9c:17:e0:d7:91:52:9b:b1:97:06:a6:e4 +# SHA1 Fingerprint: d4:de:20:d0:5e:66:fc:53:fe:1a:50:88:2c:78:db:28:52:ca:e4:74 +# SHA256 Fingerprint: 16:af:57:a9:f6:76:b0:ab:12:60:95:aa:5e:ba:de:f2:2a:b3:11:19:d6:44:ac:95:cd:4b:93:db:f3:f2:6a:eb +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ +RTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYD +VQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoX +DTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMCSUUxEjAQBgNVBAoTCUJhbHRpbW9y +ZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFsdGltb3JlIEN5YmVy +VHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKMEuyKr +mD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjr +IZ3AQSsBUnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeK +mpYcqWe4PwzV9/lSEy/CG9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSu +XmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9XbIGevOF6uvUA65ehD5f/xXtabz5OTZy +dc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjprl3RjM71oGDHweI12v/ye +jl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoIVDaGezq1 +BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3 +DQEBBQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT92 +9hkTI7gQCvlYpNRhcL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3Wgx +jkzSswF07r51XgdIGn9w/xZchMB5hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0 +Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsaY71k5h+3zvDyny67G7fyUIhz +ksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS +R9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp +-----END CERTIFICATE----- + +# Issuer: CN=Equifax Secure Global eBusiness CA-1 O=Equifax Secure Inc. +# Subject: CN=Equifax Secure Global eBusiness CA-1 O=Equifax Secure Inc. +# Label: "Equifax Secure Global eBusiness CA" +# Serial: 1 +# MD5 Fingerprint: 8f:5d:77:06:27:c4:98:3c:5b:93:78:e7:d7:7d:9b:cc +# SHA1 Fingerprint: 7e:78:4a:10:1c:82:65:cc:2d:e1:f1:6d:47:b4:40:ca:d9:0a:19:45 +# SHA256 Fingerprint: 5f:0b:62:ea:b5:e3:53:ea:65:21:65:16:58:fb:b6:53:59:f4:43:28:0a:4a:fb:d1:04:d7:7d:10:f9:f0:4c:07 +-----BEGIN CERTIFICATE----- +MIICkDCCAfmgAwIBAgIBATANBgkqhkiG9w0BAQQFADBaMQswCQYDVQQGEwJVUzEc +MBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5jLjEtMCsGA1UEAxMkRXF1aWZheCBT +ZWN1cmUgR2xvYmFsIGVCdXNpbmVzcyBDQS0xMB4XDTk5MDYyMTA0MDAwMFoXDTIw +MDYyMTA0MDAwMFowWjELMAkGA1UEBhMCVVMxHDAaBgNVBAoTE0VxdWlmYXggU2Vj +dXJlIEluYy4xLTArBgNVBAMTJEVxdWlmYXggU2VjdXJlIEdsb2JhbCBlQnVzaW5l +c3MgQ0EtMTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAuucXkAJlsTRVPEnC +UdXfp9E3j9HngXNBUmCbnaEXJnitx7HoJpQytd4zjTov2/KaelpzmKNc6fuKcxtc +58O/gGzNqfTWK8D3+ZmqY6KxRwIP1ORROhI8bIpaVIRw28HFkM9yRcuoWcDNM50/ +o5brhTMhHD4ePmBudpxnhcXIw2ECAwEAAaNmMGQwEQYJYIZIAYb4QgEBBAQDAgAH +MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUvqigdHJQa0S3ySPY+6j/s1dr +aGwwHQYDVR0OBBYEFL6ooHRyUGtEt8kj2Puo/7NXa2hsMA0GCSqGSIb3DQEBBAUA +A4GBADDiAVGqx+pf2rnQZQ8w1j7aDRRJbpGTJxQx78T3LUX47Me/okENI7SS+RkA +Z70Br83gcfxaz2TE4JaY0KNA4gGK7ycH8WUBikQtBmV1UsCGECAhX2xrD2yuCRyv +8qIYNMR1pHMc8Y3c7635s3a0kr/clRAevsvIO1qEYBlWlKlV +-----END CERTIFICATE----- + +# Issuer: CN=Equifax Secure eBusiness CA-1 O=Equifax Secure Inc. +# Subject: CN=Equifax Secure eBusiness CA-1 O=Equifax Secure Inc. +# Label: "Equifax Secure eBusiness CA 1" +# Serial: 4 +# MD5 Fingerprint: 64:9c:ef:2e:44:fc:c6:8f:52:07:d0:51:73:8f:cb:3d +# SHA1 Fingerprint: da:40:18:8b:91:89:a3:ed:ee:ae:da:97:fe:2f:9d:f5:b7:d1:8a:41 +# SHA256 Fingerprint: cf:56:ff:46:a4:a1:86:10:9d:d9:65:84:b5:ee:b5:8a:51:0c:42:75:b0:e5:f9:4f:40:bb:ae:86:5e:19:f6:73 +-----BEGIN CERTIFICATE----- +MIICgjCCAeugAwIBAgIBBDANBgkqhkiG9w0BAQQFADBTMQswCQYDVQQGEwJVUzEc +MBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5jLjEmMCQGA1UEAxMdRXF1aWZheCBT +ZWN1cmUgZUJ1c2luZXNzIENBLTEwHhcNOTkwNjIxMDQwMDAwWhcNMjAwNjIxMDQw +MDAwWjBTMQswCQYDVQQGEwJVUzEcMBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5j +LjEmMCQGA1UEAxMdRXF1aWZheCBTZWN1cmUgZUJ1c2luZXNzIENBLTEwgZ8wDQYJ +KoZIhvcNAQEBBQADgY0AMIGJAoGBAM4vGbwXt3fek6lfWg0XTzQaDJj0ItlZ1MRo +RvC0NcWFAyDGr0WlIVFFQesWWDYyb+JQYmT5/VGcqiTZ9J2DKocKIdMSODRsjQBu +WqDZQu4aIZX5UkxVWsUPOE9G+m34LjXWHXzr4vCwdYDIqROsvojvOm6rXyo4YgKw +Env+j6YDAgMBAAGjZjBkMBEGCWCGSAGG+EIBAQQEAwIABzAPBgNVHRMBAf8EBTAD +AQH/MB8GA1UdIwQYMBaAFEp4MlIR21kWNl7fwRQ2QGpHfEyhMB0GA1UdDgQWBBRK +eDJSEdtZFjZe38EUNkBqR3xMoTANBgkqhkiG9w0BAQQFAAOBgQB1W6ibAxHm6VZM +zfmpTMANmvPMZWnmJXbMWbfWVMMdzZmsGd20hdXgPfxiIKeES1hl8eL5lSE/9dR+ +WB5Hh1Q+WKG1tfgq73HnvMP2sUlG4tega+VWeponmHxGYhTnyfxuAxJ5gDgdSIKN +/Bf+KpYrtWKmpj29f5JZzVoqgrI3eQ== +-----END CERTIFICATE----- + +# Issuer: O=Equifax Secure OU=Equifax Secure eBusiness CA-2 +# Subject: O=Equifax Secure OU=Equifax Secure eBusiness CA-2 +# Label: "Equifax Secure eBusiness CA 2" +# Serial: 930140085 +# MD5 Fingerprint: aa:bf:bf:64:97:da:98:1d:6f:c6:08:3a:95:70:33:ca +# SHA1 Fingerprint: 39:4f:f6:85:0b:06:be:52:e5:18:56:cc:10:e1:80:e8:82:b3:85:cc +# SHA256 Fingerprint: 2f:27:4e:48:ab:a4:ac:7b:76:59:33:10:17:75:50:6d:c3:0e:e3:8e:f6:ac:d5:c0:49:32:cf:e0:41:23:42:20 +-----BEGIN CERTIFICATE----- +MIIDIDCCAomgAwIBAgIEN3DPtTANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJV +UzEXMBUGA1UEChMORXF1aWZheCBTZWN1cmUxJjAkBgNVBAsTHUVxdWlmYXggU2Vj +dXJlIGVCdXNpbmVzcyBDQS0yMB4XDTk5MDYyMzEyMTQ0NVoXDTE5MDYyMzEyMTQ0 +NVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkVxdWlmYXggU2VjdXJlMSYwJAYD +VQQLEx1FcXVpZmF4IFNlY3VyZSBlQnVzaW5lc3MgQ0EtMjCBnzANBgkqhkiG9w0B +AQEFAAOBjQAwgYkCgYEA5Dk5kx5SBhsoNviyoynF7Y6yEb3+6+e0dMKP/wXn2Z0G +vxLIPw7y1tEkshHe0XMJitSxLJgJDR5QRrKDpkWNYmi7hRsgcDKqQM2mll/EcTc/ +BPO3QSQ5BxoeLmFYoBIL5aXfxavqN3HMHMg3OrmXUqesxWoklE6ce8/AatbfIb0C +AwEAAaOCAQkwggEFMHAGA1UdHwRpMGcwZaBjoGGkXzBdMQswCQYDVQQGEwJVUzEX +MBUGA1UEChMORXF1aWZheCBTZWN1cmUxJjAkBgNVBAsTHUVxdWlmYXggU2VjdXJl +IGVCdXNpbmVzcyBDQS0yMQ0wCwYDVQQDEwRDUkwxMBoGA1UdEAQTMBGBDzIwMTkw +NjIzMTIxNDQ1WjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUUJ4L6q9euSBIplBq +y/3YIHqngnYwHQYDVR0OBBYEFFCeC+qvXrkgSKZQasv92CB6p4J2MAwGA1UdEwQF +MAMBAf8wGgYJKoZIhvZ9B0EABA0wCxsFVjMuMGMDAgbAMA0GCSqGSIb3DQEBBQUA +A4GBAAyGgq3oThr1jokn4jVYPSm0B482UJW/bsGe68SQsoWou7dC4A8HOd/7npCy +0cE+U58DRLB+S/Rv5Hwf5+Kx5Lia78O9zt4LMjTZ3ijtM2vE1Nc9ElirfQkty3D1 +E4qUoSek1nDFbZS1yX2doNLGCEnZZpum0/QL3MUmV+GRMOrN +-----END CERTIFICATE----- + +# Issuer: CN=AddTrust Class 1 CA Root O=AddTrust AB OU=AddTrust TTP Network +# Subject: CN=AddTrust Class 1 CA Root O=AddTrust AB OU=AddTrust TTP Network +# Label: "AddTrust Low-Value Services Root" +# Serial: 1 +# MD5 Fingerprint: 1e:42:95:02:33:92:6b:b9:5f:c0:7f:da:d6:b2:4b:fc +# SHA1 Fingerprint: cc:ab:0e:a0:4c:23:01:d6:69:7b:dd:37:9f:cd:12:eb:24:e3:94:9d +# SHA256 Fingerprint: 8c:72:09:27:9a:c0:4e:27:5e:16:d0:7f:d3:b7:75:e8:01:54:b5:96:80:46:e3:1f:52:dd:25:76:63:24:e9:a7 +-----BEGIN CERTIFICATE----- +MIIEGDCCAwCgAwIBAgIBATANBgkqhkiG9w0BAQUFADBlMQswCQYDVQQGEwJTRTEU +MBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3 +b3JrMSEwHwYDVQQDExhBZGRUcnVzdCBDbGFzcyAxIENBIFJvb3QwHhcNMDAwNTMw +MTAzODMxWhcNMjAwNTMwMTAzODMxWjBlMQswCQYDVQQGEwJTRTEUMBIGA1UEChML +QWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3b3JrMSEwHwYD +VQQDExhBZGRUcnVzdCBDbGFzcyAxIENBIFJvb3QwggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQCWltQhSWDia+hBBwzexODcEyPNwTXH+9ZOEQpnXvUGW2ul +CDtbKRY654eyNAbFvAWlA3yCyykQruGIgb3WntP+LVbBFc7jJp0VLhD7Bo8wBN6n +tGO0/7Gcrjyvd7ZWxbWroulpOj0OM3kyP3CCkplhbY0wCI9xP6ZIVxn4JdxLZlyl +dI+Yrsj5wAYi56xz36Uu+1LcsRVlIPo1Zmne3yzxbrww2ywkEtvrNTVokMsAsJch +PXQhI2U0K7t4WaPW4XY5mqRJjox0r26kmqPZm9I4XJuiGMx1I4S+6+JNM3GOGvDC ++Mcdoq0Dlyz4zyXG9rgkMbFjXZJ/Y/AlyVMuH79NAgMBAAGjgdIwgc8wHQYDVR0O +BBYEFJWxtPCUtr3H2tERCSG+wa9J/RB7MAsGA1UdDwQEAwIBBjAPBgNVHRMBAf8E +BTADAQH/MIGPBgNVHSMEgYcwgYSAFJWxtPCUtr3H2tERCSG+wa9J/RB7oWmkZzBl +MQswCQYDVQQGEwJTRTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFk +ZFRydXN0IFRUUCBOZXR3b3JrMSEwHwYDVQQDExhBZGRUcnVzdCBDbGFzcyAxIENB +IFJvb3SCAQEwDQYJKoZIhvcNAQEFBQADggEBACxtZBsfzQ3duQH6lmM0MkhHma6X +7f1yFqZzR1r0693p9db7RcwpiURdv0Y5PejuvE1Uhh4dbOMXJ0PhiVYrqW9yTkkz +43J8KiOavD7/KCrto/8cI7pDVwlnTUtiBi34/2ydYB7YHEt9tTEv2dB8Xfjea4MY +eDdXL+gzB2ffHsdrKpV2ro9Xo/D0UrSpUwjP4E/TelOL/bscVjby/rK25Xa71SJl +pz/+0WatC7xrmYbvP33zGDLKe8bjq2RGlfgmadlVg3sslgf/WSxEo8bl6ancoWOA +WiFeIc9TVPC6b4nbqKqVz4vjccweGyBECMB6tkD9xOQ14R0WHNC8K47Wcdk= +-----END CERTIFICATE----- + +# Issuer: CN=AddTrust External CA Root O=AddTrust AB OU=AddTrust External TTP Network +# Subject: CN=AddTrust External CA Root O=AddTrust AB OU=AddTrust External TTP Network +# Label: "AddTrust External Root" +# Serial: 1 +# MD5 Fingerprint: 1d:35:54:04:85:78:b0:3f:42:42:4d:bf:20:73:0a:3f +# SHA1 Fingerprint: 02:fa:f3:e2:91:43:54:68:60:78:57:69:4d:f5:e4:5b:68:85:18:68 +# SHA256 Fingerprint: 68:7f:a4:51:38:22:78:ff:f0:c8:b1:1f:8d:43:d5:76:67:1c:6e:b2:bc:ea:b4:13:fb:83:d9:65:d0:6d:2f:f2 +-----BEGIN CERTIFICATE----- +MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEU +MBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFkZFRydXN0IEV4dGVybmFs +IFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBFeHRlcm5hbCBDQSBSb290 +MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFowbzELMAkGA1UEBhMCU0Ux +FDASBgNVBAoTC0FkZFRydXN0IEFCMSYwJAYDVQQLEx1BZGRUcnVzdCBFeHRlcm5h +bCBUVFAgTmV0d29yazEiMCAGA1UEAxMZQWRkVHJ1c3QgRXh0ZXJuYWwgQ0EgUm9v +dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALf3GjPm8gAELTngTlvt +H7xsD821+iO2zt6bETOXpClMfZOfvUq8k+0DGuOPz+VtUFrWlymUWoCwSXrbLpX9 +uMq/NzgtHj6RQa1wVsfwTz/oMp50ysiQVOnGXw94nZpAPA6sYapeFI+eh6FqUNzX +mk6vBbOmcZSccbNQYArHE504B4YCqOmoaSYYkKtMsE8jqzpPhNjfzp/haW+710LX +a0Tkx63ubUFfclpxCDezeWWkWaCUN/cALw3CknLa0Dhy2xSoRcRdKn23tNbE7qzN +E0S3ySvdQwAl+mG5aWpYIxG3pzOPVnVZ9c0p10a3CitlttNCbxWyuHv77+ldU9U0 +WicCAwEAAaOB3DCB2TAdBgNVHQ4EFgQUrb2YejS0Jvf6xCZU7wO94CTLVBowCwYD +VR0PBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wgZkGA1UdIwSBkTCBjoAUrb2YejS0 +Jvf6xCZU7wO94CTLVBqhc6RxMG8xCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRU +cnVzdCBBQjEmMCQGA1UECxMdQWRkVHJ1c3QgRXh0ZXJuYWwgVFRQIE5ldHdvcmsx +IjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENBIFJvb3SCAQEwDQYJKoZIhvcN +AQEFBQADggEBALCb4IUlwtYj4g+WBpKdQZic2YR5gdkeWxQHIzZlj7DYd7usQWxH +YINRsPkyPef89iYTx4AWpb9a/IfPeHmJIZriTAcKhjW88t5RxNKWt9x+Tu5w/Rw5 +6wwCURQtjr0W4MHfRnXnJK3s9EK0hZNwEGe6nQY1ShjTK3rMUUKhemPR5ruhxSvC +Nr4TDea9Y355e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEX +c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a +mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ= +-----END CERTIFICATE----- + +# Issuer: CN=AddTrust Public CA Root O=AddTrust AB OU=AddTrust TTP Network +# Subject: CN=AddTrust Public CA Root O=AddTrust AB OU=AddTrust TTP Network +# Label: "AddTrust Public Services Root" +# Serial: 1 +# MD5 Fingerprint: c1:62:3e:23:c5:82:73:9c:03:59:4b:2b:e9:77:49:7f +# SHA1 Fingerprint: 2a:b6:28:48:5e:78:fb:f3:ad:9e:79:10:dd:6b:df:99:72:2c:96:e5 +# SHA256 Fingerprint: 07:91:ca:07:49:b2:07:82:aa:d3:c7:d7:bd:0c:df:c9:48:58:35:84:3e:b2:d7:99:60:09:ce:43:ab:6c:69:27 +-----BEGIN CERTIFICATE----- +MIIEFTCCAv2gAwIBAgIBATANBgkqhkiG9w0BAQUFADBkMQswCQYDVQQGEwJTRTEU +MBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3 +b3JrMSAwHgYDVQQDExdBZGRUcnVzdCBQdWJsaWMgQ0EgUm9vdDAeFw0wMDA1MzAx +MDQxNTBaFw0yMDA1MzAxMDQxNTBaMGQxCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtB +ZGRUcnVzdCBBQjEdMBsGA1UECxMUQWRkVHJ1c3QgVFRQIE5ldHdvcmsxIDAeBgNV +BAMTF0FkZFRydXN0IFB1YmxpYyBDQSBSb290MIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEA6Rowj4OIFMEg2Dybjxt+A3S72mnTRqX4jsIMEZBRpS9mVEBV +6tsfSlbunyNu9DnLoblv8n75XYcmYZ4c+OLspoH4IcUkzBEMP9smcnrHAZcHF/nX +GCwwfQ56HmIexkvA/X1id9NEHif2P0tEs7c42TkfYNVRknMDtABp4/MUTu7R3AnP +dzRGULD4EfL+OHn3Bzn+UZKXC1sIXzSGAa2Il+tmzV7R/9x98oTaunet3IAIx6eH +1lWfl2royBFkuucZKT8Rs3iQhCBSWxHveNCD9tVIkNAwHM+A+WD+eeSI8t0A65RF +62WUaUC6wNW0uLp9BBGo6zEFlpROWCGOn9Bg/QIDAQABo4HRMIHOMB0GA1UdDgQW +BBSBPjfYkrAfd59ctKtzquf2NGAv+jALBgNVHQ8EBAMCAQYwDwYDVR0TAQH/BAUw +AwEB/zCBjgYDVR0jBIGGMIGDgBSBPjfYkrAfd59ctKtzquf2NGAv+qFopGYwZDEL +MAkGA1UEBhMCU0UxFDASBgNVBAoTC0FkZFRydXN0IEFCMR0wGwYDVQQLExRBZGRU +cnVzdCBUVFAgTmV0d29yazEgMB4GA1UEAxMXQWRkVHJ1c3QgUHVibGljIENBIFJv +b3SCAQEwDQYJKoZIhvcNAQEFBQADggEBAAP3FUr4JNojVhaTdt02KLmuG7jD8WS6 +IBh4lSknVwW8fCr0uVFV2ocC3g8WFzH4qnkuCRO7r7IgGRLlk/lL+YPoRNWyQSW/ +iHVv/xD8SlTQX/D67zZzfRs2RcYhbbQVuE7PnFylPVoAjgbjPGsye/Kf8Lb93/Ao +GEjwxrzQvzSAlsJKsW2Ox5BF3i9nrEUEo3rcVZLJR2bYGozH7ZxOmuASu7VqTITh +4SINhwBk/ox9Yjllpu9CtoAlEmEBqCQTcAARJl/6NVDFSMwGR+gn2HCNX2TmoUQm +XiLsks3/QppEIW1cxeMiHV9HEufOX1362KqxMy3ZdvJOOjMMK7MtkAY= +-----END CERTIFICATE----- + +# Issuer: CN=AddTrust Qualified CA Root O=AddTrust AB OU=AddTrust TTP Network +# Subject: CN=AddTrust Qualified CA Root O=AddTrust AB OU=AddTrust TTP Network +# Label: "AddTrust Qualified Certificates Root" +# Serial: 1 +# MD5 Fingerprint: 27:ec:39:47:cd:da:5a:af:e2:9a:01:65:21:a9:4c:bb +# SHA1 Fingerprint: 4d:23:78:ec:91:95:39:b5:00:7f:75:8f:03:3b:21:1e:c5:4d:8b:cf +# SHA256 Fingerprint: 80:95:21:08:05:db:4b:bc:35:5e:44:28:d8:fd:6e:c2:cd:e3:ab:5f:b9:7a:99:42:98:8e:b8:f4:dc:d0:60:16 +-----BEGIN CERTIFICATE----- +MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJTRTEU +MBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3 +b3JrMSMwIQYDVQQDExpBZGRUcnVzdCBRdWFsaWZpZWQgQ0EgUm9vdDAeFw0wMDA1 +MzAxMDQ0NTBaFw0yMDA1MzAxMDQ0NTBaMGcxCzAJBgNVBAYTAlNFMRQwEgYDVQQK +EwtBZGRUcnVzdCBBQjEdMBsGA1UECxMUQWRkVHJ1c3QgVFRQIE5ldHdvcmsxIzAh +BgNVBAMTGkFkZFRydXN0IFF1YWxpZmllZCBDQSBSb290MIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA5B6a/twJWoekn0e+EV+vhDTbYjx5eLfpMLXsDBwq +xBb/4Oxx64r1EW7tTw2R0hIYLUkVAcKkIhPHEWT/IhKauY5cLwjPcWqzZwFZ8V1G +87B4pfYOQnrjfxvM0PC3KP0q6p6zsLkEqv32x7SxuCqg+1jxGaBvcCV+PmlKfw8i +2O+tCBGaKZnhqkRFmhJePp1tUvznoD1oL/BLcHwTOK28FSXx1s6rosAx1i+f4P8U +WfyEk9mHfExUE+uf0S0R+Bg6Ot4l2ffTQO2kBhLEO+GRwVY18BTcZTYJbqukB8c1 +0cIDMzZbdSZtQvESa0NvS3GU+jQd7RNuyoB/mC9suWXY6QIDAQABo4HUMIHRMB0G +A1UdDgQWBBQ5lYtii1zJ1IC6WA+XPxUIQ8yYpzALBgNVHQ8EBAMCAQYwDwYDVR0T +AQH/BAUwAwEB/zCBkQYDVR0jBIGJMIGGgBQ5lYtii1zJ1IC6WA+XPxUIQ8yYp6Fr +pGkwZzELMAkGA1UEBhMCU0UxFDASBgNVBAoTC0FkZFRydXN0IEFCMR0wGwYDVQQL +ExRBZGRUcnVzdCBUVFAgTmV0d29yazEjMCEGA1UEAxMaQWRkVHJ1c3QgUXVhbGlm +aWVkIENBIFJvb3SCAQEwDQYJKoZIhvcNAQEFBQADggEBABmrder4i2VhlRO6aQTv +hsoToMeqT2QbPxj2qC0sVY8FtzDqQmodwCVRLae/DLPt7wh/bDxGGuoYQ992zPlm +hpwsaPXpF/gxsxjE1kh9I0xowX67ARRvxdlu3rsEQmr49lx95dr6h+sNNVJn0J6X +dgWTP5XHAeZpVTh/EGGZyeNfpso+gmNIquIISD6q8rKFYqa0p9m9N5xotS1WfbC3 +P6CxB9bpT9zeRXEwMn8bLgn5v1Kh7sKAPgZcLlVAwRv1cEWw3F369nJad9Jjzc9Y +iQBCYz95OdBEsIJuQRno3eDBiFrRHnGTHyQwdOUeqN48Jzd/g66ed8/wMLH/S5no +xqE= +-----END CERTIFICATE----- + +# Issuer: CN=Entrust Root Certification Authority O=Entrust, Inc. OU=www.entrust.net/CPS is incorporated by reference/(c) 2006 Entrust, Inc. +# Subject: CN=Entrust Root Certification Authority O=Entrust, Inc. OU=www.entrust.net/CPS is incorporated by reference/(c) 2006 Entrust, Inc. +# Label: "Entrust Root Certification Authority" +# Serial: 1164660820 +# MD5 Fingerprint: d6:a5:c3:ed:5d:dd:3e:00:c1:3d:87:92:1f:1d:3f:e4 +# SHA1 Fingerprint: b3:1e:b1:b7:40:e3:6c:84:02:da:dc:37:d4:4d:f5:d4:67:49:52:f9 +# SHA256 Fingerprint: 73:c1:76:43:4f:1b:c6:d5:ad:f4:5b:0e:76:e7:27:28:7c:8d:e5:76:16:c1:e6:e6:14:1a:2b:2c:bc:7d:8e:4c +-----BEGIN CERTIFICATE----- +MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMC +VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0 +Lm5ldC9DUFMgaXMgaW5jb3Jwb3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMW +KGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsGA1UEAxMkRW50cnVzdCBSb290IENl +cnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0MloXDTI2MTEyNzIw +NTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMTkw +NwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSBy +ZWZlcmVuY2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNV +BAMTJEVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBALaVtkNC+sZtKm9I35RMOVcF7sN5EUFo +Nu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYszA9u3g3s+IIRe7bJWKKf4 +4LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOwwCj0Yzfv9 +KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGI +rb68j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi +94DkZfs0Nw4pgHBNrziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOB +sDCBrTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAi +gA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1MzQyWjAfBgNVHSMEGDAWgBRo +kORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DHhmak8fdLQ/uE +vW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA +A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9t +O1KzKtvn1ISMY/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6Zua +AGAT/3B+XxFNSRuzFVJ7yVTav52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP +9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTSW3iDVuycNsMm4hH2Z0kdkquM++v/ +eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m +0vdXcDazv/wor3ElhVsT/h5/WrQ8 +-----END CERTIFICATE----- + +# Issuer: CN=GeoTrust Global CA O=GeoTrust Inc. +# Subject: CN=GeoTrust Global CA O=GeoTrust Inc. +# Label: "GeoTrust Global CA" +# Serial: 144470 +# MD5 Fingerprint: f7:75:ab:29:fb:51:4e:b7:77:5e:ff:05:3c:99:8e:f5 +# SHA1 Fingerprint: de:28:f4:a4:ff:e5:b9:2f:a3:c5:03:d1:a3:49:a7:f9:96:2a:82:12 +# SHA256 Fingerprint: ff:85:6a:2d:25:1d:cd:88:d3:66:56:f4:50:12:67:98:cf:ab:aa:de:40:79:9c:72:2d:e4:d2:b5:db:36:a7:3a +-----BEGIN CERTIFICATE----- +MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT +MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i +YWwgQ0EwHhcNMDIwNTIxMDQwMDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQG +EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UEAxMSR2VvVHJ1c3Qg +R2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2swYYzD9 +9BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjoBbdq +fnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDv +iS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU +1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+ +bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5aszPeE4uwc2hGKceeoW +MPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTA +ephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVkDBF9qn1l +uMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKIn +Z57QzxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfS +tQWVYrmm3ok9Nns4d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcF +PseKUgzbFbS9bZvlxrFUaKnjaZC2mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Un +hw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6pXE0zX5IJL4hmXXeXxx12E6nV +5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvmMw== +-----END CERTIFICATE----- + +# Issuer: CN=GeoTrust Global CA 2 O=GeoTrust Inc. +# Subject: CN=GeoTrust Global CA 2 O=GeoTrust Inc. +# Label: "GeoTrust Global CA 2" +# Serial: 1 +# MD5 Fingerprint: 0e:40:a7:6c:de:03:5d:8f:d1:0f:e4:d1:8d:f9:6c:a9 +# SHA1 Fingerprint: a9:e9:78:08:14:37:58:88:f2:05:19:b0:6d:2b:0d:2b:60:16:90:7d +# SHA256 Fingerprint: ca:2d:82:a0:86:77:07:2f:8a:b6:76:4f:f0:35:67:6c:fe:3e:5e:32:5e:01:21:72:df:3f:92:09:6d:b7:9b:85 +-----BEGIN CERTIFICATE----- +MIIDZjCCAk6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBEMQswCQYDVQQGEwJVUzEW +MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEdMBsGA1UEAxMUR2VvVHJ1c3QgR2xvYmFs +IENBIDIwHhcNMDQwMzA0MDUwMDAwWhcNMTkwMzA0MDUwMDAwWjBEMQswCQYDVQQG +EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEdMBsGA1UEAxMUR2VvVHJ1c3Qg +R2xvYmFsIENBIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDvPE1A +PRDfO1MA4Wf+lGAVPoWI8YkNkMgoI5kF6CsgncbzYEbYwbLVjDHZ3CB5JIG/NTL8 +Y2nbsSpr7iFY8gjpeMtvy/wWUsiRxP89c96xPqfCfWbB9X5SJBri1WeR0IIQ13hL +TytCOb1kLUCgsBDTOEhGiKEMuzozKmKY+wCdE1l/bztyqu6mD4b5BWHqZ38MN5aL +5mkWRxHCJ1kDs6ZgwiFAVvqgx306E+PsV8ez1q6diYD3Aecs9pYrEw15LNnA5IZ7 +S4wMcoKK+xfNAGw6EzywhIdLFnopsk/bHdQL82Y3vdj2V7teJHq4PIu5+pIaGoSe +2HSPqht/XvT+RSIhAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE +FHE4NvICMVNHK266ZUapEBVYIAUJMB8GA1UdIwQYMBaAFHE4NvICMVNHK266ZUap +EBVYIAUJMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQUFAAOCAQEAA/e1K6td +EPx7srJerJsOflN4WT5CBP51o62sgU7XAotexC3IUnbHLB/8gTKY0UvGkpMzNTEv +/NgdRN3ggX+d6YvhZJFiCzkIjKx0nVnZellSlxG5FntvRdOW2TF9AjYPnDtuzywN +A0ZF66D0f0hExghAzN4bcLUprbqLOzRldRtxIR0sFAqwlpW41uryZfspuk/qkZN0 +abby/+Ea0AzRdoXLiiW9l14sbxWZJue2Kf8i7MkCx1YAzUm5s2x7UwQa4qjJqhIF +I8LO57sEAszAR6LkxCkvW0VXiVHuPOtSCP8HNR6fNWpHSlaY0VqFH4z1Ir+rzoPz +4iIprn2DQKi6bA== +-----END CERTIFICATE----- + +# Issuer: CN=GeoTrust Universal CA O=GeoTrust Inc. +# Subject: CN=GeoTrust Universal CA O=GeoTrust Inc. +# Label: "GeoTrust Universal CA" +# Serial: 1 +# MD5 Fingerprint: 92:65:58:8b:a2:1a:31:72:73:68:5c:b4:a5:7a:07:48 +# SHA1 Fingerprint: e6:21:f3:35:43:79:05:9a:4b:68:30:9d:8a:2f:74:22:15:87:ec:79 +# SHA256 Fingerprint: a0:45:9b:9f:63:b2:25:59:f5:fa:5d:4c:6d:b3:f9:f7:2f:f1:93:42:03:35:78:f0:73:bf:1d:1b:46:cb:b9:12 +-----BEGIN CERTIFICATE----- +MIIFaDCCA1CgAwIBAgIBATANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJVUzEW +MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEeMBwGA1UEAxMVR2VvVHJ1c3QgVW5pdmVy +c2FsIENBMB4XDTA0MDMwNDA1MDAwMFoXDTI5MDMwNDA1MDAwMFowRTELMAkGA1UE +BhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xHjAcBgNVBAMTFUdlb1RydXN0 +IFVuaXZlcnNhbCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKYV +VaCjxuAfjJ0hUNfBvitbtaSeodlyWL0AG0y/YckUHUWCq8YdgNY96xCcOq9tJPi8 +cQGeBvV8Xx7BDlXKg5pZMK4ZyzBIle0iN430SppyZj6tlcDgFgDgEB8rMQ7XlFTT +QjOgNB0eRXbdT8oYN+yFFXoZCPzVx5zw8qkuEKmS5j1YPakWaDwvdSEYfyh3peFh +F7em6fgemdtzbvQKoiFs7tqqhZJmr/Z6a4LauiIINQ/PQvE1+mrufislzDoR5G2v +c7J2Ha3QsnhnGqQ5HFELZ1aD/ThdDc7d8Lsrlh/eezJS/R27tQahsiFepdaVaH/w +mZ7cRQg+59IJDTWU3YBOU5fXtQlEIGQWFwMCTFMNaN7VqnJNk22CDtucvc+081xd +VHppCZbW2xHBjXWotM85yM48vCR85mLK4b19p71XZQvk/iXttmkQ3CgaRr0BHdCX +teGYO8A3ZNY9lO4L4fUorgtWv3GLIylBjobFS1J72HGrH4oVpjuDWtdYAVHGTEHZ +f9hBZ3KiKN9gg6meyHv8U3NyWfWTehd2Ds735VzZC1U0oqpbtWpU5xPKV+yXbfRe +Bi9Fi1jUIxaS5BZuKGNZMN9QAZxjiRqf2xeUgnA3wySemkfWWspOqGmJch+RbNt+ +nhutxx9z3SxPGWX9f5NAEC7S8O08ni4oPmkmM8V7AgMBAAGjYzBhMA8GA1UdEwEB +/wQFMAMBAf8wHQYDVR0OBBYEFNq7LqqwDLiIJlF0XG0D08DYj3rWMB8GA1UdIwQY +MBaAFNq7LqqwDLiIJlF0XG0D08DYj3rWMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG +9w0BAQUFAAOCAgEAMXjmx7XfuJRAyXHEqDXsRh3ChfMoWIawC/yOsjmPRFWrZIRc +aanQmjg8+uUfNeVE44B5lGiku8SfPeE0zTBGi1QrlaXv9z+ZhP015s8xxtxqv6fX +IwjhmF7DWgh2qaavdy+3YL1ERmrvl/9zlcGO6JP7/TG37FcREUWbMPEaiDnBTzyn +ANXH/KttgCJwpQzgXQQpAvvLoJHRfNbDflDVnVi+QTjruXU8FdmbyUqDWcDaU/0z +uzYYm4UPFd3uLax2k7nZAY1IEKj79TiG8dsKxr2EoyNB3tZ3b4XUhRxQ4K5RirqN +Pnbiucon8l+f725ZDQbYKxek0nxru18UGkiPGkzns0ccjkxFKyDuSN/n3QmOGKja +QI2SJhFTYXNd673nxE0pN2HrrDktZy4W1vUAg4WhzH92xH3kt0tm7wNFYGm2DFKW +koRepqO1pD4r2czYG0eq8kTaT/kD6PAUyz/zg97QwVTjt+gKN02LIFkDMBmhLMi9 +ER/frslKxfMnZmaGrGiR/9nmUxwPi1xpZQomyB40w11Re9epnAahNt3ViZS82eQt +DF4JbAiXfKM9fJP/P6EUp8+1Xevb2xzEdt+Iub1FBZUbrvxGakyvSOPOrg/Sfuvm +bJxPgWp6ZKy7PtXny3YuxadIwVyQD8vIP/rmMuGNG2+k5o7Y+SlIis5z/iw= +-----END CERTIFICATE----- + +# Issuer: CN=GeoTrust Universal CA 2 O=GeoTrust Inc. +# Subject: CN=GeoTrust Universal CA 2 O=GeoTrust Inc. +# Label: "GeoTrust Universal CA 2" +# Serial: 1 +# MD5 Fingerprint: 34:fc:b8:d0:36:db:9e:14:b3:c2:f2:db:8f:e4:94:c7 +# SHA1 Fingerprint: 37:9a:19:7b:41:85:45:35:0c:a6:03:69:f3:3c:2e:af:47:4f:20:79 +# SHA256 Fingerprint: a0:23:4f:3b:c8:52:7c:a5:62:8e:ec:81:ad:5d:69:89:5d:a5:68:0d:c9:1d:1c:b8:47:7f:33:f8:78:b9:5b:0b +-----BEGIN CERTIFICATE----- +MIIFbDCCA1SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBHMQswCQYDVQQGEwJVUzEW +MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1c3QgVW5pdmVy +c2FsIENBIDIwHhcNMDQwMzA0MDUwMDAwWhcNMjkwMzA0MDUwMDAwWjBHMQswCQYD +VQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1 +c3QgVW5pdmVyc2FsIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC +AQCzVFLByT7y2dyxUxpZKeexw0Uo5dfR7cXFS6GqdHtXr0om/Nj1XqduGdt0DE81 +WzILAePb63p3NeqqWuDW6KFXlPCQo3RWlEQwAx5cTiuFJnSCegx2oG9NzkEtoBUG +FF+3Qs17j1hhNNwqCPkuwwGmIkQcTAeC5lvO0Ep8BNMZcyfwqph/Lq9O64ceJHdq +XbboW0W63MOhBW9Wjo8QJqVJwy7XQYci4E+GymC16qFjwAGXEHm9ADwSbSsVsaxL +se4YuU6W3Nx2/zu+z18DwPw76L5GG//aQMJS9/7jOvdqdzXQ2o3rXhhqMcceujwb +KNZrVMaqW9eiLBsZzKIC9ptZvTdrhrVtgrrY6slWvKk2WP0+GfPtDCapkzj4T8Fd +IgbQl+rhrcZV4IErKIM6+vR7IVEAvlI4zs1meaj0gVbi0IMJR1FbUGrP20gaXT73 +y/Zl92zxlfgCOzJWgjl6W70viRu/obTo/3+NjN8D8WBOWBFM66M/ECuDmgFz2ZRt +hAAnZqzwcEAJQpKtT5MNYQlRJNiS1QuUYbKHsu3/mjX/hVTK7URDrBs8FmtISgoc +QIgfksILAAX/8sgCSqSqqcyZlpwvWOB94b67B9xfBHJcMTTD7F8t4D1kkCLm0ey4 +Lt1ZrtmhN79UNdxzMk+MBB4zsslG8dhcyFVQyWi9qLo2CQIDAQABo2MwYTAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAfBgNV +HSMEGDAWgBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAOBgNVHQ8BAf8EBAMCAYYwDQYJ +KoZIhvcNAQEFBQADggIBAGbBxiPz2eAubl/oz66wsCVNK/g7WJtAJDday6sWSf+z +dXkzoS9tcBc0kf5nfo/sm+VegqlVHy/c1FEHEv6sFj4sNcZj/NwQ6w2jqtB8zNHQ +L1EuxBRa3ugZ4T7GzKQp5y6EqgYweHZUcyiYWTjgAA1i00J9IZ+uPTqM1fp3DRgr +Fg5fNuH8KrUwJM/gYwx7WBr+mbpCErGR9Hxo4sjoryzqyX6uuyo9DRXcNJW2GHSo +ag/HtPQTxORb7QrSpJdMKu0vbBKJPfEncKpqA1Ihn0CoZ1Dy81of398j9tx4TuaY +T1U6U+Pv8vSfx3zYWK8pIpe44L2RLrB27FcRz+8pRPPphXpgY+RdM4kX2TGq2tbz +GDVyz4crL2MjhF2EjD9XoIj8mZEoJmmZ1I+XRL6O1UixpCgp8RW04eWe3fiPpm8m +1wk8OhwRDqZsN/etRIcsKMfYdIKz0G9KV7s1KSegi+ghp4dkNl3M2Basx7InQJJV +OCiNUW7dFGdTbHFcJoRNdVq2fmBWqU2t+5sel/MN2dKXVHfaPRK34B7vCAas+YWH +6aLcr34YEoP9VhdBLtUpgn2Z9DH2canPLAEnpQW5qrJITirvn5NSUZU8UnOOVkwX +QMAJKOSLakhT2+zNVVXxxvjpoixMptEmX36vWkzaH6byHCx+rgIW0lbQL1dTR+iS +-----END CERTIFICATE----- + +# Issuer: CN=America Online Root Certification Authority 1 O=America Online Inc. +# Subject: CN=America Online Root Certification Authority 1 O=America Online Inc. +# Label: "America Online Root Certification Authority 1" +# Serial: 1 +# MD5 Fingerprint: 14:f1:08:ad:9d:fa:64:e2:89:e7:1c:cf:a8:ad:7d:5e +# SHA1 Fingerprint: 39:21:c1:15:c1:5d:0e:ca:5c:cb:5b:c4:f0:7d:21:d8:05:0b:56:6a +# SHA256 Fingerprint: 77:40:73:12:c6:3a:15:3d:5b:c0:0b:4e:51:75:9c:df:da:c2:37:dc:2a:33:b6:79:46:e9:8e:9b:fa:68:0a:e3 +-----BEGIN CERTIFICATE----- +MIIDpDCCAoygAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEc +MBoGA1UEChMTQW1lcmljYSBPbmxpbmUgSW5jLjE2MDQGA1UEAxMtQW1lcmljYSBP +bmxpbmUgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAxMB4XDTAyMDUyODA2 +MDAwMFoXDTM3MTExOTIwNDMwMFowYzELMAkGA1UEBhMCVVMxHDAaBgNVBAoTE0Ft +ZXJpY2EgT25saW5lIEluYy4xNjA0BgNVBAMTLUFtZXJpY2EgT25saW5lIFJvb3Qg +Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkgMTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAKgv6KRpBgNHw+kqmP8ZonCaxlCyfqXfaE0bfA+2l2h9LaaLl+lk +hsmj76CGv2BlnEtUiMJIxUo5vxTjWVXlGbR0yLQFOVwWpeKVBeASrlmLojNoWBym +1BW32J/X3HGrfpq/m44zDyL9Hy7nBzbvYjnF3cu6JRQj3gzGPTzOggjmZj7aUTsW +OqMFf6Dch9Wc/HKpoH145LcxVR5lu9RhsCFg7RAycsWSJR74kEoYeEfffjA3PlAb +2xzTa5qGUwew76wGePiEmf4hjUyAtgyC9mZweRrTT6PP8c9GsEsPPt2IYriMqQko +O3rHl+Ee5fSfwMCuJKDIodkP1nsmgmkyPacCAwEAAaNjMGEwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUAK3Zo/Z59m50qX8zPYEX10zPM94wHwYDVR0jBBgwFoAU +AK3Zo/Z59m50qX8zPYEX10zPM94wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB +BQUAA4IBAQB8itEfGDeC4Liwo+1WlchiYZwFos3CYiZhzRAW18y0ZTTQEYqtqKkF +Zu90821fnZmv9ov761KyBZiibyrFVL0lvV+uyIbqRizBs73B6UlwGBaXCBOMIOAb +LjpHyx7kADCVW/RFo8AasAFOq73AI25jP4BKxQft3OJvx8Fi8eNy1gTIdGcL+oir +oQHIb/AUr9KZzVGTfu0uOMe9zkZQPXLjeSWdm4grECDdpbgyn43gKd8hdIaC2y+C +MMbHNYaz+ZZfRtsMRf3zUMNvxsNIrUam4SdHCh0Om7bCd39j8uB9Gr784N/Xx6ds +sPmuujz9dLQR6FgNgLzTqIA6me11zEZ7 +-----END CERTIFICATE----- + +# Issuer: CN=America Online Root Certification Authority 2 O=America Online Inc. +# Subject: CN=America Online Root Certification Authority 2 O=America Online Inc. +# Label: "America Online Root Certification Authority 2" +# Serial: 1 +# MD5 Fingerprint: d6:ed:3c:ca:e2:66:0f:af:10:43:0d:77:9b:04:09:bf +# SHA1 Fingerprint: 85:b5:ff:67:9b:0c:79:96:1f:c8:6e:44:22:00:46:13:db:17:92:84 +# SHA256 Fingerprint: 7d:3b:46:5a:60:14:e5:26:c0:af:fc:ee:21:27:d2:31:17:27:ad:81:1c:26:84:2d:00:6a:f3:73:06:cc:80:bd +-----BEGIN CERTIFICATE----- +MIIFpDCCA4ygAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEc +MBoGA1UEChMTQW1lcmljYSBPbmxpbmUgSW5jLjE2MDQGA1UEAxMtQW1lcmljYSBP +bmxpbmUgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAyMB4XDTAyMDUyODA2 +MDAwMFoXDTM3MDkyOTE0MDgwMFowYzELMAkGA1UEBhMCVVMxHDAaBgNVBAoTE0Ft +ZXJpY2EgT25saW5lIEluYy4xNjA0BgNVBAMTLUFtZXJpY2EgT25saW5lIFJvb3Qg +Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBAMxBRR3pPU0Q9oyxQcngXssNt79Hc9PwVU3dxgz6sWYFas14tNwC +206B89enfHG8dWOgXeMHDEjsJcQDIPT/DjsS/5uN4cbVG7RtIuOx238hZK+GvFci +KtZHgVdEglZTvYYUAQv8f3SkWq7xuhG1m1hagLQ3eAkzfDJHA1zEpYNI9FdWboE2 +JxhP7JsowtS013wMPgwr38oE18aO6lhOqKSlGBxsRZijQdEt0sdtjRnxrXm3gT+9 +BoInLRBYBbV4Bbkv2wxrkJB+FFk4u5QkE+XRnRTf04JNRvCAOVIyD+OEsnpD8l7e +Xz8d3eOyG6ChKiMDbi4BFYdcpnV1x5dhvt6G3NRI270qv0pV2uh9UPu0gBe4lL8B +PeraunzgWGcXuVjgiIZGZ2ydEEdYMtA1fHkqkKJaEBEjNa0vzORKW6fIJ/KD3l67 +Xnfn6KVuY8INXWHQjNJsWiEOyiijzirplcdIz5ZvHZIlyMbGwcEMBawmxNJ10uEq +Z8A9W6Wa6897GqidFEXlD6CaZd4vKL3Ob5Rmg0gp2OpljK+T2WSfVVcmv2/LNzGZ +o2C7HK2JNDJiuEMhBnIMoVxtRsX6Kc8w3onccVvdtjc+31D1uAclJuW8tf48ArO3 ++L5DwYcRlJ4jbBeKuIonDFRH8KmzwICMoCfrHRnjB453cMor9H124HhnAgMBAAGj +YzBhMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFE1FwWg4u3OpaaEg5+31IqEj +FNeeMB8GA1UdIwQYMBaAFE1FwWg4u3OpaaEg5+31IqEjFNeeMA4GA1UdDwEB/wQE +AwIBhjANBgkqhkiG9w0BAQUFAAOCAgEAZ2sGuV9FOypLM7PmG2tZTiLMubekJcmn +xPBUlgtk87FYT15R/LKXeydlwuXK5w0MJXti4/qftIe3RUavg6WXSIylvfEWK5t2 +LHo1YGwRgJfMqZJS5ivmae2p+DYtLHe/YUjRYwu5W1LtGLBDQiKmsXeu3mnFzccc +obGlHBD7GL4acN3Bkku+KVqdPzW+5X1R+FXgJXUjhx5c3LqdsKyzadsXg8n33gy8 +CNyRnqjQ1xU3c6U1uPx+xURABsPr+CKAXEfOAuMRn0T//ZoyzH1kUQ7rVyZ2OuMe +IjzCpjbdGe+n/BLzJsBZMYVMnNjP36TMzCmT/5RtdlwTCJfy7aULTd3oyWgOZtMA +DjMSW7yV5TKQqLPGbIOtd+6Lfn6xqavT4fG2wLHqiMDn05DpKJKUe2h7lyoKZy2F +AjgQ5ANh1NolNscIWC2hp1GvMApJ9aZphwctREZ2jirlmjvXGKL8nDgQzMY70rUX +Om/9riW99XJZZLF0KjhfGEzfz3EEWjbUvy+ZnOjZurGV5gJLIaFb1cFPj65pbVPb +AZO1XB4Y3WRayhgoPmMEEf0cjQAPuDffZ4qdZqkCapH/E8ovXYO8h5Ns3CRRFgQl +Zvqz2cK6Kb6aSDiCmfS/O0oxGfm/jiEzFMpPVF/7zvuPcX/9XhmgD0uRuMRUvAaw +RY8mkaKO/qk= +-----END CERTIFICATE----- + +# Issuer: CN=AAA Certificate Services O=Comodo CA Limited +# Subject: CN=AAA Certificate Services O=Comodo CA Limited +# Label: "Comodo AAA Services root" +# Serial: 1 +# MD5 Fingerprint: 49:79:04:b0:eb:87:19:ac:47:b0:bc:11:51:9b:74:d0 +# SHA1 Fingerprint: d1:eb:23:a4:6d:17:d6:8f:d9:25:64:c2:f1:f1:60:17:64:d8:e3:49 +# SHA256 Fingerprint: d7:a7:a0:fb:5d:7e:27:31:d7:71:e9:48:4e:bc:de:f7:1d:5f:0c:3e:0a:29:48:78:2b:c8:3e:e0:ea:69:9e:f4 +-----BEGIN CERTIFICATE----- +MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb +MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow +GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj +YXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM +GEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua +BtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe +3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4 +YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR +rOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm +ez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU +oBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF +MAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v +QUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t +b2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF +AAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q +GE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz +Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2 +G9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi +l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3 +smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg== +-----END CERTIFICATE----- + +# Issuer: CN=Secure Certificate Services O=Comodo CA Limited +# Subject: CN=Secure Certificate Services O=Comodo CA Limited +# Label: "Comodo Secure Services root" +# Serial: 1 +# MD5 Fingerprint: d3:d9:bd:ae:9f:ac:67:24:b3:c8:1b:52:e1:b9:a9:bd +# SHA1 Fingerprint: 4a:65:d5:f4:1d:ef:39:b8:b8:90:4a:4a:d3:64:81:33:cf:c7:a1:d1 +# SHA256 Fingerprint: bd:81:ce:3b:4f:65:91:d1:1a:67:b5:fc:7a:47:fd:ef:25:52:1b:f9:aa:4e:18:b9:e3:df:2e:34:a7:80:3b:e8 +-----BEGIN CERTIFICATE----- +MIIEPzCCAyegAwIBAgIBATANBgkqhkiG9w0BAQUFADB+MQswCQYDVQQGEwJHQjEb +MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow +GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEkMCIGA1UEAwwbU2VjdXJlIENlcnRp +ZmljYXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVow +fjELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxJDAiBgNV +BAMMG1NlY3VyZSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBAMBxM4KK0HDrc4eCQNUd5MvJDkKQ+d40uaG6EfQlhfPM +cm3ye5drswfxdySRXyWP9nQ95IDC+DwN879A6vfIUtFyb+/Iq0G4bi4XKpVpDM3S +HpR7LZQdqnXXs5jLrLxkU0C8j6ysNstcrbvd4JQX7NFc0L/vpZXJkMWwrPsbQ996 +CF23uPJAGysnnlDOXmWCiIxe004MeuoIkbY2qitC++rCoznl2yY4rYsK7hljxxwk +3wN42ubqwUcaCwtGCd0C/N7Lh1/XMGNooa7cMqG6vv5Eq2i2pRcV/b3Vp6ea5EQz +6YiO/O1R65NxTq0B50SOqy3LqP4BSUjwwN3HaNiS/j0CAwEAAaOBxzCBxDAdBgNV +HQ4EFgQUPNiTiMLAggnMAZkGkyDpnnAJY08wDgYDVR0PAQH/BAQDAgEGMA8GA1Ud +EwEB/wQFMAMBAf8wgYEGA1UdHwR6MHgwO6A5oDeGNWh0dHA6Ly9jcmwuY29tb2Rv +Y2EuY29tL1NlY3VyZUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDmgN6A1hjNodHRw +Oi8vY3JsLmNvbW9kby5uZXQvU2VjdXJlQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmww +DQYJKoZIhvcNAQEFBQADggEBAIcBbSMdflsXfcFhMs+P5/OKlFlm4J4oqF7Tt/Q0 +5qo5spcWxYJvMqTpjOev/e/C6LlLqqP05tqNZSH7uoDrJiiFGv45jN5bBAS0VPmj +Z55B+glSzAVIqMk/IQQezkhr/IXownuvf7fM+F86/TXGDe+X3EyrEeFryzHRbPtI +gKvcnDe4IRRLDXE97IMzbtFuMhbsmMcWi1mmNKsFVy2T96oTy9IT4rcuO81rUBcJ +aD61JlfutuC23bkpgHl9j6PwpCikFcSF9CfUa7/lXORlAnZUtOM3ZiTTGWHIUhDl +izeauan5Hb/qmZJhlv8BzaFfDbxxvA6sCx1HRR3B7Hzs/Sk= +-----END CERTIFICATE----- + +# Issuer: CN=Trusted Certificate Services O=Comodo CA Limited +# Subject: CN=Trusted Certificate Services O=Comodo CA Limited +# Label: "Comodo Trusted Services root" +# Serial: 1 +# MD5 Fingerprint: 91:1b:3f:6e:cd:9e:ab:ee:07:fe:1f:71:d2:b3:61:27 +# SHA1 Fingerprint: e1:9f:e3:0e:8b:84:60:9e:80:9b:17:0d:72:a8:c5:ba:6e:14:09:bd +# SHA256 Fingerprint: 3f:06:e5:56:81:d4:96:f5:be:16:9e:b5:38:9f:9f:2b:8f:f6:1e:17:08:df:68:81:72:48:49:cd:5d:27:cb:69 +-----BEGIN CERTIFICATE----- +MIIEQzCCAyugAwIBAgIBATANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJHQjEb +MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow +GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDElMCMGA1UEAwwcVHJ1c3RlZCBDZXJ0 +aWZpY2F0ZSBTZXJ2aWNlczAeFw0wNDAxMDEwMDAwMDBaFw0yODEyMzEyMzU5NTla +MH8xCzAJBgNVBAYTAkdCMRswGQYDVQQIDBJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAO +BgNVBAcMB1NhbGZvcmQxGjAYBgNVBAoMEUNvbW9kbyBDQSBMaW1pdGVkMSUwIwYD +VQQDDBxUcnVzdGVkIENlcnRpZmljYXRlIFNlcnZpY2VzMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA33FvNlhTWvI2VFeAxHQIIO0Yfyod5jWaHiWsnOWW +fnJSoBVC21ndZHoa0Lh73TkVvFVIxO06AOoxEbrycXQaZ7jPM8yoMa+j49d/vzMt +TGo87IvDktJTdyR0nAducPy9C1t2ul/y/9c3S0pgePfw+spwtOpZqqPOSC+pw7IL +fhdyFgymBwwbOM/JYrc/oJOlh0Hyt3BAd9i+FHzjqMB6juljatEPmsbS9Is6FARW +1O24zG71++IsWL1/T2sr92AkWCTOJu80kTrV44HQsvAEAtdbtz6SrGsSivnkBbA7 +kUlcsutT6vifR4buv5XAwAaf0lteERv0xwQ1KdJVXOTt6wIDAQABo4HJMIHGMB0G +A1UdDgQWBBTFe1i97doladL3WRaoszLAeydb9DAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zCBgwYDVR0fBHwwejA8oDqgOIY2aHR0cDovL2NybC5jb21v +ZG9jYS5jb20vVHJ1c3RlZENlcnRpZmljYXRlU2VydmljZXMuY3JsMDqgOKA2hjRo +dHRwOi8vY3JsLmNvbW9kby5uZXQvVHJ1c3RlZENlcnRpZmljYXRlU2VydmljZXMu +Y3JsMA0GCSqGSIb3DQEBBQUAA4IBAQDIk4E7ibSvuIQSTI3S8NtwuleGFTQQuS9/ +HrCoiWChisJ3DFBKmwCL2Iv0QeLQg4pKHBQGsKNoBXAxMKdTmw7pSqBYaWcOrp32 +pSxBvzwGa+RZzG0Q8ZZvH9/0BAKkn0U+yNj6NkZEUD+Cl5EfKNsYEYwq5GWDVxIS +jBc/lDb+XbDABHcTuPQV1T84zJQ6VdCsmPW6AF/ghhmBeC8owH7TzEIK9a5QoNE+ +xqFx7D+gIIxmOom0jtTYsU0lR+4viMi14QVFwL4Ucd56/Y57fU0IlqUSc/Atyjcn +dBInTMu2l+nZrghtWjlA3QVHdWpaIbOjGM9O9y5Xt5hwXsjEeLBi +-----END CERTIFICATE----- + +# Issuer: CN=UTN - DATACorp SGC O=The USERTRUST Network OU=http://www.usertrust.com +# Subject: CN=UTN - DATACorp SGC O=The USERTRUST Network OU=http://www.usertrust.com +# Label: "UTN DATACorp SGC Root CA" +# Serial: 91374294542884689855167577680241077609 +# MD5 Fingerprint: b3:a5:3e:77:21:6d:ac:4a:c0:c9:fb:d5:41:3d:ca:06 +# SHA1 Fingerprint: 58:11:9f:0e:12:82:87:ea:50:fd:d9:87:45:6f:4f:78:dc:fa:d6:d4 +# SHA256 Fingerprint: 85:fb:2f:91:dd:12:27:5a:01:45:b6:36:53:4f:84:02:4a:d6:8b:69:b8:ee:88:68:4f:f7:11:37:58:05:b3:48 +-----BEGIN CERTIFICATE----- +MIIEXjCCA0agAwIBAgIQRL4Mi1AAIbQR0ypoBqmtaTANBgkqhkiG9w0BAQUFADCB +kzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug +Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho +dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xGzAZBgNVBAMTElVUTiAtIERBVEFDb3Jw +IFNHQzAeFw05OTA2MjQxODU3MjFaFw0xOTA2MjQxOTA2MzBaMIGTMQswCQYDVQQG +EwJVUzELMAkGA1UECBMCVVQxFzAVBgNVBAcTDlNhbHQgTGFrZSBDaXR5MR4wHAYD +VQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxITAfBgNVBAsTGGh0dHA6Ly93d3cu +dXNlcnRydXN0LmNvbTEbMBkGA1UEAxMSVVROIC0gREFUQUNvcnAgU0dDMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3+5YEKIrblXEjr8uRgnn4AgPLit6 +E5Qbvfa2gI5lBZMAHryv4g+OGQ0SR+ysraP6LnD43m77VkIVni5c7yPeIbkFdicZ +D0/Ww5y0vpQZY/KmEQrrU0icvvIpOxboGqBMpsn0GFlowHDyUwDAXlCCpVZvNvlK +4ESGoE1O1kduSUrLZ9emxAW5jh70/P/N5zbgnAVssjMiFdC04MwXwLLA9P4yPykq +lXvY8qdOD1R8oQ2AswkDwf9c3V6aPryuvEeKaq5xyh+xKrhfQgUL7EYw0XILyulW +bfXv33i+Ybqypa4ETLyorGkVl73v67SMvzX41MPRKA5cOp9wGDMgd8SirwIDAQAB +o4GrMIGoMAsGA1UdDwQEAwIBxjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRT +MtGzz3/64PGgXYVOktKeRR20TzA9BgNVHR8ENjA0MDKgMKAuhixodHRwOi8vY3Js +LnVzZXJ0cnVzdC5jb20vVVROLURBVEFDb3JwU0dDLmNybDAqBgNVHSUEIzAhBggr +BgEFBQcDAQYKKwYBBAGCNwoDAwYJYIZIAYb4QgQBMA0GCSqGSIb3DQEBBQUAA4IB +AQAnNZcAiosovcYzMB4p/OL31ZjUQLtgyr+rFywJNn9Q+kHcrpY6CiM+iVnJowft +Gzet/Hy+UUla3joKVAgWRcKZsYfNjGjgaQPpxE6YsjuMFrMOoAyYUJuTqXAJyCyj +j98C5OBxOvG0I3KgqgHf35g+FFCgMSa9KOlaMCZ1+XtgHI3zzVAmbQQnmt/VDUVH +KWss5nbZqSl9Mt3JNjy9rjXxEZ4du5A/EkdOjtd+D2JzHVImOBwYSf0wdJrE5SIv +2MCN7ZF6TACPcn9d2t0bi0Vr591pl6jFVkwPDPafepE39peC4N1xaf92P2BNPM/3 +mfnGV/TJVTl4uix5yaaIK/QI +-----END CERTIFICATE----- + +# Issuer: CN=UTN-USERFirst-Hardware O=The USERTRUST Network OU=http://www.usertrust.com +# Subject: CN=UTN-USERFirst-Hardware O=The USERTRUST Network OU=http://www.usertrust.com +# Label: "UTN USERFirst Hardware Root CA" +# Serial: 91374294542884704022267039221184531197 +# MD5 Fingerprint: 4c:56:41:e5:0d:bb:2b:e8:ca:a3:ed:18:08:ad:43:39 +# SHA1 Fingerprint: 04:83:ed:33:99:ac:36:08:05:87:22:ed:bc:5e:46:00:e3:be:f9:d7 +# SHA256 Fingerprint: 6e:a5:47:41:d0:04:66:7e:ed:1b:48:16:63:4a:a3:a7:9e:6e:4b:96:95:0f:82:79:da:fc:8d:9b:d8:81:21:37 +-----BEGIN CERTIFICATE----- +MIIEdDCCA1ygAwIBAgIQRL4Mi1AAJLQR0zYq/mUK/TANBgkqhkiG9w0BAQUFADCB +lzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug +Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho +dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xHzAdBgNVBAMTFlVUTi1VU0VSRmlyc3Qt +SGFyZHdhcmUwHhcNOTkwNzA5MTgxMDQyWhcNMTkwNzA5MTgxOTIyWjCBlzELMAkG +A1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2UgQ2l0eTEe +MBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExhodHRwOi8v +d3d3LnVzZXJ0cnVzdC5jb20xHzAdBgNVBAMTFlVUTi1VU0VSRmlyc3QtSGFyZHdh +cmUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCx98M4P7Sof885glFn +0G2f0v9Y8+efK+wNiVSZuTiZFvfgIXlIwrthdBKWHTxqctU8EGc6Oe0rE81m65UJ +M6Rsl7HoxuzBdXmcRl6Nq9Bq/bkqVRcQVLMZ8Jr28bFdtqdt++BxF2uiiPsA3/4a +MXcMmgF6sTLjKwEHOG7DpV4jvEWbe1DByTCP2+UretNb+zNAHqDVmBe8i4fDidNd +oI6yqqr2jmmIBsX6iSHzCJ1pLgkzmykNRg+MzEk0sGlRvfkGzWitZky8PqxhvQqI +DsjfPe58BEydCl5rkdbux+0ojatNh4lz0G6k0B4WixThdkQDf2Os5M1JnMWS9Ksy +oUhbAgMBAAGjgbkwgbYwCwYDVR0PBAQDAgHGMA8GA1UdEwEB/wQFMAMBAf8wHQYD +VR0OBBYEFKFyXyYbKJhDlV0HN9WFlp1L0sNFMEQGA1UdHwQ9MDswOaA3oDWGM2h0 +dHA6Ly9jcmwudXNlcnRydXN0LmNvbS9VVE4tVVNFUkZpcnN0LUhhcmR3YXJlLmNy +bDAxBgNVHSUEKjAoBggrBgEFBQcDAQYIKwYBBQUHAwUGCCsGAQUFBwMGBggrBgEF +BQcDBzANBgkqhkiG9w0BAQUFAAOCAQEARxkP3nTGmZev/K0oXnWO6y1n7k57K9cM +//bey1WiCuFMVGWTYGufEpytXoMs61quwOQt9ABjHbjAbPLPSbtNk28Gpgoiskli +CE7/yMgUsogWXecB5BKV5UU0s4tpvc+0hY91UZ59Ojg6FEgSxvunOxqNDYJAB+gE +CJChicsZUN/KHAG8HQQZexB2lzvukJDKxA4fFm517zP4029bHpbj4HR3dHuKom4t +3XbWOTCC8KucUvIqx69JXn7HaOWCgchqJ/kniCrVWFCVH/A7HFe7fRQ5YiuayZSS +KqMiDP+JJn1fIytH1xUdqWqeUQ0qUZ6B+dQ7XnASfxAynB67nfhmqA== +-----END CERTIFICATE----- + +# Issuer: CN=XRamp Global Certification Authority O=XRamp Security Services Inc OU=www.xrampsecurity.com +# Subject: CN=XRamp Global Certification Authority O=XRamp Security Services Inc OU=www.xrampsecurity.com +# Label: "XRamp Global CA Root" +# Serial: 107108908803651509692980124233745014957 +# MD5 Fingerprint: a1:0b:44:b3:ca:10:d8:00:6e:9d:0f:d8:0f:92:0a:d1 +# SHA1 Fingerprint: b8:01:86:d1:eb:9c:86:a5:41:04:cf:30:54:f3:4c:52:b7:e5:58:c6 +# SHA256 Fingerprint: ce:cd:dc:90:50:99:d8:da:df:c5:b1:d2:09:b7:37:cb:e2:c1:8c:fb:2c:10:c0:ff:0b:cf:0d:32:86:fc:1a:a2 +-----BEGIN CERTIFICATE----- +MIIEMDCCAxigAwIBAgIQUJRs7Bjq1ZxN1ZfvdY+grTANBgkqhkiG9w0BAQUFADCB +gjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEk +MCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2VydmljZXMgSW5jMS0wKwYDVQQDEyRY +UmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQxMTAxMTcx +NDA0WhcNMzUwMTAxMDUzNzE5WjCBgjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3 +dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2Vy +dmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCYJB69FbS6 +38eMpSe2OAtp87ZOqCwuIR1cRN8hXX4jdP5efrRKt6atH67gBhbim1vZZ3RrXYCP +KZ2GG9mcDZhtdhAoWORlsH9KmHmf4MMxfoArtYzAQDsRhtDLooY2YKTVMIJt2W7Q +DxIEM5dfT2Fa8OT5kavnHTu86M/0ay00fOJIYRyO82FEzG+gSqmUsE3a56k0enI4 +qEHMPJQRfevIpoy3hsvKMzvZPTeL+3o+hiznc9cKV6xkmxnr9A8ECIqsAxcZZPRa +JSKNNCyy9mgdEm3Tih4U2sSPpuIjhdV6Db1q4Ons7Be7QhtnqiXtRYMh/MHJfNVi +PvryxS3T/dRlAgMBAAGjgZ8wgZwwEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0P +BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMZPoj0GY4QJnM5i5ASs +jVy16bYbMDYGA1UdHwQvMC0wK6ApoCeGJWh0dHA6Ly9jcmwueHJhbXBzZWN1cml0 +eS5jb20vWEdDQS5jcmwwEAYJKwYBBAGCNxUBBAMCAQEwDQYJKoZIhvcNAQEFBQAD +ggEBAJEVOQMBG2f7Shz5CmBbodpNl2L5JFMn14JkTpAuw0kbK5rc/Kh4ZzXxHfAR +vbdI4xD2Dd8/0sm2qlWkSLoC295ZLhVbO50WfUfXN+pfTXYSNrsf16GBBEYgoyxt +qZ4Bfj8pzgCT3/3JknOJiWSe5yvkHJEs0rnOfc5vMZnT5r7SHpDwCRR5XCOrTdLa +IR9NmXmd4c8nnxCbHIgNsIpkQTG4DmyQJKSbXHGPurt+HBvbaoAPIbzp26a3QPSy +i6mx5O+aGtA9aZnuqCij4Tyz8LIRnM98QObd50N9otg6tamN8jSZxNQQ4Qb9CYQQ +O+7ETPTsJ3xCwnR8gooJybQDJbw= +-----END CERTIFICATE----- + +# Issuer: O=The Go Daddy Group, Inc. OU=Go Daddy Class 2 Certification Authority +# Subject: O=The Go Daddy Group, Inc. OU=Go Daddy Class 2 Certification Authority +# Label: "Go Daddy Class 2 CA" +# Serial: 0 +# MD5 Fingerprint: 91:de:06:25:ab:da:fd:32:17:0c:bb:25:17:2a:84:67 +# SHA1 Fingerprint: 27:96:ba:e6:3f:18:01:e2:77:26:1b:a0:d7:77:70:02:8f:20:ee:e4 +# SHA256 Fingerprint: c3:84:6b:f2:4b:9e:93:ca:64:27:4c:0e:c6:7c:1e:cc:5e:02:4f:fc:ac:d2:d7:40:19:35:0e:81:fe:54:6a:e4 +-----BEGIN CERTIFICATE----- +MIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEh +MB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBE +YWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3 +MDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkGA1UEBhMCVVMxITAfBgNVBAoTGFRo +ZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28gRGFkZHkgQ2xhc3Mg +MiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQADggEN +ADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCA +PVYYYwhv2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6w +wdhFJ2+qN1j3hybX2C32qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXi +EqITLdiOr18SPaAIBQi2XKVlOARFmR6jYGB0xUGlcmIbYsUfb18aQr4CUWWoriMY +avx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmYvLEHZ6IVDd2gWMZEewo+ +YihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0OBBYEFNLE +sNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h +/t2oatTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5 +IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD +ggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wimPQoZ+YeAEW5p5JYXMP80kWNy +OO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKtI3lpjbi2Tc7P +TMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ +HmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mER +dEr/VxqHD3VILs9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5Cuf +ReYNnyicsbkqWletNw+vHX/bvZ8= +-----END CERTIFICATE----- + +# Issuer: O=Starfield Technologies, Inc. OU=Starfield Class 2 Certification Authority +# Subject: O=Starfield Technologies, Inc. OU=Starfield Class 2 Certification Authority +# Label: "Starfield Class 2 CA" +# Serial: 0 +# MD5 Fingerprint: 32:4a:4b:bb:c8:63:69:9b:be:74:9a:c6:dd:1d:46:24 +# SHA1 Fingerprint: ad:7e:1c:28:b0:64:ef:8f:60:03:40:20:14:c3:d0:e3:37:0e:b5:8a +# SHA256 Fingerprint: 14:65:fa:20:53:97:b8:76:fa:a6:f0:a9:95:8e:55:90:e4:0f:cc:7f:aa:4f:b7:c2:c8:67:75:21:fb:5f:b6:58 +-----BEGIN CERTIFICATE----- +MIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzEl +MCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMp +U3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQw +NjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBoMQswCQYDVQQGEwJVUzElMCMGA1UE +ChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZp +ZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqGSIb3 +DQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf +8MOh2tTYbitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN ++lq2cwQlZut3f+dZxkqZJRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0 +X9tDkYI22WY8sbi5gv2cOj4QyDvvBmVmepsZGD3/cVE8MC5fvj13c7JdBmzDI1aa +K4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSNF4Azbl5KXZnJHoe0nRrA +1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HFMIHCMB0G +A1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fR +zt0fhvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0 +YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBD +bGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8w +DQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGsafPzWdqbAYcaT1epoXkJKtv3 +L7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLMPUxA2IGvd56D +eruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl +xy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynp +VSJYACPq4xJDKVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEY +WQPJIrSPnNVeKtelttQKbfi3QBFGmh95DmK/D5fs4C8fF5Q= +-----END CERTIFICATE----- + +# Issuer: CN=StartCom Certification Authority O=StartCom Ltd. OU=Secure Digital Certificate Signing +# Subject: CN=StartCom Certification Authority O=StartCom Ltd. OU=Secure Digital Certificate Signing +# Label: "StartCom Certification Authority" +# Serial: 1 +# MD5 Fingerprint: 22:4d:8f:8a:fc:f7:35:c2:bb:57:34:90:7b:8b:22:16 +# SHA1 Fingerprint: 3e:2b:f7:f2:03:1b:96:f3:8c:e6:c4:d8:a8:5d:3e:2d:58:47:6a:0f +# SHA256 Fingerprint: c7:66:a9:be:f2:d4:07:1c:86:3a:31:aa:49:20:e8:13:b2:d1:98:60:8c:b7:b7:cf:e2:11:43:b8:36:df:09:ea +-----BEGIN CERTIFICATE----- +MIIHyTCCBbGgAwIBAgIBATANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJJTDEW +MBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwg +Q2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0NjM2WhcNMzYwOTE3MTk0NjM2WjB9 +MQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMi +U2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3Rh +cnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZk +pMyONvg45iPwbm2xPN1yo4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rf +OQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/C +Ji/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/deMotHweXMAEtcnn6RtYT +Kqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt2PZE4XNi +HzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMM +Av+Z6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w ++2OqqGwaVLRcJXrJosmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+ +Gkhpi8KWTRoSsmkXwQqQ1vp5Iki/untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3 +Zzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVcUjyJthkqcwEKDwOzEmDyei+B +26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT37uMdBNSSwID +AQABo4ICUjCCAk4wDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAa4wHQYDVR0OBBYE +FE4L7xqkQFulF2mHMMo0aEPQQa7yMGQGA1UdHwRdMFswLKAqoCiGJmh0dHA6Ly9j +ZXJ0LnN0YXJ0Y29tLm9yZy9zZnNjYS1jcmwuY3JsMCugKaAnhiVodHRwOi8vY3Js +LnN0YXJ0Y29tLm9yZy9zZnNjYS1jcmwuY3JsMIIBXQYDVR0gBIIBVDCCAVAwggFM +BgsrBgEEAYG1NwEBATCCATswLwYIKwYBBQUHAgEWI2h0dHA6Ly9jZXJ0LnN0YXJ0 +Y29tLm9yZy9wb2xpY3kucGRmMDUGCCsGAQUFBwIBFilodHRwOi8vY2VydC5zdGFy +dGNvbS5vcmcvaW50ZXJtZWRpYXRlLnBkZjCB0AYIKwYBBQUHAgIwgcMwJxYgU3Rh +cnQgQ29tbWVyY2lhbCAoU3RhcnRDb20pIEx0ZC4wAwIBARqBl0xpbWl0ZWQgTGlh +YmlsaXR5LCByZWFkIHRoZSBzZWN0aW9uICpMZWdhbCBMaW1pdGF0aW9ucyogb2Yg +dGhlIFN0YXJ0Q29tIENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFBvbGljeSBhdmFp +bGFibGUgYXQgaHR0cDovL2NlcnQuc3RhcnRjb20ub3JnL3BvbGljeS5wZGYwEQYJ +YIZIAYb4QgEBBAQDAgAHMDgGCWCGSAGG+EIBDQQrFilTdGFydENvbSBGcmVlIFNT +TCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTANBgkqhkiG9w0BAQUFAAOCAgEAFmyZ +9GYMNPXQhV59CuzaEE44HF7fpiUFS5Eyweg78T3dRAlbB0mKKctmArexmvclmAk8 +jhvh3TaHK0u7aNM5Zj2gJsfyOZEdUauCe37Vzlrk4gNXcGmXCPleWKYK34wGmkUW +FjgKXlf2Ysd6AgXmvB618p70qSmD+LIU424oh0TDkBreOKk8rENNZEXO3SipXPJz +ewT4F+irsfMuXGRuczE6Eri8sxHkfY+BUZo7jYn0TZNmezwD7dOaHZrzZVD1oNB1 +ny+v8OqCQ5j4aZyJecRDjkZy42Q2Eq/3JR44iZB3fsNrarnDy0RLrHiQi+fHLB5L +EUTINFInzQpdn4XBidUaePKVEFMy3YCEZnXZtWgo+2EuvoSoOMCZEoalHmdkrQYu +L6lwhceWD3yJZfWOQ1QOq92lgDmUYMA0yZZwLKMS9R9Ie70cfmu3nZD0Ijuu+Pwq +yvqCUqDvr0tVk+vBtfAii6w0TiYiBKGHLHVKt+V9E9e4DGTANtLJL4YSjCMJwRuC +O3NJo2pXh5Tl1njFmUNj403gdy3hZZlyaQQaRwnmDwFWJPsfvw55qVguucQJAX6V +um0ABj6y6koQOdjQK/W/7HW/lwLFCRsI3FU34oH7N4RDYiDK51ZLZer+bMEkkySh +NOsF/5oirpt9P/FlUQqmMGqz9IgcgA38corog14= +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Assured ID Root CA O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Assured ID Root CA O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Assured ID Root CA" +# Serial: 17154717934120587862167794914071425081 +# MD5 Fingerprint: 87:ce:0b:7b:2a:0e:49:00:e1:58:71:9b:37:a8:93:72 +# SHA1 Fingerprint: 05:63:b8:63:0d:62:d7:5a:bb:c8:ab:1e:4b:df:b5:a8:99:b2:4d:43 +# SHA256 Fingerprint: 3e:90:99:b5:01:5e:8f:48:6c:00:bc:ea:9d:11:1e:e7:21:fa:ba:35:5a:89:bc:f1:df:69:56:1e:3d:c6:32:5c +-----BEGIN CERTIFICATE----- +MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBl +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv +b3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl +cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7c +JpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYP +mDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+ +wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4 +VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1roV9Iq4/ +AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whfGHdPAgMB +AAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYun +pyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRC +dWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTf +fwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+fT8r87cm +NW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5QZ7dsvfPx +H2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe ++o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g== +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Global Root CA O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Global Root CA O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Global Root CA" +# Serial: 10944719598952040374951832963794454346 +# MD5 Fingerprint: 79:e4:a9:84:0d:7d:3a:96:d7:c0:4f:e2:43:4c:89:2e +# SHA1 Fingerprint: a8:98:5d:3a:65:e5:e5:c4:b2:d7:d6:6d:40:c6:dd:2f:b1:9c:54:36 +# SHA256 Fingerprint: 43:48:a0:e9:44:4c:78:cb:26:5e:05:8d:5e:89:44:b4:d8:4f:96:62:bd:26:db:25:7f:89:34:a4:43:c7:01:61 +-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD +QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB +CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 +nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt +43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P +T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 +gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR +TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw +DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr +hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg +06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF +PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls +YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk +CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert High Assurance EV Root CA O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert High Assurance EV Root CA O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert High Assurance EV Root CA" +# Serial: 3553400076410547919724730734378100087 +# MD5 Fingerprint: d4:74:de:57:5c:39:b2:d3:9c:85:83:c5:c0:65:49:8a +# SHA1 Fingerprint: 5f:b7:ee:06:33:e2:59:db:ad:0c:4c:9a:e6:d3:8f:1a:61:c7:dc:25 +# SHA256 Fingerprint: 74:31:e5:f4:c3:c1:ce:46:90:77:4f:0b:61:e0:54:40:88:3b:a9:a0:1e:d0:0b:a6:ab:d7:80:6e:d3:b1:18:cf +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j +ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL +MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 +LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug +RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm ++9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW +PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM +xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB +Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3 +hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg +EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA +FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec +nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z +eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF +hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2 +Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe +vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep ++OkuE6N36B9K +-----END CERTIFICATE----- + +# Issuer: CN=GeoTrust Primary Certification Authority O=GeoTrust Inc. +# Subject: CN=GeoTrust Primary Certification Authority O=GeoTrust Inc. +# Label: "GeoTrust Primary Certification Authority" +# Serial: 32798226551256963324313806436981982369 +# MD5 Fingerprint: 02:26:c3:01:5e:08:30:37:43:a9:d0:7d:cf:37:e6:bf +# SHA1 Fingerprint: 32:3c:11:8e:1b:f7:b8:b6:52:54:e2:e2:10:0d:d6:02:90:37:f0:96 +# SHA256 Fingerprint: 37:d5:10:06:c5:12:ea:ab:62:64:21:f1:ec:8c:92:01:3f:c5:f8:2a:e9:8e:e5:33:eb:46:19:b8:de:b4:d0:6c +-----BEGIN CERTIFICATE----- +MIIDfDCCAmSgAwIBAgIQGKy1av1pthU6Y2yv2vrEoTANBgkqhkiG9w0BAQUFADBY +MQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjExMC8GA1UEAxMo +R2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEx +MjcwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMFgxCzAJBgNVBAYTAlVTMRYwFAYDVQQK +Ew1HZW9UcnVzdCBJbmMuMTEwLwYDVQQDEyhHZW9UcnVzdCBQcmltYXJ5IENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAvrgVe//UfH1nrYNke8hCUy3f9oQIIGHWAVlqnEQRr+92/ZV+zmEwu3qDXwK9 +AWbK7hWNb6EwnL2hhZ6UOvNWiAAxz9juapYC2e0DjPt1befquFUWBRaa9OBesYjA +ZIVcFU2Ix7e64HXprQU9nceJSOC7KMgD4TCTZF5SwFlwIjVXiIrxlQqD17wxcwE0 +7e9GceBrAqg1cmuXm2bgyxx5X9gaBGgeRwLmnWDiNpcB3841kt++Z8dtd1k7j53W +kBWUvEI0EME5+bEnPn7WinXFsq+W06Lem+SYvn3h6YGttm/81w7a4DSwDRp35+MI +mO9Y+pyEtzavwt+s0vQQBnBxNQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4G +A1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQULNVQQZcVi/CPNmFbSvtr2ZnJM5IwDQYJ +KoZIhvcNAQEFBQADggEBAFpwfyzdtzRP9YZRqSa+S7iq8XEN3GHHoOo0Hnp3DwQ1 +6CePbJC/kRYkRj5KTs4rFtULUh38H2eiAkUxT87z+gOneZ1TatnaYzr4gNfTmeGl +4b7UVXGYNTq+k+qurUKykG/g/CFNNWMziUnWm07Kx+dOCQD32sfvmWKZd7aVIl6K +oKv0uHiYyjgZmclynnjNS6yvGaBzEi38wkG6gZHaFloxt/m0cYASSJlyc1pZU8Fj +UjPtp8nSOQJw+uCxQmYpqptR7TBUIhRf2asdweSU8Pj1K/fqynhG1riR/aYNKxoU +AT6A8EKglQdebc3MS6RFjasS6LPeWuWgfOgPIh1a6Vk= +-----END CERTIFICATE----- + +# Issuer: CN=thawte Primary Root CA O=thawte, Inc. OU=Certification Services Division/(c) 2006 thawte, Inc. - For authorized use only +# Subject: CN=thawte Primary Root CA O=thawte, Inc. OU=Certification Services Division/(c) 2006 thawte, Inc. - For authorized use only +# Label: "thawte Primary Root CA" +# Serial: 69529181992039203566298953787712940909 +# MD5 Fingerprint: 8c:ca:dc:0b:22:ce:f5:be:72:ac:41:1a:11:a8:d8:12 +# SHA1 Fingerprint: 91:c6:d6:ee:3e:8a:c8:63:84:e5:48:c2:99:29:5c:75:6c:81:7b:81 +# SHA256 Fingerprint: 8d:72:2f:81:a9:c1:13:c0:79:1d:f1:36:a2:96:6d:b2:6c:95:0a:97:1d:b4:6b:41:99:f4:ea:54:b7:8b:fb:9f +-----BEGIN CERTIFICATE----- +MIIEIDCCAwigAwIBAgIQNE7VVyDV7exJ9C/ON9srbTANBgkqhkiG9w0BAQUFADCB +qTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMf +Q2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIw +MDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxHzAdBgNV +BAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwHhcNMDYxMTE3MDAwMDAwWhcNMzYw +NzE2MjM1OTU5WjCBqTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5j +LjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYG +A1UECxMvKGMpIDIwMDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNl +IG9ubHkxHzAdBgNVBAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCsoPD7gFnUnMekz52hWXMJEEUMDSxuaPFs +W0hoSVk3/AszGcJ3f8wQLZU0HObrTQmnHNK4yZc2AreJ1CRfBsDMRJSUjQJib+ta +3RGNKJpchJAQeg29dGYvajig4tVUROsdB58Hum/u6f1OCyn1PoSgAfGcq/gcfomk +6KHYcWUNo1F77rzSImANuVud37r8UVsLr5iy6S7pBOhih94ryNdOwUxkHt3Ph1i6 +Sk/KaAcdHJ1KxtUvkcx8cXIcxcBn6zL9yZJclNqFwJu/U30rCfSMnZEfl2pSy94J +NqR32HuHUETVPm4pafs5SSYeCaWAe0At6+gnhcn+Yf1+5nyXHdWdAgMBAAGjQjBA +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBR7W0XP +r87Lev0xkhpqtvNG61dIUDANBgkqhkiG9w0BAQUFAAOCAQEAeRHAS7ORtvzw6WfU +DW5FvlXok9LOAz/t2iWwHVfLHjp2oEzsUHboZHIMpKnxuIvW1oeEuzLlQRHAd9mz +YJ3rG9XRbkREqaYB7FViHXe4XI5ISXycO1cRrK1zN44veFyQaEfZYGDm/Ac9IiAX +xPcW6cTYcvnIc3zfFi8VqT79aie2oetaupgf1eNNZAqdE8hhuvU5HIe6uL17In/2 +/qxAeeWsEG89jxt5dovEN7MhGITlNgDrYyCZuen+MwS7QcjBAvlEYyCegc5C09Y/ +LHbTY5xZ3Y+m4Q6gLkH3LpVHz7z9M/P2C2F+fpErgUfCJzDupxBdN49cOSvkBPB7 +jVaMaA== +-----END CERTIFICATE----- + +# Issuer: CN=VeriSign Class 3 Public Primary Certification Authority - G5 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2006 VeriSign, Inc. - For authorized use only +# Subject: CN=VeriSign Class 3 Public Primary Certification Authority - G5 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2006 VeriSign, Inc. - For authorized use only +# Label: "VeriSign Class 3 Public Primary Certification Authority - G5" +# Serial: 33037644167568058970164719475676101450 +# MD5 Fingerprint: cb:17:e4:31:67:3e:e2:09:fe:45:57:93:f3:0a:fa:1c +# SHA1 Fingerprint: 4e:b6:d5:78:49:9b:1c:cf:5f:58:1e:ad:56:be:3d:9b:67:44:a5:e5 +# SHA256 Fingerprint: 9a:cf:ab:7e:43:c8:d8:80:d0:6b:26:2a:94:de:ee:e4:b4:65:99:89:c3:d0:ca:f1:9b:af:64:05:e4:1a:b7:df +-----BEGIN CERTIFICATE----- +MIIE0zCCA7ugAwIBAgIQGNrRniZ96LtKIVjNzGs7SjANBgkqhkiG9w0BAQUFADCB +yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL +ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJp +U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxW +ZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5IC0gRzUwHhcNMDYxMTA4MDAwMDAwWhcNMzYwNzE2MjM1OTU5WjCByjEL +MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW +ZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJpU2ln +biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJp +U2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9y +aXR5IC0gRzUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvJAgIKXo1 +nmAMqudLO07cfLw8RRy7K+D+KQL5VwijZIUVJ/XxrcgxiV0i6CqqpkKzj/i5Vbex +t0uz/o9+B1fs70PbZmIVYc9gDaTY3vjgw2IIPVQT60nKWVSFJuUrjxuf6/WhkcIz +SdhDY2pSS9KP6HBRTdGJaXvHcPaz3BJ023tdS1bTlr8Vd6Gw9KIl8q8ckmcY5fQG +BO+QueQA5N06tRn/Arr0PO7gi+s3i+z016zy9vA9r911kTMZHRxAy3QkGSGT2RT+ +rCpSx4/VBEnkjWNHiDxpg8v+R70rfk/Fla4OndTRQ8Bnc+MUCH7lP59zuDMKz10/ +NIeWiu5T6CUVAgMBAAGjgbIwga8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8E +BAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJaW1hZ2UvZ2lmMCEwHzAH +BgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYjaHR0cDovL2xvZ28udmVy +aXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFH/TZafC3ey78DAJ80M5+gKv +MzEzMA0GCSqGSIb3DQEBBQUAA4IBAQCTJEowX2LP2BqYLz3q3JktvXf2pXkiOOzE +p6B4Eq1iDkVwZMXnl2YtmAl+X6/WzChl8gGqCBpH3vn5fJJaCGkgDdk+bW48DW7Y +5gaRQBi5+MHt39tBquCWIMnNZBU4gcmU7qKEKQsTb47bDN0lAtukixlE0kF6BWlK +WE9gyn6CagsCqiUXObXbf+eEZSqVir2G3l6BFoMtEMze/aiCKm0oHw0LxOXnGiYZ +4fQRbxC1lfznQgUy286dUV4otp6F01vvpX1FQHKOtw5rDgb7MzVIcbidJ4vEZV8N +hnacRHr2lVz2XTIIM6RUthg/aFzyQkqFOFSDX9HoLPKsEdao7WNq +-----END CERTIFICATE----- + +# Issuer: CN=COMODO Certification Authority O=COMODO CA Limited +# Subject: CN=COMODO Certification Authority O=COMODO CA Limited +# Label: "COMODO Certification Authority" +# Serial: 104350513648249232941998508985834464573 +# MD5 Fingerprint: 5c:48:dc:f7:42:72:ec:56:94:6d:1c:cc:71:35:80:75 +# SHA1 Fingerprint: 66:31:bf:9e:f7:4f:9e:b6:c9:d5:a6:0c:ba:6a:be:d1:f7:bd:ef:7b +# SHA256 Fingerprint: 0c:2c:d6:3d:f7:80:6f:a3:99:ed:e8:09:11:6b:57:5b:f8:79:89:f0:65:18:f9:80:8c:86:05:03:17:8b:af:66 +-----BEGIN CERTIFICATE----- +MIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCB +gTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNV +BAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEyMDEwMDAw +MDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3Jl +YXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01P +RE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3 +UcEbVASY06m/weaKXTuH+7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI +2GqGd0S7WWaXUF601CxwRM/aN5VCaTwwxHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8 +Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV4EajcNxo2f8ESIl33rXp ++2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA1KGzqSX+ +DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5O +nKVIrLsm9wIDAQABo4GOMIGLMB0GA1UdDgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW +/zAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zBJBgNVHR8EQjBAMD6g +PKA6hjhodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9u +QXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOCAQEAPpiem/Yb6dc5t3iuHXIY +SdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CPOGEIqB6BCsAv +IC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/ +RxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4 +zJVSk/BwJVmcIGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5dd +BA6+C4OmF4O5MBKgxTMVBbkN+8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IB +ZQ== +-----END CERTIFICATE----- + +# Issuer: CN=Network Solutions Certificate Authority O=Network Solutions L.L.C. +# Subject: CN=Network Solutions Certificate Authority O=Network Solutions L.L.C. +# Label: "Network Solutions Certificate Authority" +# Serial: 116697915152937497490437556386812487904 +# MD5 Fingerprint: d3:f3:a6:16:c0:fa:6b:1d:59:b1:2d:96:4d:0e:11:2e +# SHA1 Fingerprint: 74:f8:a3:c3:ef:e7:b3:90:06:4b:83:90:3c:21:64:60:20:e5:df:ce +# SHA256 Fingerprint: 15:f0:ba:00:a3:ac:7a:f3:ac:88:4c:07:2b:10:11:a0:77:bd:77:c0:97:f4:01:64:b2:f8:59:8a:bd:83:86:0c +-----BEGIN CERTIFICATE----- +MIID5jCCAs6gAwIBAgIQV8szb8JcFuZHFhfjkDFo4DANBgkqhkiG9w0BAQUFADBi +MQswCQYDVQQGEwJVUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMu +MTAwLgYDVQQDEydOZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3Jp +dHkwHhcNMDYxMjAxMDAwMDAwWhcNMjkxMjMxMjM1OTU5WjBiMQswCQYDVQQGEwJV +UzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMuMTAwLgYDVQQDEydO +ZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkvH6SMG3G2I4rC7xGzuAnlt7e+foS0zwz +c7MEL7xxjOWftiJgPl9dzgn/ggwbmlFQGiaJ3dVhXRncEg8tCqJDXRfQNJIg6nPP +OCwGJgl6cvf6UDL4wpPTaaIjzkGxzOTVHzbRijr4jGPiFFlp7Q3Tf2vouAPlT2rl +mGNpSAW+Lv8ztumXWWn4Zxmuk2GWRBXTcrA/vGp97Eh/jcOrqnErU2lBUzS1sLnF +BgrEsEX1QV1uiUV7PTsmjHTC5dLRfbIR1PtYMiKagMnc/Qzpf14Dl847ABSHJ3A4 +qY5usyd2mFHgBeMhqxrVhSI8KbWaFsWAqPS7azCPL0YCorEMIuDTAgMBAAGjgZcw +gZQwHQYDVR0OBBYEFCEwyfsA106Y2oeqKtCnLrFAMadMMA4GA1UdDwEB/wQEAwIB +BjAPBgNVHRMBAf8EBTADAQH/MFIGA1UdHwRLMEkwR6BFoEOGQWh0dHA6Ly9jcmwu +bmV0c29sc3NsLmNvbS9OZXR3b3JrU29sdXRpb25zQ2VydGlmaWNhdGVBdXRob3Jp +dHkuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQC7rkvnt1frf6ott3NHhWrB5KUd5Oc8 +6fRZZXe1eltajSU24HqXLjjAV2CDmAaDn7l2em5Q4LqILPxFzBiwmZVRDuwduIj/ +h1AcgsLj4DKAv6ALR8jDMe+ZZzKATxcheQxpXN5eNK4CtSbqUN9/GGUsyfJj4akH +/nxxH2szJGoeBfcFaMBqEssuXmHLrijTfsK0ZpEmXzwuJF/LWA/rKOyvEZbz3Htv +wKeI8lN3s2Berq4o2jUsbzRF0ybh3uxbTydrFny9RAQYgrOJeRcQcT16ohZO9QHN +pGxlaKFJdlxDydi8NmdspZS11My5vWo1ViHe2MPr+8ukYEywVaCge1ey +-----END CERTIFICATE----- + +# Issuer: CN=COMODO ECC Certification Authority O=COMODO CA Limited +# Subject: CN=COMODO ECC Certification Authority O=COMODO CA Limited +# Label: "COMODO ECC Certification Authority" +# Serial: 41578283867086692638256921589707938090 +# MD5 Fingerprint: 7c:62:ff:74:9d:31:53:5e:68:4a:d5:78:aa:1e:bf:23 +# SHA1 Fingerprint: 9f:74:4e:9f:2b:4d:ba:ec:0f:31:2c:50:b6:56:3b:8e:2d:93:c3:11 +# SHA256 Fingerprint: 17:93:92:7a:06:14:54:97:89:ad:ce:2f:8f:34:f7:f0:b6:6d:0f:3a:e3:a3:b8:4d:21:ec:15:db:ba:4f:ad:c7 +-----BEGIN CERTIFICATE----- +MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT +IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw +MDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy +ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N +T0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR +FtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J +cfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW +BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm +fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv +GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= +-----END CERTIFICATE----- + +# Issuer: CN=TC TrustCenter Class 2 CA II O=TC TrustCenter GmbH OU=TC TrustCenter Class 2 CA +# Subject: CN=TC TrustCenter Class 2 CA II O=TC TrustCenter GmbH OU=TC TrustCenter Class 2 CA +# Label: "TC TrustCenter Class 2 CA II" +# Serial: 941389028203453866782103406992443 +# MD5 Fingerprint: ce:78:33:5c:59:78:01:6e:18:ea:b9:36:a0:b9:2e:23 +# SHA1 Fingerprint: ae:50:83:ed:7c:f4:5c:bc:8f:61:c6:21:fe:68:5d:79:42:21:15:6e +# SHA256 Fingerprint: e6:b8:f8:76:64:85:f8:07:ae:7f:8d:ac:16:70:46:1f:07:c0:a1:3e:ef:3a:1f:f7:17:53:8d:7a:ba:d3:91:b4 +-----BEGIN CERTIFICATE----- +MIIEqjCCA5KgAwIBAgIOLmoAAQACH9dSISwRXDswDQYJKoZIhvcNAQEFBQAwdjEL +MAkGA1UEBhMCREUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxIjAgBgNV +BAsTGVRDIFRydXN0Q2VudGVyIENsYXNzIDIgQ0ExJTAjBgNVBAMTHFRDIFRydXN0 +Q2VudGVyIENsYXNzIDIgQ0EgSUkwHhcNMDYwMTEyMTQzODQzWhcNMjUxMjMxMjI1 +OTU5WjB2MQswCQYDVQQGEwJERTEcMBoGA1UEChMTVEMgVHJ1c3RDZW50ZXIgR21i +SDEiMCAGA1UECxMZVEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMiBDQTElMCMGA1UEAxMc +VEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMiBDQSBJSTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAKuAh5uO8MN8h9foJIIRszzdQ2Lu+MNF2ujhoF/RKrLqk2jf +tMjWQ+nEdVl//OEd+DFwIxuInie5e/060smp6RQvkL4DUsFJzfb95AhmC1eKokKg +uNV/aVyQMrKXDcpK3EY+AlWJU+MaWss2xgdW94zPEfRMuzBwBJWl9jmM/XOBCH2J +XjIeIqkiRUuwZi4wzJ9l/fzLganx4Duvo4bRierERXlQXa7pIXSSTYtZgo+U4+lK +8edJsBTj9WLL1XK9H7nSn6DNqPoByNkN39r8R52zyFTfSUrxIan+GE7uSNQZu+99 +5OKdy1u2bv/jzVrndIIFuoAlOMvkaZ6vQaoahPUCAwEAAaOCATQwggEwMA8GA1Ud +EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTjq1RMgKHbVkO3 +kUrL84J6E1wIqzCB7QYDVR0fBIHlMIHiMIHfoIHcoIHZhjVodHRwOi8vd3d3LnRy +dXN0Y2VudGVyLmRlL2NybC92Mi90Y19jbGFzc18yX2NhX0lJLmNybIaBn2xkYXA6 +Ly93d3cudHJ1c3RjZW50ZXIuZGUvQ049VEMlMjBUcnVzdENlbnRlciUyMENsYXNz +JTIwMiUyMENBJTIwSUksTz1UQyUyMFRydXN0Q2VudGVyJTIwR21iSCxPVT1yb290 +Y2VydHMsREM9dHJ1c3RjZW50ZXIsREM9ZGU/Y2VydGlmaWNhdGVSZXZvY2F0aW9u +TGlzdD9iYXNlPzANBgkqhkiG9w0BAQUFAAOCAQEAjNfffu4bgBCzg/XbEeprS6iS +GNn3Bzn1LL4GdXpoUxUc6krtXvwjshOg0wn/9vYua0Fxec3ibf2uWWuFHbhOIprt +ZjluS5TmVfwLG4t3wVMTZonZKNaL80VKY7f9ewthXbhtvsPcW3nS7Yblok2+XnR8 +au0WOB9/WIFaGusyiC2y8zl3gK9etmF1KdsjTYjKUCjLhdLTEKJZbtOTVAB6okaV +hgWcqRmY5TFyDADiZ9lA4CQze28suVyrZZ0srHbqNZn1l7kPJOzHdiEoZa5X6AeI +dUpWoNIFOqTmjZKILPPy4cHGYdtBxceb9w4aUUXCYWvcZCcXjFq32nQozZfkvQ== +-----END CERTIFICATE----- + +# Issuer: CN=TC TrustCenter Class 3 CA II O=TC TrustCenter GmbH OU=TC TrustCenter Class 3 CA +# Subject: CN=TC TrustCenter Class 3 CA II O=TC TrustCenter GmbH OU=TC TrustCenter Class 3 CA +# Label: "TC TrustCenter Class 3 CA II" +# Serial: 1506523511417715638772220530020799 +# MD5 Fingerprint: 56:5f:aa:80:61:12:17:f6:67:21:e6:2b:6d:61:56:8e +# SHA1 Fingerprint: 80:25:ef:f4:6e:70:c8:d4:72:24:65:84:fe:40:3b:8a:8d:6a:db:f5 +# SHA256 Fingerprint: 8d:a0:84:fc:f9:9c:e0:77:22:f8:9b:32:05:93:98:06:fa:5c:b8:11:e1:c8:13:f6:a1:08:c7:d3:36:b3:40:8e +-----BEGIN CERTIFICATE----- +MIIEqjCCA5KgAwIBAgIOSkcAAQAC5aBd1j8AUb8wDQYJKoZIhvcNAQEFBQAwdjEL +MAkGA1UEBhMCREUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxIjAgBgNV +BAsTGVRDIFRydXN0Q2VudGVyIENsYXNzIDMgQ0ExJTAjBgNVBAMTHFRDIFRydXN0 +Q2VudGVyIENsYXNzIDMgQ0EgSUkwHhcNMDYwMTEyMTQ0MTU3WhcNMjUxMjMxMjI1 +OTU5WjB2MQswCQYDVQQGEwJERTEcMBoGA1UEChMTVEMgVHJ1c3RDZW50ZXIgR21i +SDEiMCAGA1UECxMZVEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMyBDQTElMCMGA1UEAxMc +VEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMyBDQSBJSTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBALTgu1G7OVyLBMVMeRwjhjEQY0NVJz/GRcekPewJDRoeIMJW +Ht4bNwcwIi9v8Qbxq63WyKthoy9DxLCyLfzDlml7forkzMA5EpBCYMnMNWju2l+Q +Vl/NHE1bWEnrDgFPZPosPIlY2C8u4rBo6SI7dYnWRBpl8huXJh0obazovVkdKyT2 +1oQDZogkAHhg8fir/gKya/si+zXmFtGt9i4S5Po1auUZuV3bOx4a+9P/FRQI2Alq +ukWdFHlgfa9Aigdzs5OW03Q0jTo3Kd5c7PXuLjHCINy+8U9/I1LZW+Jk2ZyqBwi1 +Rb3R0DHBq1SfqdLDYmAD8bs5SpJKPQq5ncWg/jcCAwEAAaOCATQwggEwMA8GA1Ud +EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTUovyfs8PYA9NX +XAek0CSnwPIA1DCB7QYDVR0fBIHlMIHiMIHfoIHcoIHZhjVodHRwOi8vd3d3LnRy +dXN0Y2VudGVyLmRlL2NybC92Mi90Y19jbGFzc18zX2NhX0lJLmNybIaBn2xkYXA6 +Ly93d3cudHJ1c3RjZW50ZXIuZGUvQ049VEMlMjBUcnVzdENlbnRlciUyMENsYXNz +JTIwMyUyMENBJTIwSUksTz1UQyUyMFRydXN0Q2VudGVyJTIwR21iSCxPVT1yb290 +Y2VydHMsREM9dHJ1c3RjZW50ZXIsREM9ZGU/Y2VydGlmaWNhdGVSZXZvY2F0aW9u +TGlzdD9iYXNlPzANBgkqhkiG9w0BAQUFAAOCAQEANmDkcPcGIEPZIxpC8vijsrlN +irTzwppVMXzEO2eatN9NDoqTSheLG43KieHPOh6sHfGcMrSOWXaiQYUlN6AT0PV8 +TtXqluJucsG7Kv5sbviRmEb8yRtXW+rIGjs/sFGYPAfaLFkB2otE6OF0/ado3VS6 +g0bsyEa1+K+XwDsJHI/OcpY9M1ZwvJbL2NV9IJqDnxrcOfHFcqMRA/07QlIp2+gB +95tejNaNhk4Z+rwcvsUhpYeeeC422wlxo3I0+GzjBgnyXlal092Y+tTmBvTwtiBj +S+opvaqCZh77gaqnN60TGOaSw4HBM7uIHqHn4rS9MWwOUT1v+5ZWgOI2F9Hc5A== +-----END CERTIFICATE----- + +# Issuer: CN=TC TrustCenter Universal CA I O=TC TrustCenter GmbH OU=TC TrustCenter Universal CA +# Subject: CN=TC TrustCenter Universal CA I O=TC TrustCenter GmbH OU=TC TrustCenter Universal CA +# Label: "TC TrustCenter Universal CA I" +# Serial: 601024842042189035295619584734726 +# MD5 Fingerprint: 45:e1:a5:72:c5:a9:36:64:40:9e:f5:e4:58:84:67:8c +# SHA1 Fingerprint: 6b:2f:34:ad:89:58:be:62:fd:b0:6b:5c:ce:bb:9d:d9:4f:4e:39:f3 +# SHA256 Fingerprint: eb:f3:c0:2a:87:89:b1:fb:7d:51:19:95:d6:63:b7:29:06:d9:13:ce:0d:5e:10:56:8a:8a:77:e2:58:61:67:e7 +-----BEGIN CERTIFICATE----- +MIID3TCCAsWgAwIBAgIOHaIAAQAC7LdggHiNtgYwDQYJKoZIhvcNAQEFBQAweTEL +MAkGA1UEBhMCREUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxJDAiBgNV +BAsTG1RDIFRydXN0Q2VudGVyIFVuaXZlcnNhbCBDQTEmMCQGA1UEAxMdVEMgVHJ1 +c3RDZW50ZXIgVW5pdmVyc2FsIENBIEkwHhcNMDYwMzIyMTU1NDI4WhcNMjUxMjMx +MjI1OTU5WjB5MQswCQYDVQQGEwJERTEcMBoGA1UEChMTVEMgVHJ1c3RDZW50ZXIg +R21iSDEkMCIGA1UECxMbVEMgVHJ1c3RDZW50ZXIgVW5pdmVyc2FsIENBMSYwJAYD +VQQDEx1UQyBUcnVzdENlbnRlciBVbml2ZXJzYWwgQ0EgSTCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBAKR3I5ZEr5D0MacQ9CaHnPM42Q9e3s9B6DGtxnSR +JJZ4Hgmgm5qVSkr1YnwCqMqs+1oEdjneX/H5s7/zA1hV0qq34wQi0fiU2iIIAI3T +fCZdzHd55yx4Oagmcw6iXSVphU9VDprvxrlE4Vc93x9UIuVvZaozhDrzznq+VZeu +jRIPFDPiUHDDSYcTvFHe15gSWu86gzOSBnWLknwSaHtwag+1m7Z3W0hZneTvWq3z +wZ7U10VOylY0Ibw+F1tvdwxIAUMpsN0/lm7mlaoMwCC2/T42J5zjXM9OgdwZu5GQ +fezmlwQek8wiSdeXhrYTCjxDI3d+8NzmzSQfO4ObNDqDNOMCAwEAAaNjMGEwHwYD +VR0jBBgwFoAUkqR1LKSevoFE63n8isWVpesQdXMwDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFJKkdSyknr6BROt5/IrFlaXrEHVzMA0G +CSqGSIb3DQEBBQUAA4IBAQAo0uCG1eb4e/CX3CJrO5UUVg8RMKWaTzqwOuAGy2X1 +7caXJ/4l8lfmXpWMPmRgFVp/Lw0BxbFg/UU1z/CyvwbZ71q+s2IhtNerNXxTPqYn +8aEt2hojnczd7Dwtnic0XQ/CNnm8yUpiLe1r2X1BQ3y2qsrtYbE3ghUJGooWMNjs +ydZHcnhLEEYUjl8Or+zHL6sQ17bxbuyGssLoDZJz3KL0Dzq/YSMQiZxIQG5wALPT +ujdEWBF6AmqI8Dc08BnprNRlc/ZpjGSUOnmFKbAWKwyCPwacx/0QK54PLLae4xW/ +2TYcuiUaUj0a7CIMHOCkoj3w6DnPgcB77V0fb8XQC9eY +-----END CERTIFICATE----- + +# Issuer: CN=Cybertrust Global Root O=Cybertrust, Inc +# Subject: CN=Cybertrust Global Root O=Cybertrust, Inc +# Label: "Cybertrust Global Root" +# Serial: 4835703278459682877484360 +# MD5 Fingerprint: 72:e4:4a:87:e3:69:40:80:77:ea:bc:e3:f4:ff:f0:e1 +# SHA1 Fingerprint: 5f:43:e5:b1:bf:f8:78:8c:ac:1c:c7:ca:4a:9a:c6:22:2b:cc:34:c6 +# SHA256 Fingerprint: 96:0a:df:00:63:e9:63:56:75:0c:29:65:dd:0a:08:67:da:0b:9c:bd:6e:77:71:4a:ea:fb:23:49:ab:39:3d:a3 +-----BEGIN CERTIFICATE----- +MIIDoTCCAomgAwIBAgILBAAAAAABD4WqLUgwDQYJKoZIhvcNAQEFBQAwOzEYMBYG +A1UEChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2Jh +bCBSb290MB4XDTA2MTIxNTA4MDAwMFoXDTIxMTIxNTA4MDAwMFowOzEYMBYGA1UE +ChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2JhbCBS +b290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA+Mi8vRRQZhP/8NN5 +7CPytxrHjoXxEnOmGaoQ25yiZXRadz5RfVb23CO21O1fWLE3TdVJDm71aofW0ozS +J8bi/zafmGWgE07GKmSb1ZASzxQG9Dvj1Ci+6A74q05IlG2OlTEQXO2iLb3VOm2y +HLtgwEZLAfVJrn5GitB0jaEMAs7u/OePuGtm839EAL9mJRQr3RAwHQeWP032a7iP +t3sMpTjr3kfb1V05/Iin89cqdPHoWqI7n1C6poxFNcJQZZXcY4Lv3b93TZxiyWNz +FtApD0mpSPCzqrdsxacwOUBdrsTiXSZT8M4cIwhhqJQZugRiQOwfOHB3EgZxpzAY +XSUnpQIDAQABo4GlMIGiMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/ +MB0GA1UdDgQWBBS2CHsNesysIEyGVjJez6tuhS1wVzA/BgNVHR8EODA2MDSgMqAw +hi5odHRwOi8vd3d3Mi5wdWJsaWMtdHJ1c3QuY29tL2NybC9jdC9jdHJvb3QuY3Js +MB8GA1UdIwQYMBaAFLYIew16zKwgTIZWMl7Pq26FLXBXMA0GCSqGSIb3DQEBBQUA +A4IBAQBW7wojoFROlZfJ+InaRcHUowAl9B8Tq7ejhVhpwjCt2BWKLePJzYFa+HMj +Wqd8BfP9IjsO0QbE2zZMcwSO5bAi5MXzLqXZI+O4Tkogp24CJJ8iYGd7ix1yCcUx +XOl5n4BHPa2hCwcUPUf/A2kaDAtE52Mlp3+yybh2hO0j9n0Hq0V+09+zv+mKts2o +omcrUtW3ZfA5TGOgkXmTUg9U3YO7n9GPp1Nzw8v/MOx8BLjYRB+TX3EJIrduPuoc +A06dGiBh+4E37F78CkWr1+cXVdCg6mCbpvbjjFspwgZgFJ0tl0ypkxWdYcQBX0jW +WL1WMRJOEcgh4LMRkWXbtKaIOM5V +-----END CERTIFICATE----- + +# Issuer: CN=GeoTrust Primary Certification Authority - G3 O=GeoTrust Inc. OU=(c) 2008 GeoTrust Inc. - For authorized use only +# Subject: CN=GeoTrust Primary Certification Authority - G3 O=GeoTrust Inc. OU=(c) 2008 GeoTrust Inc. - For authorized use only +# Label: "GeoTrust Primary Certification Authority - G3" +# Serial: 28809105769928564313984085209975885599 +# MD5 Fingerprint: b5:e8:34:36:c9:10:44:58:48:70:6d:2e:83:d4:b8:05 +# SHA1 Fingerprint: 03:9e:ed:b8:0b:e7:a0:3c:69:53:89:3b:20:d2:d9:32:3a:4c:2a:fd +# SHA256 Fingerprint: b4:78:b8:12:25:0d:f8:78:63:5c:2a:a7:ec:7d:15:5e:aa:62:5e:e8:29:16:e2:cd:29:43:61:88:6c:d1:fb:d4 +-----BEGIN CERTIFICATE----- +MIID/jCCAuagAwIBAgIQFaxulBmyeUtB9iepwxgPHzANBgkqhkiG9w0BAQsFADCB +mDELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsT +MChjKSAyMDA4IEdlb1RydXN0IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25s +eTE2MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhv +cml0eSAtIEczMB4XDTA4MDQwMjAwMDAwMFoXDTM3MTIwMTIzNTk1OVowgZgxCzAJ +BgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAoYykg +MjAwOCBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0 +BgNVBAMTLUdlb1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg +LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANziXmJYHTNXOTIz ++uvLh4yn1ErdBojqZI4xmKU4kB6Yzy5jK/BGvESyiaHAKAxJcCGVn2TAppMSAmUm +hsalifD614SgcK9PGpc/BkTVyetyEH3kMSj7HGHmKAdEc5IiaacDiGydY8hS2pgn +5whMcD60yRLBxWeDXTPzAxHsatBT4tG6NmCUgLthY2xbF37fQJQeqw3CIShwiP/W +JmxsYAQlTlV+fe+/lEjetx3dcI0FX4ilm/LC7urRQEFtYjgdVgbFA0dRIBn8exAL +DmKudlW/X3e+PkkBUz2YJQN2JFodtNuJ6nnltrM7P7pMKEF/BqxqjsHQ9gUdfeZC +huOl1UcCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw +HQYDVR0OBBYEFMR5yo6hTgMdHNxr2zFblD4/MH8tMA0GCSqGSIb3DQEBCwUAA4IB +AQAtxRPPVoB7eni9n64smefv2t+UXglpp+duaIy9cr5HqQ6XErhK8WTTOd8lNNTB +zU6B8A8ExCSzNJbGpqow32hhc9f5joWJ7w5elShKKiePEI4ufIbEAp7aDHdlDkQN +kv39sxY2+hENHYwOB4lqKVb3cvTdFZx3NWZXqxNT2I7BQMXXExZacse3aQHEerGD +AWh9jUGhlBjBJVz88P6DAod8DQ3PLghcSkANPuyBYeYk28rgDi0Hsj5W3I31QYUH +SJsMC8tJP33st/3LjWeJGqvtux6jAAgIFyqCXDFdRootD4abdNlF+9RAsXqqaC2G +spki4cErx5z481+oghLrGREt +-----END CERTIFICATE----- + +# Issuer: CN=thawte Primary Root CA - G2 O=thawte, Inc. OU=(c) 2007 thawte, Inc. - For authorized use only +# Subject: CN=thawte Primary Root CA - G2 O=thawte, Inc. OU=(c) 2007 thawte, Inc. - For authorized use only +# Label: "thawte Primary Root CA - G2" +# Serial: 71758320672825410020661621085256472406 +# MD5 Fingerprint: 74:9d:ea:60:24:c4:fd:22:53:3e:cc:3a:72:d9:29:4f +# SHA1 Fingerprint: aa:db:bc:22:23:8f:c4:01:a1:27:bb:38:dd:f4:1d:db:08:9e:f0:12 +# SHA256 Fingerprint: a4:31:0d:50:af:18:a6:44:71:90:37:2a:86:af:af:8b:95:1f:fb:43:1d:83:7f:1e:56:88:b4:59:71:ed:15:57 +-----BEGIN CERTIFICATE----- +MIICiDCCAg2gAwIBAgIQNfwmXNmET8k9Jj1Xm67XVjAKBggqhkjOPQQDAzCBhDEL +MAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjE4MDYGA1UECxMvKGMp +IDIwMDcgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxJDAi +BgNVBAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EgLSBHMjAeFw0wNzExMDUwMDAw +MDBaFw0zODAxMTgyMzU5NTlaMIGEMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhh +d3RlLCBJbmMuMTgwNgYDVQQLEy8oYykgMjAwNyB0aGF3dGUsIEluYy4gLSBGb3Ig +YXV0aG9yaXplZCB1c2Ugb25seTEkMCIGA1UEAxMbdGhhd3RlIFByaW1hcnkgUm9v +dCBDQSAtIEcyMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEotWcgnuVnfFSeIf+iha/ +BebfowJPDQfGAFG6DAJSLSKkQjnE/o/qycG+1E3/n3qe4rF8mq2nhglzh9HnmuN6 +papu+7qzcMBniKI11KOasf2twu8x+qi58/sIxpHR+ymVo0IwQDAPBgNVHRMBAf8E +BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUmtgAMADna3+FGO6Lts6K +DPgR4bswCgYIKoZIzj0EAwMDaQAwZgIxAN344FdHW6fmCsO99YCKlzUNG4k8VIZ3 +KMqh9HneteY4sPBlcIx/AlTCv//YoT7ZzwIxAMSNlPzcU9LcnXgWHxUzI1NS41ox +XZ3Krr0TKUQNJ1uo52icEvdYPy5yAlejj6EULg== +-----END CERTIFICATE----- + +# Issuer: CN=thawte Primary Root CA - G3 O=thawte, Inc. OU=Certification Services Division/(c) 2008 thawte, Inc. - For authorized use only +# Subject: CN=thawte Primary Root CA - G3 O=thawte, Inc. OU=Certification Services Division/(c) 2008 thawte, Inc. - For authorized use only +# Label: "thawte Primary Root CA - G3" +# Serial: 127614157056681299805556476275995414779 +# MD5 Fingerprint: fb:1b:5d:43:8a:94:cd:44:c6:76:f2:43:4b:47:e7:31 +# SHA1 Fingerprint: f1:8b:53:8d:1b:e9:03:b6:a6:f0:56:43:5b:17:15:89:ca:f3:6b:f2 +# SHA256 Fingerprint: 4b:03:f4:58:07:ad:70:f2:1b:fc:2c:ae:71:c9:fd:e4:60:4c:06:4c:f5:ff:b6:86:ba:e5:db:aa:d7:fd:d3:4c +-----BEGIN CERTIFICATE----- +MIIEKjCCAxKgAwIBAgIQYAGXt0an6rS0mtZLL/eQ+zANBgkqhkiG9w0BAQsFADCB +rjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMf +Q2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIw +MDggdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxJDAiBgNV +BAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EgLSBHMzAeFw0wODA0MDIwMDAwMDBa +Fw0zNzEyMDEyMzU5NTlaMIGuMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhhd3Rl +LCBJbmMuMSgwJgYDVQQLEx9DZXJ0aWZpY2F0aW9uIFNlcnZpY2VzIERpdmlzaW9u +MTgwNgYDVQQLEy8oYykgMjAwOCB0aGF3dGUsIEluYy4gLSBGb3IgYXV0aG9yaXpl +ZCB1c2Ugb25seTEkMCIGA1UEAxMbdGhhd3RlIFByaW1hcnkgUm9vdCBDQSAtIEcz +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsr8nLPvb2FvdeHsbnndm +gcs+vHyu86YnmjSjaDFxODNi5PNxZnmxqWWjpYvVj2AtP0LMqmsywCPLLEHd5N/8 +YZzic7IilRFDGF/Eth9XbAoFWCLINkw6fKXRz4aviKdEAhN0cXMKQlkC+BsUa0Lf +b1+6a4KinVvnSr0eAXLbS3ToO39/fR8EtCab4LRarEc9VbjXsCZSKAExQGbY2SS9 +9irY7CFJXJv2eul/VTV+lmuNk5Mny5K76qxAwJ/C+IDPXfRa3M50hqY+bAtTyr2S +zhkGcuYMXDhpxwTWvGzOW/b3aJzcJRVIiKHpqfiYnODz1TEoYRFsZ5aNOZnLwkUk +OQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNV +HQ4EFgQUrWyqlGCc7eT/+j4KdCtjA/e2Wb8wDQYJKoZIhvcNAQELBQADggEBABpA +2JVlrAmSicY59BDlqQ5mU1143vokkbvnRFHfxhY0Cu9qRFHqKweKA3rD6z8KLFIW +oCtDuSWQP3CpMyVtRRooOyfPqsMpQhvfO0zAMzRbQYi/aytlryjvsvXDqmbOe1bu +t8jLZ8HJnBoYuMTDSQPxYA5QzUbF83d597YV4Djbxy8ooAw/dyZ02SUS2jHaGh7c +KUGRIjxpp7sC8rZcJwOJ9Abqm+RyguOhCcHpABnTPtRwa7pxpqpYrvS76Wy274fM +m7v/OeZWYdMKp8RcTGB7BXcmer/YB1IsYvdwY9k5vG8cwnncdimvzsUsZAReiDZu +MdRAGmI0Nj81Aa6sY6A= +-----END CERTIFICATE----- + +# Issuer: CN=GeoTrust Primary Certification Authority - G2 O=GeoTrust Inc. OU=(c) 2007 GeoTrust Inc. - For authorized use only +# Subject: CN=GeoTrust Primary Certification Authority - G2 O=GeoTrust Inc. OU=(c) 2007 GeoTrust Inc. - For authorized use only +# Label: "GeoTrust Primary Certification Authority - G2" +# Serial: 80682863203381065782177908751794619243 +# MD5 Fingerprint: 01:5e:d8:6b:bd:6f:3d:8e:a1:31:f8:12:e0:98:73:6a +# SHA1 Fingerprint: 8d:17:84:d5:37:f3:03:7d:ec:70:fe:57:8b:51:9a:99:e6:10:d7:b0 +# SHA256 Fingerprint: 5e:db:7a:c4:3b:82:a0:6a:87:61:e8:d7:be:49:79:eb:f2:61:1f:7d:d7:9b:f9:1c:1c:6b:56:6a:21:9e:d7:66 +-----BEGIN CERTIFICATE----- +MIICrjCCAjWgAwIBAgIQPLL0SAoA4v7rJDteYD7DazAKBggqhkjOPQQDAzCBmDEL +MAkGA1UEBhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsTMChj +KSAyMDA3IEdlb1RydXN0IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTE2 +MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0 +eSAtIEcyMB4XDTA3MTEwNTAwMDAwMFoXDTM4MDExODIzNTk1OVowgZgxCzAJBgNV +BAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAoYykgMjAw +NyBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0BgNV +BAMTLUdlb1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBH +MjB2MBAGByqGSM49AgEGBSuBBAAiA2IABBWx6P0DFUPlrOuHNxFi79KDNlJ9RVcL +So17VDs6bl8VAsBQps8lL33KSLjHUGMcKiEIfJo22Av+0SbFWDEwKCXzXV2juLal +tJLtbCyf691DiaI8S0iRHVDsJt/WYC69IaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBVfNVdRVfslsq0DafwBo/q+EVXVMAoG +CCqGSM49BAMDA2cAMGQCMGSWWaboCd6LuvpaiIjwH5HTRqjySkwCY/tsXzjbLkGT +qQ7mndwxHLKgpxgceeHHNgIwOlavmnRs9vuD4DPTCF+hnMJbn0bWtsuRBmOiBucz +rD6ogRLQy7rQkgu2npaqBA+K +-----END CERTIFICATE----- + +# Issuer: CN=VeriSign Universal Root Certification Authority O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2008 VeriSign, Inc. - For authorized use only +# Subject: CN=VeriSign Universal Root Certification Authority O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2008 VeriSign, Inc. - For authorized use only +# Label: "VeriSign Universal Root Certification Authority" +# Serial: 85209574734084581917763752644031726877 +# MD5 Fingerprint: 8e:ad:b5:01:aa:4d:81:e4:8c:1d:d1:e1:14:00:95:19 +# SHA1 Fingerprint: 36:79:ca:35:66:87:72:30:4d:30:a5:fb:87:3b:0f:a7:7b:b7:0d:54 +# SHA256 Fingerprint: 23:99:56:11:27:a5:71:25:de:8c:ef:ea:61:0d:df:2f:a0:78:b5:c8:06:7f:4e:82:82:90:bf:b8:60:e8:4b:3c +-----BEGIN CERTIFICATE----- +MIIEuTCCA6GgAwIBAgIQQBrEZCGzEyEDDrvkEhrFHTANBgkqhkiG9w0BAQsFADCB +vTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL +ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwOCBWZXJp +U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MTgwNgYDVQQDEy9W +ZXJpU2lnbiBVbml2ZXJzYWwgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe +Fw0wODA0MDIwMDAwMDBaFw0zNzEyMDEyMzU5NTlaMIG9MQswCQYDVQQGEwJVUzEX +MBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRydXN0 +IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAyMDA4IFZlcmlTaWduLCBJbmMuIC0gRm9y +IGF1dGhvcml6ZWQgdXNlIG9ubHkxODA2BgNVBAMTL1ZlcmlTaWduIFVuaXZlcnNh +bCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAx2E3XrEBNNti1xWb/1hajCMj1mCOkdeQmIN65lgZOIzF +9uVkhbSicfvtvbnazU0AtMgtc6XHaXGVHzk8skQHnOgO+k1KxCHfKWGPMiJhgsWH +H26MfF8WIFFE0XBPV+rjHOPMee5Y2A7Cs0WTwCznmhcrewA3ekEzeOEz4vMQGn+H +LL729fdC4uW/h2KJXwBL38Xd5HVEMkE6HnFuacsLdUYI0crSK5XQz/u5QGtkjFdN +/BMReYTtXlT2NJ8IAfMQJQYXStrxHXpma5hgZqTZ79IugvHw7wnqRMkVauIDbjPT +rJ9VAMf2CGqUuV/c4DPxhGD5WycRtPwW8rtWaoAljQIDAQABo4GyMIGvMA8GA1Ud +EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMG0GCCsGAQUFBwEMBGEwX6FdoFsw +WTBXMFUWCWltYWdlL2dpZjAhMB8wBwYFKw4DAhoEFI/l0xqGrI2Oa8PPgGrUSBgs +exkuMCUWI2h0dHA6Ly9sb2dvLnZlcmlzaWduLmNvbS92c2xvZ28uZ2lmMB0GA1Ud +DgQWBBS2d/ppSEefUxLVwuoHMnYH0ZcHGTANBgkqhkiG9w0BAQsFAAOCAQEASvj4 +sAPmLGd75JR3Y8xuTPl9Dg3cyLk1uXBPY/ok+myDjEedO2Pzmvl2MpWRsXe8rJq+ +seQxIcaBlVZaDrHC1LGmWazxY8u4TB1ZkErvkBYoH1quEPuBUDgMbMzxPcP1Y+Oz +4yHJJDnp/RVmRvQbEdBNc6N9Rvk97ahfYtTxP/jgdFcrGJ2BtMQo2pSXpXDrrB2+ +BxHw1dvd5Yzw1TKwg+ZX4o+/vqGqvz0dtdQ46tewXDpPaj+PwGZsY6rp2aQW9IHR +lRQOfc2VNNnSj3BzgXucfr2YYdhFh5iQxeuGMMY1v/D/w1WIg0vvBZIGcfK4mJO3 +7M2CYfE45k+XmCpajQ== +-----END CERTIFICATE----- + +# Issuer: CN=VeriSign Class 3 Public Primary Certification Authority - G4 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2007 VeriSign, Inc. - For authorized use only +# Subject: CN=VeriSign Class 3 Public Primary Certification Authority - G4 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2007 VeriSign, Inc. - For authorized use only +# Label: "VeriSign Class 3 Public Primary Certification Authority - G4" +# Serial: 63143484348153506665311985501458640051 +# MD5 Fingerprint: 3a:52:e1:e7:fd:6f:3a:e3:6f:f3:6f:99:1b:f9:22:41 +# SHA1 Fingerprint: 22:d5:d8:df:8f:02:31:d1:8d:f7:9d:b7:cf:8a:2d:64:c9:3f:6c:3a +# SHA256 Fingerprint: 69:dd:d7:ea:90:bb:57:c9:3e:13:5d:c8:5e:a6:fc:d5:48:0b:60:32:39:bd:c4:54:fc:75:8b:2a:26:cf:7f:79 +-----BEGIN CERTIFICATE----- +MIIDhDCCAwqgAwIBAgIQL4D+I4wOIg9IZxIokYesszAKBggqhkjOPQQDAzCByjEL +MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW +ZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2ln +biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJp +U2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9y +aXR5IC0gRzQwHhcNMDcxMTA1MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCByjELMAkG +A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJp +U2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2lnbiwg +SW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2ln +biBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5 +IC0gRzQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASnVnp8Utpkmw4tXNherJI9/gHm +GUo9FANL+mAnINmDiWn6VMaaGF5VKmTeBvaNSjutEDxlPZCIBIngMGGzrl0Bp3ve +fLK+ymVhAIau2o970ImtTR1ZmkGxvEeA3J5iw/mjgbIwga8wDwYDVR0TAQH/BAUw +AwEB/zAOBgNVHQ8BAf8EBAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJ +aW1hZ2UvZ2lmMCEwHzAHBgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYj +aHR0cDovL2xvZ28udmVyaXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFLMW +kf3upm7ktS5Jj4d4gYDs5bG1MAoGCCqGSM49BAMDA2gAMGUCMGYhDBgmYFo4e1ZC +4Kf8NoRRkSAsdk1DPcQdhCPQrNZ8NQbOzWm9kA3bbEhCHQ6qQgIxAJw9SDkjOVga +FRJZap7v1VmyHVIsmXHNxynfGyphe3HR3vPA5Q06Sqotp9iGKt0uEA== +-----END CERTIFICATE----- + +# Issuer: O=VeriSign, Inc. OU=Class 3 Public Primary Certification Authority +# Subject: O=VeriSign, Inc. OU=Class 3 Public Primary Certification Authority +# Label: "Verisign Class 3 Public Primary Certification Authority" +# Serial: 80507572722862485515306429940691309246 +# MD5 Fingerprint: ef:5a:f1:33:ef:f1:cd:bb:51:02:ee:12:14:4b:96:c4 +# SHA1 Fingerprint: a1:db:63:93:91:6f:17:e4:18:55:09:40:04:15:c7:02:40:b0:ae:6b +# SHA256 Fingerprint: a4:b6:b3:99:6f:c2:f3:06:b3:fd:86:81:bd:63:41:3d:8c:50:09:cc:4f:a3:29:c2:cc:f0:e2:fa:1b:14:03:05 +-----BEGIN CERTIFICATE----- +MIICPDCCAaUCEDyRMcsf9tAbDpq40ES/Er4wDQYJKoZIhvcNAQEFBQAwXzELMAkG +A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz +cyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2 +MDEyOTAwMDAwMFoXDTI4MDgwMjIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV +BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmlt +YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN +ADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhE +BarsAx94f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/is +I19wKTakyYbnsZogy1Olhec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0G +CSqGSIb3DQEBBQUAA4GBABByUqkFFBkyCEHwxWsKzH4PIRnN5GfcX6kb5sroc50i +2JhucwNhkcV8sEVAbkSdjbCxlnRhLQ2pRdKkkirWmnWXbj9T/UWZYB2oK0z5XqcJ +2HUw19JlYD1n1khVdWk/kfVIC0dpImmClr7JyDiGSnoscxlIaU5rfGW/D/xwzoiQ +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3 +# Label: "GlobalSign Root CA - R3" +# Serial: 4835703278459759426209954 +# MD5 Fingerprint: c5:df:b8:49:ca:05:13:55:ee:2d:ba:1a:c3:3e:b0:28 +# SHA1 Fingerprint: d6:9b:56:11:48:f0:1c:77:c5:45:78:c1:09:26:df:5b:85:69:76:ad +# SHA256 Fingerprint: cb:b5:22:d7:b7:f1:27:ad:6a:01:13:86:5b:df:1c:d4:10:2e:7d:07:59:af:63:5a:7c:f4:72:0d:c9:63:c5:3b +-----BEGIN CERTIFICATE----- +MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G +A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp +Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4 +MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG +A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8 +RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT +gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm +KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd +QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ +XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw +DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o +LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU +RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp +jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK +6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX +mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs +Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH +WD9f +-----END CERTIFICATE----- + +# Issuer: CN=TC TrustCenter Universal CA III O=TC TrustCenter GmbH OU=TC TrustCenter Universal CA +# Subject: CN=TC TrustCenter Universal CA III O=TC TrustCenter GmbH OU=TC TrustCenter Universal CA +# Label: "TC TrustCenter Universal CA III" +# Serial: 2010889993983507346460533407902964 +# MD5 Fingerprint: 9f:dd:db:ab:ff:8e:ff:45:21:5f:f0:6c:9d:8f:fe:2b +# SHA1 Fingerprint: 96:56:cd:7b:57:96:98:95:d0:e1:41:46:68:06:fb:b8:c6:11:06:87 +# SHA256 Fingerprint: 30:9b:4a:87:f6:ca:56:c9:31:69:aa:a9:9c:6d:98:88:54:d7:89:2b:d5:43:7e:2d:07:b2:9c:be:da:55:d3:5d +-----BEGIN CERTIFICATE----- +MIID4TCCAsmgAwIBAgIOYyUAAQACFI0zFQLkbPQwDQYJKoZIhvcNAQEFBQAwezEL +MAkGA1UEBhMCREUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxJDAiBgNV +BAsTG1RDIFRydXN0Q2VudGVyIFVuaXZlcnNhbCBDQTEoMCYGA1UEAxMfVEMgVHJ1 +c3RDZW50ZXIgVW5pdmVyc2FsIENBIElJSTAeFw0wOTA5MDkwODE1MjdaFw0yOTEy +MzEyMzU5NTlaMHsxCzAJBgNVBAYTAkRFMRwwGgYDVQQKExNUQyBUcnVzdENlbnRl +ciBHbWJIMSQwIgYDVQQLExtUQyBUcnVzdENlbnRlciBVbml2ZXJzYWwgQ0ExKDAm +BgNVBAMTH1RDIFRydXN0Q2VudGVyIFVuaXZlcnNhbCBDQSBJSUkwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDC2pxisLlxErALyBpXsq6DFJmzNEubkKLF +5+cvAqBNLaT6hdqbJYUtQCggbergvbFIgyIpRJ9Og+41URNzdNW88jBmlFPAQDYv +DIRlzg9uwliT6CwLOunBjvvya8o84pxOjuT5fdMnnxvVZ3iHLX8LR7PH6MlIfK8v +zArZQe+f/prhsq75U7Xl6UafYOPfjdN/+5Z+s7Vy+EutCHnNaYlAJ/Uqwa1D7KRT +yGG299J5KmcYdkhtWyUB0SbFt1dpIxVbYYqt8Bst2a9c8SaQaanVDED1M4BDj5yj +dipFtK+/fz6HP3bFzSreIMUWWMv5G/UPyw0RUmS40nZid4PxWJ//AgMBAAGjYzBh +MB8GA1UdIwQYMBaAFFbn4VslQ4Dg9ozhcbyO5YAvxEjiMA8GA1UdEwEB/wQFMAMB +Af8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRW5+FbJUOA4PaM4XG8juWAL8RI +4jANBgkqhkiG9w0BAQUFAAOCAQEAg8ev6n9NCjw5sWi+e22JLumzCecYV42Fmhfz +dkJQEw/HkG8zrcVJYCtsSVgZ1OK+t7+rSbyUyKu+KGwWaODIl0YgoGhnYIg5IFHY +aAERzqf2EQf27OysGh+yZm5WZ2B6dF7AbZc2rrUNXWZzwCUyRdhKBgePxLcHsU0G +DeGl6/R1yrqc0L2z0zIkTO5+4nYES0lT2PLpVDP85XEfPRRclkvxOvIAu2y0+pZV +CIgJwcyRGSmwIC3/yzikQOEXvnlhgP8HA4ZMTnsGnxGGjYnuJ8Tb4rwZjgvDwxPH +LQNjO9Po5KIqwoIIlBZU8O8fJ5AluA0OKBtHd0e9HKgl8ZS0Zg== +-----END CERTIFICATE----- + +# Issuer: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc. +# Subject: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc. +# Label: "Go Daddy Root Certificate Authority - G2" +# Serial: 0 +# MD5 Fingerprint: 80:3a:bc:22:c1:e6:fb:8d:9b:3b:27:4a:32:1b:9a:01 +# SHA1 Fingerprint: 47:be:ab:c9:22:ea:e8:0e:78:78:34:62:a7:9f:45:c2:54:fd:e6:8b +# SHA256 Fingerprint: 45:14:0b:32:47:eb:9c:c8:c5:b4:f0:d7:b5:30:91:f7:32:92:08:9e:6e:5a:63:e2:74:9d:d3:ac:a9:19:8e:da +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT +EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp +ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz +NTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH +EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE +AxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD +E6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH +/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy +DfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh +GkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR +tDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA +AaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX +WWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu +9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr +gIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo +2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO +LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI +4uJEvlz36hz1 +-----END CERTIFICATE----- + +# Issuer: CN=Starfield Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Subject: CN=Starfield Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Label: "Starfield Root Certificate Authority - G2" +# Serial: 0 +# MD5 Fingerprint: d6:39:81:c6:52:7e:96:69:fc:fc:ca:66:ed:05:f2:96 +# SHA1 Fingerprint: b5:1c:06:7c:ee:2b:0c:3d:f8:55:ab:2d:92:f4:fe:39:d4:e7:0f:0e +# SHA256 Fingerprint: 2c:e1:cb:0b:f9:d2:f9:e1:02:99:3f:be:21:51:52:c3:b2:dd:0c:ab:de:1c:68:e5:31:9b:83:91:54:db:b7:f5 +-----BEGIN CERTIFICATE----- +MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs +ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw +MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 +b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj +aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp +Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg +nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1 +HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N +Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN +dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0 +HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G +CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU +sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3 +4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg +8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K +pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1 +mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 +-----END CERTIFICATE----- + +# Issuer: CN=Starfield Services Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Subject: CN=Starfield Services Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Label: "Starfield Services Root Certificate Authority - G2" +# Serial: 0 +# MD5 Fingerprint: 17:35:74:af:7b:61:1c:eb:f4:f9:3c:e2:ee:40:f9:a2 +# SHA1 Fingerprint: 92:5a:8f:8d:2c:6d:04:e0:66:5f:59:6a:ff:22:d8:63:e8:25:6f:3f +# SHA256 Fingerprint: 56:8d:69:05:a2:c8:87:08:a4:b3:02:51:90:ed:cf:ed:b1:97:4a:60:6a:13:c6:e5:29:0f:cb:2a:e6:3e:da:b5 +-----BEGIN CERTIFICATE----- +MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVs +ZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5 +MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNVBAYTAlVTMRAwDgYD +VQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFy +ZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2Vy +dmljZXMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20p +OsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm2 +8xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4PahHQUw2eeBGg6345AWh1K +Ts9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLPLJGmpufe +hRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk +6mFBrMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAw +DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+q +AdcwKziIorhtSpzyEZGDMA0GCSqGSIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMI +bw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPPE95Dz+I0swSdHynVv/heyNXB +ve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTyxQGjhdByPq1z +qwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd +iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn +0q23KXB56jzaYyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCN +sSi6 +-----END CERTIFICATE----- + +# Issuer: CN=AffirmTrust Commercial O=AffirmTrust +# Subject: CN=AffirmTrust Commercial O=AffirmTrust +# Label: "AffirmTrust Commercial" +# Serial: 8608355977964138876 +# MD5 Fingerprint: 82:92:ba:5b:ef:cd:8a:6f:a6:3d:55:f9:84:f6:d6:b7 +# SHA1 Fingerprint: f9:b5:b6:32:45:5f:9c:be:ec:57:5f:80:dc:e9:6e:2c:c7:b2:78:b7 +# SHA256 Fingerprint: 03:76:ab:1d:54:c5:f9:80:3c:e4:b2:e2:01:a0:ee:7e:ef:7b:57:b6:36:e8:a9:3c:9b:8d:48:60:c9:6f:5f:a7 +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz +dCBDb21tZXJjaWFsMB4XDTEwMDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDEL +MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp +cm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6EqdbDuKP +Hx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yr +ba0F8PrVC8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPAL +MeIrJmqbTFeurCA+ukV6BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1 +yHp52UKqK39c/s4mT6NmgTWvRLpUHhwwMmWd5jyTXlBOeuM61G7MGvv50jeuJCqr +VwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNVHQ4EFgQUnZPGU4teyq8/ +nx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ +KoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYG +XUPGhi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNj +vbz4YYCanrHOQnDiqX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivt +Z8SOyUOyXGsViQK8YvxO8rUzqrJv0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9g +N53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0khsUlHRUe072o0EclNmsxZt9YC +nlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8= +-----END CERTIFICATE----- + +# Issuer: CN=AffirmTrust Networking O=AffirmTrust +# Subject: CN=AffirmTrust Networking O=AffirmTrust +# Label: "AffirmTrust Networking" +# Serial: 8957382827206547757 +# MD5 Fingerprint: 42:65:ca:be:01:9a:9a:4c:a9:8c:41:49:cd:c0:d5:7f +# SHA1 Fingerprint: 29:36:21:02:8b:20:ed:02:f5:66:c5:32:d1:d6:ed:90:9f:45:00:2f +# SHA256 Fingerprint: 0a:81:ec:5a:92:97:77:f1:45:90:4a:f3:8d:5d:50:9f:66:b5:e2:c5:8f:cd:b5:31:05:8b:0e:17:f3:f0:b4:1b +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz +dCBOZXR3b3JraW5nMB4XDTEwMDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDEL +MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp +cm1UcnVzdCBOZXR3b3JraW5nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWREZY9nZOIG41w3SfYvm4SEHi3y +YJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ/Ls6rnla1fTWcbua +kCNrmreIdIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXLviRmVSRL +QESxG9fhwoXA3hA/Pe24/PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp +6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gbh+0t+nvujArjqWaJGctB+d1ENmHP4ndG +yH329JKBNv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNVHQ4EFgQUBx/S55zawm6i +QLSwelAQUHTEyL0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ +KoZIhvcNAQEFBQADggEBAIlXshZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfO +tDIuUFUaNU52Q3Eg75N3ThVwLofDwR1t3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzu +QY0x2+c06lkh1QF612S4ZDnNye2v7UsDSKegmQGA3GWjNq5lWUhPgkvIZfFXHeVZ +Lgo/bNjR9eUJtGxUAArgFU2HdW23WJZa3W3SAKD0m0i+wzekujbgfIeFlxoVot4u +olu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9/ZFvgrG+CJPbFEfxojfHRZ48 +x3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s= +-----END CERTIFICATE----- + +# Issuer: CN=AffirmTrust Premium O=AffirmTrust +# Subject: CN=AffirmTrust Premium O=AffirmTrust +# Label: "AffirmTrust Premium" +# Serial: 7893706540734352110 +# MD5 Fingerprint: c4:5d:0e:48:b6:ac:28:30:4e:0a:bc:f9:38:16:87:57 +# SHA1 Fingerprint: d8:a6:33:2c:e0:03:6f:b1:85:f6:63:4f:7d:6a:06:65:26:32:28:27 +# SHA256 Fingerprint: 70:a7:3f:7f:37:6b:60:07:42:48:90:45:34:b1:14:82:d5:bf:0e:69:8e:cc:49:8d:f5:25:77:eb:f2:e9:3b:9a +-----BEGIN CERTIFICATE----- +MIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVz +dCBQcmVtaXVtMB4XDTEwMDEyOTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkG +A1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1U +cnVzdCBQcmVtaXVtMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxBLf +qV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64tb+eT2TZwamjPjlGjhVtnBKAQ +JG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/0qRY7iZNyaqoe5rZ ++jjeRFcV5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/K+k8rNrS +s8PhaJyJ+HoAVt70VZVs+7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5 +HMQxK9VfvFMSF5yZVylmd2EhMQcuJUmdGPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d7 +70O/Nbua2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5Rp9EixAqnOEhss/n/fauG +V+O61oV4d7pD6kh/9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NIS+LI+H+S +qHZGnEJlPqQewQcDWkYtuJfzt9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S +5u046uwBHjxIVkkJx0w3AJ6IDsBz4W9m6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4Ia +C1nEWTJ3s7xgaVY5/bQGeyzWZDbZvUjthB9+pSKPKrhC9IK31FOQeE4tGv2Bb0TX +OwF0lkLgAOIua+rF7nKsu7/+6qqo+Nz2snmKtmcCAwEAAaNCMEAwHQYDVR0OBBYE +FJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByvMiPIs0laUZx2 +KI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMg +Nt58D2kTiKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B +8OWycvpEgjNC6C1Y91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQ +MKSOyARiqcTtNd56l+0OOF6SL5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc +0jlzCFfemQa0W50QBuHCAKi4HEoCChTQwUHK+4w1IX2COPKpVJEZNZOUbWo6xbLQ +u4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFooC8k4gmVBtWVyuEklut89pMF +u+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5YwH2AG7hsj/oFgIxpH +YoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/qzWaVYa8 +GKa1qF60g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaO +RtGdFNrHF+QFlozEJLUbzxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6e +KeC2uAloGRwYQw== +-----END CERTIFICATE----- + +# Issuer: CN=AffirmTrust Premium ECC O=AffirmTrust +# Subject: CN=AffirmTrust Premium ECC O=AffirmTrust +# Label: "AffirmTrust Premium ECC" +# Serial: 8401224907861490260 +# MD5 Fingerprint: 64:b0:09:55:cf:b1:d5:99:e2:be:13:ab:a6:5d:ea:4d +# SHA1 Fingerprint: b8:23:6b:00:2f:1d:16:86:53:01:55:6c:11:a4:37:ca:eb:ff:c3:bb +# SHA256 Fingerprint: bd:71:fd:f6:da:97:e4:cf:62:d1:64:7a:dd:25:81:b0:7d:79:ad:f8:39:7e:b4:ec:ba:9c:5e:84:88:82:14:23 +-----BEGIN CERTIFICATE----- +MIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMC +VVMxFDASBgNVBAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQ +cmVtaXVtIEVDQzAeFw0xMDAxMjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJ +BgNVBAYTAlVTMRQwEgYDVQQKDAtBZmZpcm1UcnVzdDEgMB4GA1UEAwwXQWZmaXJt +VHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQNMF4bFZ0D +0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQUX+iOGasvLkjmrBhDeKzQN8O9 +ss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR4ptlKymjQjBAMB0G +A1UdDgQWBBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTADAQH/MA4G +A1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/Vs +aobgxCd05DhT1wV/GzTjxi+zygk8N53X57hG8f2h4nECMEJZh0PUUd+60wkyWs6I +flc9nF9Ca/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKMeQ== +-----END CERTIFICATE----- + +# Issuer: CN=StartCom Certification Authority O=StartCom Ltd. OU=Secure Digital Certificate Signing +# Subject: CN=StartCom Certification Authority O=StartCom Ltd. OU=Secure Digital Certificate Signing +# Label: "StartCom Certification Authority" +# Serial: 45 +# MD5 Fingerprint: c9:3b:0d:84:41:fc:a4:76:79:23:08:57:de:10:19:16 +# SHA1 Fingerprint: a3:f1:33:3f:e2:42:bf:cf:c5:d1:4e:8f:39:42:98:40:68:10:d1:a0 +# SHA256 Fingerprint: e1:78:90:ee:09:a3:fb:f4:f4:8b:9c:41:4a:17:d6:37:b7:a5:06:47:e9:bc:75:23:22:72:7f:cc:17:42:a9:11 +-----BEGIN CERTIFICATE----- +MIIHhzCCBW+gAwIBAgIBLTANBgkqhkiG9w0BAQsFADB9MQswCQYDVQQGEwJJTDEW +MBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwg +Q2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0NjM3WhcNMzYwOTE3MTk0NjM2WjB9 +MQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMi +U2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3Rh +cnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZk +pMyONvg45iPwbm2xPN1yo4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rf +OQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/C +Ji/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/deMotHweXMAEtcnn6RtYT +Kqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt2PZE4XNi +HzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMM +Av+Z6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w ++2OqqGwaVLRcJXrJosmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+ +Gkhpi8KWTRoSsmkXwQqQ1vp5Iki/untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3 +Zzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVcUjyJthkqcwEKDwOzEmDyei+B +26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT37uMdBNSSwID +AQABo4ICEDCCAgwwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD +VR0OBBYEFE4L7xqkQFulF2mHMMo0aEPQQa7yMB8GA1UdIwQYMBaAFE4L7xqkQFul +F2mHMMo0aEPQQa7yMIIBWgYDVR0gBIIBUTCCAU0wggFJBgsrBgEEAYG1NwEBATCC +ATgwLgYIKwYBBQUHAgEWImh0dHA6Ly93d3cuc3RhcnRzc2wuY29tL3BvbGljeS5w +ZGYwNAYIKwYBBQUHAgEWKGh0dHA6Ly93d3cuc3RhcnRzc2wuY29tL2ludGVybWVk +aWF0ZS5wZGYwgc8GCCsGAQUFBwICMIHCMCcWIFN0YXJ0IENvbW1lcmNpYWwgKFN0 +YXJ0Q29tKSBMdGQuMAMCAQEagZZMaW1pdGVkIExpYWJpbGl0eSwgcmVhZCB0aGUg +c2VjdGlvbiAqTGVnYWwgTGltaXRhdGlvbnMqIG9mIHRoZSBTdGFydENvbSBDZXJ0 +aWZpY2F0aW9uIEF1dGhvcml0eSBQb2xpY3kgYXZhaWxhYmxlIGF0IGh0dHA6Ly93 +d3cuc3RhcnRzc2wuY29tL3BvbGljeS5wZGYwEQYJYIZIAYb4QgEBBAQDAgAHMDgG +CWCGSAGG+EIBDQQrFilTdGFydENvbSBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1 +dGhvcml0eTANBgkqhkiG9w0BAQsFAAOCAgEAjo/n3JR5fPGFf59Jb2vKXfuM/gTF +wWLRfUKKvFO3lANmMD+x5wqnUCBVJX92ehQN6wQOQOY+2IirByeDqXWmN3PH/UvS +Ta0XQMhGvjt/UfzDtgUx3M2FIk5xt/JxXrAaxrqTi3iSSoX4eA+D/i+tLPfkpLst +0OcNOrg+zvZ49q5HJMqjNTbOx8aHmNrs++myziebiMMEofYLWWivydsQD032ZGNc +pRJvkrKTlMeIFw6Ttn5ii5B/q06f/ON1FE8qMt9bDeD1e5MNq6HPh+GlBEXoPBKl +CcWw0bdT82AUuoVpaiF8H3VhFyAXe2w7QSlc4axa0c2Mm+tgHRns9+Ww2vl5GKVF +P0lDV9LdJNUso/2RjSe15esUBppMeyG7Oq0wBhjA2MFrLH9ZXF2RsXAiV+uKa0hK +1Q8p7MZAwC+ITGgBF3f0JBlPvfrhsiAhS90a2Cl9qrjeVOwhVYBsHvUwyKMQ5bLm +KhQxw4UtjJixhlpPiVktucf3HMiKf8CdBUrmQk9io20ppB+Fq9vlgcitKj1MXVuE +JnHEhV5xJMqlG2zYYdMa4FTbzrqpMrUi9nNBCV24F10OD5mQ1kfabwo6YigUZ4LZ +8dCAWZvLMdibD4x3TrVoivJs9iQOLWxwxXPR3hTQcY+203sC9uO41Alua551hDnm +fyWl8kgAwKQB2j8= +-----END CERTIFICATE----- + +# Issuer: CN=StartCom Certification Authority G2 O=StartCom Ltd. +# Subject: CN=StartCom Certification Authority G2 O=StartCom Ltd. +# Label: "StartCom Certification Authority G2" +# Serial: 59 +# MD5 Fingerprint: 78:4b:fb:9e:64:82:0a:d3:b8:4c:62:f3:64:f2:90:64 +# SHA1 Fingerprint: 31:f1:fd:68:22:63:20:ee:c6:3b:3f:9d:ea:4a:3e:53:7c:7c:39:17 +# SHA256 Fingerprint: c7:ba:65:67:de:93:a7:98:ae:1f:aa:79:1e:71:2d:37:8f:ae:1f:93:c4:39:7f:ea:44:1b:b7:cb:e6:fd:59:95 +-----BEGIN CERTIFICATE----- +MIIFYzCCA0ugAwIBAgIBOzANBgkqhkiG9w0BAQsFADBTMQswCQYDVQQGEwJJTDEW +MBQGA1UEChMNU3RhcnRDb20gTHRkLjEsMCoGA1UEAxMjU3RhcnRDb20gQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkgRzIwHhcNMTAwMTAxMDEwMDAxWhcNMzkxMjMxMjM1 +OTAxWjBTMQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjEsMCoG +A1UEAxMjU3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgRzIwggIiMA0G +CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2iTZbB7cgNr2Cu+EWIAOVeq8Oo1XJ +JZlKxdBWQYeQTSFgpBSHO839sj60ZwNq7eEPS8CRhXBF4EKe3ikj1AENoBB5uNsD +vfOpL9HG4A/LnooUCri99lZi8cVytjIl2bLzvWXFDSxu1ZJvGIsAQRSCb0AgJnoo +D/Uefyf3lLE3PbfHkffiAez9lInhzG7TNtYKGXmu1zSCZf98Qru23QumNK9LYP5/ +Q0kGi4xDuFby2X8hQxfqp0iVAXV16iulQ5XqFYSdCI0mblWbq9zSOdIxHWDirMxW +RST1HFSr7obdljKF+ExP6JV2tgXdNiNnvP8V4so75qbsO+wmETRIjfaAKxojAuuK +HDp2KntWFhxyKrOq42ClAJ8Em+JvHhRYW6Vsi1g8w7pOOlz34ZYrPu8HvKTlXcxN +nw3h3Kq74W4a7I/htkxNeXJdFzULHdfBR9qWJODQcqhaX2YtENwvKhOuJv4KHBnM +0D4LnMgJLvlblnpHnOl68wVQdJVznjAJ85eCXuaPOQgeWeU1FEIT/wCc976qUM/i +UUjXuG+v+E5+M5iSFGI6dWPPe/regjupuznixL0sAA7IF6wT700ljtizkC+p2il9 +Ha90OrInwMEePnWjFqmveiJdnxMaz6eg6+OGCtP95paV1yPIN93EfKo2rJgaErHg +TuixO/XWb/Ew1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQE +AwIBBjAdBgNVHQ4EFgQUS8W0QGutHLOlHGVuRjaJhwUMDrYwDQYJKoZIhvcNAQEL +BQADggIBAHNXPyzVlTJ+N9uWkusZXn5T50HsEbZH77Xe7XRcxfGOSeD8bpkTzZ+K +2s06Ctg6Wgk/XzTQLwPSZh0avZyQN8gMjgdalEVGKua+etqhqaRpEpKwfTbURIfX +UfEpY9Z1zRbkJ4kd+MIySP3bmdCPX1R0zKxnNBFi2QwKN4fRoxdIjtIXHfbX/dtl +6/2o1PXWT6RbdejF0mCy2wl+JYt7ulKSnj7oxXehPOBKc2thz4bcQ///If4jXSRK +9dNtD2IEBVeC2m6kMyV5Sy5UGYvMLD0w6dEG/+gyRr61M3Z3qAFdlsHB1b6uJcDJ +HgoJIIihDsnzb02CVAAgp9KP5DlUFy6NHrgbuxu9mk47EDTcnIhT76IxW1hPkWLI +wpqazRVdOKnWvvgTtZ8SafJQYqz7Fzf07rh1Z2AQ+4NQ+US1dZxAF7L+/XldblhY +XzD8AK6vM8EOTmy6p6ahfzLbOOCxchcKK5HsamMm7YnUeMx0HgX4a/6ManY5Ka5l +IxKVCCIcl85bBu4M4ru8H0ST9tg4RQUh7eStqxK2A6RCLi3ECToDZ2mEmuFZkIoo +hdVddLHRDiBYmxOlsGOm7XtH/UVVMKTumtTm4ofvmMkyghEpIrwACjFeLQ/Ajulr +so8uBtjRkcfGEvRM/TAXw8HaOFvjqermobp573PYtlNXLfbQ4ddI +-----END CERTIFICATE----- diff --git a/appengine/dashdemo/httplib2/iri2uri.py b/appengine/dashdemo/httplib2/iri2uri.py new file mode 100644 index 0000000..d88c91f --- /dev/null +++ b/appengine/dashdemo/httplib2/iri2uri.py @@ -0,0 +1,110 @@ +""" +iri2uri + +Converts an IRI to a URI. + +""" +__author__ = "Joe Gregorio (joe@bitworking.org)" +__copyright__ = "Copyright 2006, Joe Gregorio" +__contributors__ = [] +__version__ = "1.0.0" +__license__ = "MIT" +__history__ = """ +""" + +import urlparse + + +# Convert an IRI to a URI following the rules in RFC 3987 +# +# The characters we need to enocde and escape are defined in the spec: +# +# iprivate = %xE000-F8FF / %xF0000-FFFFD / %x100000-10FFFD +# ucschar = %xA0-D7FF / %xF900-FDCF / %xFDF0-FFEF +# / %x10000-1FFFD / %x20000-2FFFD / %x30000-3FFFD +# / %x40000-4FFFD / %x50000-5FFFD / %x60000-6FFFD +# / %x70000-7FFFD / %x80000-8FFFD / %x90000-9FFFD +# / %xA0000-AFFFD / %xB0000-BFFFD / %xC0000-CFFFD +# / %xD0000-DFFFD / %xE1000-EFFFD + +escape_range = [ + (0xA0, 0xD7FF), + (0xE000, 0xF8FF), + (0xF900, 0xFDCF), + (0xFDF0, 0xFFEF), + (0x10000, 0x1FFFD), + (0x20000, 0x2FFFD), + (0x30000, 0x3FFFD), + (0x40000, 0x4FFFD), + (0x50000, 0x5FFFD), + (0x60000, 0x6FFFD), + (0x70000, 0x7FFFD), + (0x80000, 0x8FFFD), + (0x90000, 0x9FFFD), + (0xA0000, 0xAFFFD), + (0xB0000, 0xBFFFD), + (0xC0000, 0xCFFFD), + (0xD0000, 0xDFFFD), + (0xE1000, 0xEFFFD), + (0xF0000, 0xFFFFD), + (0x100000, 0x10FFFD), +] + +def encode(c): + retval = c + i = ord(c) + for low, high in escape_range: + if i < low: + break + if i >= low and i <= high: + retval = "".join(["%%%2X" % ord(o) for o in c.encode('utf-8')]) + break + return retval + + +def iri2uri(uri): + """Convert an IRI to a URI. Note that IRIs must be + passed in a unicode strings. That is, do not utf-8 encode + the IRI before passing it into the function.""" + if isinstance(uri ,unicode): + (scheme, authority, path, query, fragment) = urlparse.urlsplit(uri) + authority = authority.encode('idna') + # For each character in 'ucschar' or 'iprivate' + # 1. encode as utf-8 + # 2. then %-encode each octet of that utf-8 + uri = urlparse.urlunsplit((scheme, authority, path, query, fragment)) + uri = "".join([encode(c) for c in uri]) + return uri + +if __name__ == "__main__": + import unittest + + class Test(unittest.TestCase): + + def test_uris(self): + """Test that URIs are invariant under the transformation.""" + invariant = [ + u"ftp://ftp.is.co.za/rfc/rfc1808.txt", + u"http://www.ietf.org/rfc/rfc2396.txt", + u"ldap://[2001:db8::7]/c=GB?objectClass?one", + u"mailto:John.Doe@example.com", + u"news:comp.infosystems.www.servers.unix", + u"tel:+1-816-555-1212", + u"telnet://192.0.2.16:80/", + u"urn:oasis:names:specification:docbook:dtd:xml:4.1.2" ] + for uri in invariant: + self.assertEqual(uri, iri2uri(uri)) + + def test_iri(self): + """ Test that the right type of escaping is done for each part of the URI.""" + self.assertEqual("http://xn--o3h.com/%E2%98%84", iri2uri(u"http://\N{COMET}.com/\N{COMET}")) + self.assertEqual("http://bitworking.org/?fred=%E2%98%84", iri2uri(u"http://bitworking.org/?fred=\N{COMET}")) + self.assertEqual("http://bitworking.org/#%E2%98%84", iri2uri(u"http://bitworking.org/#\N{COMET}")) + self.assertEqual("#%E2%98%84", iri2uri(u"#\N{COMET}")) + self.assertEqual("/fred?bar=%E2%98%9A#%E2%98%84", iri2uri(u"/fred?bar=\N{BLACK LEFT POINTING INDEX}#\N{COMET}")) + self.assertEqual("/fred?bar=%E2%98%9A#%E2%98%84", iri2uri(iri2uri(u"/fred?bar=\N{BLACK LEFT POINTING INDEX}#\N{COMET}"))) + self.assertNotEqual("/fred?bar=%E2%98%9A#%E2%98%84", iri2uri(u"/fred?bar=\N{BLACK LEFT POINTING INDEX}#\N{COMET}".encode('utf-8'))) + + unittest.main() + + diff --git a/appengine/dashdemo/httplib2/socks.py b/appengine/dashdemo/httplib2/socks.py new file mode 100644 index 0000000..0991f4c --- /dev/null +++ b/appengine/dashdemo/httplib2/socks.py @@ -0,0 +1,438 @@ +"""SocksiPy - Python SOCKS module. +Version 1.00 + +Copyright 2006 Dan-Haim. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +3. Neither the name of Dan Haim nor the names of his contributors may be used + to endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY DAN HAIM "AS IS" AND ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +EVENT SHALL DAN HAIM OR HIS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMANGE. + + +This module provides a standard socket-like interface for Python +for tunneling connections through SOCKS proxies. + +""" + +""" + +Minor modifications made by Christopher Gilbert (http://motomastyle.com/) +for use in PyLoris (http://pyloris.sourceforge.net/) + +Minor modifications made by Mario Vilas (http://breakingcode.wordpress.com/) +mainly to merge bug fixes found in Sourceforge + +""" + +import base64 +import socket +import struct +import sys + +if getattr(socket, 'socket', None) is None: + raise ImportError('socket.socket missing, proxy support unusable') + +PROXY_TYPE_SOCKS4 = 1 +PROXY_TYPE_SOCKS5 = 2 +PROXY_TYPE_HTTP = 3 +PROXY_TYPE_HTTP_NO_TUNNEL = 4 + +_defaultproxy = None +_orgsocket = socket.socket + +class ProxyError(Exception): pass +class GeneralProxyError(ProxyError): pass +class Socks5AuthError(ProxyError): pass +class Socks5Error(ProxyError): pass +class Socks4Error(ProxyError): pass +class HTTPError(ProxyError): pass + +_generalerrors = ("success", + "invalid data", + "not connected", + "not available", + "bad proxy type", + "bad input") + +_socks5errors = ("succeeded", + "general SOCKS server failure", + "connection not allowed by ruleset", + "Network unreachable", + "Host unreachable", + "Connection refused", + "TTL expired", + "Command not supported", + "Address type not supported", + "Unknown error") + +_socks5autherrors = ("succeeded", + "authentication is required", + "all offered authentication methods were rejected", + "unknown username or invalid password", + "unknown error") + +_socks4errors = ("request granted", + "request rejected or failed", + "request rejected because SOCKS server cannot connect to identd on the client", + "request rejected because the client program and identd report different user-ids", + "unknown error") + +def setdefaultproxy(proxytype=None, addr=None, port=None, rdns=True, username=None, password=None): + """setdefaultproxy(proxytype, addr[, port[, rdns[, username[, password]]]]) + Sets a default proxy which all further socksocket objects will use, + unless explicitly changed. + """ + global _defaultproxy + _defaultproxy = (proxytype, addr, port, rdns, username, password) + +def wrapmodule(module): + """wrapmodule(module) + Attempts to replace a module's socket library with a SOCKS socket. Must set + a default proxy using setdefaultproxy(...) first. + This will only work on modules that import socket directly into the namespace; + most of the Python Standard Library falls into this category. + """ + if _defaultproxy != None: + module.socket.socket = socksocket + else: + raise GeneralProxyError((4, "no proxy specified")) + +class socksocket(socket.socket): + """socksocket([family[, type[, proto]]]) -> socket object + Open a SOCKS enabled socket. The parameters are the same as + those of the standard socket init. In order for SOCKS to work, + you must specify family=AF_INET, type=SOCK_STREAM and proto=0. + """ + + def __init__(self, family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0, _sock=None): + _orgsocket.__init__(self, family, type, proto, _sock) + if _defaultproxy != None: + self.__proxy = _defaultproxy + else: + self.__proxy = (None, None, None, None, None, None) + self.__proxysockname = None + self.__proxypeername = None + self.__httptunnel = True + + def __recvall(self, count): + """__recvall(count) -> data + Receive EXACTLY the number of bytes requested from the socket. + Blocks until the required number of bytes have been received. + """ + data = self.recv(count) + while len(data) < count: + d = self.recv(count-len(data)) + if not d: raise GeneralProxyError((0, "connection closed unexpectedly")) + data = data + d + return data + + def sendall(self, content, *args): + """ override socket.socket.sendall method to rewrite the header + for non-tunneling proxies if needed + """ + if not self.__httptunnel: + content = self.__rewriteproxy(content) + return super(socksocket, self).sendall(content, *args) + + def __rewriteproxy(self, header): + """ rewrite HTTP request headers to support non-tunneling proxies + (i.e. those which do not support the CONNECT method). + This only works for HTTP (not HTTPS) since HTTPS requires tunneling. + """ + host, endpt = None, None + hdrs = header.split("\r\n") + for hdr in hdrs: + if hdr.lower().startswith("host:"): + host = hdr + elif hdr.lower().startswith("get") or hdr.lower().startswith("post"): + endpt = hdr + if host and endpt: + hdrs.remove(host) + hdrs.remove(endpt) + host = host.split(" ")[1] + endpt = endpt.split(" ") + if (self.__proxy[4] != None and self.__proxy[5] != None): + hdrs.insert(0, self.__getauthheader()) + hdrs.insert(0, "Host: %s" % host) + hdrs.insert(0, "%s http://%s%s %s" % (endpt[0], host, endpt[1], endpt[2])) + return "\r\n".join(hdrs) + + def __getauthheader(self): + auth = self.__proxy[4] + ":" + self.__proxy[5] + return "Proxy-Authorization: Basic " + base64.b64encode(auth) + + def setproxy(self, proxytype=None, addr=None, port=None, rdns=True, username=None, password=None): + """setproxy(proxytype, addr[, port[, rdns[, username[, password]]]]) + Sets the proxy to be used. + proxytype - The type of the proxy to be used. Three types + are supported: PROXY_TYPE_SOCKS4 (including socks4a), + PROXY_TYPE_SOCKS5 and PROXY_TYPE_HTTP + addr - The address of the server (IP or DNS). + port - The port of the server. Defaults to 1080 for SOCKS + servers and 8080 for HTTP proxy servers. + rdns - Should DNS queries be preformed on the remote side + (rather than the local side). The default is True. + Note: This has no effect with SOCKS4 servers. + username - Username to authenticate with to the server. + The default is no authentication. + password - Password to authenticate with to the server. + Only relevant when username is also provided. + """ + self.__proxy = (proxytype, addr, port, rdns, username, password) + + def __negotiatesocks5(self, destaddr, destport): + """__negotiatesocks5(self,destaddr,destport) + Negotiates a connection through a SOCKS5 server. + """ + # First we'll send the authentication packages we support. + if (self.__proxy[4]!=None) and (self.__proxy[5]!=None): + # The username/password details were supplied to the + # setproxy method so we support the USERNAME/PASSWORD + # authentication (in addition to the standard none). + self.sendall(struct.pack('BBBB', 0x05, 0x02, 0x00, 0x02)) + else: + # No username/password were entered, therefore we + # only support connections with no authentication. + self.sendall(struct.pack('BBB', 0x05, 0x01, 0x00)) + # We'll receive the server's response to determine which + # method was selected + chosenauth = self.__recvall(2) + if chosenauth[0:1] != chr(0x05).encode(): + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + # Check the chosen authentication method + if chosenauth[1:2] == chr(0x00).encode(): + # No authentication is required + pass + elif chosenauth[1:2] == chr(0x02).encode(): + # Okay, we need to perform a basic username/password + # authentication. + self.sendall(chr(0x01).encode() + chr(len(self.__proxy[4])) + self.__proxy[4] + chr(len(self.__proxy[5])) + self.__proxy[5]) + authstat = self.__recvall(2) + if authstat[0:1] != chr(0x01).encode(): + # Bad response + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + if authstat[1:2] != chr(0x00).encode(): + # Authentication failed + self.close() + raise Socks5AuthError((3, _socks5autherrors[3])) + # Authentication succeeded + else: + # Reaching here is always bad + self.close() + if chosenauth[1] == chr(0xFF).encode(): + raise Socks5AuthError((2, _socks5autherrors[2])) + else: + raise GeneralProxyError((1, _generalerrors[1])) + # Now we can request the actual connection + req = struct.pack('BBB', 0x05, 0x01, 0x00) + # If the given destination address is an IP address, we'll + # use the IPv4 address request even if remote resolving was specified. + try: + ipaddr = socket.inet_aton(destaddr) + req = req + chr(0x01).encode() + ipaddr + except socket.error: + # Well it's not an IP number, so it's probably a DNS name. + if self.__proxy[3]: + # Resolve remotely + ipaddr = None + req = req + chr(0x03).encode() + chr(len(destaddr)).encode() + destaddr + else: + # Resolve locally + ipaddr = socket.inet_aton(socket.gethostbyname(destaddr)) + req = req + chr(0x01).encode() + ipaddr + req = req + struct.pack(">H", destport) + self.sendall(req) + # Get the response + resp = self.__recvall(4) + if resp[0:1] != chr(0x05).encode(): + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + elif resp[1:2] != chr(0x00).encode(): + # Connection failed + self.close() + if ord(resp[1:2])<=8: + raise Socks5Error((ord(resp[1:2]), _socks5errors[ord(resp[1:2])])) + else: + raise Socks5Error((9, _socks5errors[9])) + # Get the bound address/port + elif resp[3:4] == chr(0x01).encode(): + boundaddr = self.__recvall(4) + elif resp[3:4] == chr(0x03).encode(): + resp = resp + self.recv(1) + boundaddr = self.__recvall(ord(resp[4:5])) + else: + self.close() + raise GeneralProxyError((1,_generalerrors[1])) + boundport = struct.unpack(">H", self.__recvall(2))[0] + self.__proxysockname = (boundaddr, boundport) + if ipaddr != None: + self.__proxypeername = (socket.inet_ntoa(ipaddr), destport) + else: + self.__proxypeername = (destaddr, destport) + + def getproxysockname(self): + """getsockname() -> address info + Returns the bound IP address and port number at the proxy. + """ + return self.__proxysockname + + def getproxypeername(self): + """getproxypeername() -> address info + Returns the IP and port number of the proxy. + """ + return _orgsocket.getpeername(self) + + def getpeername(self): + """getpeername() -> address info + Returns the IP address and port number of the destination + machine (note: getproxypeername returns the proxy) + """ + return self.__proxypeername + + def __negotiatesocks4(self,destaddr,destport): + """__negotiatesocks4(self,destaddr,destport) + Negotiates a connection through a SOCKS4 server. + """ + # Check if the destination address provided is an IP address + rmtrslv = False + try: + ipaddr = socket.inet_aton(destaddr) + except socket.error: + # It's a DNS name. Check where it should be resolved. + if self.__proxy[3]: + ipaddr = struct.pack("BBBB", 0x00, 0x00, 0x00, 0x01) + rmtrslv = True + else: + ipaddr = socket.inet_aton(socket.gethostbyname(destaddr)) + # Construct the request packet + req = struct.pack(">BBH", 0x04, 0x01, destport) + ipaddr + # The username parameter is considered userid for SOCKS4 + if self.__proxy[4] != None: + req = req + self.__proxy[4] + req = req + chr(0x00).encode() + # DNS name if remote resolving is required + # NOTE: This is actually an extension to the SOCKS4 protocol + # called SOCKS4A and may not be supported in all cases. + if rmtrslv: + req = req + destaddr + chr(0x00).encode() + self.sendall(req) + # Get the response from the server + resp = self.__recvall(8) + if resp[0:1] != chr(0x00).encode(): + # Bad data + self.close() + raise GeneralProxyError((1,_generalerrors[1])) + if resp[1:2] != chr(0x5A).encode(): + # Server returned an error + self.close() + if ord(resp[1:2]) in (91, 92, 93): + self.close() + raise Socks4Error((ord(resp[1:2]), _socks4errors[ord(resp[1:2]) - 90])) + else: + raise Socks4Error((94, _socks4errors[4])) + # Get the bound address/port + self.__proxysockname = (socket.inet_ntoa(resp[4:]), struct.unpack(">H", resp[2:4])[0]) + if rmtrslv != None: + self.__proxypeername = (socket.inet_ntoa(ipaddr), destport) + else: + self.__proxypeername = (destaddr, destport) + + def __negotiatehttp(self, destaddr, destport): + """__negotiatehttp(self,destaddr,destport) + Negotiates a connection through an HTTP server. + """ + # If we need to resolve locally, we do this now + if not self.__proxy[3]: + addr = socket.gethostbyname(destaddr) + else: + addr = destaddr + headers = ["CONNECT ", addr, ":", str(destport), " HTTP/1.1\r\n"] + headers += ["Host: ", destaddr, "\r\n"] + if (self.__proxy[4] != None and self.__proxy[5] != None): + headers += [self.__getauthheader(), "\r\n"] + headers.append("\r\n") + self.sendall("".join(headers).encode()) + # We read the response until we get the string "\r\n\r\n" + resp = self.recv(1) + while resp.find("\r\n\r\n".encode()) == -1: + resp = resp + self.recv(1) + # We just need the first line to check if the connection + # was successful + statusline = resp.splitlines()[0].split(" ".encode(), 2) + if statusline[0] not in ("HTTP/1.0".encode(), "HTTP/1.1".encode()): + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + try: + statuscode = int(statusline[1]) + except ValueError: + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + if statuscode != 200: + self.close() + raise HTTPError((statuscode, statusline[2])) + self.__proxysockname = ("0.0.0.0", 0) + self.__proxypeername = (addr, destport) + + def connect(self, destpair): + """connect(self, despair) + Connects to the specified destination through a proxy. + destpar - A tuple of the IP/DNS address and the port number. + (identical to socket's connect). + To select the proxy server use setproxy(). + """ + # Do a minimal input check first + if (not type(destpair) in (list,tuple)) or (len(destpair) < 2) or (not isinstance(destpair[0], basestring)) or (type(destpair[1]) != int): + raise GeneralProxyError((5, _generalerrors[5])) + if self.__proxy[0] == PROXY_TYPE_SOCKS5: + if self.__proxy[2] != None: + portnum = self.__proxy[2] + else: + portnum = 1080 + _orgsocket.connect(self, (self.__proxy[1], portnum)) + self.__negotiatesocks5(destpair[0], destpair[1]) + elif self.__proxy[0] == PROXY_TYPE_SOCKS4: + if self.__proxy[2] != None: + portnum = self.__proxy[2] + else: + portnum = 1080 + _orgsocket.connect(self,(self.__proxy[1], portnum)) + self.__negotiatesocks4(destpair[0], destpair[1]) + elif self.__proxy[0] == PROXY_TYPE_HTTP: + if self.__proxy[2] != None: + portnum = self.__proxy[2] + else: + portnum = 8080 + _orgsocket.connect(self,(self.__proxy[1], portnum)) + self.__negotiatehttp(destpair[0], destpair[1]) + elif self.__proxy[0] == PROXY_TYPE_HTTP_NO_TUNNEL: + if self.__proxy[2] != None: + portnum = self.__proxy[2] + else: + portnum = 8080 + _orgsocket.connect(self,(self.__proxy[1],portnum)) + if destpair[1] == 443: + self.__negotiatehttp(destpair[0],destpair[1]) + else: + self.__httptunnel = False + elif self.__proxy[0] == None: + _orgsocket.connect(self, (destpair[0], destpair[1])) + else: + raise GeneralProxyError((4, _generalerrors[4])) diff --git a/appengine/dashdemo/index.html b/appengine/dashdemo/index.html new file mode 100644 index 0000000..6c893ff --- /dev/null +++ b/appengine/dashdemo/index.html @@ -0,0 +1,38 @@ + + + hellodashboard + + + + +
+

A simple example of a graphical dashboard, backed by + Google's BigQuery and running in Python on App Engine.

+

Click on the map to highlight the value of each state, as + generated by this query:

+
 {{ query }} 
+ + diff --git a/appengine/dashdemo/main.py b/appengine/dashdemo/main.py new file mode 100644 index 0000000..4eb8b80 --- /dev/null +++ b/appengine/dashdemo/main.py @@ -0,0 +1,94 @@ +import webapp2 +import bqclient +import httplib2 +import logging +import os +from django.utils import simplejson as json +from google.appengine.ext.webapp.template import render +from oauth2client.appengine import oauth2decorator_from_clientsecrets + +# CLIENT_SECRETS, name of a file containing +# the OAuth 2.0 information for this application, +# including client_id and client_secret, which are found +# on the API Access tab on the Google APIs Console # +CLIENT_SECRETS = os.path.join(os.path.dirname(__file__), 'client_secrets.json') + +# Project ID for a project where you and your users +# are viewing members. This is where the bill will be sent. +# During the limited availability preview, there is no bill. +# Replace this value with the Client ID value from your project, +# the same numeric value you used in client_secrets.json +BILLING_PROJECT_ID = "475473128136" +DATA_PROJECT_ID = "publicdata" +DATASET = "samples" +TABLE = "natality" +QUERY = """ +select state, SUM(gestation_weeks) / COUNT(gestation_weeks) as weeks +from publicdata:samples.natality +where year > 1990 and year < 2005 and IS_EXPLICITLY_DEFINED(gestation_weeks) +group by state order by weeks +""" +decorator = oauth2decorator_from_clientsecrets(CLIENT_SECRETS, + 'https://www.googleapis.com/auth/bigquery') + + +class InfoHandler(webapp2.RequestHandler): + @decorator.oauth_aware + def get(self): + self.response.out.write(decorator.authorize_url()) + +class MainHandler(webapp2.RequestHandler): + def _bq4geo(self, bqdata): + """geodata output for region maps must be in the format region, value. + Assume the BigQuery query output is in this format and get names from schema. + """ + columnNameGeo = bqdata["schema"]["fields"][0]["name"] + columnNameVal = bqdata["schema"]["fields"][1]["name"] + #logging.info("Column Names=%s, %s" % (columnNameGeo, columnNameVal)) + geodata = []; + geodata.append((columnNameGeo,columnNameVal)) + logging.info(geodata) + for row in bqdata["rows"]: + geodata.append(("US-"+row["f"][0]["v"],row["f"][1]["v"])) + Wlogging.info("FINAL GEODATA---") + #logging.info(geodata) + return json.dumps(geodata, ensure_ascii=True) + + @decorator.oauth_required + def _bq2geo(self ): + """geodata output for region maps must be in the format region, value. + Assume the BigQuery query output is in this format and get names from schema. + """ + http = decorator.http() + bq = bqclient.BigQueryClient(http, decorator) + bqdata = bq.Query(QUERY, BILLING_PROJECT_ID) + #logging.info(bqdata) + columnNameGeo = bqdata["schema"]["fields"][0]["name"] + columnNameVal = bqdata["schema"]["fields"][1]["name"] + #logging.info("Column Names=%s, %s" % (columnNameGeo, columnNameVal)) + geodata = {} + geodata["rows"] = []; + logging.info(geodata) + for row in bqdata["rows"]: + newrow = ({"c":[]}) + newrow["c"].append({"v": "US-"+row["f"][0]["v"]}) + newrow["c"].append({"v":row["f"][1]["v"]}) + geodata["rows"].append(newrow) + geodata["cols"] = ({"id":columnNameGeo,"label":columnNameGeo,"type":"string"}, + {"id":columnNameVal, "label":columnNameVal, "type":"number"}) + logging.info("FINAL GEODATA---") + logging.info(geodata) + return json.dumps(geodata, ensure_ascii=True) + + def get(self): + values = self._bq2geo() + data = { 'data': values, + 'query': QUERY } + template = os.path.join(os.path.dirname(__file__), 'index.html') + self.response.out.write(render(template, data)) + +application = webapp2.WSGIApplication([ + ('/', MainHandler), + ('/info', InfoHandler), + (decorator.callback_path, decorator.callback_handler()), +], debug=True) diff --git a/appengine/dashdemo/oauth2client/__init__.py b/appengine/dashdemo/oauth2client/__init__.py new file mode 100644 index 0000000..ac84748 --- /dev/null +++ b/appengine/dashdemo/oauth2client/__init__.py @@ -0,0 +1,5 @@ +__version__ = "1.2" + +GOOGLE_AUTH_URI = 'https://accounts.google.com/o/oauth2/auth' +GOOGLE_REVOKE_URI = 'https://accounts.google.com/o/oauth2/revoke' +GOOGLE_TOKEN_URI = 'https://accounts.google.com/o/oauth2/token' diff --git a/appengine/dashdemo/oauth2client/anyjson.py b/appengine/dashdemo/oauth2client/anyjson.py new file mode 100644 index 0000000..ae21c33 --- /dev/null +++ b/appengine/dashdemo/oauth2client/anyjson.py @@ -0,0 +1,32 @@ +# Copyright (C) 2010 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Utility module to import a JSON module + +Hides all the messy details of exactly where +we get a simplejson module from. +""" + +__author__ = 'jcgregorio@google.com (Joe Gregorio)' + + +try: # pragma: no cover + # Should work for Python2.6 and higher. + import json as simplejson +except ImportError: # pragma: no cover + try: + import simplejson + except ImportError: + # Try to import from django, should work on App Engine + from django.utils import simplejson diff --git a/appengine/dashdemo/oauth2client/appengine.py b/appengine/dashdemo/oauth2client/appengine.py new file mode 100644 index 0000000..b463d4a --- /dev/null +++ b/appengine/dashdemo/oauth2client/appengine.py @@ -0,0 +1,975 @@ +# Copyright (C) 2010 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Utilities for Google App Engine + +Utilities for making it easier to use OAuth 2.0 on Google App Engine. +""" + +__author__ = 'jcgregorio@google.com (Joe Gregorio)' + +import base64 +import cgi +import httplib2 +import logging +import os +import pickle +import threading +import time + +from google.appengine.api import app_identity +from google.appengine.api import memcache +from google.appengine.api import users +from google.appengine.ext import db +from google.appengine.ext import webapp +from google.appengine.ext.webapp.util import login_required +from google.appengine.ext.webapp.util import run_wsgi_app +from oauth2client import GOOGLE_AUTH_URI +from oauth2client import GOOGLE_REVOKE_URI +from oauth2client import GOOGLE_TOKEN_URI +from oauth2client import clientsecrets +from oauth2client import util +from oauth2client import xsrfutil +from oauth2client.anyjson import simplejson +from oauth2client.client import AccessTokenRefreshError +from oauth2client.client import AssertionCredentials +from oauth2client.client import Credentials +from oauth2client.client import Flow +from oauth2client.client import OAuth2WebServerFlow +from oauth2client.client import Storage + +# TODO(dhermes): Resolve import issue. +# This is a temporary fix for a Google internal issue. +try: + from google.appengine.ext import ndb +except ImportError: + ndb = None + + +logger = logging.getLogger(__name__) + +OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns' + +XSRF_MEMCACHE_ID = 'xsrf_secret_key' + + +def _safe_html(s): + """Escape text to make it safe to display. + + Args: + s: string, The text to escape. + + Returns: + The escaped text as a string. + """ + return cgi.escape(s, quote=1).replace("'", ''') + + +class InvalidClientSecretsError(Exception): + """The client_secrets.json file is malformed or missing required fields.""" + + +class InvalidXsrfTokenError(Exception): + """The XSRF token is invalid or expired.""" + + +class SiteXsrfSecretKey(db.Model): + """Storage for the sites XSRF secret key. + + There will only be one instance stored of this model, the one used for the + site. + """ + secret = db.StringProperty() + +if ndb is not None: + class SiteXsrfSecretKeyNDB(ndb.Model): + """NDB Model for storage for the sites XSRF secret key. + + Since this model uses the same kind as SiteXsrfSecretKey, it can be used + interchangeably. This simply provides an NDB model for interacting with the + same data the DB model interacts with. + + There should only be one instance stored of this model, the one used for the + site. + """ + secret = ndb.StringProperty() + + @classmethod + def _get_kind(cls): + """Return the kind name for this class.""" + return 'SiteXsrfSecretKey' + + +def _generate_new_xsrf_secret_key(): + """Returns a random XSRF secret key. + """ + return os.urandom(16).encode("hex") + + +def xsrf_secret_key(): + """Return the secret key for use for XSRF protection. + + If the Site entity does not have a secret key, this method will also create + one and persist it. + + Returns: + The secret key. + """ + secret = memcache.get(XSRF_MEMCACHE_ID, namespace=OAUTH2CLIENT_NAMESPACE) + if not secret: + # Load the one and only instance of SiteXsrfSecretKey. + model = SiteXsrfSecretKey.get_or_insert(key_name='site') + if not model.secret: + model.secret = _generate_new_xsrf_secret_key() + model.put() + secret = model.secret + memcache.add(XSRF_MEMCACHE_ID, secret, namespace=OAUTH2CLIENT_NAMESPACE) + + return str(secret) + + +class AppAssertionCredentials(AssertionCredentials): + """Credentials object for App Engine Assertion Grants + + This object will allow an App Engine application to identify itself to Google + and other OAuth 2.0 servers that can verify assertions. It can be used for the + purpose of accessing data stored under an account assigned to the App Engine + application itself. + + This credential does not require a flow to instantiate because it represents + a two legged flow, and therefore has all of the required information to + generate and refresh its own access tokens. + """ + + @util.positional(2) + def __init__(self, scope, **kwargs): + """Constructor for AppAssertionCredentials + + Args: + scope: string or iterable of strings, scope(s) of the credentials being + requested. + kwargs: optional keyword args, including: + service_account_id: service account id of the application. If None or + unspecified, the default service account for the app is used. + """ + self.scope = util.scopes_to_string(scope) + self.service_account_id = kwargs.get('service_account_id', None) + + # Assertion type is no longer used, but still in the parent class signature. + super(AppAssertionCredentials, self).__init__(None) + + @classmethod + def from_json(cls, json): + data = simplejson.loads(json) + return AppAssertionCredentials(data['scope']) + + def _refresh(self, http_request): + """Refreshes the access_token. + + Since the underlying App Engine app_identity implementation does its own + caching we can skip all the storage hoops and just to a refresh using the + API. + + Args: + http_request: callable, a callable that matches the method signature of + httplib2.Http.request, used to make the refresh request. + + Raises: + AccessTokenRefreshError: When the refresh fails. + """ + try: + scopes = self.scope.split() + (token, _) = app_identity.get_access_token( + scopes, service_account_id=self.service_account_id) + except app_identity.Error, e: + raise AccessTokenRefreshError(str(e)) + self.access_token = token + + +class FlowProperty(db.Property): + """App Engine datastore Property for Flow. + + Utility property that allows easy storage and retrieval of an + oauth2client.Flow""" + + # Tell what the user type is. + data_type = Flow + + # For writing to datastore. + def get_value_for_datastore(self, model_instance): + flow = super(FlowProperty, + self).get_value_for_datastore(model_instance) + return db.Blob(pickle.dumps(flow)) + + # For reading from datastore. + def make_value_from_datastore(self, value): + if value is None: + return None + return pickle.loads(value) + + def validate(self, value): + if value is not None and not isinstance(value, Flow): + raise db.BadValueError('Property %s must be convertible ' + 'to a FlowThreeLegged instance (%s)' % + (self.name, value)) + return super(FlowProperty, self).validate(value) + + def empty(self, value): + return not value + + +if ndb is not None: + class FlowNDBProperty(ndb.PickleProperty): + """App Engine NDB datastore Property for Flow. + + Serves the same purpose as the DB FlowProperty, but for NDB models. Since + PickleProperty inherits from BlobProperty, the underlying representation of + the data in the datastore will be the same as in the DB case. + + Utility property that allows easy storage and retrieval of an + oauth2client.Flow + """ + + def _validate(self, value): + """Validates a value as a proper Flow object. + + Args: + value: A value to be set on the property. + + Raises: + TypeError if the value is not an instance of Flow. + """ + logger.info('validate: Got type %s', type(value)) + if value is not None and not isinstance(value, Flow): + raise TypeError('Property %s must be convertible to a flow ' + 'instance; received: %s.' % (self._name, value)) + + +class CredentialsProperty(db.Property): + """App Engine datastore Property for Credentials. + + Utility property that allows easy storage and retrieval of + oath2client.Credentials + """ + + # Tell what the user type is. + data_type = Credentials + + # For writing to datastore. + def get_value_for_datastore(self, model_instance): + logger.info("get: Got type " + str(type(model_instance))) + cred = super(CredentialsProperty, + self).get_value_for_datastore(model_instance) + if cred is None: + cred = '' + else: + cred = cred.to_json() + return db.Blob(cred) + + # For reading from datastore. + def make_value_from_datastore(self, value): + logger.info("make: Got type " + str(type(value))) + if value is None: + return None + if len(value) == 0: + return None + try: + credentials = Credentials.new_from_json(value) + except ValueError: + credentials = None + return credentials + + def validate(self, value): + value = super(CredentialsProperty, self).validate(value) + logger.info("validate: Got type " + str(type(value))) + if value is not None and not isinstance(value, Credentials): + raise db.BadValueError('Property %s must be convertible ' + 'to a Credentials instance (%s)' % + (self.name, value)) + #if value is not None and not isinstance(value, Credentials): + # return None + return value + + +if ndb is not None: + # TODO(dhermes): Turn this into a JsonProperty and overhaul the Credentials + # and subclass mechanics to use new_from_dict, to_dict, + # from_dict, etc. + class CredentialsNDBProperty(ndb.BlobProperty): + """App Engine NDB datastore Property for Credentials. + + Serves the same purpose as the DB CredentialsProperty, but for NDB models. + Since CredentialsProperty stores data as a blob and this inherits from + BlobProperty, the data in the datastore will be the same as in the DB case. + + Utility property that allows easy storage and retrieval of Credentials and + subclasses. + """ + def _validate(self, value): + """Validates a value as a proper credentials object. + + Args: + value: A value to be set on the property. + + Raises: + TypeError if the value is not an instance of Credentials. + """ + logger.info('validate: Got type %s', type(value)) + if value is not None and not isinstance(value, Credentials): + raise TypeError('Property %s must be convertible to a credentials ' + 'instance; received: %s.' % (self._name, value)) + + def _to_base_type(self, value): + """Converts our validated value to a JSON serialized string. + + Args: + value: A value to be set in the datastore. + + Returns: + A JSON serialized version of the credential, else '' if value is None. + """ + if value is None: + return '' + else: + return value.to_json() + + def _from_base_type(self, value): + """Converts our stored JSON string back to the desired type. + + Args: + value: A value from the datastore to be converted to the desired type. + + Returns: + A deserialized Credentials (or subclass) object, else None if the + value can't be parsed. + """ + if not value: + return None + try: + # Uses the from_json method of the implied class of value + credentials = Credentials.new_from_json(value) + except ValueError: + credentials = None + return credentials + + +class StorageByKeyName(Storage): + """Store and retrieve a credential to and from the App Engine datastore. + + This Storage helper presumes the Credentials have been stored as a + CredentialsProperty or CredentialsNDBProperty on a datastore model class, and + that entities are stored by key_name. + """ + + @util.positional(4) + def __init__(self, model, key_name, property_name, cache=None, user=None): + """Constructor for Storage. + + Args: + model: db.Model or ndb.Model, model class + key_name: string, key name for the entity that has the credentials + property_name: string, name of the property that is a CredentialsProperty + or CredentialsNDBProperty. + cache: memcache, a write-through cache to put in front of the datastore. + If the model you are using is an NDB model, using a cache will be + redundant since the model uses an instance cache and memcache for you. + user: users.User object, optional. Can be used to grab user ID as a + key_name if no key name is specified. + """ + if key_name is None: + if user is None: + raise ValueError('StorageByKeyName called with no key name or user.') + key_name = user.user_id() + + self._model = model + self._key_name = key_name + self._property_name = property_name + self._cache = cache + + def _is_ndb(self): + """Determine whether the model of the instance is an NDB model. + + Returns: + Boolean indicating whether or not the model is an NDB or DB model. + """ + # issubclass will fail if one of the arguments is not a class, only need + # worry about new-style classes since ndb and db models are new-style + if isinstance(self._model, type): + if ndb is not None and issubclass(self._model, ndb.Model): + return True + elif issubclass(self._model, db.Model): + return False + + raise TypeError('Model class not an NDB or DB model: %s.' % (self._model,)) + + def _get_entity(self): + """Retrieve entity from datastore. + + Uses a different model method for db or ndb models. + + Returns: + Instance of the model corresponding to the current storage object + and stored using the key name of the storage object. + """ + if self._is_ndb(): + return self._model.get_by_id(self._key_name) + else: + return self._model.get_by_key_name(self._key_name) + + def _delete_entity(self): + """Delete entity from datastore. + + Attempts to delete using the key_name stored on the object, whether or not + the given key is in the datastore. + """ + if self._is_ndb(): + ndb.Key(self._model, self._key_name).delete() + else: + entity_key = db.Key.from_path(self._model.kind(), self._key_name) + db.delete(entity_key) + + @db.non_transactional(allow_existing=True) + def locked_get(self): + """Retrieve Credential from datastore. + + Returns: + oauth2client.Credentials + """ + credentials = None + if self._cache: + json = self._cache.get(self._key_name) + if json: + credentials = Credentials.new_from_json(json) + if credentials is None: + entity = self._get_entity() + if entity is not None: + credentials = getattr(entity, self._property_name) + if self._cache: + self._cache.set(self._key_name, credentials.to_json()) + + if credentials and hasattr(credentials, 'set_store'): + credentials.set_store(self) + return credentials + + @db.non_transactional(allow_existing=True) + def locked_put(self, credentials): + """Write a Credentials to the datastore. + + Args: + credentials: Credentials, the credentials to store. + """ + entity = self._model.get_or_insert(self._key_name) + setattr(entity, self._property_name, credentials) + entity.put() + if self._cache: + self._cache.set(self._key_name, credentials.to_json()) + + @db.non_transactional(allow_existing=True) + def locked_delete(self): + """Delete Credential from datastore.""" + + if self._cache: + self._cache.delete(self._key_name) + + self._delete_entity() + + +class CredentialsModel(db.Model): + """Storage for OAuth 2.0 Credentials + + Storage of the model is keyed by the user.user_id(). + """ + credentials = CredentialsProperty() + + +if ndb is not None: + class CredentialsNDBModel(ndb.Model): + """NDB Model for storage of OAuth 2.0 Credentials + + Since this model uses the same kind as CredentialsModel and has a property + which can serialize and deserialize Credentials correctly, it can be used + interchangeably with a CredentialsModel to access, insert and delete the + same entities. This simply provides an NDB model for interacting with the + same data the DB model interacts with. + + Storage of the model is keyed by the user.user_id(). + """ + credentials = CredentialsNDBProperty() + + @classmethod + def _get_kind(cls): + """Return the kind name for this class.""" + return 'CredentialsModel' + + +def _build_state_value(request_handler, user): + """Composes the value for the 'state' parameter. + + Packs the current request URI and an XSRF token into an opaque string that + can be passed to the authentication server via the 'state' parameter. + + Args: + request_handler: webapp.RequestHandler, The request. + user: google.appengine.api.users.User, The current user. + + Returns: + The state value as a string. + """ + uri = request_handler.request.url + token = xsrfutil.generate_token(xsrf_secret_key(), user.user_id(), + action_id=str(uri)) + return uri + ':' + token + + +def _parse_state_value(state, user): + """Parse the value of the 'state' parameter. + + Parses the value and validates the XSRF token in the state parameter. + + Args: + state: string, The value of the state parameter. + user: google.appengine.api.users.User, The current user. + + Raises: + InvalidXsrfTokenError: if the XSRF token is invalid. + + Returns: + The redirect URI. + """ + uri, token = state.rsplit(':', 1) + if not xsrfutil.validate_token(xsrf_secret_key(), token, user.user_id(), + action_id=uri): + raise InvalidXsrfTokenError() + + return uri + + +class OAuth2Decorator(object): + """Utility for making OAuth 2.0 easier. + + Instantiate and then use with oauth_required or oauth_aware + as decorators on webapp.RequestHandler methods. + + Example: + + decorator = OAuth2Decorator( + client_id='837...ent.com', + client_secret='Qh...wwI', + scope='https://www.googleapis.com/auth/plus') + + + class MainHandler(webapp.RequestHandler): + + @decorator.oauth_required + def get(self): + http = decorator.http() + # http is authorized with the user's Credentials and can be used + # in API calls + + """ + + def set_credentials(self, credentials): + self._tls.credentials = credentials + + def get_credentials(self): + """A thread local Credentials object. + + Returns: + A client.Credentials object, or None if credentials hasn't been set in + this thread yet, which may happen when calling has_credentials inside + oauth_aware. + """ + return getattr(self._tls, 'credentials', None) + + credentials = property(get_credentials, set_credentials) + + def set_flow(self, flow): + self._tls.flow = flow + + def get_flow(self): + """A thread local Flow object. + + Returns: + A credentials.Flow object, or None if the flow hasn't been set in this + thread yet, which happens in _create_flow() since Flows are created + lazily. + """ + return getattr(self._tls, 'flow', None) + + flow = property(get_flow, set_flow) + + + @util.positional(4) + def __init__(self, client_id, client_secret, scope, + auth_uri=GOOGLE_AUTH_URI, + token_uri=GOOGLE_TOKEN_URI, + revoke_uri=GOOGLE_REVOKE_URI, + user_agent=None, + message=None, + callback_path='/oauth2callback', + token_response_param=None, + _storage_class=StorageByKeyName, + _credentials_class=CredentialsModel, + _credentials_property_name='credentials', + **kwargs): + + """Constructor for OAuth2Decorator + + Args: + client_id: string, client identifier. + client_secret: string client secret. + scope: string or iterable of strings, scope(s) of the credentials being + requested. + auth_uri: string, URI for authorization endpoint. For convenience + defaults to Google's endpoints but any OAuth 2.0 provider can be used. + token_uri: string, URI for token endpoint. For convenience + defaults to Google's endpoints but any OAuth 2.0 provider can be used. + revoke_uri: string, URI for revoke endpoint. For convenience + defaults to Google's endpoints but any OAuth 2.0 provider can be used. + user_agent: string, User agent of your application, default to None. + message: Message to display if there are problems with the OAuth 2.0 + configuration. The message may contain HTML and will be presented on the + web interface for any method that uses the decorator. + callback_path: string, The absolute path to use as the callback URI. Note + that this must match up with the URI given when registering the + application in the APIs Console. + token_response_param: string. If provided, the full JSON response + to the access token request will be encoded and included in this query + parameter in the callback URI. This is useful with providers (e.g. + wordpress.com) that include extra fields that the client may want. + _storage_class: "Protected" keyword argument not typically provided to + this constructor. A storage class to aid in storing a Credentials object + for a user in the datastore. Defaults to StorageByKeyName. + _credentials_class: "Protected" keyword argument not typically provided to + this constructor. A db or ndb Model class to hold credentials. Defaults + to CredentialsModel. + _credentials_property_name: "Protected" keyword argument not typically + provided to this constructor. A string indicating the name of the field + on the _credentials_class where a Credentials object will be stored. + Defaults to 'credentials'. + **kwargs: dict, Keyword arguments are be passed along as kwargs to the + OAuth2WebServerFlow constructor. + """ + self._tls = threading.local() + self.flow = None + self.credentials = None + self._client_id = client_id + self._client_secret = client_secret + self._scope = util.scopes_to_string(scope) + self._auth_uri = auth_uri + self._token_uri = token_uri + self._revoke_uri = revoke_uri + self._user_agent = user_agent + self._kwargs = kwargs + self._message = message + self._in_error = False + self._callback_path = callback_path + self._token_response_param = token_response_param + self._storage_class = _storage_class + self._credentials_class = _credentials_class + self._credentials_property_name = _credentials_property_name + + def _display_error_message(self, request_handler): + request_handler.response.out.write('') + request_handler.response.out.write(_safe_html(self._message)) + request_handler.response.out.write('') + + def oauth_required(self, method): + """Decorator that starts the OAuth 2.0 dance. + + Starts the OAuth dance for the logged in user if they haven't already + granted access for this application. + + Args: + method: callable, to be decorated method of a webapp.RequestHandler + instance. + """ + + def check_oauth(request_handler, *args, **kwargs): + if self._in_error: + self._display_error_message(request_handler) + return + + user = users.get_current_user() + # Don't use @login_decorator as this could be used in a POST request. + if not user: + request_handler.redirect(users.create_login_url( + request_handler.request.uri)) + return + + self._create_flow(request_handler) + + # Store the request URI in 'state' so we can use it later + self.flow.params['state'] = _build_state_value(request_handler, user) + self.credentials = self._storage_class( + self._credentials_class, None, + self._credentials_property_name, user=user).get() + + if not self.has_credentials(): + return request_handler.redirect(self.authorize_url()) + try: + resp = method(request_handler, *args, **kwargs) + except AccessTokenRefreshError: + return request_handler.redirect(self.authorize_url()) + finally: + self.credentials = None + return resp + + return check_oauth + + def _create_flow(self, request_handler): + """Create the Flow object. + + The Flow is calculated lazily since we don't know where this app is + running until it receives a request, at which point redirect_uri can be + calculated and then the Flow object can be constructed. + + Args: + request_handler: webapp.RequestHandler, the request handler. + """ + if self.flow is None: + redirect_uri = request_handler.request.relative_url( + self._callback_path) # Usually /oauth2callback + self.flow = OAuth2WebServerFlow(self._client_id, self._client_secret, + self._scope, redirect_uri=redirect_uri, + user_agent=self._user_agent, + auth_uri=self._auth_uri, + token_uri=self._token_uri, + revoke_uri=self._revoke_uri, + **self._kwargs) + + def oauth_aware(self, method): + """Decorator that sets up for OAuth 2.0 dance, but doesn't do it. + + Does all the setup for the OAuth dance, but doesn't initiate it. + This decorator is useful if you want to create a page that knows + whether or not the user has granted access to this application. + From within a method decorated with @oauth_aware the has_credentials() + and authorize_url() methods can be called. + + Args: + method: callable, to be decorated method of a webapp.RequestHandler + instance. + """ + + def setup_oauth(request_handler, *args, **kwargs): + if self._in_error: + self._display_error_message(request_handler) + return + + user = users.get_current_user() + # Don't use @login_decorator as this could be used in a POST request. + if not user: + request_handler.redirect(users.create_login_url( + request_handler.request.uri)) + return + + self._create_flow(request_handler) + + self.flow.params['state'] = _build_state_value(request_handler, user) + self.credentials = self._storage_class( + self._credentials_class, None, + self._credentials_property_name, user=user).get() + try: + resp = method(request_handler, *args, **kwargs) + finally: + self.credentials = None + return resp + return setup_oauth + + + def has_credentials(self): + """True if for the logged in user there are valid access Credentials. + + Must only be called from with a webapp.RequestHandler subclassed method + that had been decorated with either @oauth_required or @oauth_aware. + """ + return self.credentials is not None and not self.credentials.invalid + + def authorize_url(self): + """Returns the URL to start the OAuth dance. + + Must only be called from with a webapp.RequestHandler subclassed method + that had been decorated with either @oauth_required or @oauth_aware. + """ + url = self.flow.step1_get_authorize_url() + return str(url) + + def http(self, *args, **kwargs): + """Returns an authorized http instance. + + Must only be called from within an @oauth_required decorated method, or + from within an @oauth_aware decorated method where has_credentials() + returns True. + + Args: + args: Positional arguments passed to httplib2.Http constructor. + kwargs: Positional arguments passed to httplib2.Http constructor. + """ + return self.credentials.authorize(httplib2.Http(*args, **kwargs)) + + @property + def callback_path(self): + """The absolute path where the callback will occur. + + Note this is the absolute path, not the absolute URI, that will be + calculated by the decorator at runtime. See callback_handler() for how this + should be used. + + Returns: + The callback path as a string. + """ + return self._callback_path + + + def callback_handler(self): + """RequestHandler for the OAuth 2.0 redirect callback. + + Usage: + app = webapp.WSGIApplication([ + ('/index', MyIndexHandler), + ..., + (decorator.callback_path, decorator.callback_handler()) + ]) + + Returns: + A webapp.RequestHandler that handles the redirect back from the + server during the OAuth 2.0 dance. + """ + decorator = self + + class OAuth2Handler(webapp.RequestHandler): + """Handler for the redirect_uri of the OAuth 2.0 dance.""" + + @login_required + def get(self): + error = self.request.get('error') + if error: + errormsg = self.request.get('error_description', error) + self.response.out.write( + 'The authorization request failed: %s' % _safe_html(errormsg)) + else: + user = users.get_current_user() + decorator._create_flow(self) + credentials = decorator.flow.step2_exchange(self.request.params) + decorator._storage_class( + decorator._credentials_class, None, + decorator._credentials_property_name, user=user).put(credentials) + redirect_uri = _parse_state_value(str(self.request.get('state')), + user) + + if decorator._token_response_param and credentials.token_response: + resp_json = simplejson.dumps(credentials.token_response) + redirect_uri = util._add_query_parameter( + redirect_uri, decorator._token_response_param, resp_json) + + self.redirect(redirect_uri) + + return OAuth2Handler + + def callback_application(self): + """WSGI application for handling the OAuth 2.0 redirect callback. + + If you need finer grained control use `callback_handler` which returns just + the webapp.RequestHandler. + + Returns: + A webapp.WSGIApplication that handles the redirect back from the + server during the OAuth 2.0 dance. + """ + return webapp.WSGIApplication([ + (self.callback_path, self.callback_handler()) + ]) + + +class OAuth2DecoratorFromClientSecrets(OAuth2Decorator): + """An OAuth2Decorator that builds from a clientsecrets file. + + Uses a clientsecrets file as the source for all the information when + constructing an OAuth2Decorator. + + Example: + + decorator = OAuth2DecoratorFromClientSecrets( + os.path.join(os.path.dirname(__file__), 'client_secrets.json') + scope='https://www.googleapis.com/auth/plus') + + + class MainHandler(webapp.RequestHandler): + + @decorator.oauth_required + def get(self): + http = decorator.http() + # http is authorized with the user's Credentials and can be used + # in API calls + """ + + @util.positional(3) + def __init__(self, filename, scope, message=None, cache=None): + """Constructor + + Args: + filename: string, File name of client secrets. + scope: string or iterable of strings, scope(s) of the credentials being + requested. + message: string, A friendly string to display to the user if the + clientsecrets file is missing or invalid. The message may contain HTML + and will be presented on the web interface for any method that uses the + decorator. + cache: An optional cache service client that implements get() and set() + methods. See clientsecrets.loadfile() for details. + """ + client_type, client_info = clientsecrets.loadfile(filename, cache=cache) + if client_type not in [ + clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]: + raise InvalidClientSecretsError( + 'OAuth2Decorator doesn\'t support this OAuth 2.0 flow.') + constructor_kwargs = { + 'auth_uri': client_info['auth_uri'], + 'token_uri': client_info['token_uri'], + 'message': message, + } + revoke_uri = client_info.get('revoke_uri') + if revoke_uri is not None: + constructor_kwargs['revoke_uri'] = revoke_uri + super(OAuth2DecoratorFromClientSecrets, self).__init__( + client_info['client_id'], client_info['client_secret'], + scope, **constructor_kwargs) + if message is not None: + self._message = message + else: + self._message = 'Please configure your application for OAuth 2.0.' + + +@util.positional(2) +def oauth2decorator_from_clientsecrets(filename, scope, + message=None, cache=None): + """Creates an OAuth2Decorator populated from a clientsecrets file. + + Args: + filename: string, File name of client secrets. + scope: string or list of strings, scope(s) of the credentials being + requested. + message: string, A friendly string to display to the user if the + clientsecrets file is missing or invalid. The message may contain HTML and + will be presented on the web interface for any method that uses the + decorator. + cache: An optional cache service client that implements get() and set() + methods. See clientsecrets.loadfile() for details. + + Returns: An OAuth2Decorator + + """ + return OAuth2DecoratorFromClientSecrets(filename, scope, + message=message, cache=cache) diff --git a/appengine/dashdemo/oauth2client/client.py b/appengine/dashdemo/oauth2client/client.py new file mode 100644 index 0000000..99873e2 --- /dev/null +++ b/appengine/dashdemo/oauth2client/client.py @@ -0,0 +1,1364 @@ +# Copyright (C) 2010 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""An OAuth 2.0 client. + +Tools for interacting with OAuth 2.0 protected resources. +""" + +__author__ = 'jcgregorio@google.com (Joe Gregorio)' + +import base64 +import clientsecrets +import copy +import datetime +import httplib2 +import logging +import os +import sys +import time +import urllib +import urlparse + +from oauth2client import GOOGLE_AUTH_URI +from oauth2client import GOOGLE_REVOKE_URI +from oauth2client import GOOGLE_TOKEN_URI +from oauth2client import util +from oauth2client.anyjson import simplejson + +HAS_OPENSSL = False +HAS_CRYPTO = False +try: + from oauth2client import crypt + HAS_CRYPTO = True + if crypt.OpenSSLVerifier is not None: + HAS_OPENSSL = True +except ImportError: + pass + +try: + from urlparse import parse_qsl +except ImportError: + from cgi import parse_qsl + +logger = logging.getLogger(__name__) + +# Expiry is stored in RFC3339 UTC format +EXPIRY_FORMAT = '%Y-%m-%dT%H:%M:%SZ' + +# Which certs to use to validate id_tokens received. +ID_TOKEN_VERIFICATON_CERTS = 'https://www.googleapis.com/oauth2/v1/certs' + +# Constant to use for the out of band OAuth 2.0 flow. +OOB_CALLBACK_URN = 'urn:ietf:wg:oauth:2.0:oob' + +# Google Data client libraries may need to set this to [401, 403]. +REFRESH_STATUS_CODES = [401] + + +class Error(Exception): + """Base error for this module.""" + + +class FlowExchangeError(Error): + """Error trying to exchange an authorization grant for an access token.""" + + +class AccessTokenRefreshError(Error): + """Error trying to refresh an expired access token.""" + + +class TokenRevokeError(Error): + """Error trying to revoke a token.""" + + +class UnknownClientSecretsFlowError(Error): + """The client secrets file called for an unknown type of OAuth 2.0 flow. """ + + +class AccessTokenCredentialsError(Error): + """Having only the access_token means no refresh is possible.""" + + +class VerifyJwtTokenError(Error): + """Could on retrieve certificates for validation.""" + + +class NonAsciiHeaderError(Error): + """Header names and values must be ASCII strings.""" + + +def _abstract(): + raise NotImplementedError('You need to override this function') + + +class MemoryCache(object): + """httplib2 Cache implementation which only caches locally.""" + + def __init__(self): + self.cache = {} + + def get(self, key): + return self.cache.get(key) + + def set(self, key, value): + self.cache[key] = value + + def delete(self, key): + self.cache.pop(key, None) + + +class Credentials(object): + """Base class for all Credentials objects. + + Subclasses must define an authorize() method that applies the credentials to + an HTTP transport. + + Subclasses must also specify a classmethod named 'from_json' that takes a JSON + string as input and returns an instaniated Credentials object. + """ + + NON_SERIALIZED_MEMBERS = ['store'] + + def authorize(self, http): + """Take an httplib2.Http instance (or equivalent) and authorizes it. + + Authorizes it for the set of credentials, usually by replacing + http.request() with a method that adds in the appropriate headers and then + delegates to the original Http.request() method. + + Args: + http: httplib2.Http, an http object to be used to make the refresh + request. + """ + _abstract() + + def refresh(self, http): + """Forces a refresh of the access_token. + + Args: + http: httplib2.Http, an http object to be used to make the refresh + request. + """ + _abstract() + + def revoke(self, http): + """Revokes a refresh_token and makes the credentials void. + + Args: + http: httplib2.Http, an http object to be used to make the revoke + request. + """ + _abstract() + + def apply(self, headers): + """Add the authorization to the headers. + + Args: + headers: dict, the headers to add the Authorization header to. + """ + _abstract() + + def _to_json(self, strip): + """Utility function that creates JSON repr. of a Credentials object. + + Args: + strip: array, An array of names of members to not include in the JSON. + + Returns: + string, a JSON representation of this instance, suitable to pass to + from_json(). + """ + t = type(self) + d = copy.copy(self.__dict__) + for member in strip: + if member in d: + del d[member] + if 'token_expiry' in d and isinstance(d['token_expiry'], datetime.datetime): + d['token_expiry'] = d['token_expiry'].strftime(EXPIRY_FORMAT) + # Add in information we will need later to reconsistitue this instance. + d['_class'] = t.__name__ + d['_module'] = t.__module__ + return simplejson.dumps(d) + + def to_json(self): + """Creating a JSON representation of an instance of Credentials. + + Returns: + string, a JSON representation of this instance, suitable to pass to + from_json(). + """ + return self._to_json(Credentials.NON_SERIALIZED_MEMBERS) + + @classmethod + def new_from_json(cls, s): + """Utility class method to instantiate a Credentials subclass from a JSON + representation produced by to_json(). + + Args: + s: string, JSON from to_json(). + + Returns: + An instance of the subclass of Credentials that was serialized with + to_json(). + """ + data = simplejson.loads(s) + # Find and call the right classmethod from_json() to restore the object. + module = data['_module'] + try: + m = __import__(module) + except ImportError: + # In case there's an object from the old package structure, update it + module = module.replace('.googleapiclient', '') + m = __import__(module) + + m = __import__(module, fromlist=module.split('.')[:-1]) + kls = getattr(m, data['_class']) + from_json = getattr(kls, 'from_json') + return from_json(s) + + @classmethod + def from_json(cls, s): + """Instantiate a Credentials object from a JSON description of it. + + The JSON should have been produced by calling .to_json() on the object. + + Args: + data: dict, A deserialized JSON object. + + Returns: + An instance of a Credentials subclass. + """ + return Credentials() + + +class Flow(object): + """Base class for all Flow objects.""" + pass + + +class Storage(object): + """Base class for all Storage objects. + + Store and retrieve a single credential. This class supports locking + such that multiple processes and threads can operate on a single + store. + """ + + def acquire_lock(self): + """Acquires any lock necessary to access this Storage. + + This lock is not reentrant. + """ + pass + + def release_lock(self): + """Release the Storage lock. + + Trying to release a lock that isn't held will result in a + RuntimeError. + """ + pass + + def locked_get(self): + """Retrieve credential. + + The Storage lock must be held when this is called. + + Returns: + oauth2client.client.Credentials + """ + _abstract() + + def locked_put(self, credentials): + """Write a credential. + + The Storage lock must be held when this is called. + + Args: + credentials: Credentials, the credentials to store. + """ + _abstract() + + def locked_delete(self): + """Delete a credential. + + The Storage lock must be held when this is called. + """ + _abstract() + + def get(self): + """Retrieve credential. + + The Storage lock must *not* be held when this is called. + + Returns: + oauth2client.client.Credentials + """ + self.acquire_lock() + try: + return self.locked_get() + finally: + self.release_lock() + + def put(self, credentials): + """Write a credential. + + The Storage lock must be held when this is called. + + Args: + credentials: Credentials, the credentials to store. + """ + self.acquire_lock() + try: + self.locked_put(credentials) + finally: + self.release_lock() + + def delete(self): + """Delete credential. + + Frees any resources associated with storing the credential. + The Storage lock must *not* be held when this is called. + + Returns: + None + """ + self.acquire_lock() + try: + return self.locked_delete() + finally: + self.release_lock() + + +def clean_headers(headers): + """Forces header keys and values to be strings, i.e not unicode. + + The httplib module just concats the header keys and values in a way that may + make the message header a unicode string, which, if it then tries to + contatenate to a binary request body may result in a unicode decode error. + + Args: + headers: dict, A dictionary of headers. + + Returns: + The same dictionary but with all the keys converted to strings. + """ + clean = {} + try: + for k, v in headers.iteritems(): + clean[str(k)] = str(v) + except UnicodeEncodeError: + raise NonAsciiHeaderError(k + ': ' + v) + return clean + + +def _update_query_params(uri, params): + """Updates a URI with new query parameters. + + Args: + uri: string, A valid URI, with potential existing query parameters. + params: dict, A dictionary of query parameters. + + Returns: + The same URI but with the new query parameters added. + """ + parts = list(urlparse.urlparse(uri)) + query_params = dict(parse_qsl(parts[4])) # 4 is the index of the query part + query_params.update(params) + parts[4] = urllib.urlencode(query_params) + return urlparse.urlunparse(parts) + + +class OAuth2Credentials(Credentials): + """Credentials object for OAuth 2.0. + + Credentials can be applied to an httplib2.Http object using the authorize() + method, which then adds the OAuth 2.0 access token to each request. + + OAuth2Credentials objects may be safely pickled and unpickled. + """ + + @util.positional(8) + def __init__(self, access_token, client_id, client_secret, refresh_token, + token_expiry, token_uri, user_agent, revoke_uri=None, + id_token=None, token_response=None): + """Create an instance of OAuth2Credentials. + + This constructor is not usually called by the user, instead + OAuth2Credentials objects are instantiated by the OAuth2WebServerFlow. + + Args: + access_token: string, access token. + client_id: string, client identifier. + client_secret: string, client secret. + refresh_token: string, refresh token. + token_expiry: datetime, when the access_token expires. + token_uri: string, URI of token endpoint. + user_agent: string, The HTTP User-Agent to provide for this application. + revoke_uri: string, URI for revoke endpoint. Defaults to None; a token + can't be revoked if this is None. + id_token: object, The identity of the resource owner. + token_response: dict, the decoded response to the token request. None + if a token hasn't been requested yet. Stored because some providers + (e.g. wordpress.com) include extra fields that clients may want. + + Notes: + store: callable, A callable that when passed a Credential + will store the credential back to where it came from. + This is needed to store the latest access_token if it + has expired and been refreshed. + """ + self.access_token = access_token + self.client_id = client_id + self.client_secret = client_secret + self.refresh_token = refresh_token + self.store = None + self.token_expiry = token_expiry + self.token_uri = token_uri + self.user_agent = user_agent + self.revoke_uri = revoke_uri + self.id_token = id_token + self.token_response = token_response + + # True if the credentials have been revoked or expired and can't be + # refreshed. + self.invalid = False + + def authorize(self, http): + """Authorize an httplib2.Http instance with these credentials. + + The modified http.request method will add authentication headers to each + request and will refresh access_tokens when a 401 is received on a + request. In addition the http.request method has a credentials property, + http.request.credentials, which is the Credentials object that authorized + it. + + Args: + http: An instance of httplib2.Http + or something that acts like it. + + Returns: + A modified instance of http that was passed in. + + Example: + + h = httplib2.Http() + h = credentials.authorize(h) + + You can't create a new OAuth subclass of httplib2.Authenication + because it never gets passed the absolute URI, which is needed for + signing. So instead we have to overload 'request' with a closure + that adds in the Authorization header and then calls the original + version of 'request()'. + """ + request_orig = http.request + + # The closure that will replace 'httplib2.Http.request'. + @util.positional(1) + def new_request(uri, method='GET', body=None, headers=None, + redirections=httplib2.DEFAULT_MAX_REDIRECTS, + connection_type=None): + if not self.access_token: + logger.info('Attempting refresh to obtain initial access_token') + self._refresh(request_orig) + + # Modify the request headers to add the appropriate + # Authorization header. + if headers is None: + headers = {} + self.apply(headers) + + if self.user_agent is not None: + if 'user-agent' in headers: + headers['user-agent'] = self.user_agent + ' ' + headers['user-agent'] + else: + headers['user-agent'] = self.user_agent + + resp, content = request_orig(uri, method, body, clean_headers(headers), + redirections, connection_type) + + if resp.status in REFRESH_STATUS_CODES: + logger.info('Refreshing due to a %s' % str(resp.status)) + self._refresh(request_orig) + self.apply(headers) + return request_orig(uri, method, body, clean_headers(headers), + redirections, connection_type) + else: + return (resp, content) + + # Replace the request method with our own closure. + http.request = new_request + + # Set credentials as a property of the request method. + setattr(http.request, 'credentials', self) + + return http + + def refresh(self, http): + """Forces a refresh of the access_token. + + Args: + http: httplib2.Http, an http object to be used to make the refresh + request. + """ + self._refresh(http.request) + + def revoke(self, http): + """Revokes a refresh_token and makes the credentials void. + + Args: + http: httplib2.Http, an http object to be used to make the revoke + request. + """ + self._revoke(http.request) + + def apply(self, headers): + """Add the authorization to the headers. + + Args: + headers: dict, the headers to add the Authorization header to. + """ + headers['Authorization'] = 'Bearer ' + self.access_token + + def to_json(self): + return self._to_json(Credentials.NON_SERIALIZED_MEMBERS) + + @classmethod + def from_json(cls, s): + """Instantiate a Credentials object from a JSON description of it. The JSON + should have been produced by calling .to_json() on the object. + + Args: + data: dict, A deserialized JSON object. + + Returns: + An instance of a Credentials subclass. + """ + data = simplejson.loads(s) + if 'token_expiry' in data and not isinstance(data['token_expiry'], + datetime.datetime): + try: + data['token_expiry'] = datetime.datetime.strptime( + data['token_expiry'], EXPIRY_FORMAT) + except: + data['token_expiry'] = None + retval = cls( + data['access_token'], + data['client_id'], + data['client_secret'], + data['refresh_token'], + data['token_expiry'], + data['token_uri'], + data['user_agent'], + revoke_uri=data.get('revoke_uri', None), + id_token=data.get('id_token', None), + token_response=data.get('token_response', None)) + retval.invalid = data['invalid'] + return retval + + @property + def access_token_expired(self): + """True if the credential is expired or invalid. + + If the token_expiry isn't set, we assume the token doesn't expire. + """ + if self.invalid: + return True + + if not self.token_expiry: + return False + + now = datetime.datetime.utcnow() + if now >= self.token_expiry: + logger.info('access_token is expired. Now: %s, token_expiry: %s', + now, self.token_expiry) + return True + return False + + def set_store(self, store): + """Set the Storage for the credential. + + Args: + store: Storage, an implementation of Stroage object. + This is needed to store the latest access_token if it + has expired and been refreshed. This implementation uses + locking to check for updates before updating the + access_token. + """ + self.store = store + + def _updateFromCredential(self, other): + """Update this Credential from another instance.""" + self.__dict__.update(other.__getstate__()) + + def __getstate__(self): + """Trim the state down to something that can be pickled.""" + d = copy.copy(self.__dict__) + del d['store'] + return d + + def __setstate__(self, state): + """Reconstitute the state of the object from being pickled.""" + self.__dict__.update(state) + self.store = None + + def _generate_refresh_request_body(self): + """Generate the body that will be used in the refresh request.""" + body = urllib.urlencode({ + 'grant_type': 'refresh_token', + 'client_id': self.client_id, + 'client_secret': self.client_secret, + 'refresh_token': self.refresh_token, + }) + return body + + def _generate_refresh_request_headers(self): + """Generate the headers that will be used in the refresh request.""" + headers = { + 'content-type': 'application/x-www-form-urlencoded', + } + + if self.user_agent is not None: + headers['user-agent'] = self.user_agent + + return headers + + def _refresh(self, http_request): + """Refreshes the access_token. + + This method first checks by reading the Storage object if available. + If a refresh is still needed, it holds the Storage lock until the + refresh is completed. + + Args: + http_request: callable, a callable that matches the method signature of + httplib2.Http.request, used to make the refresh request. + + Raises: + AccessTokenRefreshError: When the refresh fails. + """ + if not self.store: + self._do_refresh_request(http_request) + else: + self.store.acquire_lock() + try: + new_cred = self.store.locked_get() + if (new_cred and not new_cred.invalid and + new_cred.access_token != self.access_token): + logger.info('Updated access_token read from Storage') + self._updateFromCredential(new_cred) + else: + self._do_refresh_request(http_request) + finally: + self.store.release_lock() + + def _do_refresh_request(self, http_request): + """Refresh the access_token using the refresh_token. + + Args: + http_request: callable, a callable that matches the method signature of + httplib2.Http.request, used to make the refresh request. + + Raises: + AccessTokenRefreshError: When the refresh fails. + """ + body = self._generate_refresh_request_body() + headers = self._generate_refresh_request_headers() + + logger.info('Refreshing access_token') + resp, content = http_request( + self.token_uri, method='POST', body=body, headers=headers) + if resp.status == 200: + # TODO(jcgregorio) Raise an error if loads fails? + d = simplejson.loads(content) + self.token_response = d + self.access_token = d['access_token'] + self.refresh_token = d.get('refresh_token', self.refresh_token) + if 'expires_in' in d: + self.token_expiry = datetime.timedelta( + seconds=int(d['expires_in'])) + datetime.datetime.utcnow() + else: + self.token_expiry = None + if self.store: + self.store.locked_put(self) + else: + # An {'error':...} response body means the token is expired or revoked, + # so we flag the credentials as such. + logger.info('Failed to retrieve access token: %s' % content) + error_msg = 'Invalid response %s.' % resp['status'] + try: + d = simplejson.loads(content) + if 'error' in d: + error_msg = d['error'] + self.invalid = True + if self.store: + self.store.locked_put(self) + except StandardError: + pass + raise AccessTokenRefreshError(error_msg) + + def _revoke(self, http_request): + """Revokes the refresh_token and deletes the store if available. + + Args: + http_request: callable, a callable that matches the method signature of + httplib2.Http.request, used to make the revoke request. + """ + self._do_revoke(http_request, self.refresh_token) + + def _do_revoke(self, http_request, token): + """Revokes the credentials and deletes the store if available. + + Args: + http_request: callable, a callable that matches the method signature of + httplib2.Http.request, used to make the refresh request. + token: A string used as the token to be revoked. Can be either an + access_token or refresh_token. + + Raises: + TokenRevokeError: If the revoke request does not return with a 200 OK. + """ + logger.info('Revoking token') + query_params = {'token': token} + token_revoke_uri = _update_query_params(self.revoke_uri, query_params) + resp, content = http_request(token_revoke_uri) + if resp.status == 200: + self.invalid = True + else: + error_msg = 'Invalid response %s.' % resp.status + try: + d = simplejson.loads(content) + if 'error' in d: + error_msg = d['error'] + except StandardError: + pass + raise TokenRevokeError(error_msg) + + if self.store: + self.store.delete() + + +class AccessTokenCredentials(OAuth2Credentials): + """Credentials object for OAuth 2.0. + + Credentials can be applied to an httplib2.Http object using the + authorize() method, which then signs each request from that object + with the OAuth 2.0 access token. This set of credentials is for the + use case where you have acquired an OAuth 2.0 access_token from + another place such as a JavaScript client or another web + application, and wish to use it from Python. Because only the + access_token is present it can not be refreshed and will in time + expire. + + AccessTokenCredentials objects may be safely pickled and unpickled. + + Usage: + credentials = AccessTokenCredentials('', + 'my-user-agent/1.0') + http = httplib2.Http() + http = credentials.authorize(http) + + Exceptions: + AccessTokenCredentialsExpired: raised when the access_token expires or is + revoked. + """ + + def __init__(self, access_token, user_agent, revoke_uri=None): + """Create an instance of OAuth2Credentials + + This is one of the few types if Credentials that you should contrust, + Credentials objects are usually instantiated by a Flow. + + Args: + access_token: string, access token. + user_agent: string, The HTTP User-Agent to provide for this application. + revoke_uri: string, URI for revoke endpoint. Defaults to None; a token + can't be revoked if this is None. + """ + super(AccessTokenCredentials, self).__init__( + access_token, + None, + None, + None, + None, + None, + user_agent, + revoke_uri=revoke_uri) + + + @classmethod + def from_json(cls, s): + data = simplejson.loads(s) + retval = AccessTokenCredentials( + data['access_token'], + data['user_agent']) + return retval + + def _refresh(self, http_request): + raise AccessTokenCredentialsError( + 'The access_token is expired or invalid and can\'t be refreshed.') + + def _revoke(self, http_request): + """Revokes the access_token and deletes the store if available. + + Args: + http_request: callable, a callable that matches the method signature of + httplib2.Http.request, used to make the revoke request. + """ + self._do_revoke(http_request, self.access_token) + + +class AssertionCredentials(OAuth2Credentials): + """Abstract Credentials object used for OAuth 2.0 assertion grants. + + This credential does not require a flow to instantiate because it + represents a two legged flow, and therefore has all of the required + information to generate and refresh its own access tokens. It must + be subclassed to generate the appropriate assertion string. + + AssertionCredentials objects may be safely pickled and unpickled. + """ + + @util.positional(2) + def __init__(self, assertion_type, user_agent=None, + token_uri=GOOGLE_TOKEN_URI, + revoke_uri=GOOGLE_REVOKE_URI, + **unused_kwargs): + """Constructor for AssertionFlowCredentials. + + Args: + assertion_type: string, assertion type that will be declared to the auth + server + user_agent: string, The HTTP User-Agent to provide for this application. + token_uri: string, URI for token endpoint. For convenience + defaults to Google's endpoints but any OAuth 2.0 provider can be used. + revoke_uri: string, URI for revoke endpoint. + """ + super(AssertionCredentials, self).__init__( + None, + None, + None, + None, + None, + token_uri, + user_agent, + revoke_uri=revoke_uri) + self.assertion_type = assertion_type + + def _generate_refresh_request_body(self): + assertion = self._generate_assertion() + + body = urllib.urlencode({ + 'assertion': assertion, + 'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer', + }) + + return body + + def _generate_assertion(self): + """Generate the assertion string that will be used in the access token + request. + """ + _abstract() + + def _revoke(self, http_request): + """Revokes the access_token and deletes the store if available. + + Args: + http_request: callable, a callable that matches the method signature of + httplib2.Http.request, used to make the revoke request. + """ + self._do_revoke(http_request, self.access_token) + + +if HAS_CRYPTO: + # PyOpenSSL and PyCrypto are not prerequisites for oauth2client, so if it is + # missing then don't create the SignedJwtAssertionCredentials or the + # verify_id_token() method. + + class SignedJwtAssertionCredentials(AssertionCredentials): + """Credentials object used for OAuth 2.0 Signed JWT assertion grants. + + This credential does not require a flow to instantiate because it represents + a two legged flow, and therefore has all of the required information to + generate and refresh its own access tokens. + + SignedJwtAssertionCredentials requires either PyOpenSSL, or PyCrypto 2.6 or + later. For App Engine you may also consider using AppAssertionCredentials. + """ + + MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds + + @util.positional(4) + def __init__(self, + service_account_name, + private_key, + scope, + private_key_password='notasecret', + user_agent=None, + token_uri=GOOGLE_TOKEN_URI, + revoke_uri=GOOGLE_REVOKE_URI, + **kwargs): + """Constructor for SignedJwtAssertionCredentials. + + Args: + service_account_name: string, id for account, usually an email address. + private_key: string, private key in PKCS12 or PEM format. + scope: string or iterable of strings, scope(s) of the credentials being + requested. + private_key_password: string, password for private_key, unused if + private_key is in PEM format. + user_agent: string, HTTP User-Agent to provide for this application. + token_uri: string, URI for token endpoint. For convenience + defaults to Google's endpoints but any OAuth 2.0 provider can be used. + revoke_uri: string, URI for revoke endpoint. + kwargs: kwargs, Additional parameters to add to the JWT token, for + example sub=joe@xample.org.""" + + super(SignedJwtAssertionCredentials, self).__init__( + None, + user_agent=user_agent, + token_uri=token_uri, + revoke_uri=revoke_uri, + ) + + self.scope = util.scopes_to_string(scope) + + # Keep base64 encoded so it can be stored in JSON. + self.private_key = base64.b64encode(private_key) + + self.private_key_password = private_key_password + self.service_account_name = service_account_name + self.kwargs = kwargs + + @classmethod + def from_json(cls, s): + data = simplejson.loads(s) + retval = SignedJwtAssertionCredentials( + data['service_account_name'], + base64.b64decode(data['private_key']), + data['scope'], + private_key_password=data['private_key_password'], + user_agent=data['user_agent'], + token_uri=data['token_uri'], + **data['kwargs'] + ) + retval.invalid = data['invalid'] + retval.access_token = data['access_token'] + return retval + + def _generate_assertion(self): + """Generate the assertion that will be used in the request.""" + now = long(time.time()) + payload = { + 'aud': self.token_uri, + 'scope': self.scope, + 'iat': now, + 'exp': now + SignedJwtAssertionCredentials.MAX_TOKEN_LIFETIME_SECS, + 'iss': self.service_account_name + } + payload.update(self.kwargs) + logger.debug(str(payload)) + + private_key = base64.b64decode(self.private_key) + return crypt.make_signed_jwt(crypt.Signer.from_string( + private_key, self.private_key_password), payload) + + # Only used in verify_id_token(), which is always calling to the same URI + # for the certs. + _cached_http = httplib2.Http(MemoryCache()) + + @util.positional(2) + def verify_id_token(id_token, audience, http=None, + cert_uri=ID_TOKEN_VERIFICATON_CERTS): + """Verifies a signed JWT id_token. + + This function requires PyOpenSSL and because of that it does not work on + App Engine. + + Args: + id_token: string, A Signed JWT. + audience: string, The audience 'aud' that the token should be for. + http: httplib2.Http, instance to use to make the HTTP request. Callers + should supply an instance that has caching enabled. + cert_uri: string, URI of the certificates in JSON format to + verify the JWT against. + + Returns: + The deserialized JSON in the JWT. + + Raises: + oauth2client.crypt.AppIdentityError if the JWT fails to verify. + """ + if http is None: + http = _cached_http + + resp, content = http.request(cert_uri) + + if resp.status == 200: + certs = simplejson.loads(content) + return crypt.verify_signed_jwt_with_certs(id_token, certs, audience) + else: + raise VerifyJwtTokenError('Status code: %d' % resp.status) + + +def _urlsafe_b64decode(b64string): + # Guard against unicode strings, which base64 can't handle. + b64string = b64string.encode('ascii') + padded = b64string + '=' * (4 - len(b64string) % 4) + return base64.urlsafe_b64decode(padded) + + +def _extract_id_token(id_token): + """Extract the JSON payload from a JWT. + + Does the extraction w/o checking the signature. + + Args: + id_token: string, OAuth 2.0 id_token. + + Returns: + object, The deserialized JSON payload. + """ + segments = id_token.split('.') + + if (len(segments) != 3): + raise VerifyJwtTokenError( + 'Wrong number of segments in token: %s' % id_token) + + return simplejson.loads(_urlsafe_b64decode(segments[1])) + + +def _parse_exchange_token_response(content): + """Parses response of an exchange token request. + + Most providers return JSON but some (e.g. Facebook) return a + url-encoded string. + + Args: + content: The body of a response + + Returns: + Content as a dictionary object. Note that the dict could be empty, + i.e. {}. That basically indicates a failure. + """ + resp = {} + try: + resp = simplejson.loads(content) + except StandardError: + # different JSON libs raise different exceptions, + # so we just do a catch-all here + resp = dict(parse_qsl(content)) + + # some providers respond with 'expires', others with 'expires_in' + if resp and 'expires' in resp: + resp['expires_in'] = resp.pop('expires') + + return resp + + +@util.positional(4) +def credentials_from_code(client_id, client_secret, scope, code, + redirect_uri='postmessage', http=None, + user_agent=None, token_uri=GOOGLE_TOKEN_URI, + auth_uri=GOOGLE_AUTH_URI, + revoke_uri=GOOGLE_REVOKE_URI): + """Exchanges an authorization code for an OAuth2Credentials object. + + Args: + client_id: string, client identifier. + client_secret: string, client secret. + scope: string or iterable of strings, scope(s) to request. + code: string, An authroization code, most likely passed down from + the client + redirect_uri: string, this is generally set to 'postmessage' to match the + redirect_uri that the client specified + http: httplib2.Http, optional http instance to use to do the fetch + token_uri: string, URI for token endpoint. For convenience + defaults to Google's endpoints but any OAuth 2.0 provider can be used. + auth_uri: string, URI for authorization endpoint. For convenience + defaults to Google's endpoints but any OAuth 2.0 provider can be used. + revoke_uri: string, URI for revoke endpoint. For convenience + defaults to Google's endpoints but any OAuth 2.0 provider can be used. + + Returns: + An OAuth2Credentials object. + + Raises: + FlowExchangeError if the authorization code cannot be exchanged for an + access token + """ + flow = OAuth2WebServerFlow(client_id, client_secret, scope, + redirect_uri=redirect_uri, user_agent=user_agent, + auth_uri=auth_uri, token_uri=token_uri, + revoke_uri=revoke_uri) + + credentials = flow.step2_exchange(code, http=http) + return credentials + + +@util.positional(3) +def credentials_from_clientsecrets_and_code(filename, scope, code, + message = None, + redirect_uri='postmessage', + http=None, + cache=None): + """Returns OAuth2Credentials from a clientsecrets file and an auth code. + + Will create the right kind of Flow based on the contents of the clientsecrets + file or will raise InvalidClientSecretsError for unknown types of Flows. + + Args: + filename: string, File name of clientsecrets. + scope: string or iterable of strings, scope(s) to request. + code: string, An authorization code, most likely passed down from + the client + message: string, A friendly string to display to the user if the + clientsecrets file is missing or invalid. If message is provided then + sys.exit will be called in the case of an error. If message in not + provided then clientsecrets.InvalidClientSecretsError will be raised. + redirect_uri: string, this is generally set to 'postmessage' to match the + redirect_uri that the client specified + http: httplib2.Http, optional http instance to use to do the fetch + cache: An optional cache service client that implements get() and set() + methods. See clientsecrets.loadfile() for details. + + Returns: + An OAuth2Credentials object. + + Raises: + FlowExchangeError if the authorization code cannot be exchanged for an + access token + UnknownClientSecretsFlowError if the file describes an unknown kind of Flow. + clientsecrets.InvalidClientSecretsError if the clientsecrets file is + invalid. + """ + flow = flow_from_clientsecrets(filename, scope, message=message, cache=cache, + redirect_uri=redirect_uri) + credentials = flow.step2_exchange(code, http=http) + return credentials + + +class OAuth2WebServerFlow(Flow): + """Does the Web Server Flow for OAuth 2.0. + + OAuth2WebServerFlow objects may be safely pickled and unpickled. + """ + + @util.positional(4) + def __init__(self, client_id, client_secret, scope, + redirect_uri=None, + user_agent=None, + auth_uri=GOOGLE_AUTH_URI, + token_uri=GOOGLE_TOKEN_URI, + revoke_uri=GOOGLE_REVOKE_URI, + **kwargs): + """Constructor for OAuth2WebServerFlow. + + The kwargs argument is used to set extra query parameters on the + auth_uri. For example, the access_type and approval_prompt + query parameters can be set via kwargs. + + Args: + client_id: string, client identifier. + client_secret: string client secret. + scope: string or iterable of strings, scope(s) of the credentials being + requested. + redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for + a non-web-based application, or a URI that handles the callback from + the authorization server. + user_agent: string, HTTP User-Agent to provide for this application. + auth_uri: string, URI for authorization endpoint. For convenience + defaults to Google's endpoints but any OAuth 2.0 provider can be used. + token_uri: string, URI for token endpoint. For convenience + defaults to Google's endpoints but any OAuth 2.0 provider can be used. + revoke_uri: string, URI for revoke endpoint. For convenience + defaults to Google's endpoints but any OAuth 2.0 provider can be used. + **kwargs: dict, The keyword arguments are all optional and required + parameters for the OAuth calls. + """ + self.client_id = client_id + self.client_secret = client_secret + self.scope = util.scopes_to_string(scope) + self.redirect_uri = redirect_uri + self.user_agent = user_agent + self.auth_uri = auth_uri + self.token_uri = token_uri + self.revoke_uri = revoke_uri + self.params = { + 'access_type': 'offline', + 'response_type': 'code', + } + self.params.update(kwargs) + + @util.positional(1) + def step1_get_authorize_url(self, redirect_uri=None): + """Returns a URI to redirect to the provider. + + Args: + redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for + a non-web-based application, or a URI that handles the callback from + the authorization server. This parameter is deprecated, please move to + passing the redirect_uri in via the constructor. + + Returns: + A URI as a string to redirect the user to begin the authorization flow. + """ + if redirect_uri is not None: + logger.warning(('The redirect_uri parameter for' + 'OAuth2WebServerFlow.step1_get_authorize_url is deprecated. Please' + 'move to passing the redirect_uri in via the constructor.')) + self.redirect_uri = redirect_uri + + if self.redirect_uri is None: + raise ValueError('The value of redirect_uri must not be None.') + + query_params = { + 'client_id': self.client_id, + 'redirect_uri': self.redirect_uri, + 'scope': self.scope, + } + query_params.update(self.params) + return _update_query_params(self.auth_uri, query_params) + + @util.positional(2) + def step2_exchange(self, code, http=None): + """Exhanges a code for OAuth2Credentials. + + Args: + code: string or dict, either the code as a string, or a dictionary + of the query parameters to the redirect_uri, which contains + the code. + http: httplib2.Http, optional http instance to use to do the fetch + + Returns: + An OAuth2Credentials object that can be used to authorize requests. + + Raises: + FlowExchangeError if a problem occured exchanging the code for a + refresh_token. + """ + + if not (isinstance(code, str) or isinstance(code, unicode)): + if 'code' not in code: + if 'error' in code: + error_msg = code['error'] + else: + error_msg = 'No code was supplied in the query parameters.' + raise FlowExchangeError(error_msg) + else: + code = code['code'] + + body = urllib.urlencode({ + 'grant_type': 'authorization_code', + 'client_id': self.client_id, + 'client_secret': self.client_secret, + 'code': code, + 'redirect_uri': self.redirect_uri, + 'scope': self.scope, + }) + headers = { + 'content-type': 'application/x-www-form-urlencoded', + } + + if self.user_agent is not None: + headers['user-agent'] = self.user_agent + + if http is None: + http = httplib2.Http() + + resp, content = http.request(self.token_uri, method='POST', body=body, + headers=headers) + d = _parse_exchange_token_response(content) + if resp.status == 200 and 'access_token' in d: + access_token = d['access_token'] + refresh_token = d.get('refresh_token', None) + token_expiry = None + if 'expires_in' in d: + token_expiry = datetime.datetime.utcnow() + datetime.timedelta( + seconds=int(d['expires_in'])) + + if 'id_token' in d: + d['id_token'] = _extract_id_token(d['id_token']) + + logger.info('Successfully retrieved access token') + return OAuth2Credentials(access_token, self.client_id, + self.client_secret, refresh_token, token_expiry, + self.token_uri, self.user_agent, + revoke_uri=self.revoke_uri, + id_token=d.get('id_token', None), + token_response=d) + else: + logger.info('Failed to retrieve access token: %s' % content) + if 'error' in d: + # you never know what those providers got to say + error_msg = unicode(d['error']) + else: + error_msg = 'Invalid response: %s.' % str(resp.status) + raise FlowExchangeError(error_msg) + + +@util.positional(2) +def flow_from_clientsecrets(filename, scope, redirect_uri=None, + message=None, cache=None): + """Create a Flow from a clientsecrets file. + + Will create the right kind of Flow based on the contents of the clientsecrets + file or will raise InvalidClientSecretsError for unknown types of Flows. + + Args: + filename: string, File name of client secrets. + scope: string or iterable of strings, scope(s) to request. + redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for + a non-web-based application, or a URI that handles the callback from + the authorization server. + message: string, A friendly string to display to the user if the + clientsecrets file is missing or invalid. If message is provided then + sys.exit will be called in the case of an error. If message in not + provided then clientsecrets.InvalidClientSecretsError will be raised. + cache: An optional cache service client that implements get() and set() + methods. See clientsecrets.loadfile() for details. + + Returns: + A Flow object. + + Raises: + UnknownClientSecretsFlowError if the file describes an unknown kind of Flow. + clientsecrets.InvalidClientSecretsError if the clientsecrets file is + invalid. + """ + try: + client_type, client_info = clientsecrets.loadfile(filename, cache=cache) + if client_type in (clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED): + constructor_kwargs = { + 'redirect_uri': redirect_uri, + 'auth_uri': client_info['auth_uri'], + 'token_uri': client_info['token_uri'], + } + revoke_uri = client_info.get('revoke_uri') + if revoke_uri is not None: + constructor_kwargs['revoke_uri'] = revoke_uri + return OAuth2WebServerFlow( + client_info['client_id'], client_info['client_secret'], + scope, **constructor_kwargs) + + except clientsecrets.InvalidClientSecretsError: + if message: + sys.exit(message) + else: + raise + else: + raise UnknownClientSecretsFlowError( + 'This OAuth 2.0 flow is unsupported: %r' % client_type) diff --git a/appengine/dashdemo/oauth2client/clientsecrets.py b/appengine/dashdemo/oauth2client/clientsecrets.py new file mode 100644 index 0000000..ac99aae --- /dev/null +++ b/appengine/dashdemo/oauth2client/clientsecrets.py @@ -0,0 +1,153 @@ +# Copyright (C) 2011 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Utilities for reading OAuth 2.0 client secret files. + +A client_secrets.json file contains all the information needed to interact with +an OAuth 2.0 protected service. +""" + +__author__ = 'jcgregorio@google.com (Joe Gregorio)' + + +from anyjson import simplejson + +# Properties that make a client_secrets.json file valid. +TYPE_WEB = 'web' +TYPE_INSTALLED = 'installed' + +VALID_CLIENT = { + TYPE_WEB: { + 'required': [ + 'client_id', + 'client_secret', + 'redirect_uris', + 'auth_uri', + 'token_uri', + ], + 'string': [ + 'client_id', + 'client_secret', + ], + }, + TYPE_INSTALLED: { + 'required': [ + 'client_id', + 'client_secret', + 'redirect_uris', + 'auth_uri', + 'token_uri', + ], + 'string': [ + 'client_id', + 'client_secret', + ], + }, +} + + +class Error(Exception): + """Base error for this module.""" + pass + + +class InvalidClientSecretsError(Error): + """Format of ClientSecrets file is invalid.""" + pass + + +def _validate_clientsecrets(obj): + if obj is None or len(obj) != 1: + raise InvalidClientSecretsError('Invalid file format.') + client_type = obj.keys()[0] + if client_type not in VALID_CLIENT.keys(): + raise InvalidClientSecretsError('Unknown client type: %s.' % client_type) + client_info = obj[client_type] + for prop_name in VALID_CLIENT[client_type]['required']: + if prop_name not in client_info: + raise InvalidClientSecretsError( + 'Missing property "%s" in a client type of "%s".' % (prop_name, + client_type)) + for prop_name in VALID_CLIENT[client_type]['string']: + if client_info[prop_name].startswith('[['): + raise InvalidClientSecretsError( + 'Property "%s" is not configured.' % prop_name) + return client_type, client_info + + +def load(fp): + obj = simplejson.load(fp) + return _validate_clientsecrets(obj) + + +def loads(s): + obj = simplejson.loads(s) + return _validate_clientsecrets(obj) + + +def _loadfile(filename): + try: + fp = file(filename, 'r') + try: + obj = simplejson.load(fp) + finally: + fp.close() + except IOError: + raise InvalidClientSecretsError('File not found: "%s"' % filename) + return _validate_clientsecrets(obj) + + +def loadfile(filename, cache=None): + """Loading of client_secrets JSON file, optionally backed by a cache. + + Typical cache storage would be App Engine memcache service, + but you can pass in any other cache client that implements + these methods: + - get(key, namespace=ns) + - set(key, value, namespace=ns) + + Usage: + # without caching + client_type, client_info = loadfile('secrets.json') + # using App Engine memcache service + from google.appengine.api import memcache + client_type, client_info = loadfile('secrets.json', cache=memcache) + + Args: + filename: string, Path to a client_secrets.json file on a filesystem. + cache: An optional cache service client that implements get() and set() + methods. If not specified, the file is always being loaded from + a filesystem. + + Raises: + InvalidClientSecretsError: In case of a validation error or some + I/O failure. Can happen only on cache miss. + + Returns: + (client_type, client_info) tuple, as _loadfile() normally would. + JSON contents is validated only during first load. Cache hits are not + validated. + """ + _SECRET_NAMESPACE = 'oauth2client:secrets#ns' + + if not cache: + return _loadfile(filename) + + obj = cache.get(filename, namespace=_SECRET_NAMESPACE) + if obj is None: + client_type, client_info = _loadfile(filename) + obj = {client_type: client_info} + cache.set(filename, obj, namespace=_SECRET_NAMESPACE) + + return obj.iteritems().next() diff --git a/appengine/dashdemo/oauth2client/crypt.py b/appengine/dashdemo/oauth2client/crypt.py new file mode 100644 index 0000000..d2d7a3b --- /dev/null +++ b/appengine/dashdemo/oauth2client/crypt.py @@ -0,0 +1,396 @@ +#!/usr/bin/python2.4 +# -*- coding: utf-8 -*- +# +# Copyright (C) 2011 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import base64 +import hashlib +import logging +import time + +from anyjson import simplejson + + +CLOCK_SKEW_SECS = 300 # 5 minutes in seconds +AUTH_TOKEN_LIFETIME_SECS = 300 # 5 minutes in seconds +MAX_TOKEN_LIFETIME_SECS = 86400 # 1 day in seconds + + +logger = logging.getLogger(__name__) + + +class AppIdentityError(Exception): + pass + + +try: + from OpenSSL import crypto + + class OpenSSLVerifier(object): + """Verifies the signature on a message.""" + + def __init__(self, pubkey): + """Constructor. + + Args: + pubkey, OpenSSL.crypto.PKey, The public key to verify with. + """ + self._pubkey = pubkey + + def verify(self, message, signature): + """Verifies a message against a signature. + + Args: + message: string, The message to verify. + signature: string, The signature on the message. + + Returns: + True if message was signed by the private key associated with the public + key that this object was constructed with. + """ + try: + crypto.verify(self._pubkey, signature, message, 'sha256') + return True + except: + return False + + @staticmethod + def from_string(key_pem, is_x509_cert): + """Construct a Verified instance from a string. + + Args: + key_pem: string, public key in PEM format. + is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it is + expected to be an RSA key in PEM format. + + Returns: + Verifier instance. + + Raises: + OpenSSL.crypto.Error if the key_pem can't be parsed. + """ + if is_x509_cert: + pubkey = crypto.load_certificate(crypto.FILETYPE_PEM, key_pem) + else: + pubkey = crypto.load_privatekey(crypto.FILETYPE_PEM, key_pem) + return OpenSSLVerifier(pubkey) + + + class OpenSSLSigner(object): + """Signs messages with a private key.""" + + def __init__(self, pkey): + """Constructor. + + Args: + pkey, OpenSSL.crypto.PKey (or equiv), The private key to sign with. + """ + self._key = pkey + + def sign(self, message): + """Signs a message. + + Args: + message: string, Message to be signed. + + Returns: + string, The signature of the message for the given key. + """ + return crypto.sign(self._key, message, 'sha256') + + @staticmethod + def from_string(key, password='notasecret'): + """Construct a Signer instance from a string. + + Args: + key: string, private key in PKCS12 or PEM format. + password: string, password for the private key file. + + Returns: + Signer instance. + + Raises: + OpenSSL.crypto.Error if the key can't be parsed. + """ + parsed_pem_key = _parse_pem_key(key) + if parsed_pem_key: + pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, parsed_pem_key) + else: + pkey = crypto.load_pkcs12(key, password).get_privatekey() + return OpenSSLSigner(pkey) + +except ImportError: + OpenSSLVerifier = None + OpenSSLSigner = None + + +try: + from Crypto.PublicKey import RSA + from Crypto.Hash import SHA256 + from Crypto.Signature import PKCS1_v1_5 + + + class PyCryptoVerifier(object): + """Verifies the signature on a message.""" + + def __init__(self, pubkey): + """Constructor. + + Args: + pubkey, OpenSSL.crypto.PKey (or equiv), The public key to verify with. + """ + self._pubkey = pubkey + + def verify(self, message, signature): + """Verifies a message against a signature. + + Args: + message: string, The message to verify. + signature: string, The signature on the message. + + Returns: + True if message was signed by the private key associated with the public + key that this object was constructed with. + """ + try: + return PKCS1_v1_5.new(self._pubkey).verify( + SHA256.new(message), signature) + except: + return False + + @staticmethod + def from_string(key_pem, is_x509_cert): + """Construct a Verified instance from a string. + + Args: + key_pem: string, public key in PEM format. + is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it is + expected to be an RSA key in PEM format. + + Returns: + Verifier instance. + + Raises: + NotImplementedError if is_x509_cert is true. + """ + if is_x509_cert: + raise NotImplementedError( + 'X509 certs are not supported by the PyCrypto library. ' + 'Try using PyOpenSSL if native code is an option.') + else: + pubkey = RSA.importKey(key_pem) + return PyCryptoVerifier(pubkey) + + + class PyCryptoSigner(object): + """Signs messages with a private key.""" + + def __init__(self, pkey): + """Constructor. + + Args: + pkey, OpenSSL.crypto.PKey (or equiv), The private key to sign with. + """ + self._key = pkey + + def sign(self, message): + """Signs a message. + + Args: + message: string, Message to be signed. + + Returns: + string, The signature of the message for the given key. + """ + return PKCS1_v1_5.new(self._key).sign(SHA256.new(message)) + + @staticmethod + def from_string(key, password='notasecret'): + """Construct a Signer instance from a string. + + Args: + key: string, private key in PEM format. + password: string, password for private key file. Unused for PEM files. + + Returns: + Signer instance. + + Raises: + NotImplementedError if they key isn't in PEM format. + """ + parsed_pem_key = _parse_pem_key(key) + if parsed_pem_key: + pkey = RSA.importKey(parsed_pem_key) + else: + raise NotImplementedError( + 'PKCS12 format is not supported by the PyCrpto library. ' + 'Try converting to a "PEM" ' + '(openssl pkcs12 -in xxxxx.p12 -nodes -nocerts > privatekey.pem) ' + 'or using PyOpenSSL if native code is an option.') + return PyCryptoSigner(pkey) + +except ImportError: + PyCryptoVerifier = None + PyCryptoSigner = None + + +if OpenSSLSigner: + Signer = OpenSSLSigner + Verifier = OpenSSLVerifier +elif PyCryptoSigner: + Signer = PyCryptoSigner + Verifier = PyCryptoVerifier +else: + raise ImportError('No encryption library found. Please install either ' + 'PyOpenSSL, or PyCrypto 2.6 or later') + + +def _parse_pem_key(raw_key_input): + """Identify and extract PEM keys. + + Determines whether the given key is in the format of PEM key, and extracts + the relevant part of the key if it is. + + Args: + raw_key_input: The contents of a private key file (either PEM or PKCS12). + + Returns: + string, The actual key if the contents are from a PEM file, or else None. + """ + offset = raw_key_input.find('-----BEGIN ') + if offset != -1: + return raw_key_input[offset:] + else: + return None + +def _urlsafe_b64encode(raw_bytes): + return base64.urlsafe_b64encode(raw_bytes).rstrip('=') + + +def _urlsafe_b64decode(b64string): + # Guard against unicode strings, which base64 can't handle. + b64string = b64string.encode('ascii') + padded = b64string + '=' * (4 - len(b64string) % 4) + return base64.urlsafe_b64decode(padded) + + +def _json_encode(data): + return simplejson.dumps(data, separators = (',', ':')) + + +def make_signed_jwt(signer, payload): + """Make a signed JWT. + + See http://self-issued.info/docs/draft-jones-json-web-token.html. + + Args: + signer: crypt.Signer, Cryptographic signer. + payload: dict, Dictionary of data to convert to JSON and then sign. + + Returns: + string, The JWT for the payload. + """ + header = {'typ': 'JWT', 'alg': 'RS256'} + + segments = [ + _urlsafe_b64encode(_json_encode(header)), + _urlsafe_b64encode(_json_encode(payload)), + ] + signing_input = '.'.join(segments) + + signature = signer.sign(signing_input) + segments.append(_urlsafe_b64encode(signature)) + + logger.debug(str(segments)) + + return '.'.join(segments) + + +def verify_signed_jwt_with_certs(jwt, certs, audience): + """Verify a JWT against public certs. + + See http://self-issued.info/docs/draft-jones-json-web-token.html. + + Args: + jwt: string, A JWT. + certs: dict, Dictionary where values of public keys in PEM format. + audience: string, The audience, 'aud', that this JWT should contain. If + None then the JWT's 'aud' parameter is not verified. + + Returns: + dict, The deserialized JSON payload in the JWT. + + Raises: + AppIdentityError if any checks are failed. + """ + segments = jwt.split('.') + + if (len(segments) != 3): + raise AppIdentityError( + 'Wrong number of segments in token: %s' % jwt) + signed = '%s.%s' % (segments[0], segments[1]) + + signature = _urlsafe_b64decode(segments[2]) + + # Parse token. + json_body = _urlsafe_b64decode(segments[1]) + try: + parsed = simplejson.loads(json_body) + except: + raise AppIdentityError('Can\'t parse token: %s' % json_body) + + # Check signature. + verified = False + for (keyname, pem) in certs.items(): + verifier = Verifier.from_string(pem, True) + if (verifier.verify(signed, signature)): + verified = True + break + if not verified: + raise AppIdentityError('Invalid token signature: %s' % jwt) + + # Check creation timestamp. + iat = parsed.get('iat') + if iat is None: + raise AppIdentityError('No iat field in token: %s' % json_body) + earliest = iat - CLOCK_SKEW_SECS + + # Check expiration timestamp. + now = long(time.time()) + exp = parsed.get('exp') + if exp is None: + raise AppIdentityError('No exp field in token: %s' % json_body) + if exp >= now + MAX_TOKEN_LIFETIME_SECS: + raise AppIdentityError( + 'exp field too far in future: %s' % json_body) + latest = exp + CLOCK_SKEW_SECS + + if now < earliest: + raise AppIdentityError('Token used too early, %d < %d: %s' % + (now, earliest, json_body)) + if now > latest: + raise AppIdentityError('Token used too late, %d > %d: %s' % + (now, latest, json_body)) + + # Check audience. + if audience is not None: + aud = parsed.get('aud') + if aud is None: + raise AppIdentityError('No aud field in token: %s' % json_body) + if aud != audience: + raise AppIdentityError('Wrong recipient, %s != %s: %s' % + (aud, audience, json_body)) + + return parsed diff --git a/appengine/dashdemo/oauth2client/django_orm.py b/appengine/dashdemo/oauth2client/django_orm.py new file mode 100644 index 0000000..d54d20c --- /dev/null +++ b/appengine/dashdemo/oauth2client/django_orm.py @@ -0,0 +1,134 @@ +# Copyright (C) 2010 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""OAuth 2.0 utilities for Django. + +Utilities for using OAuth 2.0 in conjunction with +the Django datastore. +""" + +__author__ = 'jcgregorio@google.com (Joe Gregorio)' + +import oauth2client +import base64 +import pickle + +from django.db import models +from oauth2client.client import Storage as BaseStorage + +class CredentialsField(models.Field): + + __metaclass__ = models.SubfieldBase + + def __init__(self, *args, **kwargs): + if 'null' not in kwargs: + kwargs['null'] = True + super(CredentialsField, self).__init__(*args, **kwargs) + + def get_internal_type(self): + return "TextField" + + def to_python(self, value): + if value is None: + return None + if isinstance(value, oauth2client.client.Credentials): + return value + return pickle.loads(base64.b64decode(value)) + + def get_db_prep_value(self, value, connection, prepared=False): + if value is None: + return None + return base64.b64encode(pickle.dumps(value)) + + +class FlowField(models.Field): + + __metaclass__ = models.SubfieldBase + + def __init__(self, *args, **kwargs): + if 'null' not in kwargs: + kwargs['null'] = True + super(FlowField, self).__init__(*args, **kwargs) + + def get_internal_type(self): + return "TextField" + + def to_python(self, value): + if value is None: + return None + if isinstance(value, oauth2client.client.Flow): + return value + return pickle.loads(base64.b64decode(value)) + + def get_db_prep_value(self, value, connection, prepared=False): + if value is None: + return None + return base64.b64encode(pickle.dumps(value)) + + +class Storage(BaseStorage): + """Store and retrieve a single credential to and from + the datastore. + + This Storage helper presumes the Credentials + have been stored as a CredenialsField + on a db model class. + """ + + def __init__(self, model_class, key_name, key_value, property_name): + """Constructor for Storage. + + Args: + model: db.Model, model class + key_name: string, key name for the entity that has the credentials + key_value: string, key value for the entity that has the credentials + property_name: string, name of the property that is an CredentialsProperty + """ + self.model_class = model_class + self.key_name = key_name + self.key_value = key_value + self.property_name = property_name + + def locked_get(self): + """Retrieve Credential from datastore. + + Returns: + oauth2client.Credentials + """ + credential = None + + query = {self.key_name: self.key_value} + entities = self.model_class.objects.filter(**query) + if len(entities) > 0: + credential = getattr(entities[0], self.property_name) + if credential and hasattr(credential, 'set_store'): + credential.set_store(self) + return credential + + def locked_put(self, credentials): + """Write a Credentials to the datastore. + + Args: + credentials: Credentials, the credentials to store. + """ + args = {self.key_name: self.key_value} + entity = self.model_class(**args) + setattr(entity, self.property_name, credentials) + entity.save() + + def locked_delete(self): + """Delete Credentials from the datastore.""" + + query = {self.key_name: self.key_value} + entities = self.model_class.objects.filter(**query).delete() diff --git a/appengine/dashdemo/oauth2client/file.py b/appengine/dashdemo/oauth2client/file.py new file mode 100644 index 0000000..1895f94 --- /dev/null +++ b/appengine/dashdemo/oauth2client/file.py @@ -0,0 +1,124 @@ +# Copyright (C) 2010 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Utilities for OAuth. + +Utilities for making it easier to work with OAuth 2.0 +credentials. +""" + +__author__ = 'jcgregorio@google.com (Joe Gregorio)' + +import os +import stat +import threading + +from anyjson import simplejson +from client import Storage as BaseStorage +from client import Credentials + + +class CredentialsFileSymbolicLinkError(Exception): + """Credentials files must not be symbolic links.""" + + +class Storage(BaseStorage): + """Store and retrieve a single credential to and from a file.""" + + def __init__(self, filename): + self._filename = filename + self._lock = threading.Lock() + + def _validate_file(self): + if os.path.islink(self._filename): + raise CredentialsFileSymbolicLinkError( + 'File: %s is a symbolic link.' % self._filename) + + def acquire_lock(self): + """Acquires any lock necessary to access this Storage. + + This lock is not reentrant.""" + self._lock.acquire() + + def release_lock(self): + """Release the Storage lock. + + Trying to release a lock that isn't held will result in a + RuntimeError. + """ + self._lock.release() + + def locked_get(self): + """Retrieve Credential from file. + + Returns: + oauth2client.client.Credentials + + Raises: + CredentialsFileSymbolicLinkError if the file is a symbolic link. + """ + credentials = None + self._validate_file() + try: + f = open(self._filename, 'rb') + content = f.read() + f.close() + except IOError: + return credentials + + try: + credentials = Credentials.new_from_json(content) + credentials.set_store(self) + except ValueError: + pass + + return credentials + + def _create_file_if_needed(self): + """Create an empty file if necessary. + + This method will not initialize the file. Instead it implements a + simple version of "touch" to ensure the file has been created. + """ + if not os.path.exists(self._filename): + old_umask = os.umask(0177) + try: + open(self._filename, 'a+b').close() + finally: + os.umask(old_umask) + + def locked_put(self, credentials): + """Write Credentials to file. + + Args: + credentials: Credentials, the credentials to store. + + Raises: + CredentialsFileSymbolicLinkError if the file is a symbolic link. + """ + + self._create_file_if_needed() + self._validate_file() + f = open(self._filename, 'wb') + f.write(credentials.to_json()) + f.close() + + def locked_delete(self): + """Delete Credentials file. + + Args: + credentials: Credentials, the credentials to store. + """ + + os.unlink(self._filename) diff --git a/appengine/dashdemo/oauth2client/gce.py b/appengine/dashdemo/oauth2client/gce.py new file mode 100644 index 0000000..c7fd7c1 --- /dev/null +++ b/appengine/dashdemo/oauth2client/gce.py @@ -0,0 +1,90 @@ +# Copyright (C) 2012 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Utilities for Google Compute Engine + +Utilities for making it easier to use OAuth 2.0 on Google Compute Engine. +""" + +__author__ = 'jcgregorio@google.com (Joe Gregorio)' + +import httplib2 +import logging +import uritemplate + +from oauth2client import util +from oauth2client.anyjson import simplejson +from oauth2client.client import AccessTokenRefreshError +from oauth2client.client import AssertionCredentials + +logger = logging.getLogger(__name__) + +# URI Template for the endpoint that returns access_tokens. +META = ('http://metadata.google.internal/0.1/meta-data/service-accounts/' + 'default/acquire{?scope}') + + +class AppAssertionCredentials(AssertionCredentials): + """Credentials object for Compute Engine Assertion Grants + + This object will allow a Compute Engine instance to identify itself to + Google and other OAuth 2.0 servers that can verify assertions. It can be used + for the purpose of accessing data stored under an account assigned to the + Compute Engine instance itself. + + This credential does not require a flow to instantiate because it represents + a two legged flow, and therefore has all of the required information to + generate and refresh its own access tokens. + """ + + @util.positional(2) + def __init__(self, scope, **kwargs): + """Constructor for AppAssertionCredentials + + Args: + scope: string or iterable of strings, scope(s) of the credentials being + requested. + """ + self.scope = util.scopes_to_string(scope) + + # Assertion type is no longer used, but still in the parent class signature. + super(AppAssertionCredentials, self).__init__(None) + + @classmethod + def from_json(cls, json): + data = simplejson.loads(json) + return AppAssertionCredentials(data['scope']) + + def _refresh(self, http_request): + """Refreshes the access_token. + + Skip all the storage hoops and just refresh using the API. + + Args: + http_request: callable, a callable that matches the method signature of + httplib2.Http.request, used to make the refresh request. + + Raises: + AccessTokenRefreshError: When the refresh fails. + """ + uri = uritemplate.expand(META, {'scope': self.scope}) + response, content = http_request(uri) + if response.status == 200: + try: + d = simplejson.loads(content) + except StandardError, e: + raise AccessTokenRefreshError(str(e)) + self.access_token = d['accessToken'] + else: + raise AccessTokenRefreshError(content) diff --git a/appengine/dashdemo/oauth2client/keyring_storage.py b/appengine/dashdemo/oauth2client/keyring_storage.py new file mode 100644 index 0000000..efe2949 --- /dev/null +++ b/appengine/dashdemo/oauth2client/keyring_storage.py @@ -0,0 +1,109 @@ +# Copyright (C) 2012 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""A keyring based Storage. + +A Storage for Credentials that uses the keyring module. +""" + +__author__ = 'jcgregorio@google.com (Joe Gregorio)' + +import keyring +import threading + +from client import Storage as BaseStorage +from client import Credentials + + +class Storage(BaseStorage): + """Store and retrieve a single credential to and from the keyring. + + To use this module you must have the keyring module installed. See + . This is an optional module and is not + installed with oauth2client by default because it does not work on all the + platforms that oauth2client supports, such as Google App Engine. + + The keyring module is a cross-platform + library for access the keyring capabilities of the local system. The user will + be prompted for their keyring password when this module is used, and the + manner in which the user is prompted will vary per platform. + + Usage: + from oauth2client.keyring_storage import Storage + + s = Storage('name_of_application', 'user1') + credentials = s.get() + + """ + + def __init__(self, service_name, user_name): + """Constructor. + + Args: + service_name: string, The name of the service under which the credentials + are stored. + user_name: string, The name of the user to store credentials for. + """ + self._service_name = service_name + self._user_name = user_name + self._lock = threading.Lock() + + def acquire_lock(self): + """Acquires any lock necessary to access this Storage. + + This lock is not reentrant.""" + self._lock.acquire() + + def release_lock(self): + """Release the Storage lock. + + Trying to release a lock that isn't held will result in a + RuntimeError. + """ + self._lock.release() + + def locked_get(self): + """Retrieve Credential from file. + + Returns: + oauth2client.client.Credentials + """ + credentials = None + content = keyring.get_password(self._service_name, self._user_name) + + if content is not None: + try: + credentials = Credentials.new_from_json(content) + credentials.set_store(self) + except ValueError: + pass + + return credentials + + def locked_put(self, credentials): + """Write Credentials to file. + + Args: + credentials: Credentials, the credentials to store. + """ + keyring.set_password(self._service_name, self._user_name, + credentials.to_json()) + + def locked_delete(self): + """Delete Credentials file. + + Args: + credentials: Credentials, the credentials to store. + """ + keyring.set_password(self._service_name, self._user_name, '') diff --git a/appengine/dashdemo/oauth2client/locked_file.py b/appengine/dashdemo/oauth2client/locked_file.py new file mode 100644 index 0000000..31514dc --- /dev/null +++ b/appengine/dashdemo/oauth2client/locked_file.py @@ -0,0 +1,373 @@ +# Copyright 2011 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Locked file interface that should work on Unix and Windows pythons. + +This module first tries to use fcntl locking to ensure serialized access +to a file, then falls back on a lock file if that is unavialable. + +Usage: + f = LockedFile('filename', 'r+b', 'rb') + f.open_and_lock() + if f.is_locked(): + print 'Acquired filename with r+b mode' + f.file_handle().write('locked data') + else: + print 'Aquired filename with rb mode' + f.unlock_and_close() +""" + +__author__ = 'cache@google.com (David T McWherter)' + +import errno +import logging +import os +import time + +from oauth2client import util + +logger = logging.getLogger(__name__) + + +class CredentialsFileSymbolicLinkError(Exception): + """Credentials files must not be symbolic links.""" + + +class AlreadyLockedException(Exception): + """Trying to lock a file that has already been locked by the LockedFile.""" + pass + + +def validate_file(filename): + if os.path.islink(filename): + raise CredentialsFileSymbolicLinkError( + 'File: %s is a symbolic link.' % filename) + +class _Opener(object): + """Base class for different locking primitives.""" + + def __init__(self, filename, mode, fallback_mode): + """Create an Opener. + + Args: + filename: string, The pathname of the file. + mode: string, The preferred mode to access the file with. + fallback_mode: string, The mode to use if locking fails. + """ + self._locked = False + self._filename = filename + self._mode = mode + self._fallback_mode = fallback_mode + self._fh = None + + def is_locked(self): + """Was the file locked.""" + return self._locked + + def file_handle(self): + """The file handle to the file. Valid only after opened.""" + return self._fh + + def filename(self): + """The filename that is being locked.""" + return self._filename + + def open_and_lock(self, timeout, delay): + """Open the file and lock it. + + Args: + timeout: float, How long to try to lock for. + delay: float, How long to wait between retries. + """ + pass + + def unlock_and_close(self): + """Unlock and close the file.""" + pass + + +class _PosixOpener(_Opener): + """Lock files using Posix advisory lock files.""" + + def open_and_lock(self, timeout, delay): + """Open the file and lock it. + + Tries to create a .lock file next to the file we're trying to open. + + Args: + timeout: float, How long to try to lock for. + delay: float, How long to wait between retries. + + Raises: + AlreadyLockedException: if the lock is already acquired. + IOError: if the open fails. + CredentialsFileSymbolicLinkError if the file is a symbolic link. + """ + if self._locked: + raise AlreadyLockedException('File %s is already locked' % + self._filename) + self._locked = False + + validate_file(self._filename) + try: + self._fh = open(self._filename, self._mode) + except IOError, e: + # If we can't access with _mode, try _fallback_mode and don't lock. + if e.errno == errno.EACCES: + self._fh = open(self._filename, self._fallback_mode) + return + + lock_filename = self._posix_lockfile(self._filename) + start_time = time.time() + while True: + try: + self._lock_fd = os.open(lock_filename, + os.O_CREAT|os.O_EXCL|os.O_RDWR) + self._locked = True + break + + except OSError, e: + if e.errno != errno.EEXIST: + raise + if (time.time() - start_time) >= timeout: + logger.warn('Could not acquire lock %s in %s seconds' % ( + lock_filename, timeout)) + # Close the file and open in fallback_mode. + if self._fh: + self._fh.close() + self._fh = open(self._filename, self._fallback_mode) + return + time.sleep(delay) + + def unlock_and_close(self): + """Unlock a file by removing the .lock file, and close the handle.""" + if self._locked: + lock_filename = self._posix_lockfile(self._filename) + os.close(self._lock_fd) + os.unlink(lock_filename) + self._locked = False + self._lock_fd = None + if self._fh: + self._fh.close() + + def _posix_lockfile(self, filename): + """The name of the lock file to use for posix locking.""" + return '%s.lock' % filename + + +try: + import fcntl + + class _FcntlOpener(_Opener): + """Open, lock, and unlock a file using fcntl.lockf.""" + + def open_and_lock(self, timeout, delay): + """Open the file and lock it. + + Args: + timeout: float, How long to try to lock for. + delay: float, How long to wait between retries + + Raises: + AlreadyLockedException: if the lock is already acquired. + IOError: if the open fails. + CredentialsFileSymbolicLinkError if the file is a symbolic link. + """ + if self._locked: + raise AlreadyLockedException('File %s is already locked' % + self._filename) + start_time = time.time() + + validate_file(self._filename) + try: + self._fh = open(self._filename, self._mode) + except IOError, e: + # If we can't access with _mode, try _fallback_mode and don't lock. + if e.errno == errno.EACCES: + self._fh = open(self._filename, self._fallback_mode) + return + + # We opened in _mode, try to lock the file. + while True: + try: + fcntl.lockf(self._fh.fileno(), fcntl.LOCK_EX) + self._locked = True + return + except IOError, e: + # If not retrying, then just pass on the error. + if timeout == 0: + raise e + if e.errno != errno.EACCES: + raise e + # We could not acquire the lock. Try again. + if (time.time() - start_time) >= timeout: + logger.warn('Could not lock %s in %s seconds' % ( + self._filename, timeout)) + if self._fh: + self._fh.close() + self._fh = open(self._filename, self._fallback_mode) + return + time.sleep(delay) + + def unlock_and_close(self): + """Close and unlock the file using the fcntl.lockf primitive.""" + if self._locked: + fcntl.lockf(self._fh.fileno(), fcntl.LOCK_UN) + self._locked = False + if self._fh: + self._fh.close() +except ImportError: + _FcntlOpener = None + + +try: + import pywintypes + import win32con + import win32file + + class _Win32Opener(_Opener): + """Open, lock, and unlock a file using windows primitives.""" + + # Error #33: + # 'The process cannot access the file because another process' + FILE_IN_USE_ERROR = 33 + + # Error #158: + # 'The segment is already unlocked.' + FILE_ALREADY_UNLOCKED_ERROR = 158 + + def open_and_lock(self, timeout, delay): + """Open the file and lock it. + + Args: + timeout: float, How long to try to lock for. + delay: float, How long to wait between retries + + Raises: + AlreadyLockedException: if the lock is already acquired. + IOError: if the open fails. + CredentialsFileSymbolicLinkError if the file is a symbolic link. + """ + if self._locked: + raise AlreadyLockedException('File %s is already locked' % + self._filename) + start_time = time.time() + + validate_file(self._filename) + try: + self._fh = open(self._filename, self._mode) + except IOError, e: + # If we can't access with _mode, try _fallback_mode and don't lock. + if e.errno == errno.EACCES: + self._fh = open(self._filename, self._fallback_mode) + return + + # We opened in _mode, try to lock the file. + while True: + try: + hfile = win32file._get_osfhandle(self._fh.fileno()) + win32file.LockFileEx( + hfile, + (win32con.LOCKFILE_FAIL_IMMEDIATELY| + win32con.LOCKFILE_EXCLUSIVE_LOCK), 0, -0x10000, + pywintypes.OVERLAPPED()) + self._locked = True + return + except pywintypes.error, e: + if timeout == 0: + raise e + + # If the error is not that the file is already in use, raise. + if e[0] != _Win32Opener.FILE_IN_USE_ERROR: + raise + + # We could not acquire the lock. Try again. + if (time.time() - start_time) >= timeout: + logger.warn('Could not lock %s in %s seconds' % ( + self._filename, timeout)) + if self._fh: + self._fh.close() + self._fh = open(self._filename, self._fallback_mode) + return + time.sleep(delay) + + def unlock_and_close(self): + """Close and unlock the file using the win32 primitive.""" + if self._locked: + try: + hfile = win32file._get_osfhandle(self._fh.fileno()) + win32file.UnlockFileEx(hfile, 0, -0x10000, pywintypes.OVERLAPPED()) + except pywintypes.error, e: + if e[0] != _Win32Opener.FILE_ALREADY_UNLOCKED_ERROR: + raise + self._locked = False + if self._fh: + self._fh.close() +except ImportError: + _Win32Opener = None + + +class LockedFile(object): + """Represent a file that has exclusive access.""" + + @util.positional(4) + def __init__(self, filename, mode, fallback_mode, use_native_locking=True): + """Construct a LockedFile. + + Args: + filename: string, The path of the file to open. + mode: string, The mode to try to open the file with. + fallback_mode: string, The mode to use if locking fails. + use_native_locking: bool, Whether or not fcntl/win32 locking is used. + """ + opener = None + if not opener and use_native_locking: + if _Win32Opener: + opener = _Win32Opener(filename, mode, fallback_mode) + if _FcntlOpener: + opener = _FcntlOpener(filename, mode, fallback_mode) + + if not opener: + opener = _PosixOpener(filename, mode, fallback_mode) + + self._opener = opener + + def filename(self): + """Return the filename we were constructed with.""" + return self._opener._filename + + def file_handle(self): + """Return the file_handle to the opened file.""" + return self._opener.file_handle() + + def is_locked(self): + """Return whether we successfully locked the file.""" + return self._opener.is_locked() + + def open_and_lock(self, timeout=0, delay=0.05): + """Open the file, trying to lock it. + + Args: + timeout: float, The number of seconds to try to acquire the lock. + delay: float, The number of seconds to wait between retry attempts. + + Raises: + AlreadyLockedException: if the lock is already acquired. + IOError: if the open fails. + """ + self._opener.open_and_lock(timeout, delay) + + def unlock_and_close(self): + """Unlock and close a file.""" + self._opener.unlock_and_close() diff --git a/appengine/dashdemo/oauth2client/multistore_file.py b/appengine/dashdemo/oauth2client/multistore_file.py new file mode 100644 index 0000000..ce7a519 --- /dev/null +++ b/appengine/dashdemo/oauth2client/multistore_file.py @@ -0,0 +1,465 @@ +# Copyright 2011 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Multi-credential file store with lock support. + +This module implements a JSON credential store where multiple +credentials can be stored in one file. That file supports locking +both in a single process and across processes. + +The credential themselves are keyed off of: +* client_id +* user_agent +* scope + +The format of the stored data is like so: +{ + 'file_version': 1, + 'data': [ + { + 'key': { + 'clientId': '', + 'userAgent': '', + 'scope': '' + }, + 'credential': { + # JSON serialized Credentials. + } + } + ] +} +""" + +__author__ = 'jbeda@google.com (Joe Beda)' + +import base64 +import errno +import logging +import os +import threading + +from anyjson import simplejson +from oauth2client.client import Storage as BaseStorage +from oauth2client.client import Credentials +from oauth2client import util +from locked_file import LockedFile + +logger = logging.getLogger(__name__) + +# A dict from 'filename'->_MultiStore instances +_multistores = {} +_multistores_lock = threading.Lock() + + +class Error(Exception): + """Base error for this module.""" + pass + + +class NewerCredentialStoreError(Error): + """The credential store is a newer version that supported.""" + pass + + +@util.positional(4) +def get_credential_storage(filename, client_id, user_agent, scope, + warn_on_readonly=True): + """Get a Storage instance for a credential. + + Args: + filename: The JSON file storing a set of credentials + client_id: The client_id for the credential + user_agent: The user agent for the credential + scope: string or iterable of strings, Scope(s) being requested + warn_on_readonly: if True, log a warning if the store is readonly + + Returns: + An object derived from client.Storage for getting/setting the + credential. + """ + # Recreate the legacy key with these specific parameters + key = {'clientId': client_id, 'userAgent': user_agent, + 'scope': util.scopes_to_string(scope)} + return get_credential_storage_custom_key( + filename, key, warn_on_readonly=warn_on_readonly) + + +@util.positional(2) +def get_credential_storage_custom_string_key( + filename, key_string, warn_on_readonly=True): + """Get a Storage instance for a credential using a single string as a key. + + Allows you to provide a string as a custom key that will be used for + credential storage and retrieval. + + Args: + filename: The JSON file storing a set of credentials + key_string: A string to use as the key for storing this credential. + warn_on_readonly: if True, log a warning if the store is readonly + + Returns: + An object derived from client.Storage for getting/setting the + credential. + """ + # Create a key dictionary that can be used + key_dict = {'key': key_string} + return get_credential_storage_custom_key( + filename, key_dict, warn_on_readonly=warn_on_readonly) + + +@util.positional(2) +def get_credential_storage_custom_key( + filename, key_dict, warn_on_readonly=True): + """Get a Storage instance for a credential using a dictionary as a key. + + Allows you to provide a dictionary as a custom key that will be used for + credential storage and retrieval. + + Args: + filename: The JSON file storing a set of credentials + key_dict: A dictionary to use as the key for storing this credential. There + is no ordering of the keys in the dictionary. Logically equivalent + dictionaries will produce equivalent storage keys. + warn_on_readonly: if True, log a warning if the store is readonly + + Returns: + An object derived from client.Storage for getting/setting the + credential. + """ + multistore = _get_multistore(filename, warn_on_readonly=warn_on_readonly) + key = util.dict_to_tuple_key(key_dict) + return multistore._get_storage(key) + + +@util.positional(1) +def get_all_credential_keys(filename, warn_on_readonly=True): + """Gets all the registered credential keys in the given Multistore. + + Args: + filename: The JSON file storing a set of credentials + warn_on_readonly: if True, log a warning if the store is readonly + + Returns: + A list of the credential keys present in the file. They are returned as + dictionaries that can be passed into get_credential_storage_custom_key to + get the actual credentials. + """ + multistore = _get_multistore(filename, warn_on_readonly=warn_on_readonly) + multistore._lock() + try: + return multistore._get_all_credential_keys() + finally: + multistore._unlock() + + +@util.positional(1) +def _get_multistore(filename, warn_on_readonly=True): + """A helper method to initialize the multistore with proper locking. + + Args: + filename: The JSON file storing a set of credentials + warn_on_readonly: if True, log a warning if the store is readonly + + Returns: + A multistore object + """ + filename = os.path.expanduser(filename) + _multistores_lock.acquire() + try: + multistore = _multistores.setdefault( + filename, _MultiStore(filename, warn_on_readonly=warn_on_readonly)) + finally: + _multistores_lock.release() + return multistore + + +class _MultiStore(object): + """A file backed store for multiple credentials.""" + + @util.positional(2) + def __init__(self, filename, warn_on_readonly=True): + """Initialize the class. + + This will create the file if necessary. + """ + self._file = LockedFile(filename, 'r+b', 'rb') + self._thread_lock = threading.Lock() + self._read_only = False + self._warn_on_readonly = warn_on_readonly + + self._create_file_if_needed() + + # Cache of deserialized store. This is only valid after the + # _MultiStore is locked or _refresh_data_cache is called. This is + # of the form of: + # + # ((key, value), (key, value)...) -> OAuth2Credential + # + # If this is None, then the store hasn't been read yet. + self._data = None + + class _Storage(BaseStorage): + """A Storage object that knows how to read/write a single credential.""" + + def __init__(self, multistore, key): + self._multistore = multistore + self._key = key + + def acquire_lock(self): + """Acquires any lock necessary to access this Storage. + + This lock is not reentrant. + """ + self._multistore._lock() + + def release_lock(self): + """Release the Storage lock. + + Trying to release a lock that isn't held will result in a + RuntimeError. + """ + self._multistore._unlock() + + def locked_get(self): + """Retrieve credential. + + The Storage lock must be held when this is called. + + Returns: + oauth2client.client.Credentials + """ + credential = self._multistore._get_credential(self._key) + if credential: + credential.set_store(self) + return credential + + def locked_put(self, credentials): + """Write a credential. + + The Storage lock must be held when this is called. + + Args: + credentials: Credentials, the credentials to store. + """ + self._multistore._update_credential(self._key, credentials) + + def locked_delete(self): + """Delete a credential. + + The Storage lock must be held when this is called. + + Args: + credentials: Credentials, the credentials to store. + """ + self._multistore._delete_credential(self._key) + + def _create_file_if_needed(self): + """Create an empty file if necessary. + + This method will not initialize the file. Instead it implements a + simple version of "touch" to ensure the file has been created. + """ + if not os.path.exists(self._file.filename()): + old_umask = os.umask(0177) + try: + open(self._file.filename(), 'a+b').close() + finally: + os.umask(old_umask) + + def _lock(self): + """Lock the entire multistore.""" + self._thread_lock.acquire() + self._file.open_and_lock() + if not self._file.is_locked(): + self._read_only = True + if self._warn_on_readonly: + logger.warn('The credentials file (%s) is not writable. Opening in ' + 'read-only mode. Any refreshed credentials will only be ' + 'valid for this run.' % self._file.filename()) + if os.path.getsize(self._file.filename()) == 0: + logger.debug('Initializing empty multistore file') + # The multistore is empty so write out an empty file. + self._data = {} + self._write() + elif not self._read_only or self._data is None: + # Only refresh the data if we are read/write or we haven't + # cached the data yet. If we are readonly, we assume is isn't + # changing out from under us and that we only have to read it + # once. This prevents us from whacking any new access keys that + # we have cached in memory but were unable to write out. + self._refresh_data_cache() + + def _unlock(self): + """Release the lock on the multistore.""" + self._file.unlock_and_close() + self._thread_lock.release() + + def _locked_json_read(self): + """Get the raw content of the multistore file. + + The multistore must be locked when this is called. + + Returns: + The contents of the multistore decoded as JSON. + """ + assert self._thread_lock.locked() + self._file.file_handle().seek(0) + return simplejson.load(self._file.file_handle()) + + def _locked_json_write(self, data): + """Write a JSON serializable data structure to the multistore. + + The multistore must be locked when this is called. + + Args: + data: The data to be serialized and written. + """ + assert self._thread_lock.locked() + if self._read_only: + return + self._file.file_handle().seek(0) + simplejson.dump(data, self._file.file_handle(), sort_keys=True, indent=2) + self._file.file_handle().truncate() + + def _refresh_data_cache(self): + """Refresh the contents of the multistore. + + The multistore must be locked when this is called. + + Raises: + NewerCredentialStoreError: Raised when a newer client has written the + store. + """ + self._data = {} + try: + raw_data = self._locked_json_read() + except Exception: + logger.warn('Credential data store could not be loaded. ' + 'Will ignore and overwrite.') + return + + version = 0 + try: + version = raw_data['file_version'] + except Exception: + logger.warn('Missing version for credential data store. It may be ' + 'corrupt or an old version. Overwriting.') + if version > 1: + raise NewerCredentialStoreError( + 'Credential file has file_version of %d. ' + 'Only file_version of 1 is supported.' % version) + + credentials = [] + try: + credentials = raw_data['data'] + except (TypeError, KeyError): + pass + + for cred_entry in credentials: + try: + (key, credential) = self._decode_credential_from_json(cred_entry) + self._data[key] = credential + except: + # If something goes wrong loading a credential, just ignore it + logger.info('Error decoding credential, skipping', exc_info=True) + + def _decode_credential_from_json(self, cred_entry): + """Load a credential from our JSON serialization. + + Args: + cred_entry: A dict entry from the data member of our format + + Returns: + (key, cred) where the key is the key tuple and the cred is the + OAuth2Credential object. + """ + raw_key = cred_entry['key'] + key = util.dict_to_tuple_key(raw_key) + credential = None + credential = Credentials.new_from_json(simplejson.dumps(cred_entry['credential'])) + return (key, credential) + + def _write(self): + """Write the cached data back out. + + The multistore must be locked. + """ + raw_data = {'file_version': 1} + raw_creds = [] + raw_data['data'] = raw_creds + for (cred_key, cred) in self._data.items(): + raw_key = dict(cred_key) + raw_cred = simplejson.loads(cred.to_json()) + raw_creds.append({'key': raw_key, 'credential': raw_cred}) + self._locked_json_write(raw_data) + + def _get_all_credential_keys(self): + """Gets all the registered credential keys in the multistore. + + Returns: + A list of dictionaries corresponding to all the keys currently registered + """ + return [dict(key) for key in self._data.keys()] + + def _get_credential(self, key): + """Get a credential from the multistore. + + The multistore must be locked. + + Args: + key: The key used to retrieve the credential + + Returns: + The credential specified or None if not present + """ + return self._data.get(key, None) + + def _update_credential(self, key, cred): + """Update a credential and write the multistore. + + This must be called when the multistore is locked. + + Args: + key: The key used to retrieve the credential + cred: The OAuth2Credential to update/set + """ + self._data[key] = cred + self._write() + + def _delete_credential(self, key): + """Delete a credential and write the multistore. + + This must be called when the multistore is locked. + + Args: + key: The key used to retrieve the credential + """ + try: + del self._data[key] + except KeyError: + pass + self._write() + + def _get_storage(self, key): + """Get a Storage object to get/set a credential. + + This Storage is a 'view' into the multistore. + + Args: + key: The key used to retrieve the credential + + Returns: + A Storage object that can be used to get/set this cred + """ + return self._Storage(self, key) diff --git a/appengine/dashdemo/oauth2client/old_run.py b/appengine/dashdemo/oauth2client/old_run.py new file mode 100644 index 0000000..da23358 --- /dev/null +++ b/appengine/dashdemo/oauth2client/old_run.py @@ -0,0 +1,160 @@ +# Copyright (C) 2013 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""This module holds the old run() function which is deprecated, the +tools.run_flow() function should be used in its place.""" + + +import logging +import socket +import sys +import webbrowser + +import gflags + +from oauth2client import client +from oauth2client import util +from tools import ClientRedirectHandler +from tools import ClientRedirectServer + + +FLAGS = gflags.FLAGS + +gflags.DEFINE_boolean('auth_local_webserver', True, + ('Run a local web server to handle redirects during ' + 'OAuth authorization.')) + +gflags.DEFINE_string('auth_host_name', 'localhost', + ('Host name to use when running a local web server to ' + 'handle redirects during OAuth authorization.')) + +gflags.DEFINE_multi_int('auth_host_port', [8080, 8090], + ('Port to use when running a local web server to ' + 'handle redirects during OAuth authorization.')) + + +@util.positional(2) +def run(flow, storage, http=None): + """Core code for a command-line application. + + The run() function is called from your application and runs through all + the steps to obtain credentials. It takes a Flow argument and attempts to + open an authorization server page in the user's default web browser. The + server asks the user to grant your application access to the user's data. + If the user grants access, the run() function returns new credentials. The + new credentials are also stored in the Storage argument, which updates the + file associated with the Storage object. + + It presumes it is run from a command-line application and supports the + following flags: + + --auth_host_name: Host name to use when running a local web server + to handle redirects during OAuth authorization. + (default: 'localhost') + + --auth_host_port: Port to use when running a local web server to handle + redirects during OAuth authorization.; + repeat this option to specify a list of values + (default: '[8080, 8090]') + (an integer) + + --[no]auth_local_webserver: Run a local web server to handle redirects + during OAuth authorization. + (default: 'true') + + Since it uses flags make sure to initialize the gflags module before + calling run(). + + Args: + flow: Flow, an OAuth 2.0 Flow to step through. + storage: Storage, a Storage to store the credential in. + http: An instance of httplib2.Http.request + or something that acts like it. + + Returns: + Credentials, the obtained credential. + """ + logging.warning('This function, oauth2client.tools.run(), and the use of ' + 'the gflags library are deprecated and will be removed in a future ' + 'version of the library.') + if FLAGS.auth_local_webserver: + success = False + port_number = 0 + for port in FLAGS.auth_host_port: + port_number = port + try: + httpd = ClientRedirectServer((FLAGS.auth_host_name, port), + ClientRedirectHandler) + except socket.error, e: + pass + else: + success = True + break + FLAGS.auth_local_webserver = success + if not success: + print 'Failed to start a local webserver listening on either port 8080' + print 'or port 9090. Please check your firewall settings and locally' + print 'running programs that may be blocking or using those ports.' + print + print 'Falling back to --noauth_local_webserver and continuing with', + print 'authorization.' + print + + if FLAGS.auth_local_webserver: + oauth_callback = 'http://%s:%s/' % (FLAGS.auth_host_name, port_number) + else: + oauth_callback = client.OOB_CALLBACK_URN + flow.redirect_uri = oauth_callback + authorize_url = flow.step1_get_authorize_url() + + if FLAGS.auth_local_webserver: + webbrowser.open(authorize_url, new=1, autoraise=True) + print 'Your browser has been opened to visit:' + print + print ' ' + authorize_url + print + print 'If your browser is on a different machine then exit and re-run' + print 'this application with the command-line parameter ' + print + print ' --noauth_local_webserver' + print + else: + print 'Go to the following link in your browser:' + print + print ' ' + authorize_url + print + + code = None + if FLAGS.auth_local_webserver: + httpd.handle_request() + if 'error' in httpd.query_params: + sys.exit('Authentication request was rejected.') + if 'code' in httpd.query_params: + code = httpd.query_params['code'] + else: + print 'Failed to find "code" in the query parameters of the redirect.' + sys.exit('Try running with --noauth_local_webserver.') + else: + code = raw_input('Enter verification code: ').strip() + + try: + credential = flow.step2_exchange(code, http=http) + except client.FlowExchangeError, e: + sys.exit('Authentication has failed: %s' % e) + + storage.put(credential) + credential.set_store(storage) + print 'Authentication successful.' + + return credential diff --git a/appengine/dashdemo/oauth2client/tools.py b/appengine/dashdemo/oauth2client/tools.py new file mode 100644 index 0000000..12c68bf --- /dev/null +++ b/appengine/dashdemo/oauth2client/tools.py @@ -0,0 +1,243 @@ +# Copyright (C) 2013 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Command-line tools for authenticating via OAuth 2.0 + +Do the OAuth 2.0 Web Server dance for a command line application. Stores the +generated credentials in a common file that is used by other example apps in +the same directory. +""" + +__author__ = 'jcgregorio@google.com (Joe Gregorio)' +__all__ = ['argparser', 'run_flow', 'run', 'message_if_missing'] + + +import BaseHTTPServer +import argparse +import httplib2 +import logging +import os +import socket +import sys +import webbrowser + +from oauth2client import client +from oauth2client import file +from oauth2client import util + +try: + from urlparse import parse_qsl +except ImportError: + from cgi import parse_qsl + +_CLIENT_SECRETS_MESSAGE = """WARNING: Please configure OAuth 2.0 + +To make this sample run you will need to populate the client_secrets.json file +found at: + + %s + +with information from the APIs Console . + +""" + +# run_parser is an ArgumentParser that contains command-line options expected +# by tools.run(). Pass it in as part of the 'parents' argument to your own +# ArgumentParser. +argparser = argparse.ArgumentParser(add_help=False) +argparser.add_argument('--auth_host_name', default='localhost', + help='Hostname when running a local web server.') +argparser.add_argument('--noauth_local_webserver', action='store_true', + default=False, help='Do not run a local web server.') +argparser.add_argument('--auth_host_port', default=[8080, 8090], type=int, + nargs='*', help='Port web server should listen on.') +argparser.add_argument('--logging_level', default='ERROR', + choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', + 'CRITICAL'], + help='Set the logging level of detail.') + + +class ClientRedirectServer(BaseHTTPServer.HTTPServer): + """A server to handle OAuth 2.0 redirects back to localhost. + + Waits for a single request and parses the query parameters + into query_params and then stops serving. + """ + query_params = {} + + +class ClientRedirectHandler(BaseHTTPServer.BaseHTTPRequestHandler): + """A handler for OAuth 2.0 redirects back to localhost. + + Waits for a single request and parses the query parameters + into the servers query_params and then stops serving. + """ + + def do_GET(s): + """Handle a GET request. + + Parses the query parameters and prints a message + if the flow has completed. Note that we can't detect + if an error occurred. + """ + s.send_response(200) + s.send_header("Content-type", "text/html") + s.end_headers() + query = s.path.split('?', 1)[-1] + query = dict(parse_qsl(query)) + s.server.query_params = query + s.wfile.write("Authentication Status") + s.wfile.write("

The authentication flow has completed.

") + s.wfile.write("") + + def log_message(self, format, *args): + """Do not log messages to stdout while running as command line program.""" + pass + + +@util.positional(3) +def run_flow(flow, storage, flags, http=None): + """Core code for a command-line application. + + The run() function is called from your application and runs through all the + steps to obtain credentials. It takes a Flow argument and attempts to open an + authorization server page in the user's default web browser. The server asks + the user to grant your application access to the user's data. If the user + grants access, the run() function returns new credentials. The new credentials + are also stored in the Storage argument, which updates the file associated + with the Storage object. + + It presumes it is run from a command-line application and supports the + following flags: + + --auth_host_name: Host name to use when running a local web server + to handle redirects during OAuth authorization. + (default: 'localhost') + + --auth_host_port: Port to use when running a local web server to handle + redirects during OAuth authorization.; + repeat this option to specify a list of values + (default: '[8080, 8090]') + (an integer) + + --[no]auth_local_webserver: Run a local web server to handle redirects + during OAuth authorization. + (default: 'true') + + The tools module defines an ArgumentParser the already contains the flag + definitions that run() requires. You can pass that ArgumentParser to your + ArgumentParser constructor: + + parser = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + parents=[tools.run_parser]) + flags = parser.parse_args(argv) + + Args: + flow: Flow, an OAuth 2.0 Flow to step through. + storage: Storage, a Storage to store the credential in. + flags: argparse.ArgumentParser, the command-line flags. + http: An instance of httplib2.Http.request + or something that acts like it. + + Returns: + Credentials, the obtained credential. + """ + logging.getLogger().setLevel(getattr(logging, flags.logging_level)) + if not flags.noauth_local_webserver: + success = False + port_number = 0 + for port in flags.auth_host_port: + port_number = port + try: + httpd = ClientRedirectServer((flags.auth_host_name, port), + ClientRedirectHandler) + except socket.error, e: + pass + else: + success = True + break + flags.noauth_local_webserver = not success + if not success: + print 'Failed to start a local webserver listening on either port 8080' + print 'or port 9090. Please check your firewall settings and locally' + print 'running programs that may be blocking or using those ports.' + print + print 'Falling back to --noauth_local_webserver and continuing with', + print 'authorization.' + print + + if not flags.noauth_local_webserver: + oauth_callback = 'http://%s:%s/' % (flags.auth_host_name, port_number) + else: + oauth_callback = client.OOB_CALLBACK_URN + flow.redirect_uri = oauth_callback + authorize_url = flow.step1_get_authorize_url() + + if not flags.noauth_local_webserver: + webbrowser.open(authorize_url, new=1, autoraise=True) + print 'Your browser has been opened to visit:' + print + print ' ' + authorize_url + print + print 'If your browser is on a different machine then exit and re-run this' + print 'application with the command-line parameter ' + print + print ' --noauth_local_webserver' + print + else: + print 'Go to the following link in your browser:' + print + print ' ' + authorize_url + print + + code = None + if not flags.noauth_local_webserver: + httpd.handle_request() + if 'error' in httpd.query_params: + sys.exit('Authentication request was rejected.') + if 'code' in httpd.query_params: + code = httpd.query_params['code'] + else: + print 'Failed to find "code" in the query parameters of the redirect.' + sys.exit('Try running with --noauth_local_webserver.') + else: + code = raw_input('Enter verification code: ').strip() + + try: + credential = flow.step2_exchange(code, http=http) + except client.FlowExchangeError, e: + sys.exit('Authentication has failed: %s' % e) + + storage.put(credential) + credential.set_store(storage) + print 'Authentication successful.' + + return credential + + +def message_if_missing(filename): + """Helpful message to display if the CLIENT_SECRETS file is missing.""" + + return _CLIENT_SECRETS_MESSAGE % filename + +try: + from old_run import run + from old_run import FLAGS +except ImportError: + def run(*args, **kwargs): + raise NotImplementedError( + 'The gflags library must be installed to use tools.run(). ' + 'Please install gflags or preferrably switch to using ' + 'tools.run_flow().') diff --git a/appengine/dashdemo/oauth2client/util.py b/appengine/dashdemo/oauth2client/util.py new file mode 100644 index 0000000..90dff15 --- /dev/null +++ b/appengine/dashdemo/oauth2client/util.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python +# +# Copyright 2010 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""Common utility library.""" + +__author__ = ['rafek@google.com (Rafe Kaplan)', + 'guido@google.com (Guido van Rossum)', +] +__all__ = [ + 'positional', + 'POSITIONAL_WARNING', + 'POSITIONAL_EXCEPTION', + 'POSITIONAL_IGNORE', +] + +import inspect +import logging +import types +import urllib +import urlparse + +try: + from urlparse import parse_qsl +except ImportError: + from cgi import parse_qsl + +logger = logging.getLogger(__name__) + +POSITIONAL_WARNING = 'WARNING' +POSITIONAL_EXCEPTION = 'EXCEPTION' +POSITIONAL_IGNORE = 'IGNORE' +POSITIONAL_SET = frozenset([POSITIONAL_WARNING, POSITIONAL_EXCEPTION, + POSITIONAL_IGNORE]) + +positional_parameters_enforcement = POSITIONAL_WARNING + +def positional(max_positional_args): + """A decorator to declare that only the first N arguments my be positional. + + This decorator makes it easy to support Python 3 style key-word only + parameters. For example, in Python 3 it is possible to write: + + def fn(pos1, *, kwonly1=None, kwonly1=None): + ... + + All named parameters after * must be a keyword: + + fn(10, 'kw1', 'kw2') # Raises exception. + fn(10, kwonly1='kw1') # Ok. + + Example: + To define a function like above, do: + + @positional(1) + def fn(pos1, kwonly1=None, kwonly2=None): + ... + + If no default value is provided to a keyword argument, it becomes a required + keyword argument: + + @positional(0) + def fn(required_kw): + ... + + This must be called with the keyword parameter: + + fn() # Raises exception. + fn(10) # Raises exception. + fn(required_kw=10) # Ok. + + When defining instance or class methods always remember to account for + 'self' and 'cls': + + class MyClass(object): + + @positional(2) + def my_method(self, pos1, kwonly1=None): + ... + + @classmethod + @positional(2) + def my_method(cls, pos1, kwonly1=None): + ... + + The positional decorator behavior is controlled by + util.positional_parameters_enforcement, which may be set to + POSITIONAL_EXCEPTION, POSITIONAL_WARNING or POSITIONAL_IGNORE to raise an + exception, log a warning, or do nothing, respectively, if a declaration is + violated. + + Args: + max_positional_arguments: Maximum number of positional arguments. All + parameters after the this index must be keyword only. + + Returns: + A decorator that prevents using arguments after max_positional_args from + being used as positional parameters. + + Raises: + TypeError if a key-word only argument is provided as a positional + parameter, but only if util.positional_parameters_enforcement is set to + POSITIONAL_EXCEPTION. + """ + def positional_decorator(wrapped): + def positional_wrapper(*args, **kwargs): + if len(args) > max_positional_args: + plural_s = '' + if max_positional_args != 1: + plural_s = 's' + message = '%s() takes at most %d positional argument%s (%d given)' % ( + wrapped.__name__, max_positional_args, plural_s, len(args)) + if positional_parameters_enforcement == POSITIONAL_EXCEPTION: + raise TypeError(message) + elif positional_parameters_enforcement == POSITIONAL_WARNING: + logger.warning(message) + else: # IGNORE + pass + return wrapped(*args, **kwargs) + return positional_wrapper + + if isinstance(max_positional_args, (int, long)): + return positional_decorator + else: + args, _, _, defaults = inspect.getargspec(max_positional_args) + return positional(len(args) - len(defaults))(max_positional_args) + + +def scopes_to_string(scopes): + """Converts scope value to a string. + + If scopes is a string then it is simply passed through. If scopes is an + iterable then a string is returned that is all the individual scopes + concatenated with spaces. + + Args: + scopes: string or iterable of strings, the scopes. + + Returns: + The scopes formatted as a single string. + """ + if isinstance(scopes, types.StringTypes): + return scopes + else: + return ' '.join(scopes) + + +def dict_to_tuple_key(dictionary): + """Converts a dictionary to a tuple that can be used as an immutable key. + + The resulting key is always sorted so that logically equivalent dictionaries + always produce an identical tuple for a key. + + Args: + dictionary: the dictionary to use as the key. + + Returns: + A tuple representing the dictionary in it's naturally sorted ordering. + """ + return tuple(sorted(dictionary.items())) + + +def _add_query_parameter(url, name, value): + """Adds a query parameter to a url. + + Replaces the current value if it already exists in the URL. + + Args: + url: string, url to add the query parameter to. + name: string, query parameter name. + value: string, query parameter value. + + Returns: + Updated query parameter. Does not update the url if value is None. + """ + if value is None: + return url + else: + parsed = list(urlparse.urlparse(url)) + q = dict(parse_qsl(parsed[4])) + q[name] = value + parsed[4] = urllib.urlencode(q) + return urlparse.urlunparse(parsed) diff --git a/appengine/dashdemo/oauth2client/xsrfutil.py b/appengine/dashdemo/oauth2client/xsrfutil.py new file mode 100644 index 0000000..7e1fe5c --- /dev/null +++ b/appengine/dashdemo/oauth2client/xsrfutil.py @@ -0,0 +1,113 @@ +#!/usr/bin/python2.5 +# +# Copyright 2010 the Melange authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Helper methods for creating & verifying XSRF tokens.""" + +__authors__ = [ + '"Doug Coker" ', + '"Joe Gregorio" ', +] + + +import base64 +import hmac +import os # for urandom +import time + +from oauth2client import util + + +# Delimiter character +DELIMITER = ':' + +# 1 hour in seconds +DEFAULT_TIMEOUT_SECS = 1*60*60 + +@util.positional(2) +def generate_token(key, user_id, action_id="", when=None): + """Generates a URL-safe token for the given user, action, time tuple. + + Args: + key: secret key to use. + user_id: the user ID of the authenticated user. + action_id: a string identifier of the action they requested + authorization for. + when: the time in seconds since the epoch at which the user was + authorized for this action. If not set the current time is used. + + Returns: + A string XSRF protection token. + """ + when = when or int(time.time()) + digester = hmac.new(key) + digester.update(str(user_id)) + digester.update(DELIMITER) + digester.update(action_id) + digester.update(DELIMITER) + digester.update(str(when)) + digest = digester.digest() + + token = base64.urlsafe_b64encode('%s%s%d' % (digest, + DELIMITER, + when)) + return token + + +@util.positional(3) +def validate_token(key, token, user_id, action_id="", current_time=None): + """Validates that the given token authorizes the user for the action. + + Tokens are invalid if the time of issue is too old or if the token + does not match what generateToken outputs (i.e. the token was forged). + + Args: + key: secret key to use. + token: a string of the token generated by generateToken. + user_id: the user ID of the authenticated user. + action_id: a string identifier of the action they requested + authorization for. + + Returns: + A boolean - True if the user is authorized for the action, False + otherwise. + """ + if not token: + return False + try: + decoded = base64.urlsafe_b64decode(str(token)) + token_time = long(decoded.split(DELIMITER)[-1]) + except (TypeError, ValueError): + return False + if current_time is None: + current_time = time.time() + # If the token is too old it's not valid. + if current_time - token_time > DEFAULT_TIMEOUT_SECS: + return False + + # The given token should match the generated one with the same time. + expected_token = generate_token(key, user_id, action_id=action_id, + when=token_time) + if len(token) != len(expected_token): + return False + + # Perform constant time comparison to avoid timing attacks + different = 0 + for x, y in zip(token, expected_token): + different |= ord(x) ^ ord(y) + if different: + return False + + return True diff --git a/appengine/dashdemo/oauth_utils.py b/appengine/dashdemo/oauth_utils.py new file mode 100644 index 0000000..043c329 --- /dev/null +++ b/appengine/dashdemo/oauth_utils.py @@ -0,0 +1,15 @@ +import os + +from oauth2client.appengine import oauth2decorator_from_clientsecrets + + +# CLIENT_SECRETS, name of a file containing the OAuth 2.0 information for this +# application, including client_id and client_secret, which are found +# on the API Access tab on the Google APIs +# Console +CLIENT_SECRETS = os.path.join(os.path.dirname(__file__), 'client_secrets.json') + + +decorator = oauth2decorator_from_clientsecrets( + CLIENT_SECRETS, + scope='https://www.googleapis.com/auth/bigquery') diff --git a/appengine/dashdemo/uritemplate/__init__.py b/appengine/dashdemo/uritemplate/__init__.py new file mode 100644 index 0000000..5d0ebce --- /dev/null +++ b/appengine/dashdemo/uritemplate/__init__.py @@ -0,0 +1,147 @@ +# Early, and incomplete implementation of -04. +# +import re +import urllib + +RESERVED = ":/?#[]@!$&'()*+,;=" +OPERATOR = "+./;?|!@" +EXPLODE = "*+" +MODIFIER = ":^" +TEMPLATE = re.compile(r"{(?P[\+\./;\?|!@])?(?P[^}]+)}", re.UNICODE) +VAR = re.compile(r"^(?P[^=\+\*:\^]+)((?P[\+\*])|(?P[:\^]-?[0-9]+))?(=(?P.*))?$", re.UNICODE) + +def _tostring(varname, value, explode, operator, safe=""): + if type(value) == type([]): + if explode == "+": + return ",".join([varname + "." + urllib.quote(x, safe) for x in value]) + else: + return ",".join([urllib.quote(x, safe) for x in value]) + if type(value) == type({}): + keys = value.keys() + keys.sort() + if explode == "+": + return ",".join([varname + "." + urllib.quote(key, safe) + "," + urllib.quote(value[key], safe) for key in keys]) + else: + return ",".join([urllib.quote(key, safe) + "," + urllib.quote(value[key], safe) for key in keys]) + else: + return urllib.quote(value, safe) + + +def _tostring_path(varname, value, explode, operator, safe=""): + joiner = operator + if type(value) == type([]): + if explode == "+": + return joiner.join([varname + "." + urllib.quote(x, safe) for x in value]) + elif explode == "*": + return joiner.join([urllib.quote(x, safe) for x in value]) + else: + return ",".join([urllib.quote(x, safe) for x in value]) + elif type(value) == type({}): + keys = value.keys() + keys.sort() + if explode == "+": + return joiner.join([varname + "." + urllib.quote(key, safe) + joiner + urllib.quote(value[key], safe) for key in keys]) + elif explode == "*": + return joiner.join([urllib.quote(key, safe) + joiner + urllib.quote(value[key], safe) for key in keys]) + else: + return ",".join([urllib.quote(key, safe) + "," + urllib.quote(value[key], safe) for key in keys]) + else: + if value: + return urllib.quote(value, safe) + else: + return "" + +def _tostring_query(varname, value, explode, operator, safe=""): + joiner = operator + varprefix = "" + if operator == "?": + joiner = "&" + varprefix = varname + "=" + if type(value) == type([]): + if 0 == len(value): + return "" + if explode == "+": + return joiner.join([varname + "=" + urllib.quote(x, safe) for x in value]) + elif explode == "*": + return joiner.join([urllib.quote(x, safe) for x in value]) + else: + return varprefix + ",".join([urllib.quote(x, safe) for x in value]) + elif type(value) == type({}): + if 0 == len(value): + return "" + keys = value.keys() + keys.sort() + if explode == "+": + return joiner.join([varname + "." + urllib.quote(key, safe) + "=" + urllib.quote(value[key], safe) for key in keys]) + elif explode == "*": + return joiner.join([urllib.quote(key, safe) + "=" + urllib.quote(value[key], safe) for key in keys]) + else: + return varprefix + ",".join([urllib.quote(key, safe) + "," + urllib.quote(value[key], safe) for key in keys]) + else: + if value: + return varname + "=" + urllib.quote(value, safe) + else: + return varname + +TOSTRING = { + "" : _tostring, + "+": _tostring, + ";": _tostring_query, + "?": _tostring_query, + "/": _tostring_path, + ".": _tostring_path, + } + + +def expand(template, vars): + def _sub(match): + groupdict = match.groupdict() + operator = groupdict.get('operator') + if operator is None: + operator = '' + varlist = groupdict.get('varlist') + + safe = "@" + if operator == '+': + safe = RESERVED + varspecs = varlist.split(",") + varnames = [] + defaults = {} + for varspec in varspecs: + m = VAR.search(varspec) + groupdict = m.groupdict() + varname = groupdict.get('varname') + explode = groupdict.get('explode') + partial = groupdict.get('partial') + default = groupdict.get('default') + if default: + defaults[varname] = default + varnames.append((varname, explode, partial)) + + retval = [] + joiner = operator + prefix = operator + if operator == "+": + prefix = "" + joiner = "," + if operator == "?": + joiner = "&" + if operator == "": + joiner = "," + for varname, explode, partial in varnames: + if varname in vars: + value = vars[varname] + #if not value and (type(value) == type({}) or type(value) == type([])) and varname in defaults: + if not value and value != "" and varname in defaults: + value = defaults[varname] + elif varname in defaults: + value = defaults[varname] + else: + continue + retval.append(TOSTRING[operator](varname, value, explode, operator, safe=safe)) + if "".join(retval): + return prefix + joiner.join(retval) + else: + return "" + + return TEMPLATE.sub(_sub, template) diff --git a/appengine/guestbook-training b/appengine/guestbook-training new file mode 160000 index 0000000..a9d16b0 --- /dev/null +++ b/appengine/guestbook-training @@ -0,0 +1 @@ +Subproject commit a9d16b05b49e87983c451b35858b5ce9b4866467 diff --git a/appengine/remote-api-blacklist/app.yaml b/appengine/remote-api-blacklist/app.yaml new file mode 100644 index 0000000..987c81e --- /dev/null +++ b/appengine/remote-api-blacklist/app.yaml @@ -0,0 +1,15 @@ +# Sample datastore_admin application. +# You can use the -A flag for appcfg to upload this to your application. +# For more information, see +# http://developers.google.com/appengine/docs/adminconsole/ + + +application: [APPID] +version: [VERSIONNUMBER] +runtime: python27 +api_version: 1 +threadsafe: true + +handlers: +- url: /mod_remote_api.* + script: remote_api_handler_mod.application diff --git a/appengine/remote-api-blacklist/appengine_config.py b/appengine/remote-api-blacklist/appengine_config.py new file mode 100644 index 0000000..b72ceab --- /dev/null +++ b/appengine/remote-api-blacklist/appengine_config.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python +# +# Copyright 2010 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""Sample remote_api appengine_config for copying datastore across apps. + +For more information, see +http://developers.google.com/appengine/docs/adminconsole/ + +Note that this appengine_config.py file is the same one that you would +use for appstats; if you are bundling this with your existing app you may +wish to copy the version from +google/appengine/ext/appstats/sample_appengine_config.py instead. +""" + +######################################### +# Remote_API Authentication configuration. +# +# See google/appengine/ext/remote_api/handler.py for more information. +# For datastore_admin datastore copy, you should set the source appid +# value. 'HTTP_X_APPENGINE_INBOUND_APPID', ['trusted source appid here'] +# +remoteapi_CUSTOM_ENVIRONMENT_AUTHENTICATION = ( + 'HTTP_X_APPENGINE_INBOUND_APPID', ['APPID']) + + diff --git a/appengine/remote-api-blacklist/remote_api_handler_mod.py b/appengine/remote-api-blacklist/remote_api_handler_mod.py new file mode 100644 index 0000000..78a08b6 --- /dev/null +++ b/appengine/remote-api-blacklist/remote_api_handler_mod.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python + + +"""Modification of the ApiCallHandler in order to add a blacklist. +""" + +import logging + +import wsgiref.handlers + +from google.appengine.api import users +from google.appengine.ext import webapp +from google.appengine.ext.remote_api import handler as remote_api_handler + +email_blacklist = ['user1@email.com', 'user2@email.com'] + + +class ApiCallHandler(remote_api_handler.ApiCallHandler): + + def CheckIsAdmin(self): + user_email = users.get_current_user().email() + + if user_email in email_blacklist: + logging.warning('User %s is denied.' % user_email) + self.response.set_status(403) + self.response.out.write('Usage not permitted. ' + 'Please contact your administrator') + self.response.headers['Content-Type'] = 'text/plain' + return False + return remote_api_handler.ApiCallHandler.CheckIsAdmin(self) + + +application = webapp.WSGIApplication([('.*', ApiCallHandler)]) + + +def main(): + # remote_api_handler.main() + wsgiref.handlers.CGIHandler().run(application) + + +if __name__ == '__main__': + main() diff --git a/turtle/lindmayer.py b/turtle/lindmayer.py new file mode 100644 index 0000000..f576537 --- /dev/null +++ b/turtle/lindmayer.py @@ -0,0 +1,27 @@ +from pysvg.turtle import Turtle, Vector +from pysvg.structure import Svg + +def testLindenMayer(): + s=Svg(0, 0, 2000, 2000) + commands='F+F-F-FF+F+F-F+F+F-F-FF+F+F-F+F+F-F-FF+F+F-F+F+F-F-FF+F+F-F' + t=Turtle() + t.moveTo(Vector(500,250)) + t.penDown() + angle=90 + distance=40 + for cmd in commands: + print(cmd) + if cmd=='F': + t.forward(distance) + elif cmd=='+': + t.right(angle) + elif cmd=='-': + t.left(angle) + print(t.getPosition()) + t.penDown() + print (t.getXML()) + s=t.addTurtlePathToSVG(s) + s.save('./testoutput/testTurtle.svg') + +if __name__ == '__main__': + testLindenMayer() diff --git a/turtle/myshapes.py b/turtle/myshapes.py new file mode 100644 index 0000000..50dc7bf --- /dev/null +++ b/turtle/myshapes.py @@ -0,0 +1,25 @@ +from turtle import * +import doctest + +pendown() + + +def angle_calc(sides): + """ Angle calculator + This will calculate the angles + >>> angle_calc(4) + 90 + """ + return 360.0/sides + + +def shape(sides, size): + for i in range(sides): + forward(size) + left(angle_calc(sides+i)) + +if __name__ == '__main__': + doctest.testmod() + shape(4, 100) + while True: + pass diff --git a/turtle/shapes.py b/turtle/shapes.py new file mode 100644 index 0000000..6aa4967 --- /dev/null +++ b/turtle/shapes.py @@ -0,0 +1,20 @@ +from turtle import * + +pendown() + + +def shape(sides, size, angle): + for i in range(sides): + forward(size) + left(angle) + +shape(4, 100, 90) +forward(100) +shape(3, 90, 120) +forward(100) +shape(4, 80, 90) +forward(100) +shape(4, 70, 90) + +while True: + pass diff --git a/turtle/shapes_debug.py b/turtle/shapes_debug.py new file mode 100644 index 0000000..4e2b1e1 --- /dev/null +++ b/turtle/shapes_debug.py @@ -0,0 +1,27 @@ +from turtle import * + +pendown() + + +def angle_calc(sides): + """Angle Calculator + + >>> angle_calc(4) + 90 + """ + return 360//sides + + +def shape(sides, size): + for i in range(sides): + forward(size) + left(angle_calc(sides)) + return True + + +if __name__ == '__main__': + import doctest + doctest.testmod() + shape(4, 100) + while True: + pass diff --git a/turtle/shapes_debug2.py b/turtle/shapes_debug2.py new file mode 100644 index 0000000..a8f8a3d --- /dev/null +++ b/turtle/shapes_debug2.py @@ -0,0 +1,25 @@ +from turtle import * +import doctest + +pendown() + + +def angle_calc(sides): + """ Angle calculator + This will calculate the angles + >>> angle_calc(4) + 90 + """ + return 361.0/sides + + +def shape(sides, size): + for i in range(sides): + forward(size) + left(angle_calc(sides)) + +if __name__ == '__main__': + doctest.testmod() + shape(4, 100) + while True: + pass diff --git a/turtle/testoutput/testTurtle.svg b/turtle/testoutput/testTurtle.svg new file mode 100644 index 0000000..f85fc2a --- /dev/null +++ b/turtle/testoutput/testTurtle.svg @@ -0,0 +1,3 @@ + + +