├── Charles Sessions └── setDesktopSync-uploadKeyFileWrite.chls ├── Documentation ├── AsciiEyeFi.txt ├── EyeFi Protocol.txt ├── EyeFi Technicals.doc └── EyeFiFirmwareStrings.txt ├── FireEyeFi ├── chrome.manifest ├── chrome │ └── content │ │ └── sample.xul └── install.rdf ├── Old ├── EyeFiServer.py └── EyeFiServerv2.py ├── Release 2.0 ├── DebugSettings.ini ├── DefaultSettings.ini ├── EyeFiCrypto.py ├── EyeFiCrypto.pyc ├── EyeFiLogo.jpg ├── EyeFiLogo.jpg.tar ├── EyeFiSOAPMessages.py ├── EyeFiSOAPMessages.pyc ├── EyeFiServer.py ├── EyeFiServerRegressionTests.py ├── configobj.py ├── configobj.pyc └── pictures │ ├── CIMG1859.JPG │ ├── CIMG1860.JPG │ ├── CIMG1861.JPG │ └── Thumbs.db ├── eyefi-config.py └── rebootEyeFi.py /Charles Sessions/setDesktopSync-uploadKeyFileWrite.chls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tachang/EyeFiServer/4c473e76bcf97472393773ffe352c9078ec1bf39/Charles Sessions/setDesktopSync-uploadKeyFileWrite.chls -------------------------------------------------------------------------------- /Documentation/EyeFi Protocol.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The EyeFi server listens on port 59278. 5 | 6 | On startup the EyeFi card scans the subnet it is currently on and attempts to get an IP address via DHCP. 7 | 8 | 9 | card -> server = md5sum( mac + upload_key + nonce); 10 | credentialString = "0018560304f8" + "c686e547e3728c63a8f78729c1592757" + "99208c155fc1883579cf0812ec0fe6d2" 11 | 12 | 13 | server -> card = md5sum( mac + nonce + upload_key); 14 | 15 | Step 1) 16 | 17 | The EyeFi card attempts to POST to "/api/soap/eyefilm/v1". One of the HTTP headers sent is SoapAction: "urn:StartSession". 18 | 19 | The start session request has the follow elements: 20 | 21 | transfermode 22 | macaddress 23 | cnonce 24 | transfermodetimestamp 25 | 26 | StartSession response: 27 | 28 | 29 | The following is an actual conversation between the Eye-Fi card and server. 30 | 31 | Eye-Fi Card: 32 | 33 | POST /api/soap/eyefilm/v1 HTTP/1.1 34 | Host: api.eye.fi 35 | User-Agent: Eye-Fi Card/2.0001 36 | Accept: text/xml, application/soap 37 | Connection: Keep-Alive 38 | SOAPAction: "urn:StartSession" 39 | Content-Length: 407 40 | 41 | 42 | 43 | 44 | 45 | 0018560304f8 46 | 9219c72db0ecbd7e585bb10551f6bc38 47 | 2 48 | 315532800 49 | 50 | 51 | 52 | 53 | 54 | Server: 55 | 56 | 57 | HTTP/1.1 200 OK 58 | Server: Eye-Fi Agent/2.0.4.0 (Windows XP SP2) 59 | Date: Fri, 20 Mar 2009 18:17:09 GMT 60 | Pragma: no-cache 61 | Server: Eye-Fi Agent/2.0.4.0 (Windows XP SP2) 62 | Content-Type: text/xml; charset="utf-8" 63 | Content-Length: 483 64 | 65 | 66 | 67 | 68 | 69 | f138ce5977a8962a089b87e17155e537 70 | 99208c155fc1883579cf0812ec0fe6d2 71 | 2 72 | 1230268824 73 | false 74 | 75 | 76 | 77 | 78 | 79 | GetPhotoStatus allows the Eye-Fi card to query the server as to the current uploaded status of a file. Even more important is that it authenticates the card to the server by the use of the field. Essentially if the credential is correct the server should allow the filename, filesize, and filesignature to be uploaded. 80 | 81 | 82 | POST /api/soap/eyefilm/v1 HTTP/1.1 83 | Host: api.eye.fi 84 | User-Agent: Eye-Fi Card/2.0001 85 | Accept: text/xml, application/soap 86 | Connection: Keep-Alive 87 | SOAPAction: "urn:GetPhotoStatus" 88 | Content-Length: 461 89 | 90 | 91 | 92 | 93 | 94 | 10ff036d3861ed3d1c47eb52d14841d2 95 | 0018560304f8 96 | CIMG1738.JPG.tar 97 | 4518912 98 | 1077ffb9ac2718b116a33475ad809bf7 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /Documentation/EyeFi Technicals.doc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tachang/EyeFiServer/4c473e76bcf97472393773ffe352c9078ec1bf39/Documentation/EyeFi Technicals.doc -------------------------------------------------------------------------------- /FireEyeFi/chrome.manifest: -------------------------------------------------------------------------------- 1 | content sample chrome/content/ 2 | overlay chrome://browser/content/browser.xul chrome://sample/content/sample.xul 3 | -------------------------------------------------------------------------------- /FireEyeFi/chrome/content/sample.xul: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /FireEyeFi/install.rdf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | sample@example.net 8 | 1.0 9 | 2 10 | 11 | 13 | 14 | 15 | {ec8030f7-c20a-464f-9b0e-13a3a9e97384} 16 | 1.5 17 | 3.0.* 18 | 19 | 20 | 21 | 22 | sample 23 | A test extension 24 | Your Name Here 25 | http://www.example.com/ 26 | 27 | 28 | -------------------------------------------------------------------------------- /Old/EyeFiServer.py: -------------------------------------------------------------------------------- 1 | """ 2 | * Copyright (c) 2009, Jeffrey Tchang 3 | * 4 | * All rights reserved. 5 | * 6 | * 7 | * THIS SOFTWARE IS PROVIDED ''AS IS'' AND ANY 8 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 9 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 10 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY 11 | * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 12 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 13 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 14 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 15 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 16 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 17 | """ 18 | 19 | import string 20 | import cgi 21 | import time 22 | 23 | import sys 24 | import os 25 | import socket 26 | import thread 27 | import StringIO 28 | 29 | import hashlib 30 | import binascii 31 | import select 32 | import tarfile 33 | 34 | import xml.sax 35 | from xml.sax.handler import ContentHandler 36 | import xml.dom.minidom 37 | 38 | from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer 39 | import BaseHTTPServer 40 | 41 | import SocketServer 42 | 43 | import logging 44 | 45 | """ 46 | General architecture notes 47 | 48 | 49 | This is a standalone Eye-Fi Server that is designed to take the place of the Eye-Fi Manager. 50 | 51 | 52 | Starting this server creates a listener on port 59278. I use the BaseHTTPServer class included 53 | with Python. I look for specific POST/GET request URLs and execute functions based on those 54 | URLs. 55 | 56 | 57 | Currently all files are downloaded to the directory in which this script is run. 58 | 59 | 60 | To use this script you need to have your Eye-Fi upload key. 61 | It is in C:\Documents and Settings\\Application Data\Eye-Fi\Settings.xml 62 | 63 | Simple search for "eyeFiUploadKey" and replace it with your key. 64 | 65 | """ 66 | 67 | 68 | 69 | 70 | # Create the main logger 71 | eyeFiLogger = logging.Logger("eyeFiLogger",logging.DEBUG) 72 | 73 | # Create two handlers. One to print to the log and one to print to the console 74 | consoleHandler = logging.StreamHandler(sys.stdout) 75 | fileHandler = logging.FileHandler("EyeFiServer.log","w",encoding=None, delay=0) 76 | 77 | # Set how both handlers will print the pretty log events 78 | eyeFiLoggingFormat = logging.Formatter("[%(asctime)s][%(funcName)s] - %(message)s",'%m/%d/%y %I:%M%p') 79 | consoleHandler.setFormatter(eyeFiLoggingFormat) 80 | fileHandler.setFormatter(eyeFiLoggingFormat) 81 | 82 | # Append both handlers to the main Eye Fi Server logger 83 | eyeFiLogger.addHandler(consoleHandler) 84 | eyeFiLogger.addHandler(fileHandler) 85 | 86 | 87 | def shiftyshiftydothething 88 | #copyright 2009 this wholes file to john deweese 89 | #specials price for you 200 baht 90 | 91 | 92 | # Eye Fi XML SAX ContentHandler 93 | class EyeFiContentHandler(ContentHandler): 94 | 95 | # These are the element names that I want to parse out of the XML 96 | elementNamesToExtract = ["macaddress","cnonce","transfermode","transfermodetimestamp","fileid","filename","filesize","filesignature"] 97 | 98 | # For each of the element names I create a dictionary with the value to False 99 | elementsToExtract = {} 100 | 101 | # Where to put the extracted values 102 | extractedElements = {} 103 | 104 | 105 | def __init__(self): 106 | self.extractedElements = {} 107 | 108 | for elementName in self.elementNamesToExtract: 109 | self.elementsToExtract[elementName] = False 110 | 111 | def startElement(self, name, attributes): 112 | 113 | # If the name of the element is a key in the dictionary elementsToExtract 114 | # set the value to True 115 | if name in self.elementsToExtract: 116 | self.elementsToExtract[name] = True 117 | 118 | def endElement(self, name): 119 | 120 | # If the name of the element is a key in the dictionary elementsToExtract 121 | # set the value to False 122 | if name in self.elementsToExtract: 123 | self.elementsToExtract[name] = False 124 | 125 | 126 | def characters(self, content): 127 | 128 | for elementName in self.elementsToExtract: 129 | if self.elementsToExtract[elementName] == True: 130 | self.extractedElements[elementName] = content 131 | 132 | # Implements an EyeFi server 133 | class EyeFiServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer): 134 | 135 | 136 | def server_bind(self): 137 | 138 | BaseHTTPServer.HTTPServer.server_bind(self) 139 | self.socket.settimeout(None) 140 | self.run = True 141 | 142 | def get_request(self): 143 | while self.run: 144 | try: 145 | connection, address = self.socket.accept() 146 | eyeFiLogger.debug("Incoming connection from client %s" % address[0]) 147 | 148 | connection.settimeout(None) 149 | return (connection, address) 150 | 151 | except socket.timeout: 152 | pass 153 | 154 | def stop(self): 155 | self.run = False 156 | 157 | def serve(self): 158 | while self.run: 159 | self.handle_request() 160 | 161 | 162 | 163 | # This class is responsible for handling HTTP requests passed to it. 164 | # It implements the two most common HTTP methods, do_GET() and do_POST() 165 | 166 | class EyeFiRequestHandler(BaseHTTPRequestHandler): 167 | 168 | protocol_version = 'HTTP/1.1' 169 | sys_version = "" 170 | server_version = "Eye-Fi Agent/2.0.4.0 (Windows XP SP2)" 171 | 172 | 173 | def do_GET(self): 174 | eyeFiLogger.debug(self.command + " " + self.path + " " + self.request_version) 175 | 176 | self.send_response(200) 177 | self.send_header('Content-type','text/html') 178 | # I should be sending a Content-Length header with HTTP/1.1 but I am being lazy 179 | # self.send_header('Content-length', '123') 180 | self.end_headers() 181 | self.wfile.write(self.client_address) 182 | self.wfile.write(self.headers) 183 | self.close_connection = 0 184 | 185 | 186 | def do_POST(self): 187 | eyeFiLogger.debug(self.command + " " + self.path + " " + self.request_version) 188 | 189 | SOAPAction = "" 190 | contentLength = "" 191 | 192 | # Loop through all the request headers and pick out ones that are relevant 193 | 194 | eyeFiLogger.debug("Headers received in POST request:") 195 | for headerName in self.headers.keys(): 196 | for headerValue in self.headers.getheaders(headerName): 197 | 198 | if( headerName == "soapaction"): 199 | SOAPAction = headerValue 200 | 201 | if( headerName == "content-length"): 202 | contentLength = int(headerValue) 203 | 204 | eyeFiLogger.debug(headerName + ": " + headerValue) 205 | 206 | 207 | # Read contentLength bytes worth of data 208 | eyeFiLogger.debug("Attempting to read " + str(contentLength) + " bytes of data") 209 | postData = self.rfile.read(contentLength) 210 | eyeFiLogger.debug("Finished reading " + str(contentLength) + " bytes of data") 211 | 212 | # TODO: Implement some kind of visual progress bar 213 | # bytesRead = 0 214 | # postData = "" 215 | 216 | # while(bytesRead < contentLength): 217 | # postData = postData + self.rfile.read(1) 218 | # bytesRead = bytesRead + 1 219 | 220 | # if(bytesRead % 10000 == 0): 221 | # print "#", 222 | 223 | 224 | # Perform action based on path and SOAPAction 225 | # A SOAPAction of StartSession indicates the beginning of an EyeFi 226 | # authentication request 227 | if((self.path == "/api/soap/eyefilm/v1") and (SOAPAction == "\"urn:StartSession\"")): 228 | eyeFiLogger.debug("Got StartSession request") 229 | response = self.startSession(postData) 230 | contentLength = len(response) 231 | 232 | eyeFiLogger.debug("StartSession response: " + response) 233 | 234 | self.send_response(200) 235 | self.send_header('Date', self.date_time_string()) 236 | self.send_header('Pragma','no-cache') 237 | self.send_header('Server','Eye-Fi Agent/2.0.4.0 (Windows XP SP2)') 238 | self.send_header('Content-Type','text/xml; charset="utf-8"') 239 | self.send_header('Content-Length', contentLength) 240 | self.end_headers() 241 | 242 | self.wfile.write(response) 243 | self.wfile.flush() 244 | self.handle_one_request() 245 | 246 | # GetPhotoStatus allows the card to query if a photo has been uploaded 247 | # to the server yet 248 | if((self.path == "/api/soap/eyefilm/v1") and (SOAPAction == "\"urn:GetPhotoStatus\"")): 249 | eyeFiLogger.debug("Got GetPhotoStatus request") 250 | 251 | response = self.getPhotoStatus(postData) 252 | contentLength = len(response) 253 | 254 | eyeFiLogger.debug("GetPhotoStatus response: " + response) 255 | 256 | self.send_response(200) 257 | self.send_header('Date', self.date_time_string()) 258 | self.send_header('Pragma','no-cache') 259 | self.send_header('Server','Eye-Fi Agent/2.0.4.0 (Windows XP SP2)') 260 | self.send_header('Content-Type','text/xml; charset="utf-8"') 261 | self.send_header('Content-Length', contentLength) 262 | self.end_headers() 263 | 264 | self.wfile.write(response) 265 | self.wfile.flush() 266 | 267 | 268 | # If the URL is upload and there is no SOAPAction the card is ready to send a picture to me 269 | if((self.path == "/api/soap/eyefilm/v1/upload") and (SOAPAction == "")): 270 | eyeFiLogger.debug("Got upload request") 271 | response = self.uploadPhoto(postData) 272 | contentLength = len(response) 273 | 274 | eyeFiLogger.debug("Upload response: " + response) 275 | 276 | self.send_response(200) 277 | self.send_header('Date', self.date_time_string()) 278 | self.send_header('Pragma','no-cache') 279 | self.send_header('Server','Eye-Fi Agent/2.0.4.0 (Windows XP SP2)') 280 | self.send_header('Content-Type','text/xml; charset="utf-8"') 281 | self.send_header('Content-Length', contentLength) 282 | self.end_headers() 283 | 284 | self.wfile.write(response) 285 | self.wfile.flush() 286 | 287 | # If the URL is upload and SOAPAction is MarkLastPhotoInRoll 288 | if((self.path == "/api/soap/eyefilm/v1") and (SOAPAction == "\"urn:MarkLastPhotoInRoll\"")): 289 | eyeFiLogger.debug("Got MarkLastPhotoInRoll request") 290 | response = self.markLastPhotoInRoll(postData) 291 | contentLength = len(response) 292 | 293 | eyeFiLogger.debug("MarkLastPhotoInRoll response: " + response) 294 | self.send_response(200) 295 | self.send_header('Date', self.date_time_string()) 296 | self.send_header('Pragma','no-cache') 297 | self.send_header('Server','Eye-Fi Agent/2.0.4.0 (Windows XP SP2)') 298 | self.send_header('Content-Type','text/xml; charset="utf-8"') 299 | self.send_header('Content-Length', contentLength) 300 | self.send_header('Connection', 'Close') 301 | self.end_headers() 302 | 303 | self.wfile.write(response) 304 | self.wfile.flush() 305 | 306 | eyeFiLogger.debug("Connection closed.") 307 | 308 | 309 | # Handles MarkLastPhotoInRoll action 310 | def markLastPhotoInRoll(self,postData): 311 | # Create the XML document to send back 312 | doc = xml.dom.minidom.Document() 313 | 314 | SOAPElement = doc.createElementNS("http://schemas.xmlsoap.org/soap/envelope/","SOAP-ENV:Envelope") 315 | SOAPElement.setAttribute("xmlns:SOAP-ENV","http://schemas.xmlsoap.org/soap/envelope/") 316 | SOAPBodyElement = doc.createElement("SOAP-ENV:Body") 317 | 318 | markLastPhotoInRollResponseElement = doc.createElement("MarkLastPhotoInRollResponse") 319 | 320 | SOAPBodyElement.appendChild(markLastPhotoInRollResponseElement) 321 | SOAPElement.appendChild(SOAPBodyElement) 322 | doc.appendChild(SOAPElement) 323 | 324 | return doc.toxml(encoding="UTF-8") 325 | 326 | 327 | # Handles receiving the actual photograph from the card. 328 | # postData will most likely contain multipart binary post data that needs to be parsed 329 | def uploadPhoto(self,postData): 330 | 331 | # Take the postData string and work with it as if it were a file object 332 | postDataInMemoryFile = StringIO.StringIO(postData) 333 | 334 | # Get the content-type header which looks something like this 335 | # content-type: multipart/form-data; boundary=---------------------------02468ace13579bdfcafebabef00d 336 | contentTypeHeader = self.headers.getheaders('content-type').pop() 337 | eyeFiLogger.debug(contentTypeHeader) 338 | 339 | # Extract the boundary parameter in the content-type header 340 | headerParameters = contentTypeHeader.split(";") 341 | eyeFiLogger.debug(headerParameters) 342 | 343 | boundary = headerParameters[1].split("=") 344 | boundary = boundary[1].strip() 345 | eyeFiLogger.debug("Extracted boundary: " + boundary) 346 | 347 | # eyeFiLogger.debug("uploadPhoto postData: " + postData) 348 | 349 | # Parse the multipart/form-data 350 | form = cgi.parse_multipart(postDataInMemoryFile, {"boundary":boundary,"content-disposition":self.headers.getheaders('content-disposition')}) 351 | eyeFiLogger.debug("Available multipart/form-data: " + str(form.keys())) 352 | 353 | # Parse the SOAPENVELOPE using the EyeFiContentHandler() 354 | soapEnvelope = form['SOAPENVELOPE'][0] 355 | eyeFiLogger.debug("SOAPENVELOPE: " + soapEnvelope) 356 | handler = EyeFiContentHandler() 357 | parser = xml.sax.parseString(soapEnvelope,handler) 358 | 359 | eyeFiLogger.debug("Extracted elements: " + str(handler.extractedElements)) 360 | 361 | 362 | imageTarfileName = handler.extractedElements["filename"] 363 | fileHandle = open(imageTarfileName, 'wb') 364 | eyeFiLogger.debug("Opened file " + imageTarfileName + " for binary writing") 365 | 366 | fileHandle.write(form['FILENAME'][0]) 367 | eyeFiLogger.debug("Wrote file " + imageTarfileName) 368 | 369 | fileHandle.close() 370 | eyeFiLogger.debug("Closed file " + imageTarfileName) 371 | 372 | eyeFiLogger.debug("Extracting TAR file " + imageTarfileName) 373 | imageTarfile = tarfile.open(imageTarfileName) 374 | imageTarfile.extractall() 375 | 376 | eyeFiLogger.debug("Closing TAR file " + imageTarfileName) 377 | imageTarfile.close() 378 | 379 | eyeFiLogger.debug("Deleting TAR file " + imageTarfileName) 380 | os.remove(imageTarfileName) 381 | 382 | # Create the XML document to send back 383 | doc = xml.dom.minidom.Document() 384 | 385 | SOAPElement = doc.createElementNS("http://schemas.xmlsoap.org/soap/envelope/","SOAP-ENV:Envelope") 386 | SOAPElement.setAttribute("xmlns:SOAP-ENV","http://schemas.xmlsoap.org/soap/envelope/") 387 | SOAPBodyElement = doc.createElement("SOAP-ENV:Body") 388 | 389 | uploadPhotoResponseElement = doc.createElement("UploadPhotoResponse") 390 | successElement = doc.createElement("success") 391 | successElementText = doc.createTextNode("true") 392 | 393 | successElement.appendChild(successElementText) 394 | uploadPhotoResponseElement.appendChild(successElement) 395 | 396 | SOAPBodyElement.appendChild(uploadPhotoResponseElement) 397 | SOAPElement.appendChild(SOAPBodyElement) 398 | doc.appendChild(SOAPElement) 399 | 400 | return doc.toxml(encoding="UTF-8") 401 | 402 | 403 | def getPhotoStatus(self,postData): 404 | handler = EyeFiContentHandler() 405 | parser = xml.sax.parseString(postData,handler) 406 | 407 | # Create the XML document to send back 408 | doc = xml.dom.minidom.Document() 409 | 410 | SOAPElement = doc.createElementNS("http://schemas.xmlsoap.org/soap/envelope/","SOAP-ENV:Envelope") 411 | SOAPElement.setAttribute("xmlns:SOAP-ENV","http://schemas.xmlsoap.org/soap/envelope/") 412 | SOAPBodyElement = doc.createElement("SOAP-ENV:Body") 413 | 414 | getPhotoStatusResponseElement = doc.createElement("GetPhotoStatusResponse") 415 | getPhotoStatusResponseElement.setAttribute("xmlns","http://localhost/api/soap/eyefilm") 416 | 417 | fileidElement = doc.createElement("fileid") 418 | fileidElementText = doc.createTextNode("1") 419 | fileidElement.appendChild(fileidElementText) 420 | 421 | offsetElement = doc.createElement("offset") 422 | offsetElementText = doc.createTextNode("0") 423 | offsetElement.appendChild(offsetElementText) 424 | 425 | getPhotoStatusResponseElement.appendChild(fileidElement) 426 | getPhotoStatusResponseElement.appendChild(offsetElement) 427 | 428 | SOAPBodyElement.appendChild(getPhotoStatusResponseElement) 429 | 430 | SOAPElement.appendChild(SOAPBodyElement) 431 | doc.appendChild(SOAPElement) 432 | 433 | return doc.toxml(encoding="UTF-8") 434 | 435 | 436 | def startSession(self, postData): 437 | eyeFiLogger.debug("Delegating the XML parsing of startSession postData to EyeFiContentHandler()") 438 | handler = EyeFiContentHandler() 439 | parser = xml.sax.parseString(postData,handler) 440 | 441 | eyeFiLogger.debug("Extracted elements: " + str(handler.extractedElements)) 442 | 443 | # Retrieve it from C:\Documents and Settings\\Application Data\Eye-Fi\Settings.xml 444 | eyeFiUploadKey = "c686e547e3728c63a8f78729c1592757" 445 | eyeFiLogger.debug("Setting Eye-Fi upload key to " + eyeFiUploadKey) 446 | 447 | credentialString = handler.extractedElements["macaddress"] + handler.extractedElements["cnonce"] + eyeFiUploadKey; 448 | eyeFiLogger.debug("Concatenated credential string (pre MD5): " + credentialString) 449 | 450 | # Return the binary data represented by the hexadecimal string 451 | # resulting in something that looks like "\x00\x18V\x03\x04..." 452 | binaryCredentialString = binascii.unhexlify(credentialString) 453 | 454 | # Now MD5 hash the binary string 455 | m = hashlib.md5() 456 | m.update(binaryCredentialString) 457 | 458 | # Hex encode the hash to obtain the final credential string 459 | credential = m.hexdigest() 460 | 461 | # Create the XML document to send back 462 | doc = xml.dom.minidom.Document() 463 | 464 | SOAPElement = doc.createElementNS("http://schemas.xmlsoap.org/soap/envelope/","SOAP-ENV:Envelope") 465 | SOAPElement.setAttribute("xmlns:SOAP-ENV","http://schemas.xmlsoap.org/soap/envelope/") 466 | SOAPBodyElement = doc.createElement("SOAP-ENV:Body") 467 | 468 | 469 | startSessionResponseElement = doc.createElement("StartSessionResponse") 470 | startSessionResponseElement.setAttribute("xmlns","http://localhost/api/soap/eyefilm") 471 | 472 | credentialElement = doc.createElement("credential") 473 | credentialElementText = doc.createTextNode(credential) 474 | credentialElement.appendChild(credentialElementText) 475 | 476 | snonceElement = doc.createElement("snonce") 477 | snonceElementText = doc.createTextNode("99208c155fc1883579cf0812ec0fe6d2") 478 | snonceElement.appendChild(snonceElementText) 479 | 480 | transfermodeElement = doc.createElement("transfermode") 481 | transfermodeElementText = doc.createTextNode("2") 482 | transfermodeElement.appendChild(transfermodeElementText) 483 | 484 | transfermodetimestampElement = doc.createElement("transfermodetimestamp") 485 | transfermodetimestampElementText = doc.createTextNode("1230268824") 486 | transfermodetimestampElement.appendChild(transfermodetimestampElementText) 487 | 488 | upsyncallowedElement = doc.createElement("upsyncallowed") 489 | upsyncallowedElementText = doc.createTextNode("false") 490 | upsyncallowedElement.appendChild(upsyncallowedElementText) 491 | 492 | 493 | startSessionResponseElement.appendChild(credentialElement) 494 | startSessionResponseElement.appendChild(snonceElement) 495 | startSessionResponseElement.appendChild(transfermodeElement) 496 | startSessionResponseElement.appendChild(transfermodetimestampElement) 497 | startSessionResponseElement.appendChild(upsyncallowedElement) 498 | 499 | SOAPBodyElement.appendChild(startSessionResponseElement) 500 | 501 | SOAPElement.appendChild(SOAPBodyElement) 502 | doc.appendChild(SOAPElement) 503 | 504 | 505 | return doc.toxml(encoding="UTF-8") 506 | 507 | 508 | def main(): 509 | 510 | # This is the hostname and port which the server will listen 511 | # for requests. A blank hostname indicates all interfaces. 512 | server_address = ('', 59278) 513 | 514 | try: 515 | # Create an instance of an HTTP server. Requests will be handled 516 | # by the class EyeFiRequestHandler 517 | eyeFiServer = EyeFiServer(server_address, EyeFiRequestHandler) 518 | 519 | # Spawn a new thread for the server 520 | thread.start_new_thread(eyeFiServer.serve, ()) 521 | eyeFiLogger.info("Eye-Fi server started listening on port " + str(server_address[1])) 522 | raw_input("\nPress to stop server\n") 523 | eyeFiServer.stop() 524 | 525 | eyeFiLogger.info("Eye-Fi server stopped") 526 | 527 | except KeyboardInterrupt: 528 | eyeFiServer.socket.close() 529 | 530 | 531 | if __name__ == '__main__': 532 | main() 533 | 534 | -------------------------------------------------------------------------------- /Old/EyeFiServerv2.py: -------------------------------------------------------------------------------- 1 | """ 2 | * Copyright (c) 2009, Jeffrey Tchang 3 | * 4 | * All rights reserved. 5 | * 6 | * 7 | * THIS SOFTWARE IS PROVIDED ''AS IS'' AND ANY 8 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 9 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 10 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY 11 | * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 12 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 13 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 14 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 15 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 16 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 17 | """ 18 | 19 | import string 20 | import cgi 21 | import time 22 | 23 | import sys 24 | import os 25 | import socket 26 | import threading 27 | import StringIO 28 | 29 | import hashlib 30 | import binascii 31 | import select 32 | import tarfile 33 | 34 | import xml.sax 35 | from xml.sax.handler import ContentHandler 36 | import xml.dom.minidom 37 | 38 | from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer 39 | import BaseHTTPServer 40 | 41 | import SocketServer 42 | 43 | import logging 44 | import optparse 45 | 46 | import subprocess 47 | import Queue 48 | 49 | 50 | """ 51 | General architecture notes 52 | 53 | 54 | This is a standalone Eye-Fi Server that is designed to take the place of the Eye-Fi Manager. 55 | 56 | 57 | Starting this server creates a listener on port 59278. I use the BaseHTTPServer class included 58 | with Python. I look for specific POST/GET request URLs and execute functions based on those 59 | URLs. 60 | 61 | 62 | Currently all files are downloaded to the directory in which this script is run. 63 | 64 | 65 | To use this script you need to have your Eye-Fi upload key. 66 | It is in C:\Documents and Settings\\Application Data\Eye-Fi\Settings.xml 67 | 68 | Simple search for "eyeFiUploadKey" and replace it with your key. 69 | 70 | """ 71 | 72 | 73 | 74 | # Create an instance of the options parser. This object will hold 75 | # all the command line options 76 | optionsParser = optparse.OptionParser() 77 | 78 | 79 | # A list holding valid file signatures 80 | fileSignatureList = [] 81 | 82 | 83 | 84 | # Eye Fi XML SAX ContentHandler 85 | class EyeFiContentHandler(ContentHandler): 86 | 87 | # These are the element names that I want to parse out of the XML 88 | elementNamesToExtract = ["macaddress","cnonce","transfermode","transfermodetimestamp","fileid","filename","filesize","filesignature","credential"] 89 | 90 | # For each of the element names I create a dictionary with the value to False 91 | elementsToExtract = {} 92 | 93 | # Where to put the extracted values 94 | extractedElements = {} 95 | 96 | 97 | def __init__(self): 98 | self.extractedElements = {} 99 | 100 | for elementName in self.elementNamesToExtract: 101 | self.elementsToExtract[elementName] = False 102 | 103 | def startElement(self, name, attributes): 104 | 105 | # If the name of the element is a key in the dictionary elementsToExtract 106 | # set the value to True 107 | if name in self.elementsToExtract: 108 | self.elementsToExtract[name] = True 109 | 110 | def endElement(self, name): 111 | 112 | # If the name of the element is a key in the dictionary elementsToExtract 113 | # set the value to False 114 | if name in self.elementsToExtract: 115 | self.elementsToExtract[name] = False 116 | 117 | 118 | def characters(self, content): 119 | 120 | for elementName in self.elementsToExtract: 121 | if self.elementsToExtract[elementName] == True: 122 | self.extractedElements[elementName] = content 123 | 124 | # Implements an EyeFi server 125 | class EyeFiServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer): 126 | 127 | 128 | def server_bind(self): 129 | 130 | BaseHTTPServer.HTTPServer.server_bind(self) 131 | self.socket.settimeout(None) 132 | self.run = True 133 | 134 | def get_request(self): 135 | while self.run: 136 | try: 137 | connection, address = self.socket.accept() 138 | eyeFiLogger.debug("Incoming connection from client %s" % address[0]) 139 | 140 | # Set the timeout of the socket to 60 seconds 141 | connection.settimeout(None) 142 | return (connection, address) 143 | 144 | except socket.timeout: 145 | pass 146 | 147 | def stop(self): 148 | self.run = False 149 | 150 | def serve(self): 151 | while self.run: 152 | self.handle_request() 153 | 154 | 155 | 156 | # This class is responsible for handling HTTP requests passed to it. 157 | # It implements the two most common HTTP methods, do_GET() and do_POST() 158 | 159 | class EyeFiRequestHandler(BaseHTTPRequestHandler): 160 | 161 | protocol_version = 'HTTP/1.1' 162 | sys_version = "" 163 | server_version = "Eye-Fi Agent/2.0.4.0 (Windows XP SP2)" 164 | 165 | 166 | def do_GET(self): 167 | eyeFiLogger.debug(self.command + " " + self.path + " " + self.request_version) 168 | 169 | self.send_response(200) 170 | self.send_header('Content-type','text/html') 171 | # I should be sending a Content-Length header with HTTP/1.1 but I am being lazy 172 | # self.send_header('Content-length', '123') 173 | self.end_headers() 174 | self.wfile.write(self.client_address) 175 | self.wfile.write(self.headers) 176 | self.close_connection = 0 177 | 178 | 179 | def do_POST(self): 180 | eyeFiLogger.debug(self.command + " " + self.path + " " + self.request_version) 181 | 182 | SOAPAction = "" 183 | contentLength = "" 184 | 185 | # Loop through all the request headers and pick out ones that are relevant 186 | 187 | eyeFiLogger.debug("Headers received in POST request:") 188 | for headerName in self.headers.keys(): 189 | for headerValue in self.headers.getheaders(headerName): 190 | 191 | if( headerName == "soapaction"): 192 | SOAPAction = headerValue 193 | 194 | if( headerName == "content-length"): 195 | contentLength = int(headerValue) 196 | 197 | eyeFiLogger.debug(headerName + ": " + headerValue) 198 | 199 | 200 | # Read contentLength bytes worth of data 201 | eyeFiLogger.debug("Attempting to read " + str(contentLength) + " bytes of data") 202 | postData = self.rfile.read(contentLength) 203 | eyeFiLogger.debug("Finished reading " + str(contentLength) + " bytes of data") 204 | 205 | # To avoid logging the entire photograph only log postData that is under 2K 206 | if( contentLength <= 2048 ): 207 | eyeFiLogger.debug("postData: " + postData) 208 | 209 | # TODO: Implement some kind of visual progress bar 210 | # bytesRead = 0 211 | # postData = "" 212 | 213 | # while(bytesRead < contentLength): 214 | # postData = postData + self.rfile.read(1) 215 | # bytesRead = bytesRead + 1 216 | 217 | # if(bytesRead % 10000 == 0): 218 | # print "#", 219 | 220 | 221 | # Perform action based on path and SOAPAction 222 | # A SOAPAction of StartSession indicates the beginning of an EyeFi 223 | # authentication request 224 | if((self.path == "/api/soap/eyefilm/v1") and (SOAPAction == "\"urn:StartSession\"")): 225 | eyeFiLogger.debug("Got StartSession request") 226 | response = self.startSession(postData) 227 | contentLength = len(response) 228 | 229 | eyeFiLogger.debug("StartSession response: " + response) 230 | 231 | self.send_response(200) 232 | self.send_header('Date', self.date_time_string()) 233 | self.send_header('Pragma','no-cache') 234 | self.send_header('Server','Eye-Fi Agent/2.0.4.0 (Windows XP SP2)') 235 | self.send_header('Content-Type','text/xml; charset="utf-8"') 236 | self.send_header('Content-Length', contentLength) 237 | self.end_headers() 238 | 239 | self.wfile.write(response) 240 | self.wfile.flush() 241 | self.handle_one_request() 242 | 243 | # GetPhotoStatus allows the card to query if a photo has been uploaded 244 | # to the server yet 245 | if((self.path == "/api/soap/eyefilm/v1") and (SOAPAction == "\"urn:GetPhotoStatus\"")): 246 | eyeFiLogger.debug("Got GetPhotoStatus request") 247 | 248 | response = self.getPhotoStatus(postData) 249 | contentLength = len(response) 250 | 251 | eyeFiLogger.debug("GetPhotoStatus response: " + response) 252 | 253 | self.send_response(200) 254 | self.send_header('Date', self.date_time_string()) 255 | self.send_header('Pragma','no-cache') 256 | self.send_header('Server','Eye-Fi Agent/2.0.4.0 (Windows XP SP2)') 257 | self.send_header('Content-Type','text/xml; charset="utf-8"') 258 | self.send_header('Content-Length', contentLength) 259 | self.end_headers() 260 | 261 | self.wfile.write(response) 262 | self.wfile.flush() 263 | 264 | 265 | # If the URL is upload and there is no SOAPAction the card is ready to send a picture to me 266 | if((self.path == "/api/soap/eyefilm/v1/upload") and (SOAPAction == "")): 267 | eyeFiLogger.debug("Got upload request") 268 | response = self.uploadPhoto(postData) 269 | contentLength = len(response) 270 | 271 | eyeFiLogger.debug("Upload response: " + response) 272 | 273 | self.send_response(200) 274 | self.send_header('Date', self.date_time_string()) 275 | self.send_header('Pragma','no-cache') 276 | self.send_header('Server','Eye-Fi Agent/2.0.4.0 (Windows XP SP2)') 277 | self.send_header('Content-Type','text/xml; charset="utf-8"') 278 | self.send_header('Content-Length', contentLength) 279 | self.end_headers() 280 | 281 | self.wfile.write(response) 282 | self.wfile.flush() 283 | 284 | # If the URL is upload and SOAPAction is MarkLastPhotoInRoll 285 | if((self.path == "/api/soap/eyefilm/v1") and (SOAPAction == "\"urn:MarkLastPhotoInRoll\"")): 286 | eyeFiLogger.debug("Got MarkLastPhotoInRoll request") 287 | response = self.markLastPhotoInRoll(postData) 288 | contentLength = len(response) 289 | 290 | eyeFiLogger.debug("MarkLastPhotoInRoll response: " + response) 291 | self.send_response(200) 292 | self.send_header('Date', self.date_time_string()) 293 | self.send_header('Pragma','no-cache') 294 | self.send_header('Server','Eye-Fi Agent/2.0.4.0 (Windows XP SP2)') 295 | self.send_header('Content-Type','text/xml; charset="utf-8"') 296 | self.send_header('Content-Length', contentLength) 297 | self.send_header('Connection', 'Close') 298 | self.end_headers() 299 | 300 | self.wfile.write(response) 301 | self.wfile.flush() 302 | 303 | eyeFiLogger.debug("Connection closed.") 304 | 305 | 306 | # Handles MarkLastPhotoInRoll action 307 | def markLastPhotoInRoll(self,postData): 308 | # Create the XML document to send back 309 | doc = xml.dom.minidom.Document() 310 | 311 | SOAPElement = doc.createElementNS("http://schemas.xmlsoap.org/soap/envelope/","SOAP-ENV:Envelope") 312 | SOAPElement.setAttribute("xmlns:SOAP-ENV","http://schemas.xmlsoap.org/soap/envelope/") 313 | SOAPBodyElement = doc.createElement("SOAP-ENV:Body") 314 | 315 | markLastPhotoInRollResponseElement = doc.createElement("MarkLastPhotoInRollResponse") 316 | 317 | SOAPBodyElement.appendChild(markLastPhotoInRollResponseElement) 318 | SOAPElement.appendChild(SOAPBodyElement) 319 | doc.appendChild(SOAPElement) 320 | 321 | return doc.toxml(encoding="UTF-8") 322 | 323 | 324 | # Handles receiving the actual photograph from the card. 325 | # postData will most likely contain multipart binary post data that needs to be parsed 326 | def uploadPhoto(self,postData): 327 | 328 | # Take the postData string and work with it as if it were a file object 329 | postDataInMemoryFile = StringIO.StringIO(postData) 330 | 331 | # Get the content-type header which looks something like this 332 | # content-type: multipart/form-data; boundary=---------------------------02468ace13579bdfcafebabef00d 333 | contentTypeHeader = self.headers.getheaders('content-type').pop() 334 | eyeFiLogger.debug(contentTypeHeader) 335 | 336 | # Extract the boundary parameter in the content-type header 337 | headerParameters = contentTypeHeader.split(";") 338 | eyeFiLogger.debug(headerParameters) 339 | 340 | boundary = headerParameters[1].split("=") 341 | boundary = boundary[1].strip() 342 | eyeFiLogger.debug("Extracted boundary: " + boundary) 343 | 344 | # eyeFiLogger.debug("uploadPhoto postData: " + postData) 345 | 346 | # Parse the multipart/form-data 347 | form = cgi.parse_multipart(postDataInMemoryFile, {"boundary":boundary,"content-disposition":self.headers.getheaders('content-disposition')}) 348 | eyeFiLogger.debug("Available multipart/form-data: " + str(form.keys())) 349 | 350 | # Parse the SOAPENVELOPE using the EyeFiContentHandler() 351 | soapEnvelope = form['SOAPENVELOPE'][0] 352 | eyeFiLogger.debug("SOAPENVELOPE: " + soapEnvelope) 353 | handler = EyeFiContentHandler() 354 | parser = xml.sax.parseString(soapEnvelope,handler) 355 | 356 | eyeFiLogger.debug("Extracted elements: " + str(handler.extractedElements)) 357 | 358 | 359 | imageTarfileName = handler.extractedElements["filename"] 360 | fileHandle = open(imageTarfileName, 'wb') 361 | eyeFiLogger.debug("Opened file " + imageTarfileName + " for binary writing") 362 | 363 | fileHandle.write(form['FILENAME'][0]) 364 | eyeFiLogger.debug("Wrote file " + imageTarfileName) 365 | 366 | fileHandle.close() 367 | eyeFiLogger.debug("Closed file " + imageTarfileName) 368 | 369 | eyeFiLogger.debug("Extracting TAR file " + imageTarfileName) 370 | imageTarfile = tarfile.open(imageTarfileName) 371 | imageNames = imageTarfile.getnames() 372 | imageTarfile.extractall() 373 | 374 | eyeFiLogger.debug("Closing TAR file " + imageTarfileName) 375 | imageTarfile.close() 376 | 377 | eyeFiLogger.debug("Deleting TAR file " + imageTarfileName) 378 | os.remove(imageTarfileName) 379 | 380 | # Run a command on the file if specified 381 | if( options.command != None ): 382 | eyeFiLogger.debug("Executing command \"" + options.command + " " + imageNames[0] + "\"") 383 | pid = subprocess.Popen([options.command, imageNames[0]]).pid 384 | 385 | # Create the XML document to send back 386 | doc = xml.dom.minidom.Document() 387 | 388 | SOAPElement = doc.createElementNS("http://schemas.xmlsoap.org/soap/envelope/","SOAP-ENV:Envelope") 389 | SOAPElement.setAttribute("xmlns:SOAP-ENV","http://schemas.xmlsoap.org/soap/envelope/") 390 | SOAPBodyElement = doc.createElement("SOAP-ENV:Body") 391 | 392 | uploadPhotoResponseElement = doc.createElement("UploadPhotoResponse") 393 | successElement = doc.createElement("success") 394 | successElementText = doc.createTextNode("true") 395 | 396 | successElement.appendChild(successElementText) 397 | uploadPhotoResponseElement.appendChild(successElement) 398 | 399 | SOAPBodyElement.appendChild(uploadPhotoResponseElement) 400 | SOAPElement.appendChild(SOAPBodyElement) 401 | doc.appendChild(SOAPElement) 402 | 403 | return doc.toxml(encoding="UTF-8") 404 | 405 | # GetPhotoStatus allows the Eye-Fi card to query the server as to the current uploaded 406 | # status of a file. Even more important is that it authenticates the card to the server 407 | # by the use of the field. Essentially if the credential is correct the 408 | # server should allow files with the given filesignature to be uploaded. 409 | def getPhotoStatus(self,postData): 410 | handler = EyeFiContentHandler() 411 | parser = xml.sax.parseString(postData,handler) 412 | 413 | eyeFiLogger.debug("Extracted elements: " + str(handler.extractedElements)) 414 | 415 | # Calculate the credential string that I am expecting the card to send to me 416 | #credentialString = handler.extractedElements["macaddress"] + handler.extractedElements["cnonce"] + eyeFiUploadKey; 417 | #eyeFiLogger.debug("Concatenated credential string (pre MD5): " + credentialString) 418 | 419 | #credentialString = "0018560304f8" + "c686e547e3728c63a8f78729c1592757" + "99208c155fc1883579cf0812ec0fe6d2" 420 | #binaryCredentialString = binascii.unhexlify(credentialString) 421 | #m = hashlib.md5() 422 | #m.update(binaryCredentialString) 423 | #credential = m.hexdigest() 424 | #print credential 425 | 426 | 427 | #handler.credential 428 | #fileSignatureList.append(handler.filesignature) 429 | 430 | 431 | # Create the XML document to send back 432 | doc = xml.dom.minidom.Document() 433 | 434 | SOAPElement = doc.createElementNS("http://schemas.xmlsoap.org/soap/envelope/","SOAP-ENV:Envelope") 435 | SOAPElement.setAttribute("xmlns:SOAP-ENV","http://schemas.xmlsoap.org/soap/envelope/") 436 | SOAPBodyElement = doc.createElement("SOAP-ENV:Body") 437 | 438 | getPhotoStatusResponseElement = doc.createElement("GetPhotoStatusResponse") 439 | getPhotoStatusResponseElement.setAttribute("xmlns","http://localhost/api/soap/eyefilm") 440 | 441 | fileidElement = doc.createElement("fileid") 442 | fileidElementText = doc.createTextNode("1") 443 | fileidElement.appendChild(fileidElementText) 444 | 445 | offsetElement = doc.createElement("offset") 446 | offsetElementText = doc.createTextNode("0") 447 | offsetElement.appendChild(offsetElementText) 448 | 449 | getPhotoStatusResponseElement.appendChild(fileidElement) 450 | getPhotoStatusResponseElement.appendChild(offsetElement) 451 | 452 | SOAPBodyElement.appendChild(getPhotoStatusResponseElement) 453 | 454 | SOAPElement.appendChild(SOAPBodyElement) 455 | doc.appendChild(SOAPElement) 456 | 457 | return doc.toxml(encoding="UTF-8") 458 | 459 | 460 | def startSession(self, postData): 461 | eyeFiLogger.debug("Delegating the XML parsing of startSession postData to EyeFiContentHandler()") 462 | handler = EyeFiContentHandler() 463 | parser = xml.sax.parseString(postData,handler) 464 | 465 | eyeFiLogger.debug("Extracted elements: " + str(handler.extractedElements)) 466 | 467 | # Retrieve it from C:\Documents and Settings\\Application Data\Eye-Fi\Settings.xml 468 | eyeFiUploadKey = "c686e547e3728c63a8f78729c1592757" 469 | eyeFiLogger.debug("Setting Eye-Fi upload key to " + eyeFiUploadKey) 470 | 471 | credentialString = handler.extractedElements["macaddress"] + handler.extractedElements["cnonce"] + eyeFiUploadKey; 472 | eyeFiLogger.debug("Concatenated credential string (pre MD5): " + credentialString) 473 | 474 | # Return the binary data represented by the hexadecimal string 475 | # resulting in something that looks like "\x00\x18V\x03\x04..." 476 | binaryCredentialString = binascii.unhexlify(credentialString) 477 | 478 | # Now MD5 hash the binary string 479 | m = hashlib.md5() 480 | m.update(binaryCredentialString) 481 | 482 | # Hex encode the hash to obtain the final credential string 483 | credential = m.hexdigest() 484 | 485 | # Create the XML document to send back 486 | doc = xml.dom.minidom.Document() 487 | 488 | SOAPElement = doc.createElementNS("http://schemas.xmlsoap.org/soap/envelope/","SOAP-ENV:Envelope") 489 | SOAPElement.setAttribute("xmlns:SOAP-ENV","http://schemas.xmlsoap.org/soap/envelope/") 490 | SOAPBodyElement = doc.createElement("SOAP-ENV:Body") 491 | 492 | 493 | startSessionResponseElement = doc.createElement("StartSessionResponse") 494 | startSessionResponseElement.setAttribute("xmlns","http://localhost/api/soap/eyefilm") 495 | 496 | credentialElement = doc.createElement("credential") 497 | credentialElementText = doc.createTextNode(credential) 498 | credentialElement.appendChild(credentialElementText) 499 | 500 | snonceElement = doc.createElement("snonce") 501 | snonceElementText = doc.createTextNode("99208c155fc1883579cf0812ec0fe6d2") 502 | snonceElement.appendChild(snonceElementText) 503 | 504 | transfermodeElement = doc.createElement("transfermode") 505 | transfermodeElementText = doc.createTextNode("2") 506 | transfermodeElement.appendChild(transfermodeElementText) 507 | 508 | transfermodetimestampElement = doc.createElement("transfermodetimestamp") 509 | transfermodetimestampElementText = doc.createTextNode("1230268824") 510 | transfermodetimestampElement.appendChild(transfermodetimestampElementText) 511 | 512 | upsyncallowedElement = doc.createElement("upsyncallowed") 513 | upsyncallowedElementText = doc.createTextNode("false") 514 | upsyncallowedElement.appendChild(upsyncallowedElementText) 515 | 516 | 517 | startSessionResponseElement.appendChild(credentialElement) 518 | startSessionResponseElement.appendChild(snonceElement) 519 | startSessionResponseElement.appendChild(transfermodeElement) 520 | startSessionResponseElement.appendChild(transfermodetimestampElement) 521 | startSessionResponseElement.appendChild(upsyncallowedElement) 522 | 523 | SOAPBodyElement.appendChild(startSessionResponseElement) 524 | 525 | SOAPElement.appendChild(SOAPBodyElement) 526 | doc.appendChild(SOAPElement) 527 | 528 | 529 | return doc.toxml(encoding="UTF-8") 530 | 531 | 532 | def commandLineOptions(): 533 | optionsParser.add_option("-p", "--port", action="store", type="int", dest="listenport", 534 | help="Force the EyeFiServer to listen on the given port (default 59278)", metavar="PORT") 535 | 536 | optionsParser.add_option("-b", "--background", action="store_true", dest="daemonize", 537 | help="Background the EyeFiServer after starting up (Unix only)") 538 | 539 | optionsParser.add_option("-l", "--log", action="store", dest="logfilename", 540 | help="Log output to the given file", metavar="FILE") 541 | 542 | optionsParser.add_option("-v", action="count", dest="verbosity", default=1, 543 | help="Increase debug level. Can be specified multiple times.") 544 | 545 | optionsParser.add_option("-q", "--quiet", action="store_true", dest="suppressConsole", 546 | help="Suppress all console messages") 547 | 548 | optionsParser.add_option("-c", "--command", action="store", dest="command", 549 | help="Execute the specified command on each incoming file passing in the full file path as the first argument") 550 | 551 | 552 | 553 | def setupLogging(options): 554 | 555 | # Declare the main logger as a global 556 | global eyeFiLogger 557 | 558 | # Determine the log level based on the options dictionary 559 | loglevel = None 560 | 561 | if(options.verbosity == 1): 562 | loglevel = logging.ERROR 563 | 564 | elif(options.verbosity == 2): 565 | loglevel = logging.INFO 566 | 567 | elif(options.verbosity >= 3): 568 | loglevel = logging.DEBUG 569 | 570 | else: 571 | loglevel = logging.ERROR 572 | 573 | # Create the logger with the appropriate log level 574 | eyeFiLogger = logging.Logger("eyeFiLogger",loglevel) 575 | 576 | # Define the logging format to be used 577 | eyeFiLoggingFormat = logging.Formatter("[%(asctime)s][%(funcName)s] - %(message)s",'%m/%d/%y %I:%M%p') 578 | 579 | 580 | # Option to suppress console messages 581 | if( options.suppressConsole != True ): 582 | consoleHandler = logging.StreamHandler(sys.stdout) 583 | consoleHandler.setFormatter(eyeFiLoggingFormat) 584 | eyeFiLogger.addHandler(consoleHandler) 585 | 586 | # Option to log to a file 587 | if( options.logfilename != None ): 588 | fileHandler = logging.FileHandler(options.logfilename,"w",encoding=None, delay=0) 589 | fileHandler.setFormatter(eyeFiLoggingFormat) 590 | eyeFiLogger.addHandler(fileHandler) 591 | 592 | # Define a do-nothing handler so that existing logging messages don't error out 593 | class NullHandler(logging.Handler): 594 | def emit(self, record): 595 | pass 596 | eyeFiLogger.addHandler(NullHandler()) 597 | 598 | def main(): 599 | 600 | # Load the available command line options 601 | commandLineOptions() 602 | 603 | # Parse the command line options and make them available globally 604 | global options 605 | (options, args) = optionsParser.parse_args() 606 | 607 | print options 608 | 609 | # Setup the logging that will be used for the rest of the program 610 | setupLogging(options) 611 | 612 | # This is the hostname and port which the server will listen 613 | # for requests. A blank hostname indicates all interfaces. 614 | if( options.listenport != None ): 615 | server_address = ('', options.listenport) 616 | else: 617 | server_address = ('', 59278) 618 | 619 | try: 620 | # Create an instance of an HTTP server. Requests will be handled 621 | # by the class EyeFiRequestHandler 622 | eyeFiServer = EyeFiServer(server_address, EyeFiRequestHandler) 623 | 624 | # Spawn a new thread for the server 625 | # thread.start_new_thread(eyeFiServer.serve, ()) 626 | eyeFiServerThread = threading.Thread(group=None, target=eyeFiServer.serve, name="EyeFiServerThread") 627 | eyeFiServerThread.daemon = True 628 | eyeFiServerThread.start() 629 | 630 | eyeFiLogger.info("Eye-Fi server started listening on port " + str(server_address[1])) 631 | 632 | while(True): 633 | time.sleep(60) 634 | 635 | except KeyboardInterrupt: 636 | eyeFiLogger.info("Eye-Fi server shutting down") 637 | 638 | # It is possible that the signal arrives before the eyeFiServer variable is initialized 639 | if( "eyeFiServer" in locals() ): 640 | eyeFiServer.stop() 641 | eyeFiServer.socket.close() 642 | 643 | eyeFiLogger.info("Eye-Fi server stopped") 644 | 645 | 646 | if __name__ == '__main__': 647 | main() 648 | 649 | -------------------------------------------------------------------------------- /Release 2.0/DebugSettings.ini: -------------------------------------------------------------------------------- 1 | # Main configuration 2 | 3 | [Global] 4 | 5 | # The directives in this section affect the overall operation 6 | # of the Eye-Fi server 7 | 8 | 9 | # 10 | # ListenPort: Allows you to bind the Eye-Fi server to a specific port. 11 | # 12 | #ListenPort=59278 13 | 14 | # 15 | # ConsoleOutput: Logging can automatically be sent to the console. 16 | # Set this to False if you don't want any console output. Console 17 | # output is also considered stdout. 18 | # 19 | #ConsoleOutput=True 20 | 21 | 22 | # 23 | # LogFile: Controls where to write the Eye-Fi logs. 24 | # 25 | #LogFile= 26 | 27 | 28 | # 29 | # LogLevel: The level of verbosity in both the logs and the console 30 | # output. From most verbose to lease verbose the settings are 31 | # DEBUG, INFO, WARNING, ERROR or CRITICAL 32 | # 33 | LogLevel=DEBUG 34 | 35 | # 36 | # DownloadLocation: The directory in which to put the incoming pictures. By 37 | # default the pictures are put in a sub directory called "pictures" from where 38 | # the script is originally started. 39 | # 40 | #On Windows: 41 | # This would set the downloads to a directory called pictures. 42 | #DownloadLocation=.\\pictures 43 | # 44 | #On Unix: 45 | #DownloadLocation=/tmp 46 | # 47 | #DownloadLocation=.\\pictures 48 | 49 | 50 | 51 | # 52 | # ExecuteOnUpload: This parameter is used to define an external program or script to 53 | # execute after a file is uploaded. By nature this command is very dangerous and should 54 | # be used carefully. Enabling this command can serve as a means of compromising a 55 | # system or disclosing information via bugs in external programs or scripts. 56 | # 57 | # This parameter executes the specified command on each incoming file passing in the full 58 | # file path as the first argument. 59 | # 60 | #On Windows: 61 | # 62 | ExecuteOnUpload=C:\\Windows\\system32\\mspaint.exe 63 | 64 | 65 | [Card] 66 | 67 | # The directives in this section affect the physical card settings 68 | 69 | # 70 | # UploadKey: The Eye-Fi upload key. It is in C:\Documents and Settings\ 71 | # \Application Data\Eye-Fi\Settings.xml. This needs to be set for 72 | # the server to function correctly. 73 | # 74 | UploadKey=c686e547e3728c63a8f78729c1592757 -------------------------------------------------------------------------------- /Release 2.0/DefaultSettings.ini: -------------------------------------------------------------------------------- 1 | # Main configuration 2 | 3 | [Global] 4 | 5 | # The directives in this section affect the overall operation 6 | # of the Eye-Fi server 7 | 8 | 9 | # 10 | # ListenPort: Allows you to bind the Eye-Fi server to a specific port. 11 | # 12 | #ListenPort=59278 13 | 14 | # 15 | # ConsoleOutput: Logging can automatically be sent to the console. 16 | # Set this to False if you don't want any console output. Console 17 | # output is also considered stdout. 18 | # 19 | #ConsoleOutput=True 20 | 21 | 22 | # 23 | # LogFile: Controls where to write the Eye-Fi logs. 24 | # 25 | #LogFile= 26 | 27 | 28 | # 29 | # LogLevel: The level of verbosity in both the logs and the console 30 | # output. From most verbose to lease verbose the settings are 31 | # DEBUG, INFO, WARNING, ERROR or CRITICAL 32 | # 33 | LogLevel=DEBUG 34 | 35 | # 36 | # DownloadLocation: The directory in which to put the incoming pictures. By 37 | # default the pictures are put in a sub directory called "pictures" from where 38 | # the script is originally started. 39 | # 40 | #On Windows: 41 | # This would set the downloads to a directory called pictures. 42 | #DownloadLocation=.\\pictures 43 | # 44 | #On Unix: 45 | #DownloadLocation=/tmp 46 | # 47 | DownloadLocation=.\\pictures 48 | 49 | 50 | 51 | # 52 | # ExecuteOnUpload: This parameter is used to define an external program or script to 53 | # execute after a file is uploaded. By nature this command is very dangerous and should 54 | # be used carefully. Enabling this command can serve as a means of compromising a 55 | # system or disclosing information via bugs in external programs or scripts. 56 | # 57 | # This parameter executes the specified command on each incoming file passing in the full 58 | # file path as the first argument. There is no default for this command (nothing is executed 59 | # if this parameter is left blank). 60 | # 61 | #On Windows: 62 | # 63 | #ExecuteOnUpload=C:\\Windows\\system32\\mspaint.exe 64 | 65 | 66 | [Card] 67 | 68 | # The directives in this section affect the physical card settings 69 | 70 | # 71 | # UploadKey: The Eye-Fi upload key. It is in C:\Documents and Settings\ 72 | # \Application Data\Eye-Fi\Settings.xml. This needs to be set for 73 | # the server to function correctly. 74 | # 75 | UploadKey=c686e547e3728c63a8f78729c1592757 -------------------------------------------------------------------------------- /Release 2.0/EyeFiCrypto.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import struct 3 | import array 4 | import hashlib 5 | 6 | class EyeFiCrypto(): 7 | 8 | # The TCP checksum requires an even number of bytes. If an even 9 | # number of bytes is not passed in then nul pad the input and then 10 | # compute the checksum 11 | def calculateTCPChecksum(self, bytes): 12 | 13 | # If the number of bytes I was given is not a multiple of 2 14 | # pad the input with a null character at the end 15 | if(len(bytes) % 2 != 0 ): 16 | bytes = bytes + "\x00" 17 | 18 | counter = 0 19 | sumOfTwoByteWords = 0 20 | 21 | # Loop over all the bytes, two at a time 22 | while(counter < len(bytes) ): 23 | 24 | # For each pair of bytes, cast them into a 2 byte integer (unsigned short) 25 | # Compute using little-endian (which is what the '<' sign if for) 26 | unsignedShort = struct.unpack("> 16): 38 | sumOfTwoByteWords = (sumOfTwoByteWords >> 16) + (sumOfTwoByteWords & 0xFFFF) 39 | 40 | # Take the one's complement of the result through the use of an xor 41 | checksum = sumOfTwoByteWords ^ 0xFFFFFFFF 42 | 43 | # Compute the final checksum by taking only the last 16 bits 44 | checksum = (checksum & 0xFFFF) 45 | 46 | return checksum 47 | 48 | 49 | def calculateIntegrityDigest(self, bytes, uploadkey): 50 | 51 | # If the number of bytes I was given is not a multiple of 512 52 | # pad the input with a null characters to get the proper alignment 53 | while(len(bytes) % 512 != 0 ): 54 | bytes = bytes + "\x00" 55 | 56 | counter = 0 57 | 58 | # Create an array of 2 byte integers 59 | concatenatedTCPChecksums = array.array('H') 60 | 61 | # Loop over all the bytes, using 512 byte blocks 62 | while(counter < len(bytes) ): 63 | 64 | tcpChecksum = self.calculateTCPChecksum(bytes[counter:counter+512]) 65 | concatenatedTCPChecksums.append(tcpChecksum) 66 | counter = counter + 512 67 | 68 | # Append the upload key 69 | concatenatedTCPChecksums.fromstring(binascii.unhexlify(uploadkey)) 70 | 71 | # Get the concatenatedTCPChecksums array as a binary string 72 | integrityDigest = concatenatedTCPChecksums.tostring() 73 | 74 | # MD5 hash the binary string 75 | m = hashlib.md5() 76 | m.update(integrityDigest) 77 | 78 | # Hex encode the hash to obtain the final integrity digest 79 | integrityDigest = m.hexdigest() 80 | 81 | return integrityDigest 82 | -------------------------------------------------------------------------------- /Release 2.0/EyeFiCrypto.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tachang/EyeFiServer/4c473e76bcf97472393773ffe352c9078ec1bf39/Release 2.0/EyeFiCrypto.pyc -------------------------------------------------------------------------------- /Release 2.0/EyeFiLogo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tachang/EyeFiServer/4c473e76bcf97472393773ffe352c9078ec1bf39/Release 2.0/EyeFiLogo.jpg -------------------------------------------------------------------------------- /Release 2.0/EyeFiLogo.jpg.tar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tachang/EyeFiServer/4c473e76bcf97472393773ffe352c9078ec1bf39/Release 2.0/EyeFiLogo.jpg.tar -------------------------------------------------------------------------------- /Release 2.0/EyeFiSOAPMessages.py: -------------------------------------------------------------------------------- 1 | import xml.sax 2 | from xml.sax.handler import ContentHandler 3 | import xml.dom.minidom 4 | 5 | 6 | class EyeFiSOAPMessages(): 7 | 8 | 9 | def getUploadPhotoXML(self, fileid, macaddress, filename, filesize, filesignature, encryption): 10 | doc = xml.dom.minidom.Document() 11 | 12 | SOAPElement = doc.createElementNS("http://schemas.xmlsoap.org/soap/envelope/","SOAP-ENV:Envelope") 13 | SOAPElement.setAttribute("xmlns:SOAP-ENV","http://schemas.xmlsoap.org/soap/envelope/") 14 | SOAPElement.setAttribute("xmlns:ns1","EyeFi/SOAP/EyeFilm") 15 | 16 | SOAPBodyElement = doc.createElement("SOAP-ENV:Body") 17 | 18 | uploadPhotoElement = doc.createElement("ns1:UploadPhoto") 19 | 20 | fileidElement = doc.createElement("fileid") 21 | fileidElementText = doc.createTextNode(str(fileid)) 22 | fileidElement.appendChild(fileidElementText) 23 | 24 | macaddressElement = doc.createElement("macaddress") 25 | macaddressElementText = doc.createTextNode(str(macaddress)) 26 | macaddressElement.appendChild(macaddressElementText) 27 | 28 | filenameElement = doc.createElement("filename") 29 | filenameElementText = doc.createTextNode(str(filename)) 30 | filenameElement.appendChild(filenameElementText) 31 | 32 | filesizeElement = doc.createElement("filesize") 33 | filesizeElementText = doc.createTextNode(str(filesize)) 34 | filesizeElement.appendChild(filesizeElementText) 35 | 36 | filesignatureElement = doc.createElement("filesignature") 37 | filesignatureElementText = doc.createTextNode(str(filesignature)) 38 | filesignatureElement.appendChild(filesignatureElementText) 39 | 40 | encryptionElement = doc.createElement("encryption") 41 | encryptionElementText = doc.createTextNode(str(encryption)) 42 | encryptionElement.appendChild(encryptionElementText) 43 | 44 | uploadPhotoElement.appendChild(fileidElement) 45 | uploadPhotoElement.appendChild(macaddressElement) 46 | uploadPhotoElement.appendChild(filenameElement) 47 | uploadPhotoElement.appendChild(filesizeElement) 48 | uploadPhotoElement.appendChild(filesignatureElement) 49 | uploadPhotoElement.appendChild(encryptionElement) 50 | 51 | SOAPBodyElement.appendChild(uploadPhotoElement) 52 | SOAPElement.appendChild(SOAPBodyElement) 53 | doc.appendChild(SOAPElement) 54 | 55 | return doc.toxml(encoding="UTF-8") 56 | 57 | def getStartSessionXML(self, macaddress, cnonce, transfermode, transfermodetimestamp): 58 | doc = xml.dom.minidom.Document() 59 | 60 | SOAPElement = doc.createElementNS("http://schemas.xmlsoap.org/soap/envelope/","SOAP-ENV:Envelope") 61 | SOAPElement.setAttribute("xmlns:SOAP-ENV","http://schemas.xmlsoap.org/soap/envelope/") 62 | SOAPBodyElement = doc.createElement("SOAP-ENV:Body") 63 | 64 | startSessionElement = doc.createElement("StartSession") 65 | startSessionElement.setAttribute("xmlns","EyeFi/SOAP/EyeFilm") 66 | 67 | macaddressElement = doc.createElement("macaddress") 68 | macaddressElementText = doc.createTextNode(str(macaddress)) 69 | macaddressElement.appendChild(macaddressElementText) 70 | 71 | cnonceElement = doc.createElement("cnonce") 72 | cnonceElementText = doc.createTextNode(str(cnonce)) 73 | cnonceElement.appendChild(cnonceElementText) 74 | 75 | transfermodeElement = doc.createElement("transfermode") 76 | transfermodeElementText = doc.createTextNode(str(transfermode)) 77 | transfermodeElement.appendChild(transfermodeElementText) 78 | 79 | transfermodetimestampElement = doc.createElement("transfermodetimestamp") 80 | transfermodetimestampElementText = doc.createTextNode(str(transfermodetimestamp)) 81 | transfermodetimestampElement.appendChild(transfermodetimestampElementText) 82 | 83 | startSessionElement.appendChild(macaddressElement) 84 | startSessionElement.appendChild(cnonceElement) 85 | startSessionElement.appendChild(transfermodeElement) 86 | startSessionElement.appendChild(transfermodetimestampElement) 87 | 88 | SOAPBodyElement.appendChild(startSessionElement) 89 | 90 | SOAPElement.appendChild(SOAPBodyElement) 91 | doc.appendChild(SOAPElement) 92 | 93 | return doc.toxml(encoding="UTF-8") 94 | 95 | def getPhotoStatusXML(self, credential, macaddress, filename, filesize, filesignature ): 96 | doc = xml.dom.minidom.Document() 97 | 98 | SOAPElement = doc.createElementNS("http://schemas.xmlsoap.org/soap/envelope/","SOAP-ENV:Envelope") 99 | SOAPElement.setAttribute("xmlns:SOAP-ENV","http://schemas.xmlsoap.org/soap/envelope/") 100 | SOAPElement.setAttribute("xmlns:ns1","EyeFi/SOAP/EyeFilm") 101 | 102 | SOAPBodyElement = doc.createElement("SOAP-ENV:Body") 103 | 104 | getPhotoStatusElement = doc.createElement("ns1:GetPhotoStatus") 105 | 106 | credentialElement = doc.createElement("credential") 107 | credentialElementText = doc.createTextNode(str(credential)) 108 | credentialElement.appendChild(credentialElementText) 109 | 110 | macaddressElement = doc.createElement("macaddress") 111 | macaddressElementText = doc.createTextNode(str(macaddress)) 112 | macaddressElement.appendChild(macaddressElementText) 113 | 114 | filenameElement = doc.createElement("filename") 115 | filenameElementText = doc.createTextNode(str(filename)) 116 | filenameElement.appendChild(filenameElementText) 117 | 118 | filesizeElement = doc.createElement("filesize") 119 | filesizeElementText = doc.createTextNode(str(filesize)) 120 | filesizeElement.appendChild(filesizeElementText) 121 | 122 | filesignatureElement = doc.createElement("filesignature") 123 | filesignatureElementText = doc.createTextNode(str(filesignature)) 124 | filesignatureElement.appendChild(filesignatureElementText) 125 | 126 | getPhotoStatusElement.appendChild(credentialElement) 127 | getPhotoStatusElement.appendChild(macaddressElement) 128 | getPhotoStatusElement.appendChild(filenameElement) 129 | getPhotoStatusElement.appendChild(filesizeElement) 130 | getPhotoStatusElement.appendChild(filesignatureElement) 131 | 132 | SOAPBodyElement.appendChild(getPhotoStatusElement) 133 | SOAPElement.appendChild(SOAPBodyElement) 134 | doc.appendChild(SOAPElement) 135 | 136 | return doc.toxml(encoding="UTF-8") 137 | 138 | def getSOAPFaultXML(self, faultvalue, faulttext): 139 | doc = xml.dom.minidom.Document() 140 | 141 | SOAPElement = doc.createElementNS("http://schemas.xmlsoap.org/soap/envelope/","SOAP-ENV:Envelope") 142 | SOAPElement.setAttribute("xmlns:SOAP-ENV","http://schemas.xmlsoap.org/soap/envelope/") 143 | SOAPBodyElement = doc.createElement("SOAP-ENV:Body") 144 | 145 | SOAPFaultElement = doc.createElement("SOAP-ENV:Fault") 146 | codeElement = doc.createElement("SOAP-ENV:Code") 147 | 148 | valueElement = doc.createElement("SOAP-ENV:Value") 149 | valueElementText = doc.createTextNode(str(faultvalue)) 150 | valueElement.appendChild(valueElementText) 151 | 152 | reasonElement = doc.createElement("SOAP-ENV:Reason") 153 | 154 | faulttextElement = doc.createElement("SOAP-ENV:Text") 155 | faulttextElement.setAttribute("xml:lang","en-US") 156 | faulttextElementText = doc.createTextNode(str(faulttext)) 157 | faulttextElement.appendChild(faulttextElementText) 158 | 159 | codeElement.appendChild(valueElement) 160 | reasonElement.appendChild(faulttextElement) 161 | 162 | SOAPFaultElement.appendChild(codeElement) 163 | SOAPFaultElement.appendChild(reasonElement) 164 | 165 | SOAPBodyElement.appendChild(SOAPFaultElement) 166 | 167 | SOAPElement.appendChild(SOAPBodyElement) 168 | doc.appendChild(SOAPElement) 169 | 170 | return doc.toxml(encoding="UTF-8") 171 | 172 | -------------------------------------------------------------------------------- /Release 2.0/EyeFiSOAPMessages.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tachang/EyeFiServer/4c473e76bcf97472393773ffe352c9078ec1bf39/Release 2.0/EyeFiSOAPMessages.pyc -------------------------------------------------------------------------------- /Release 2.0/EyeFiServer.py: -------------------------------------------------------------------------------- 1 | """ 2 | * EyeFi Python Server v2.0 3 | * 4 | * Copyright (c) 2009, Jeffrey Tchang 5 | * 6 | * All rights reserved. 7 | * 8 | * 9 | * THIS SOFTWARE IS PROVIDED ''AS IS'' AND ANY 10 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 11 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 12 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY 13 | * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 14 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 15 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 16 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 17 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 18 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 19 | """ 20 | 21 | import string 22 | import cgi 23 | import time 24 | 25 | import sys 26 | import os 27 | import socket 28 | import threading 29 | import StringIO 30 | 31 | import hashlib 32 | import binascii 33 | import select 34 | import tarfile 35 | 36 | import xml.sax 37 | from xml.sax.handler import ContentHandler 38 | import xml.dom.minidom 39 | 40 | from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer 41 | import BaseHTTPServer 42 | 43 | import SocketServer 44 | 45 | import logging 46 | import optparse 47 | import ConfigParser 48 | 49 | import subprocess 50 | import random 51 | import tempfile 52 | 53 | import EyeFiCrypto 54 | import EyeFiSOAPMessages 55 | 56 | """ 57 | General Architecture Notes 58 | 59 | 60 | This is a standalone Eye-Fi Server that is designed to take the place of the Eye-Fi Manager. 61 | 62 | Starting this server creates a listener on port 59278. I use the BaseHTTPServer class included 63 | with Python. I look for specific POST/GET request URLs and execute functions based on those 64 | URLs. Currently all files are downloaded to the directory in which this script is run. 65 | 66 | 67 | To use this script you need to have your Eye-Fi upload key. 68 | It is in C:\Documents and Settings\\Application Data\Eye-Fi\Settings.xml. 69 | 70 | This script uses a file for all its configuration parameters. An example configuration 71 | file can be found in the same directory called "DefaultSettings.ini". 72 | 73 | This script can be run with the default settings but without replacing at least the 74 | UploadKey setting in the [Card] section of the configuration file it will not work. 75 | 76 | 77 | """ 78 | 79 | 80 | 81 | # Create an instance of the options parser. This object will hold 82 | # all the command line options 83 | optionsParser = optparse.OptionParser() 84 | 85 | 86 | 87 | # Eye Fi XML SAX ContentHandler 88 | class EyeFiContentHandler(ContentHandler): 89 | 90 | # These are the element names that I want to parse out of the XML 91 | elementNamesToExtract = ["macaddress","cnonce","transfermode","transfermodetimestamp","fileid","filename","filesize","filesignature","credential"] 92 | 93 | # For each of the element names I create a dictionary with the value to False 94 | elementsToExtract = {} 95 | 96 | # Where to put the extracted values 97 | extractedElements = {} 98 | 99 | 100 | def __init__(self): 101 | self.extractedElements = {} 102 | 103 | for elementName in self.elementNamesToExtract: 104 | self.elementsToExtract[elementName] = False 105 | 106 | def startElement(self, name, attributes): 107 | 108 | # If the name of the element is a key in the dictionary elementsToExtract 109 | # set the value to True 110 | if name in self.elementsToExtract: 111 | self.elementsToExtract[name] = True 112 | 113 | def endElement(self, name): 114 | 115 | # If the name of the element is a key in the dictionary elementsToExtract 116 | # set the value to False 117 | if name in self.elementsToExtract: 118 | self.elementsToExtract[name] = False 119 | 120 | 121 | def characters(self, content): 122 | 123 | for elementName in self.elementsToExtract: 124 | if self.elementsToExtract[elementName] == True: 125 | self.extractedElements[elementName] = content 126 | 127 | # Implements an EyeFi server 128 | class EyeFiServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer): 129 | 130 | eyeFiConfiguration = "" 131 | serverNonce = "" 132 | 133 | def __init__(self, server_address, requestHandler, eyeFiConfiguration): 134 | # Set EyeFiServer.eyeFiConfiguration to the configuration object that is passed in 135 | self.eyeFiConfiguration = eyeFiConfiguration 136 | 137 | # Generate a nonce to be used by the server. The nonce should be very hard if not 138 | # impossible to predict. The method used here is to MD5 hash a random number. 139 | m = hashlib.md5() 140 | m.update(str(random.random())) 141 | self.serverNonce = m.hexdigest() 142 | 143 | # Explicitly call the base class BaseHTTPServer.HTTPServer's __init__() method 144 | BaseHTTPServer.HTTPServer.__init__(self,server_address, requestHandler) 145 | 146 | 147 | def server_bind(self): 148 | 149 | BaseHTTPServer.HTTPServer.server_bind(self) 150 | self.socket.settimeout(None) 151 | self.run = True 152 | 153 | def get_request(self): 154 | while self.run: 155 | try: 156 | connection, address = self.socket.accept() 157 | eyeFiLogger.debug("Incoming connection from client %s" % address[0]) 158 | 159 | # Set the timeout of the socket to 60 seconds 160 | connection.settimeout(None) 161 | return (connection, address) 162 | 163 | except socket.timeout: 164 | pass 165 | 166 | def stop(self): 167 | self.run = False 168 | 169 | def serve(self): 170 | while self.run: 171 | self.handle_request() 172 | 173 | # Override the method finish_request() found in the BaseServer class to insert some debugging 174 | # output. This class can be found in the file SocketServer.py. 175 | def finish_request(self, request, client_address): 176 | eyeFiLogger.debug("Creating instance of " + str(self.RequestHandlerClass) + " to service request from " + str(client_address)) 177 | self.RequestHandlerClass(request, client_address, self) 178 | 179 | 180 | # This class is responsible for handling HTTP requests passed to it. 181 | # It implements the two most common HTTP methods, do_GET() and do_POST() 182 | # 183 | # One of the more important variables that can be used in this class is 184 | # self.server.eyeFiConfiguration which holds the initial configuration data 185 | # 186 | class EyeFiRequestHandler(BaseHTTPRequestHandler): 187 | 188 | protocol_version = 'HTTP/1.1' 189 | sys_version = "" 190 | server_version = "Eye-Fi Agent/2.0.4.0 (Windows XP SP2)" 191 | 192 | def __init__(self, request, client_address, server): 193 | BaseHTTPRequestHandler.__init__(self, request, client_address, server) 194 | 195 | 196 | def do_GET(self): 197 | eyeFiLogger.debug(self.command + " " + self.path + " " + self.request_version) 198 | 199 | self.send_response(200) 200 | self.send_header('Content-type','text/html') 201 | # I should be sending a Content-Length header with HTTP/1.1 but I am being lazy 202 | # self.send_header('Content-length', '123') 203 | self.end_headers() 204 | self.wfile.write(self.client_address) 205 | self.wfile.write(self.headers) 206 | self.close_connection = 0 207 | 208 | 209 | def do_POST(self): 210 | eyeFiLogger.debug(self.command + " " + self.path + " " + self.request_version) 211 | 212 | SOAPAction = "" 213 | contentLength = "" 214 | 215 | # Loop through all the request headers and pick out ones that are relevant 216 | 217 | eyeFiLogger.debug("Headers received in POST request:") 218 | for headerName in self.headers.keys(): 219 | for headerValue in self.headers.getheaders(headerName): 220 | 221 | if( headerName == "soapaction"): 222 | SOAPAction = headerValue 223 | 224 | if( headerName == "content-length"): 225 | contentLength = int(headerValue) 226 | 227 | eyeFiLogger.debug(headerName + ": " + headerValue) 228 | 229 | 230 | # Read contentLength bytes worth of data 231 | eyeFiLogger.debug("Attempting to read " + str(contentLength) + " bytes of data") 232 | postData = self.rfile.read(contentLength) 233 | eyeFiLogger.debug("Finished reading " + str(contentLength) + " bytes of data") 234 | 235 | # To avoid logging the entire photograph only log postData that is under 2K 236 | if( contentLength <= 2048 ): 237 | eyeFiLogger.debug("postData: " + postData) 238 | 239 | # TODO: Implement some kind of visual progress bar 240 | # bytesRead = 0 241 | # postData = "" 242 | 243 | # while(bytesRead < contentLength): 244 | # postData = postData + self.rfile.read(1) 245 | # bytesRead = bytesRead + 1 246 | 247 | # if(bytesRead % 10000 == 0): 248 | # print "#", 249 | 250 | 251 | # Perform action based on path and SOAPAction 252 | # A SOAPAction of StartSession indicates the beginning of an EyeFi 253 | # authentication request 254 | if((self.path == "/api/soap/eyefilm/v1") and (SOAPAction == "\"urn:StartSession\"")): 255 | eyeFiLogger.debug("Got StartSession request") 256 | response = self.startSession(postData) 257 | contentLength = len(response) 258 | 259 | eyeFiLogger.debug("StartSession response: " + response) 260 | 261 | self.send_response(200) 262 | self.send_header('Date', self.date_time_string()) 263 | self.send_header('Pragma','no-cache') 264 | self.send_header('Server','Eye-Fi Agent/2.0.4.0 (Windows XP SP2)') 265 | self.send_header('Content-Type','text/xml; charset="utf-8"') 266 | self.send_header('Content-Length', contentLength) 267 | self.end_headers() 268 | 269 | self.wfile.write(response) 270 | self.wfile.flush() 271 | self.handle_one_request() 272 | 273 | # GetPhotoStatus allows the card to query if a photo has been uploaded 274 | # to the server yet 275 | if((self.path == "/api/soap/eyefilm/v1") and (SOAPAction == "\"urn:GetPhotoStatus\"")): 276 | eyeFiLogger.debug("Got GetPhotoStatus request") 277 | 278 | response = self.getPhotoStatus(postData) 279 | contentLength = len(response) 280 | 281 | eyeFiLogger.debug("GetPhotoStatus response: " + response) 282 | 283 | self.send_response(200) 284 | self.send_header('Date', self.date_time_string()) 285 | self.send_header('Pragma','no-cache') 286 | self.send_header('Server','Eye-Fi Agent/2.0.4.0 (Windows XP SP2)') 287 | self.send_header('Content-Type','text/xml; charset="utf-8"') 288 | self.send_header('Content-Length', contentLength) 289 | self.end_headers() 290 | 291 | self.wfile.write(response) 292 | self.wfile.flush() 293 | 294 | 295 | # If the URL is upload and there is no SOAPAction the card is ready to send a picture to me 296 | if((self.path == "/api/soap/eyefilm/v1/upload") and (SOAPAction == "")): 297 | eyeFiLogger.debug("Got upload request") 298 | response = self.uploadPhoto(postData) 299 | contentLength = len(response) 300 | 301 | eyeFiLogger.debug("Upload response: " + response) 302 | 303 | self.send_response(200) 304 | self.send_header('Date', self.date_time_string()) 305 | self.send_header('Pragma','no-cache') 306 | self.send_header('Server','Eye-Fi Agent/2.0.4.0 (Windows XP SP2)') 307 | self.send_header('Content-Type','text/xml; charset="utf-8"') 308 | self.send_header('Content-Length', contentLength) 309 | self.end_headers() 310 | 311 | self.wfile.write(response) 312 | self.wfile.flush() 313 | 314 | # If the URL is upload and SOAPAction is MarkLastPhotoInRoll 315 | if((self.path == "/api/soap/eyefilm/v1") and (SOAPAction == "\"urn:MarkLastPhotoInRoll\"")): 316 | eyeFiLogger.debug("Got MarkLastPhotoInRoll request") 317 | response = self.markLastPhotoInRoll(postData) 318 | contentLength = len(response) 319 | 320 | eyeFiLogger.debug("MarkLastPhotoInRoll response: " + response) 321 | self.send_response(200) 322 | self.send_header('Date', self.date_time_string()) 323 | self.send_header('Pragma','no-cache') 324 | self.send_header('Server','Eye-Fi Agent/2.0.4.0 (Windows XP SP2)') 325 | self.send_header('Content-Type','text/xml; charset="utf-8"') 326 | self.send_header('Content-Length', contentLength) 327 | self.send_header('Connection', 'Close') 328 | self.end_headers() 329 | 330 | self.wfile.write(response) 331 | self.wfile.flush() 332 | 333 | eyeFiLogger.debug("Connection closed.") 334 | 335 | 336 | # Handles MarkLastPhotoInRoll action 337 | def markLastPhotoInRoll(self,postData): 338 | # Create the XML document to send back 339 | doc = xml.dom.minidom.Document() 340 | 341 | SOAPElement = doc.createElementNS("http://schemas.xmlsoap.org/soap/envelope/","SOAP-ENV:Envelope") 342 | SOAPElement.setAttribute("xmlns:SOAP-ENV","http://schemas.xmlsoap.org/soap/envelope/") 343 | SOAPBodyElement = doc.createElement("SOAP-ENV:Body") 344 | 345 | markLastPhotoInRollResponseElement = doc.createElement("MarkLastPhotoInRollResponse") 346 | 347 | SOAPBodyElement.appendChild(markLastPhotoInRollResponseElement) 348 | SOAPElement.appendChild(SOAPBodyElement) 349 | doc.appendChild(SOAPElement) 350 | 351 | return doc.toxml(encoding="UTF-8") 352 | 353 | 354 | # Handles receiving the actual photograph from the card. 355 | # postData will most likely contain multipart binary post data that needs to be parsed 356 | def uploadPhoto(self,postData): 357 | 358 | # Take the postData string and work with it as if it were a file object 359 | postDataInMemoryFile = StringIO.StringIO(postData) 360 | 361 | # Get the content-type header which looks something like this 362 | # content-type: multipart/form-data; boundary=---------------------------02468ace13579bdfcafebabef00d 363 | contentTypeHeader = self.headers.getheaders('content-type').pop() 364 | eyeFiLogger.debug(contentTypeHeader) 365 | 366 | # Extract the boundary parameter in the content-type header 367 | headerParameters = contentTypeHeader.split(";") 368 | eyeFiLogger.debug(headerParameters) 369 | 370 | boundary = headerParameters[1].split("=") 371 | boundary = boundary[1].strip() 372 | eyeFiLogger.debug("Extracted boundary: " + boundary) 373 | 374 | # eyeFiLogger.debug("uploadPhoto postData: " + postData) 375 | 376 | # Parse the multipart/form-data 377 | form = cgi.parse_multipart(postDataInMemoryFile, {"boundary":boundary,"content-disposition":self.headers.getheaders('content-disposition')}) 378 | eyeFiLogger.debug("Available multipart/form-data: " + str(form.keys())) 379 | 380 | # Parse the SOAPENVELOPE using the EyeFiContentHandler() 381 | soapEnvelope = form['SOAPENVELOPE'][0] 382 | eyeFiLogger.debug("SOAPENVELOPE: " + soapEnvelope) 383 | handler = EyeFiContentHandler() 384 | parser = xml.sax.parseString(soapEnvelope,handler) 385 | 386 | eyeFiLogger.debug("Extracted elements: " + str(handler.extractedElements)) 387 | 388 | 389 | # Write the newly uploaded file to memory 390 | untrustedFile = StringIO.StringIO() 391 | untrustedFile.write(form['FILENAME'][0]) 392 | 393 | # Perform an integrity check on the file before writing it out 394 | eyeFiCrypto = EyeFiCrypto.EyeFiCrypto() 395 | verifiedDigest = eyeFiCrypto.calculateIntegrityDigest(untrustedFile.getvalue(), 396 | self.server.eyeFiConfiguration['Card']['UploadKey']) 397 | unverifiedDigest = form['INTEGRITYDIGEST'][0] 398 | 399 | # Continue only if the digests match 400 | eyeFiLogger.debug("Comparing my digest [" + verifiedDigest + "] to card's digest [" + unverifiedDigest + "].") 401 | if( verifiedDigest == unverifiedDigest ): 402 | 403 | # Figure out where I am going to put this file 404 | if( 'DownloadLocation' in self.server.eyeFiConfiguration['Global'] ): 405 | downloadLocation = os.path.normpath(self.server.eyeFiConfiguration['Global']['DownloadLocation']) 406 | tarFilePath = os.path.join(downloadLocation,handler.extractedElements["filename"]) 407 | else: 408 | downloadLocation = os.path.join(os.curdir,"pictures") 409 | tarFilePath = os.path.join(os.curdir,"pictures",handler.extractedElements["filename"]) 410 | 411 | # Check to see if the path exists, if it doesn't, create it 412 | if( os.path.exists(downloadLocation) == False ): 413 | eyeFiLogger.debug("Path " + downloadLocation + " does not exist. Creating it.") 414 | os.mkdir(downloadLocation) 415 | 416 | tarFile = open(tarFilePath,"wb") 417 | eyeFiLogger.debug("Opened file " + tarFilePath + " for binary writing") 418 | 419 | tarFile.write(untrustedFile.getvalue()) 420 | eyeFiLogger.debug("Wrote file " + tarFilePath) 421 | 422 | tarFile.close() 423 | eyeFiLogger.debug("Closed file " + tarFilePath) 424 | 425 | eyeFiLogger.debug("Extracting TAR file " + tarFilePath) 426 | imageTarfile = tarfile.open(tarFilePath) 427 | imageNames = imageTarfile.getnames() 428 | imageTarfile.extractall(downloadLocation) 429 | 430 | eyeFiLogger.debug("Closing TAR file " + tarFilePath) 431 | imageTarfile.close() 432 | 433 | eyeFiLogger.debug("Deleting TAR file " + tarFilePath) 434 | os.remove(tarFilePath) 435 | 436 | # Run a command on the file if specified 437 | if( 'ExecuteOnUpload' in self.server.eyeFiConfiguration['Global'] ): 438 | command = self.server.eyeFiConfiguration['Global']['ExecuteOnUpload'] 439 | imagePath = os.path.join(downloadLocation,imageNames[0]) 440 | eyeFiLogger.debug("Executing command \"" + command + " " + imagePath + "\"") 441 | pid = subprocess.Popen([command, imagePath]).pid 442 | 443 | responseElementText = "true" 444 | else: 445 | eyeFiLogger.error("Digests do not match. Check UploadKey setting in .ini file.") 446 | responseElementText = "false" 447 | 448 | # Close the temporary string buffer 449 | untrustedFile.close() 450 | 451 | # Create the XML document to send back 452 | doc = xml.dom.minidom.Document() 453 | 454 | SOAPElement = doc.createElementNS("http://schemas.xmlsoap.org/soap/envelope/","SOAP-ENV:Envelope") 455 | SOAPElement.setAttribute("xmlns:SOAP-ENV","http://schemas.xmlsoap.org/soap/envelope/") 456 | SOAPBodyElement = doc.createElement("SOAP-ENV:Body") 457 | 458 | uploadPhotoResponseElement = doc.createElement("UploadPhotoResponse") 459 | successElement = doc.createElement("success") 460 | successElementText = doc.createTextNode(responseElementText) 461 | 462 | successElement.appendChild(successElementText) 463 | uploadPhotoResponseElement.appendChild(successElement) 464 | 465 | SOAPBodyElement.appendChild(uploadPhotoResponseElement) 466 | SOAPElement.appendChild(SOAPBodyElement) 467 | doc.appendChild(SOAPElement) 468 | 469 | return doc.toxml(encoding="UTF-8") 470 | 471 | # GetPhotoStatus allows the Eye-Fi card to query the server as to the current uploaded 472 | # status of a file. Even more important is that it authenticates the card to the server 473 | # by the use of the field. Essentially if the credential is correct the 474 | # server should allow files with the given filesignature to be uploaded. 475 | def getPhotoStatus(self,postData): 476 | handler = EyeFiContentHandler() 477 | parser = xml.sax.parseString(postData,handler) 478 | 479 | eyeFiLogger.debug("Extracted elements: " + str(handler.extractedElements)) 480 | 481 | # Calculate the credential string that I am expecting the card to send to me 482 | credentialString = handler.extractedElements["macaddress"] + self.server.eyeFiConfiguration['Card']['UploadKey'] + self.server.serverNonce; 483 | eyeFiLogger.debug("Concatenated credential string (pre MD5): " + credentialString) 484 | 485 | binaryCredentialString = binascii.unhexlify(credentialString) 486 | m = hashlib.md5() 487 | m.update(binaryCredentialString) 488 | credential = m.hexdigest() 489 | eyeFiLogger.debug("Credential string I'm expecting from card: " + credential) 490 | eyeFiLogger.debug("Credential string I got from card: " + handler.extractedElements["credential"]) 491 | 492 | 493 | # Create the XML document to send back 494 | doc = xml.dom.minidom.Document() 495 | 496 | SOAPElement = doc.createElementNS("http://schemas.xmlsoap.org/soap/envelope/","SOAP-ENV:Envelope") 497 | SOAPElement.setAttribute("xmlns:SOAP-ENV","http://schemas.xmlsoap.org/soap/envelope/") 498 | SOAPBodyElement = doc.createElement("SOAP-ENV:Body") 499 | 500 | getPhotoStatusResponseElement = doc.createElement("GetPhotoStatusResponse") 501 | getPhotoStatusResponseElement.setAttribute("xmlns","http://localhost/api/soap/eyefilm") 502 | 503 | # Check the credentials and see what to send back 504 | if( handler.extractedElements["credential"] != credential ): 505 | eyeFiLogger.error("Eye-Fi card did not supply proper credential string in GetPhotoStatus SOAP call.") 506 | 507 | fileidElement = doc.createElement("fileid") 508 | fileidElementText = doc.createTextNode("1") 509 | fileidElement.appendChild(fileidElementText) 510 | 511 | offsetElement = doc.createElement("offset") 512 | offsetElementText = doc.createTextNode("0") 513 | offsetElement.appendChild(offsetElementText) 514 | 515 | getPhotoStatusResponseElement.appendChild(fileidElement) 516 | getPhotoStatusResponseElement.appendChild(offsetElement) 517 | 518 | SOAPBodyElement.appendChild(getPhotoStatusResponseElement) 519 | 520 | SOAPElement.appendChild(SOAPBodyElement) 521 | doc.appendChild(SOAPElement) 522 | 523 | return doc.toxml(encoding="UTF-8") 524 | 525 | 526 | def startSession(self, postData): 527 | eyeFiLogger.debug("Delegating the XML parsing of startSession postData to EyeFiContentHandler()") 528 | handler = EyeFiContentHandler() 529 | parser = xml.sax.parseString(postData,handler) 530 | 531 | eyeFiLogger.debug("Extracted elements: " + str(handler.extractedElements)) 532 | 533 | # Retrieve it from C:\Documents and Settings\\Application Data\Eye-Fi\Settings.xml 534 | eyeFiUploadKey = self.server.eyeFiConfiguration['Card']['UploadKey'] 535 | eyeFiLogger.debug("Setting Eye-Fi upload key to " + eyeFiUploadKey) 536 | 537 | credentialString = handler.extractedElements["macaddress"] + handler.extractedElements["cnonce"] + eyeFiUploadKey; 538 | eyeFiLogger.debug("Concatenated credential string (pre MD5): " + credentialString) 539 | 540 | # Return the binary data represented by the hexadecimal string 541 | # resulting in something that looks like "\x00\x18\x03\x04..." 542 | binaryCredentialString = binascii.unhexlify(credentialString) 543 | 544 | # Now MD5 hash the binary string 545 | m = hashlib.md5() 546 | m.update(binaryCredentialString) 547 | 548 | # Hex encode the hash to obtain the final credential string 549 | credential = m.hexdigest() 550 | 551 | # Create the XML document to send back 552 | doc = xml.dom.minidom.Document() 553 | 554 | SOAPElement = doc.createElementNS("http://schemas.xmlsoap.org/soap/envelope/","SOAP-ENV:Envelope") 555 | SOAPElement.setAttribute("xmlns:SOAP-ENV","http://schemas.xmlsoap.org/soap/envelope/") 556 | SOAPBodyElement = doc.createElement("SOAP-ENV:Body") 557 | 558 | 559 | startSessionResponseElement = doc.createElement("StartSessionResponse") 560 | startSessionResponseElement.setAttribute("xmlns","http://localhost/api/soap/eyefilm") 561 | 562 | credentialElement = doc.createElement("credential") 563 | credentialElementText = doc.createTextNode(credential) 564 | credentialElement.appendChild(credentialElementText) 565 | 566 | snonceElement = doc.createElement("snonce") 567 | snonceElementText = doc.createTextNode(str(self.server.serverNonce)) 568 | snonceElement.appendChild(snonceElementText) 569 | 570 | transfermodeElement = doc.createElement("transfermode") 571 | transfermodeElementText = doc.createTextNode(handler.extractedElements["transfermode"]) 572 | transfermodeElement.appendChild(transfermodeElementText) 573 | 574 | transfermodetimestampElement = doc.createElement("transfermodetimestamp") 575 | transfermodetimestampElementText = doc.createTextNode(handler.extractedElements["transfermodetimestamp"]) 576 | transfermodetimestampElement.appendChild(transfermodetimestampElementText) 577 | 578 | upsyncallowedElement = doc.createElement("upsyncallowed") 579 | upsyncallowedElementText = doc.createTextNode("false") 580 | upsyncallowedElement.appendChild(upsyncallowedElementText) 581 | 582 | 583 | startSessionResponseElement.appendChild(credentialElement) 584 | startSessionResponseElement.appendChild(snonceElement) 585 | startSessionResponseElement.appendChild(transfermodeElement) 586 | startSessionResponseElement.appendChild(transfermodetimestampElement) 587 | startSessionResponseElement.appendChild(upsyncallowedElement) 588 | 589 | SOAPBodyElement.appendChild(startSessionResponseElement) 590 | 591 | SOAPElement.appendChild(SOAPBodyElement) 592 | doc.appendChild(SOAPElement) 593 | 594 | 595 | return doc.toxml(encoding="UTF-8") 596 | 597 | 598 | 599 | def setupLogging(eyeFiConfiguration): 600 | 601 | # Declare the main logger as a global 602 | global eyeFiLogger 603 | 604 | # Determine the log level 605 | if(eyeFiConfiguration['Global']['LogLevel'] == 'DEBUG'): 606 | loglevel = logging.DEBUG 607 | 608 | elif(eyeFiConfiguration['Global']['LogLevel'] == 'INFO'): 609 | loglevel = logging.INFO 610 | 611 | elif(eyeFiConfiguration['Global']['LogLevel'] == 'WARNING'): 612 | loglevel = logging.WARNING 613 | 614 | elif(eyeFiConfiguration['Global']['LogLevel'] == 'ERROR'): 615 | loglevel = logging.ERROR 616 | 617 | elif(eyeFiConfiguration['Global']['LogLevel'] == 'CRITICAL'): 618 | loglevel = logging.CRITICAL 619 | 620 | else: 621 | loglevel = logging.ERROR 622 | 623 | # Create the logger with the appropriate log level 624 | eyeFiLogger = logging.Logger("eyeFiLogger",loglevel) 625 | 626 | # Define the logging format to be used 627 | eyeFiLoggingFormat = logging.Formatter("[%(asctime)s][%(funcName)s] - %(message)s",'%m/%d/%y %I:%M%p') 628 | 629 | 630 | # Option to suppress console messages 631 | if( eyeFiConfiguration['Global'].as_bool('ConsoleOutput') == True ): 632 | consoleHandler = logging.StreamHandler(sys.stdout) 633 | consoleHandler.setFormatter(eyeFiLoggingFormat) 634 | eyeFiLogger.addHandler(consoleHandler) 635 | 636 | # Option to log to a file 637 | if( 'LogFile' in eyeFiConfiguration['Global'] ): 638 | fileHandler = logging.FileHandler(eyeFiConfiguration['Global']['LogFile'],"w",encoding=None, delay=0) 639 | fileHandler.setFormatter(eyeFiLoggingFormat) 640 | eyeFiLogger.addHandler(fileHandler) 641 | 642 | # Define a do-nothing handler so that existing logging messages don't error out 643 | class NullHandler(logging.Handler): 644 | def emit(self, record): 645 | pass 646 | eyeFiLogger.addHandler(NullHandler()) 647 | 648 | 649 | 650 | def commandLineOptions(): 651 | 652 | optionsParser.add_option("-c", "--config", action="store", dest="configfile", 653 | help="Path to configuration file (example in DefaultSettings.ini)") 654 | 655 | 656 | # This function attempts to read the configuration file. If no configuration 657 | # was passed into the program then this function is responsible for setting 658 | # defaults before returning the ConfigParser object 659 | def readConfigurationFile(options): 660 | 661 | # Use the configobj 3rd party module 662 | from configobj import ConfigObj 663 | 664 | # Create a dictionary with default values 665 | defaultEyeFiConfiguration = { 'Global': 666 | { 'ListenPort': '59278', 667 | 'LogLevel' : 'INFO', 668 | 'ConsoleOutput': 'True'} 669 | } 670 | 671 | # Load the defaults into a configuration object 672 | eyeFiConfiguration = ConfigObj(defaultEyeFiConfiguration) 673 | 674 | # If the configuration file parameter was given attempt to read the configuration file 675 | if( options.configfile != None ): 676 | eyeFiConfiguration.merge(ConfigObj(options.configfile)) 677 | else: 678 | print "Warning: No configuration file specified! Run this server with the -h command." 679 | 680 | # Return the entire ConfigParser object 681 | return eyeFiConfiguration 682 | 683 | 684 | 685 | def main(): 686 | 687 | # Load the available command line options 688 | commandLineOptions() 689 | 690 | # Parse the command line options 691 | (options, args) = optionsParser.parse_args() 692 | 693 | # Read the configuration file 694 | eyeFiConfiguration = readConfigurationFile(options) 695 | 696 | # Setup the logging that will be used for the rest of the program 697 | setupLogging(eyeFiConfiguration) 698 | 699 | 700 | eyeFiLogger.debug("Command line options: " + str(options)) 701 | eyeFiLogger.debug("eyeFiConfiguration: " + str(eyeFiConfiguration)) 702 | 703 | 704 | # This is the hostname and port which the server will listen 705 | # for requests. A blank hostname indicates all interfaces. 706 | server_address = ('', eyeFiConfiguration['Global'].as_int('ListenPort')) 707 | 708 | try: 709 | # Create an instance of an HTTP server. Requests will be handled 710 | # by the class EyeFiRequestHandler 711 | eyeFiServer = EyeFiServer(server_address, EyeFiRequestHandler, eyeFiConfiguration) 712 | 713 | # Spawn a new thread for the server 714 | eyeFiServerThread = threading.Thread(group=None, target=eyeFiServer.serve, name="EyeFiServerThread") 715 | eyeFiServerThread.daemon = True 716 | eyeFiServerThread.start() 717 | 718 | eyeFiLogger.info("Eye-Fi server started listening on port " + str(server_address[1])) 719 | eyeFiLogger.info("Press +C to terminate.") 720 | 721 | while(True): 722 | time.sleep(60) 723 | 724 | except KeyboardInterrupt: 725 | eyeFiLogger.info("Eye-Fi server shutting down") 726 | 727 | # It is possible that the signal arrives before the eyeFiServer variable is initialized 728 | if( "eyeFiServer" in locals() ): 729 | eyeFiServer.stop() 730 | eyeFiServer.socket.close() 731 | 732 | eyeFiLogger.info("Eye-Fi server stopped") 733 | 734 | 735 | if __name__ == '__main__': 736 | main() 737 | 738 | -------------------------------------------------------------------------------- /Release 2.0/EyeFiServerRegressionTests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import socket 3 | import urllib2 4 | import hashlib 5 | import binascii 6 | import mimetypes 7 | import xml.sax 8 | from xml.sax.handler import ContentHandler 9 | import xml.dom.minidom 10 | import httplib 11 | import re 12 | 13 | import EyeFiSOAPMessages 14 | import EyeFiCrypto 15 | 16 | # void testAppendsAdditionalParameterToUrlsInHrefAttributes(){?} 17 | # void testDoesNotRewriteImageOrJavascriptLinks(){?} 18 | # void testThrowsExceptionIfHrefContainsSessionId(){?} 19 | # void testEncodesParameterValue(){?} 20 | 21 | 22 | 23 | 24 | # This class tests to see if the Eye-Fi server is listening on the correct 25 | # network port. 26 | class networkingLevelTest(unittest.TestCase): 27 | 28 | # Test to see if a socket is open on port 59278 29 | def testEyeFiServerListening(self): 30 | eyeFiServerHostname = 'localhost' 31 | eyeFiPort = 59278 32 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 33 | 34 | try: 35 | s.connect((eyeFiServerHostname, eyeFiPort)) 36 | except: 37 | self.fail("Unable to connect to " + eyeFiServerHostname + ":" + str(eyeFiPort)) 38 | 39 | s.close() 40 | 41 | 42 | # Test the StartSession SOAP method call 43 | class startSessionSOAPMethodTest(unittest.TestCase): 44 | 45 | # Send a malformed MAC address in the StartSession request 46 | def testRejectsMalformedMACAddress(self): 47 | soapMessage = EyeFiSOAPMessages.EyeFiSOAPMessages() 48 | 49 | xmlData = soapMessage.getStartSessionXML("EYEFIMALFORMEDMAC", 50 | "9219c72db0ecbd7e585bb10551f6bc38", 51 | "2", 52 | "315532800") 53 | 54 | conn = httplib.HTTPConnection("localhost", 59278) 55 | headers = {"Host": "api.eye.fi", 56 | "User-Agent": "Eye-Fi Card/2.0001", 57 | "Accept": "text/xml, application/soap", 58 | "Connection": "Keep-Alive", 59 | "SOAPAction": "\"urn:StartSession\""} 60 | 61 | conn.request("POST", "/api/soap/eyefilm/v1",xmlData,headers) 62 | response = conn.getresponse() 63 | responseBody = response.read() 64 | 65 | 66 | if( responseBody.find("Agent is not authorized to receive pictures") == -1 ): 67 | self.fail("Did not receive a SOAP fault when sending an invalid MAC address") 68 | 69 | def testRejectsMalformedClientNonce(self): 70 | pass 71 | def testRejectsMalformedTransferMode(self): 72 | pass 73 | def testRejectsMalformedTransferModeTimestamp(self): 74 | pass 75 | 76 | 77 | def testCalculatesCredentialCorrectly(self): 78 | soapMessage = EyeFiSOAPMessages.EyeFiSOAPMessages() 79 | 80 | xmlData = soapMessage.getStartSessionXML("0018560304f8", 81 | "9219c72db0ecbd7e585bb10551f6bc38", 82 | "2", 83 | "315532800") 84 | 85 | conn = httplib.HTTPConnection("localhost", 59278) 86 | headers = {"Host": "api.eye.fi", 87 | "User-Agent": "Eye-Fi Card/2.0001", 88 | "Accept": "text/xml, application/soap", 89 | "Connection": "Keep-Alive", 90 | "SOAPAction": "\"urn:StartSession\""} 91 | 92 | conn.request("POST", "/api/soap/eyefilm/v1",xmlData,headers) 93 | response = conn.getresponse() 94 | responseBody = response.read() 95 | 96 | if( responseBody.find("f138ce5977a8962a089b87e17155e537") == -1 ): 97 | self.fail("Received invalid credential after giving EyeFi server my ") 98 | 99 | 100 | xmlData =\ 101 | """ 102 | 103 | 104 | 105 | 106 | 10ff036d3861ed3d1c47eb52d14841d2 107 | 0018560304f8 108 | CIMG1812.JPG.tar 109 | 250368 110 | 22a856437b0afc4edc5a6c70f990e637 111 | 112 | 113 | 114 | """.strip() 115 | 116 | headers["SOAPAction"] = "\"urn:GetPhotoStatus\"" 117 | conn.request("POST", "/api/soap/eyefilm/v1",xmlData,headers) 118 | response = conn.getresponse() 119 | responseBody = response.read() 120 | 121 | 122 | class getPhotoStatusSOAPMethodTest(unittest.TestCase): 123 | 124 | def testGetPhotoStatusBeforeStartSession(self): 125 | soapMessage = EyeFiSOAPMessages.EyeFiSOAPMessages() 126 | 127 | xmlData = soapMessage.getPhotoStatusXML("credential", 128 | "macaddress", 129 | "filename", 130 | "filesize", 131 | "filesignature") 132 | 133 | conn = httplib.HTTPConnection("localhost", 59278) 134 | headers = {"Host": "api.eye.fi", 135 | "User-Agent": "Eye-Fi Card/2.0001", 136 | "Accept": "text/xml, application/soap", 137 | "Connection": "Keep-Alive", 138 | "SOAPAction": "\"urn:GetPhotoStatus\""} 139 | 140 | conn.request("POST", "/api/soap/eyefilm/v1",xmlData,headers) 141 | response = conn.getresponse() 142 | responseBody = response.read() 143 | 144 | print response.status 145 | 146 | print responseBody 147 | 148 | 149 | 150 | 151 | 152 | 153 | class photoUploadTest(unittest.TestCase): 154 | 155 | def testUploadSinglePhoto(self): 156 | soapMessage = EyeFiSOAPMessages.EyeFiSOAPMessages() 157 | xmlData = soapMessage.getStartSessionXML("0018560304f8", 158 | "9219c72db0ecbd7e585bb10551f6bc38", 159 | "2", 160 | "315532800") 161 | 162 | conn = httplib.HTTPConnection("localhost", 59278) 163 | headers = {"Host": "api.eye.fi", 164 | "User-Agent": "Eye-Fi Card/2.0001", 165 | "Accept": "text/xml, application/soap", 166 | "Connection": "Keep-Alive", 167 | "SOAPAction": "\"urn:StartSession\""} 168 | 169 | conn.request("POST", "/api/soap/eyefilm/v1",xmlData,headers) 170 | response = conn.getresponse() 171 | responseBody = response.read() 172 | 173 | # Find the server's nonce and trim it appropriately 174 | snonceList = re.findall("[a-f0-9]+",responseBody) 175 | snonce = snonceList[0][8:40] 176 | 177 | # Calculate the credential string to send to server 178 | credentialString = "0018560304f8" + "c686e547e3728c63a8f78729c1592757" + snonce 179 | binaryCredentialString = binascii.unhexlify(credentialString) 180 | m = hashlib.md5() 181 | m.update(binaryCredentialString) 182 | credential = m.hexdigest() 183 | 184 | 185 | xmlData = soapMessage.getPhotoStatusXML(credential, 186 | "0018560304f8", 187 | "EyeFiLogo.jpg.tar", 188 | "20480", 189 | "243b34de7406153e7f5ccf235079ccff") 190 | headers = {"Host": "api.eye.fi", 191 | "User-Agent": "Eye-Fi Card/2.0001", 192 | "Accept": "text/xml, application/soap", 193 | "Connection": "Keep-Alive", 194 | "SOAPAction": "\"urn:GetPhotoStatus\""} 195 | 196 | conn.request("POST", "/api/soap/eyefilm/v1",xmlData,headers) 197 | response = conn.getresponse() 198 | responseBody = response.read() 199 | 200 | # Find the fileid and trim it appropriately 201 | fileidList = re.findall("[0-9]+",responseBody) 202 | 203 | # From the 8th character to the end 204 | fileid = fileidList[0][8:] 205 | 206 | # Take only the beginning to 9 chars from the end 207 | fileid = fileid[0:-9] 208 | 209 | # Upload the photo 210 | xmlData = soapMessage.getUploadPhotoXML(fileid, 211 | "0018560304f8", 212 | "EyeFiLogo.jpg.tar", 213 | "20480", 214 | "243b34de7406153e7f5ccf235079ccff", 215 | "none") 216 | 217 | # Calculate the integrity digest 218 | fileToComputeDigest = open("EyeFiLogo.jpg.tar", "rb") 219 | fileBytes = fileToComputeDigest.read() 220 | 221 | eyeFiCrypto = EyeFiCrypto.EyeFiCrypto() 222 | integrityDigest = eyeFiCrypto.calculateIntegrityDigest(fileBytes,"c686e547e3728c63a8f78729c1592757") 223 | 224 | # The POST fields 225 | fields = [("SOAPENVELOPE",xmlData),("INTEGRITYDIGEST",integrityDigest)] 226 | 227 | # The files to be uploaded 228 | targetFile = open('EyeFiLogo.jpg.tar', 'rb') 229 | 230 | files = [("FILENAME","EyeFiLogo.jpg.tar",targetFile.read())] 231 | 232 | # Create the multipart form data 233 | content_type, body = self.encode_multipart_formdata(fields, files) 234 | 235 | headers = {"Host": "api.eye.fi", 236 | "User-Agent": "Eye-Fi Card/2.0001", 237 | "Accept": "text/xml, application/soap", 238 | "Connection": "Keep-Alive", 239 | "Content-Type": content_type} 240 | 241 | conn.request("POST", "/api/soap/eyefilm/v1/upload", body, headers) 242 | 243 | response = conn.getresponse() 244 | responseBody = response.read() 245 | print responseBody 246 | 247 | 248 | def encode_multipart_formdata(self, fields, files): 249 | """ 250 | fields is a sequence of (name, value) elements for regular form fields. 251 | files is a sequence of (name, filename, value) elements for data to be uploaded as files 252 | Return (content_type, body) ready for httplib.HTTP instance 253 | """ 254 | BOUNDARY = '----------ThIs_Is_tHe_bouNdaRY_$' 255 | CRLF = '\r\n' 256 | L = [] 257 | for (key, value) in fields: 258 | L.append('--' + BOUNDARY) 259 | L.append('Content-Disposition: form-data; name="%s"' % key) 260 | L.append('') 261 | L.append(value) 262 | for (key, filename, value) in files: 263 | L.append('--' + BOUNDARY) 264 | L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) 265 | L.append('Content-Type: %s' % self.get_content_type(filename)) 266 | L.append('') 267 | L.append(value) 268 | L.append('--' + BOUNDARY + '--') 269 | L.append('') 270 | body = CRLF.join(L) 271 | content_type = 'multipart/form-data; boundary=%s' % BOUNDARY 272 | return content_type, body 273 | 274 | def get_content_type(self, filename): 275 | return mimetypes.guess_type(filename)[0] or 'application/octet-stream' 276 | 277 | 278 | 279 | 280 | 281 | if __name__ == '__main__': 282 | suite = unittest.TestLoader().loadTestsFromTestCase(networkingLevelTest) 283 | #unittest.TextTestRunner(verbosity=2).run(suite) 284 | 285 | suite = unittest.TestLoader().loadTestsFromTestCase(startSessionSOAPMethodTest) 286 | #unittest.TextTestRunner(verbosity=2).run(suite) 287 | 288 | suite = unittest.TestLoader().loadTestsFromTestCase(getPhotoStatusSOAPMethodTest) 289 | #unittest.TextTestRunner(verbosity=2).run(suite) 290 | 291 | suite = unittest.TestLoader().loadTestsFromTestCase(photoUploadTest) 292 | unittest.TextTestRunner(verbosity=2).run(suite) 293 | -------------------------------------------------------------------------------- /Release 2.0/configobj.py: -------------------------------------------------------------------------------- 1 | # configobj.py 2 | # A config file reader/writer that supports nested sections in config files. 3 | # Copyright (C) 2005-2008 Michael Foord, Nicola Larosa 4 | # E-mail: fuzzyman AT voidspace DOT org DOT uk 5 | # nico AT tekNico DOT net 6 | 7 | # ConfigObj 4 8 | # http://www.voidspace.org.uk/python/configobj.html 9 | 10 | # Released subject to the BSD License 11 | # Please see http://www.voidspace.org.uk/python/license.shtml 12 | 13 | # Scripts maintained at http://www.voidspace.org.uk/python/index.shtml 14 | # For information about bugfixes, updates and support, please join the 15 | # ConfigObj mailing list: 16 | # http://lists.sourceforge.net/lists/listinfo/configobj-develop 17 | # Comments, suggestions and bug reports welcome. 18 | 19 | from __future__ import generators 20 | 21 | import sys 22 | INTP_VER = sys.version_info[:2] 23 | if INTP_VER < (2, 2): 24 | raise RuntimeError("Python v.2.2 or later needed") 25 | 26 | import os, re 27 | compiler = None 28 | try: 29 | import compiler 30 | except ImportError: 31 | # for IronPython 32 | pass 33 | from types import StringTypes 34 | from warnings import warn 35 | try: 36 | from codecs import BOM_UTF8, BOM_UTF16, BOM_UTF16_BE, BOM_UTF16_LE 37 | except ImportError: 38 | # Python 2.2 does not have these 39 | # UTF-8 40 | BOM_UTF8 = '\xef\xbb\xbf' 41 | # UTF-16, little endian 42 | BOM_UTF16_LE = '\xff\xfe' 43 | # UTF-16, big endian 44 | BOM_UTF16_BE = '\xfe\xff' 45 | if sys.byteorder == 'little': 46 | # UTF-16, native endianness 47 | BOM_UTF16 = BOM_UTF16_LE 48 | else: 49 | # UTF-16, native endianness 50 | BOM_UTF16 = BOM_UTF16_BE 51 | 52 | # A dictionary mapping BOM to 53 | # the encoding to decode with, and what to set the 54 | # encoding attribute to. 55 | BOMS = { 56 | BOM_UTF8: ('utf_8', None), 57 | BOM_UTF16_BE: ('utf16_be', 'utf_16'), 58 | BOM_UTF16_LE: ('utf16_le', 'utf_16'), 59 | BOM_UTF16: ('utf_16', 'utf_16'), 60 | } 61 | # All legal variants of the BOM codecs. 62 | # TODO: the list of aliases is not meant to be exhaustive, is there a 63 | # better way ? 64 | BOM_LIST = { 65 | 'utf_16': 'utf_16', 66 | 'u16': 'utf_16', 67 | 'utf16': 'utf_16', 68 | 'utf-16': 'utf_16', 69 | 'utf16_be': 'utf16_be', 70 | 'utf_16_be': 'utf16_be', 71 | 'utf-16be': 'utf16_be', 72 | 'utf16_le': 'utf16_le', 73 | 'utf_16_le': 'utf16_le', 74 | 'utf-16le': 'utf16_le', 75 | 'utf_8': 'utf_8', 76 | 'u8': 'utf_8', 77 | 'utf': 'utf_8', 78 | 'utf8': 'utf_8', 79 | 'utf-8': 'utf_8', 80 | } 81 | 82 | # Map of encodings to the BOM to write. 83 | BOM_SET = { 84 | 'utf_8': BOM_UTF8, 85 | 'utf_16': BOM_UTF16, 86 | 'utf16_be': BOM_UTF16_BE, 87 | 'utf16_le': BOM_UTF16_LE, 88 | None: BOM_UTF8 89 | } 90 | 91 | 92 | def match_utf8(encoding): 93 | return BOM_LIST.get(encoding.lower()) == 'utf_8' 94 | 95 | 96 | # Quote strings used for writing values 97 | squot = "'%s'" 98 | dquot = '"%s"' 99 | noquot = "%s" 100 | wspace_plus = ' \r\t\n\v\t\'"' 101 | tsquot = '"""%s"""' 102 | tdquot = "'''%s'''" 103 | 104 | try: 105 | enumerate 106 | except NameError: 107 | def enumerate(obj): 108 | """enumerate for Python 2.2.""" 109 | i = -1 110 | for item in obj: 111 | i += 1 112 | yield i, item 113 | 114 | try: 115 | True, False 116 | except NameError: 117 | True, False = 1, 0 118 | 119 | 120 | __version__ = '4.5.3' 121 | 122 | __revision__ = '$Id: configobj.py 156 2006-01-31 14:57:08Z fuzzyman $' 123 | 124 | __docformat__ = "restructuredtext en" 125 | 126 | __all__ = ( 127 | '__version__', 128 | 'DEFAULT_INDENT_TYPE', 129 | 'DEFAULT_INTERPOLATION', 130 | 'ConfigObjError', 131 | 'NestingError', 132 | 'ParseError', 133 | 'DuplicateError', 134 | 'ConfigspecError', 135 | 'ConfigObj', 136 | 'SimpleVal', 137 | 'InterpolationError', 138 | 'InterpolationLoopError', 139 | 'MissingInterpolationOption', 140 | 'RepeatSectionError', 141 | 'ReloadError', 142 | 'UnreprError', 143 | 'UnknownType', 144 | '__docformat__', 145 | 'flatten_errors', 146 | ) 147 | 148 | DEFAULT_INTERPOLATION = 'configparser' 149 | DEFAULT_INDENT_TYPE = ' ' 150 | MAX_INTERPOL_DEPTH = 10 151 | 152 | OPTION_DEFAULTS = { 153 | 'interpolation': True, 154 | 'raise_errors': False, 155 | 'list_values': True, 156 | 'create_empty': False, 157 | 'file_error': False, 158 | 'configspec': None, 159 | 'stringify': True, 160 | # option may be set to one of ('', ' ', '\t') 161 | 'indent_type': None, 162 | 'encoding': None, 163 | 'default_encoding': None, 164 | 'unrepr': False, 165 | 'write_empty_values': False, 166 | } 167 | 168 | 169 | 170 | def getObj(s): 171 | s = "a=" + s 172 | if compiler is None: 173 | raise ImportError('compiler module not available') 174 | p = compiler.parse(s) 175 | return p.getChildren()[1].getChildren()[0].getChildren()[1] 176 | 177 | 178 | class UnknownType(Exception): 179 | pass 180 | 181 | 182 | class Builder(object): 183 | 184 | def build(self, o): 185 | m = getattr(self, 'build_' + o.__class__.__name__, None) 186 | if m is None: 187 | raise UnknownType(o.__class__.__name__) 188 | return m(o) 189 | 190 | def build_List(self, o): 191 | return map(self.build, o.getChildren()) 192 | 193 | def build_Const(self, o): 194 | return o.value 195 | 196 | def build_Dict(self, o): 197 | d = {} 198 | i = iter(map(self.build, o.getChildren())) 199 | for el in i: 200 | d[el] = i.next() 201 | return d 202 | 203 | def build_Tuple(self, o): 204 | return tuple(self.build_List(o)) 205 | 206 | def build_Name(self, o): 207 | if o.name == 'None': 208 | return None 209 | if o.name == 'True': 210 | return True 211 | if o.name == 'False': 212 | return False 213 | 214 | # An undefined Name 215 | raise UnknownType('Undefined Name') 216 | 217 | def build_Add(self, o): 218 | real, imag = map(self.build_Const, o.getChildren()) 219 | try: 220 | real = float(real) 221 | except TypeError: 222 | raise UnknownType('Add') 223 | if not isinstance(imag, complex) or imag.real != 0.0: 224 | raise UnknownType('Add') 225 | return real+imag 226 | 227 | def build_Getattr(self, o): 228 | parent = self.build(o.expr) 229 | return getattr(parent, o.attrname) 230 | 231 | def build_UnarySub(self, o): 232 | return -self.build_Const(o.getChildren()[0]) 233 | 234 | def build_UnaryAdd(self, o): 235 | return self.build_Const(o.getChildren()[0]) 236 | 237 | 238 | _builder = Builder() 239 | 240 | 241 | def unrepr(s): 242 | if not s: 243 | return s 244 | return _builder.build(getObj(s)) 245 | 246 | 247 | 248 | class ConfigObjError(SyntaxError): 249 | """ 250 | This is the base class for all errors that ConfigObj raises. 251 | It is a subclass of SyntaxError. 252 | """ 253 | def __init__(self, message='', line_number=None, line=''): 254 | self.line = line 255 | self.line_number = line_number 256 | self.message = message 257 | SyntaxError.__init__(self, message) 258 | 259 | 260 | class NestingError(ConfigObjError): 261 | """ 262 | This error indicates a level of nesting that doesn't match. 263 | """ 264 | 265 | 266 | class ParseError(ConfigObjError): 267 | """ 268 | This error indicates that a line is badly written. 269 | It is neither a valid ``key = value`` line, 270 | nor a valid section marker line. 271 | """ 272 | 273 | 274 | class ReloadError(IOError): 275 | """ 276 | A 'reload' operation failed. 277 | This exception is a subclass of ``IOError``. 278 | """ 279 | def __init__(self): 280 | IOError.__init__(self, 'reload failed, filename is not set.') 281 | 282 | 283 | class DuplicateError(ConfigObjError): 284 | """ 285 | The keyword or section specified already exists. 286 | """ 287 | 288 | 289 | class ConfigspecError(ConfigObjError): 290 | """ 291 | An error occured whilst parsing a configspec. 292 | """ 293 | 294 | 295 | class InterpolationError(ConfigObjError): 296 | """Base class for the two interpolation errors.""" 297 | 298 | 299 | class InterpolationLoopError(InterpolationError): 300 | """Maximum interpolation depth exceeded in string interpolation.""" 301 | 302 | def __init__(self, option): 303 | InterpolationError.__init__( 304 | self, 305 | 'interpolation loop detected in value "%s".' % option) 306 | 307 | 308 | class RepeatSectionError(ConfigObjError): 309 | """ 310 | This error indicates additional sections in a section with a 311 | ``__many__`` (repeated) section. 312 | """ 313 | 314 | 315 | class MissingInterpolationOption(InterpolationError): 316 | """A value specified for interpolation was missing.""" 317 | 318 | def __init__(self, option): 319 | InterpolationError.__init__( 320 | self, 321 | 'missing option "%s" in interpolation.' % option) 322 | 323 | 324 | class UnreprError(ConfigObjError): 325 | """An error parsing in unrepr mode.""" 326 | 327 | 328 | 329 | class InterpolationEngine(object): 330 | """ 331 | A helper class to help perform string interpolation. 332 | 333 | This class is an abstract base class; its descendants perform 334 | the actual work. 335 | """ 336 | 337 | # compiled regexp to use in self.interpolate() 338 | _KEYCRE = re.compile(r"%\(([^)]*)\)s") 339 | 340 | def __init__(self, section): 341 | # the Section instance that "owns" this engine 342 | self.section = section 343 | 344 | 345 | def interpolate(self, key, value): 346 | def recursive_interpolate(key, value, section, backtrail): 347 | """The function that does the actual work. 348 | 349 | ``value``: the string we're trying to interpolate. 350 | ``section``: the section in which that string was found 351 | ``backtrail``: a dict to keep track of where we've been, 352 | to detect and prevent infinite recursion loops 353 | 354 | This is similar to a depth-first-search algorithm. 355 | """ 356 | # Have we been here already? 357 | if backtrail.has_key((key, section.name)): 358 | # Yes - infinite loop detected 359 | raise InterpolationLoopError(key) 360 | # Place a marker on our backtrail so we won't come back here again 361 | backtrail[(key, section.name)] = 1 362 | 363 | # Now start the actual work 364 | match = self._KEYCRE.search(value) 365 | while match: 366 | # The actual parsing of the match is implementation-dependent, 367 | # so delegate to our helper function 368 | k, v, s = self._parse_match(match) 369 | if k is None: 370 | # That's the signal that no further interpolation is needed 371 | replacement = v 372 | else: 373 | # Further interpolation may be needed to obtain final value 374 | replacement = recursive_interpolate(k, v, s, backtrail) 375 | # Replace the matched string with its final value 376 | start, end = match.span() 377 | value = ''.join((value[:start], replacement, value[end:])) 378 | new_search_start = start + len(replacement) 379 | # Pick up the next interpolation key, if any, for next time 380 | # through the while loop 381 | match = self._KEYCRE.search(value, new_search_start) 382 | 383 | # Now safe to come back here again; remove marker from backtrail 384 | del backtrail[(key, section.name)] 385 | 386 | return value 387 | 388 | # Back in interpolate(), all we have to do is kick off the recursive 389 | # function with appropriate starting values 390 | value = recursive_interpolate(key, value, self.section, {}) 391 | return value 392 | 393 | 394 | def _fetch(self, key): 395 | """Helper function to fetch values from owning section. 396 | 397 | Returns a 2-tuple: the value, and the section where it was found. 398 | """ 399 | # switch off interpolation before we try and fetch anything ! 400 | save_interp = self.section.main.interpolation 401 | self.section.main.interpolation = False 402 | 403 | # Start at section that "owns" this InterpolationEngine 404 | current_section = self.section 405 | while True: 406 | # try the current section first 407 | val = current_section.get(key) 408 | if val is not None: 409 | break 410 | # try "DEFAULT" next 411 | val = current_section.get('DEFAULT', {}).get(key) 412 | if val is not None: 413 | break 414 | # move up to parent and try again 415 | # top-level's parent is itself 416 | if current_section.parent is current_section: 417 | # reached top level, time to give up 418 | break 419 | current_section = current_section.parent 420 | 421 | # restore interpolation to previous value before returning 422 | self.section.main.interpolation = save_interp 423 | if val is None: 424 | raise MissingInterpolationOption(key) 425 | return val, current_section 426 | 427 | 428 | def _parse_match(self, match): 429 | """Implementation-dependent helper function. 430 | 431 | Will be passed a match object corresponding to the interpolation 432 | key we just found (e.g., "%(foo)s" or "$foo"). Should look up that 433 | key in the appropriate config file section (using the ``_fetch()`` 434 | helper function) and return a 3-tuple: (key, value, section) 435 | 436 | ``key`` is the name of the key we're looking for 437 | ``value`` is the value found for that key 438 | ``section`` is a reference to the section where it was found 439 | 440 | ``key`` and ``section`` should be None if no further 441 | interpolation should be performed on the resulting value 442 | (e.g., if we interpolated "$$" and returned "$"). 443 | """ 444 | raise NotImplementedError() 445 | 446 | 447 | 448 | class ConfigParserInterpolation(InterpolationEngine): 449 | """Behaves like ConfigParser.""" 450 | _KEYCRE = re.compile(r"%\(([^)]*)\)s") 451 | 452 | def _parse_match(self, match): 453 | key = match.group(1) 454 | value, section = self._fetch(key) 455 | return key, value, section 456 | 457 | 458 | 459 | class TemplateInterpolation(InterpolationEngine): 460 | """Behaves like string.Template.""" 461 | _delimiter = '$' 462 | _KEYCRE = re.compile(r""" 463 | \$(?: 464 | (?P\$) | # Two $ signs 465 | (?P[_a-z][_a-z0-9]*) | # $name format 466 | {(?P[^}]*)} # ${name} format 467 | ) 468 | """, re.IGNORECASE | re.VERBOSE) 469 | 470 | def _parse_match(self, match): 471 | # Valid name (in or out of braces): fetch value from section 472 | key = match.group('named') or match.group('braced') 473 | if key is not None: 474 | value, section = self._fetch(key) 475 | return key, value, section 476 | # Escaped delimiter (e.g., $$): return single delimiter 477 | if match.group('escaped') is not None: 478 | # Return None for key and section to indicate it's time to stop 479 | return None, self._delimiter, None 480 | # Anything else: ignore completely, just return it unchanged 481 | return None, match.group(), None 482 | 483 | 484 | interpolation_engines = { 485 | 'configparser': ConfigParserInterpolation, 486 | 'template': TemplateInterpolation, 487 | } 488 | 489 | 490 | 491 | class Section(dict): 492 | """ 493 | A dictionary-like object that represents a section in a config file. 494 | 495 | It does string interpolation if the 'interpolation' attribute 496 | of the 'main' object is set to True. 497 | 498 | Interpolation is tried first from this object, then from the 'DEFAULT' 499 | section of this object, next from the parent and its 'DEFAULT' section, 500 | and so on until the main object is reached. 501 | 502 | A Section will behave like an ordered dictionary - following the 503 | order of the ``scalars`` and ``sections`` attributes. 504 | You can use this to change the order of members. 505 | 506 | Iteration follows the order: scalars, then sections. 507 | """ 508 | 509 | def __init__(self, parent, depth, main, indict=None, name=None): 510 | """ 511 | * parent is the section above 512 | * depth is the depth level of this section 513 | * main is the main ConfigObj 514 | * indict is a dictionary to initialise the section with 515 | """ 516 | if indict is None: 517 | indict = {} 518 | dict.__init__(self) 519 | # used for nesting level *and* interpolation 520 | self.parent = parent 521 | # used for the interpolation attribute 522 | self.main = main 523 | # level of nesting depth of this Section 524 | self.depth = depth 525 | # purely for information 526 | self.name = name 527 | # 528 | self._initialise() 529 | # we do this explicitly so that __setitem__ is used properly 530 | # (rather than just passing to ``dict.__init__``) 531 | for entry, value in indict.iteritems(): 532 | self[entry] = value 533 | 534 | 535 | def _initialise(self): 536 | # the sequence of scalar values in this Section 537 | self.scalars = [] 538 | # the sequence of sections in this Section 539 | self.sections = [] 540 | # for comments :-) 541 | self.comments = {} 542 | self.inline_comments = {} 543 | # for the configspec 544 | self.configspec = {} 545 | self._order = [] 546 | self._configspec_comments = {} 547 | self._configspec_inline_comments = {} 548 | self._cs_section_comments = {} 549 | self._cs_section_inline_comments = {} 550 | # for defaults 551 | self.defaults = [] 552 | self.default_values = {} 553 | 554 | 555 | def _interpolate(self, key, value): 556 | try: 557 | # do we already have an interpolation engine? 558 | engine = self._interpolation_engine 559 | except AttributeError: 560 | # not yet: first time running _interpolate(), so pick the engine 561 | name = self.main.interpolation 562 | if name == True: # note that "if name:" would be incorrect here 563 | # backwards-compatibility: interpolation=True means use default 564 | name = DEFAULT_INTERPOLATION 565 | name = name.lower() # so that "Template", "template", etc. all work 566 | class_ = interpolation_engines.get(name, None) 567 | if class_ is None: 568 | # invalid value for self.main.interpolation 569 | self.main.interpolation = False 570 | return value 571 | else: 572 | # save reference to engine so we don't have to do this again 573 | engine = self._interpolation_engine = class_(self) 574 | # let the engine do the actual work 575 | return engine.interpolate(key, value) 576 | 577 | 578 | def __getitem__(self, key): 579 | """Fetch the item and do string interpolation.""" 580 | val = dict.__getitem__(self, key) 581 | if self.main.interpolation and isinstance(val, StringTypes): 582 | return self._interpolate(key, val) 583 | return val 584 | 585 | 586 | def __setitem__(self, key, value, unrepr=False): 587 | """ 588 | Correctly set a value. 589 | 590 | Making dictionary values Section instances. 591 | (We have to special case 'Section' instances - which are also dicts) 592 | 593 | Keys must be strings. 594 | Values need only be strings (or lists of strings) if 595 | ``main.stringify`` is set. 596 | 597 | `unrepr`` must be set when setting a value to a dictionary, without 598 | creating a new sub-section. 599 | """ 600 | if not isinstance(key, StringTypes): 601 | raise ValueError('The key "%s" is not a string.' % key) 602 | 603 | # add the comment 604 | if not self.comments.has_key(key): 605 | self.comments[key] = [] 606 | self.inline_comments[key] = '' 607 | # remove the entry from defaults 608 | if key in self.defaults: 609 | self.defaults.remove(key) 610 | # 611 | if isinstance(value, Section): 612 | if not self.has_key(key): 613 | self.sections.append(key) 614 | dict.__setitem__(self, key, value) 615 | elif isinstance(value, dict) and not unrepr: 616 | # First create the new depth level, 617 | # then create the section 618 | if not self.has_key(key): 619 | self.sections.append(key) 620 | new_depth = self.depth + 1 621 | dict.__setitem__( 622 | self, 623 | key, 624 | Section( 625 | self, 626 | new_depth, 627 | self.main, 628 | indict=value, 629 | name=key)) 630 | else: 631 | if not self.has_key(key): 632 | self.scalars.append(key) 633 | if not self.main.stringify: 634 | if isinstance(value, StringTypes): 635 | pass 636 | elif isinstance(value, (list, tuple)): 637 | for entry in value: 638 | if not isinstance(entry, StringTypes): 639 | raise TypeError('Value is not a string "%s".' % entry) 640 | else: 641 | raise TypeError('Value is not a string "%s".' % value) 642 | dict.__setitem__(self, key, value) 643 | 644 | 645 | def __delitem__(self, key): 646 | """Remove items from the sequence when deleting.""" 647 | dict. __delitem__(self, key) 648 | if key in self.scalars: 649 | self.scalars.remove(key) 650 | else: 651 | self.sections.remove(key) 652 | del self.comments[key] 653 | del self.inline_comments[key] 654 | 655 | 656 | def get(self, key, default=None): 657 | """A version of ``get`` that doesn't bypass string interpolation.""" 658 | try: 659 | return self[key] 660 | except KeyError: 661 | return default 662 | 663 | 664 | def update(self, indict): 665 | """ 666 | A version of update that uses our ``__setitem__``. 667 | """ 668 | for entry in indict: 669 | self[entry] = indict[entry] 670 | 671 | 672 | def pop(self, key, *args): 673 | """ 674 | 'D.pop(k[,d]) -> v, remove specified key and return the corresponding value. 675 | If key is not found, d is returned if given, otherwise KeyError is raised' 676 | """ 677 | val = dict.pop(self, key, *args) 678 | if key in self.scalars: 679 | del self.comments[key] 680 | del self.inline_comments[key] 681 | self.scalars.remove(key) 682 | elif key in self.sections: 683 | del self.comments[key] 684 | del self.inline_comments[key] 685 | self.sections.remove(key) 686 | if self.main.interpolation and isinstance(val, StringTypes): 687 | return self._interpolate(key, val) 688 | return val 689 | 690 | 691 | def popitem(self): 692 | """Pops the first (key,val)""" 693 | sequence = (self.scalars + self.sections) 694 | if not sequence: 695 | raise KeyError(": 'popitem(): dictionary is empty'") 696 | key = sequence[0] 697 | val = self[key] 698 | del self[key] 699 | return key, val 700 | 701 | 702 | def clear(self): 703 | """ 704 | A version of clear that also affects scalars/sections 705 | Also clears comments and configspec. 706 | 707 | Leaves other attributes alone : 708 | depth/main/parent are not affected 709 | """ 710 | dict.clear(self) 711 | self.scalars = [] 712 | self.sections = [] 713 | self.comments = {} 714 | self.inline_comments = {} 715 | self.configspec = {} 716 | 717 | 718 | def setdefault(self, key, default=None): 719 | """A version of setdefault that sets sequence if appropriate.""" 720 | try: 721 | return self[key] 722 | except KeyError: 723 | self[key] = default 724 | return self[key] 725 | 726 | 727 | def items(self): 728 | """D.items() -> list of D's (key, value) pairs, as 2-tuples""" 729 | return zip((self.scalars + self.sections), self.values()) 730 | 731 | 732 | def keys(self): 733 | """D.keys() -> list of D's keys""" 734 | return (self.scalars + self.sections) 735 | 736 | 737 | def values(self): 738 | """D.values() -> list of D's values""" 739 | return [self[key] for key in (self.scalars + self.sections)] 740 | 741 | 742 | def iteritems(self): 743 | """D.iteritems() -> an iterator over the (key, value) items of D""" 744 | return iter(self.items()) 745 | 746 | 747 | def iterkeys(self): 748 | """D.iterkeys() -> an iterator over the keys of D""" 749 | return iter((self.scalars + self.sections)) 750 | 751 | __iter__ = iterkeys 752 | 753 | 754 | def itervalues(self): 755 | """D.itervalues() -> an iterator over the values of D""" 756 | return iter(self.values()) 757 | 758 | 759 | def __repr__(self): 760 | """x.__repr__() <==> repr(x)""" 761 | return '{%s}' % ', '.join([('%s: %s' % (repr(key), repr(self[key]))) 762 | for key in (self.scalars + self.sections)]) 763 | 764 | __str__ = __repr__ 765 | __str__.__doc__ = "x.__str__() <==> str(x)" 766 | 767 | 768 | # Extra methods - not in a normal dictionary 769 | 770 | def dict(self): 771 | """ 772 | Return a deepcopy of self as a dictionary. 773 | 774 | All members that are ``Section`` instances are recursively turned to 775 | ordinary dictionaries - by calling their ``dict`` method. 776 | 777 | >>> n = a.dict() 778 | >>> n == a 779 | 1 780 | >>> n is a 781 | 0 782 | """ 783 | newdict = {} 784 | for entry in self: 785 | this_entry = self[entry] 786 | if isinstance(this_entry, Section): 787 | this_entry = this_entry.dict() 788 | elif isinstance(this_entry, list): 789 | # create a copy rather than a reference 790 | this_entry = list(this_entry) 791 | elif isinstance(this_entry, tuple): 792 | # create a copy rather than a reference 793 | this_entry = tuple(this_entry) 794 | newdict[entry] = this_entry 795 | return newdict 796 | 797 | 798 | def merge(self, indict): 799 | """ 800 | A recursive update - useful for merging config files. 801 | 802 | >>> a = '''[section1] 803 | ... option1 = True 804 | ... [[subsection]] 805 | ... more_options = False 806 | ... # end of file'''.splitlines() 807 | >>> b = '''# File is user.ini 808 | ... [section1] 809 | ... option1 = False 810 | ... # end of file'''.splitlines() 811 | >>> c1 = ConfigObj(b) 812 | >>> c2 = ConfigObj(a) 813 | >>> c2.merge(c1) 814 | >>> c2 815 | {'section1': {'option1': 'False', 'subsection': {'more_options': 'False'}}} 816 | """ 817 | for key, val in indict.items(): 818 | if (key in self and isinstance(self[key], dict) and 819 | isinstance(val, dict)): 820 | self[key].merge(val) 821 | else: 822 | self[key] = val 823 | 824 | 825 | def rename(self, oldkey, newkey): 826 | """ 827 | Change a keyname to another, without changing position in sequence. 828 | 829 | Implemented so that transformations can be made on keys, 830 | as well as on values. (used by encode and decode) 831 | 832 | Also renames comments. 833 | """ 834 | if oldkey in self.scalars: 835 | the_list = self.scalars 836 | elif oldkey in self.sections: 837 | the_list = self.sections 838 | else: 839 | raise KeyError('Key "%s" not found.' % oldkey) 840 | pos = the_list.index(oldkey) 841 | # 842 | val = self[oldkey] 843 | dict.__delitem__(self, oldkey) 844 | dict.__setitem__(self, newkey, val) 845 | the_list.remove(oldkey) 846 | the_list.insert(pos, newkey) 847 | comm = self.comments[oldkey] 848 | inline_comment = self.inline_comments[oldkey] 849 | del self.comments[oldkey] 850 | del self.inline_comments[oldkey] 851 | self.comments[newkey] = comm 852 | self.inline_comments[newkey] = inline_comment 853 | 854 | 855 | def walk(self, function, raise_errors=True, 856 | call_on_sections=False, **keywargs): 857 | """ 858 | Walk every member and call a function on the keyword and value. 859 | 860 | Return a dictionary of the return values 861 | 862 | If the function raises an exception, raise the errror 863 | unless ``raise_errors=False``, in which case set the return value to 864 | ``False``. 865 | 866 | Any unrecognised keyword arguments you pass to walk, will be pased on 867 | to the function you pass in. 868 | 869 | Note: if ``call_on_sections`` is ``True`` then - on encountering a 870 | subsection, *first* the function is called for the *whole* subsection, 871 | and then recurses into it's members. This means your function must be 872 | able to handle strings, dictionaries and lists. This allows you 873 | to change the key of subsections as well as for ordinary members. The 874 | return value when called on the whole subsection has to be discarded. 875 | 876 | See the encode and decode methods for examples, including functions. 877 | 878 | .. caution:: 879 | 880 | You can use ``walk`` to transform the names of members of a section 881 | but you mustn't add or delete members. 882 | 883 | >>> config = '''[XXXXsection] 884 | ... XXXXkey = XXXXvalue'''.splitlines() 885 | >>> cfg = ConfigObj(config) 886 | >>> cfg 887 | {'XXXXsection': {'XXXXkey': 'XXXXvalue'}} 888 | >>> def transform(section, key): 889 | ... val = section[key] 890 | ... newkey = key.replace('XXXX', 'CLIENT1') 891 | ... section.rename(key, newkey) 892 | ... if isinstance(val, (tuple, list, dict)): 893 | ... pass 894 | ... else: 895 | ... val = val.replace('XXXX', 'CLIENT1') 896 | ... section[newkey] = val 897 | >>> cfg.walk(transform, call_on_sections=True) 898 | {'CLIENT1section': {'CLIENT1key': None}} 899 | >>> cfg 900 | {'CLIENT1section': {'CLIENT1key': 'CLIENT1value'}} 901 | """ 902 | out = {} 903 | # scalars first 904 | for i in range(len(self.scalars)): 905 | entry = self.scalars[i] 906 | try: 907 | val = function(self, entry, **keywargs) 908 | # bound again in case name has changed 909 | entry = self.scalars[i] 910 | out[entry] = val 911 | except Exception: 912 | if raise_errors: 913 | raise 914 | else: 915 | entry = self.scalars[i] 916 | out[entry] = False 917 | # then sections 918 | for i in range(len(self.sections)): 919 | entry = self.sections[i] 920 | if call_on_sections: 921 | try: 922 | function(self, entry, **keywargs) 923 | except Exception: 924 | if raise_errors: 925 | raise 926 | else: 927 | entry = self.sections[i] 928 | out[entry] = False 929 | # bound again in case name has changed 930 | entry = self.sections[i] 931 | # previous result is discarded 932 | out[entry] = self[entry].walk( 933 | function, 934 | raise_errors=raise_errors, 935 | call_on_sections=call_on_sections, 936 | **keywargs) 937 | return out 938 | 939 | 940 | def decode(self, encoding): 941 | """ 942 | Decode all strings and values to unicode, using the specified encoding. 943 | 944 | Works with subsections and list values. 945 | 946 | Uses the ``walk`` method. 947 | 948 | Testing ``encode`` and ``decode``. 949 | >>> m = ConfigObj(a) 950 | >>> m.decode('ascii') 951 | >>> def testuni(val): 952 | ... for entry in val: 953 | ... if not isinstance(entry, unicode): 954 | ... print >> sys.stderr, type(entry) 955 | ... raise AssertionError, 'decode failed.' 956 | ... if isinstance(val[entry], dict): 957 | ... testuni(val[entry]) 958 | ... elif not isinstance(val[entry], unicode): 959 | ... raise AssertionError, 'decode failed.' 960 | >>> testuni(m) 961 | >>> m.encode('ascii') 962 | >>> a == m 963 | 1 964 | """ 965 | warn('use of ``decode`` is deprecated.', DeprecationWarning) 966 | def decode(section, key, encoding=encoding, warn=True): 967 | """ """ 968 | val = section[key] 969 | if isinstance(val, (list, tuple)): 970 | newval = [] 971 | for entry in val: 972 | newval.append(entry.decode(encoding)) 973 | elif isinstance(val, dict): 974 | newval = val 975 | else: 976 | newval = val.decode(encoding) 977 | newkey = key.decode(encoding) 978 | section.rename(key, newkey) 979 | section[newkey] = newval 980 | # using ``call_on_sections`` allows us to modify section names 981 | self.walk(decode, call_on_sections=True) 982 | 983 | 984 | def encode(self, encoding): 985 | """ 986 | Encode all strings and values from unicode, 987 | using the specified encoding. 988 | 989 | Works with subsections and list values. 990 | Uses the ``walk`` method. 991 | """ 992 | warn('use of ``encode`` is deprecated.', DeprecationWarning) 993 | def encode(section, key, encoding=encoding): 994 | """ """ 995 | val = section[key] 996 | if isinstance(val, (list, tuple)): 997 | newval = [] 998 | for entry in val: 999 | newval.append(entry.encode(encoding)) 1000 | elif isinstance(val, dict): 1001 | newval = val 1002 | else: 1003 | newval = val.encode(encoding) 1004 | newkey = key.encode(encoding) 1005 | section.rename(key, newkey) 1006 | section[newkey] = newval 1007 | self.walk(encode, call_on_sections=True) 1008 | 1009 | 1010 | def istrue(self, key): 1011 | """A deprecated version of ``as_bool``.""" 1012 | warn('use of ``istrue`` is deprecated. Use ``as_bool`` method ' 1013 | 'instead.', DeprecationWarning) 1014 | return self.as_bool(key) 1015 | 1016 | 1017 | def as_bool(self, key): 1018 | """ 1019 | Accepts a key as input. The corresponding value must be a string or 1020 | the objects (``True`` or 1) or (``False`` or 0). We allow 0 and 1 to 1021 | retain compatibility with Python 2.2. 1022 | 1023 | If the string is one of ``True``, ``On``, ``Yes``, or ``1`` it returns 1024 | ``True``. 1025 | 1026 | If the string is one of ``False``, ``Off``, ``No``, or ``0`` it returns 1027 | ``False``. 1028 | 1029 | ``as_bool`` is not case sensitive. 1030 | 1031 | Any other input will raise a ``ValueError``. 1032 | 1033 | >>> a = ConfigObj() 1034 | >>> a['a'] = 'fish' 1035 | >>> a.as_bool('a') 1036 | Traceback (most recent call last): 1037 | ValueError: Value "fish" is neither True nor False 1038 | >>> a['b'] = 'True' 1039 | >>> a.as_bool('b') 1040 | 1 1041 | >>> a['b'] = 'off' 1042 | >>> a.as_bool('b') 1043 | 0 1044 | """ 1045 | val = self[key] 1046 | if val == True: 1047 | return True 1048 | elif val == False: 1049 | return False 1050 | else: 1051 | try: 1052 | if not isinstance(val, StringTypes): 1053 | # TODO: Why do we raise a KeyError here? 1054 | raise KeyError() 1055 | else: 1056 | return self.main._bools[val.lower()] 1057 | except KeyError: 1058 | raise ValueError('Value "%s" is neither True nor False' % val) 1059 | 1060 | 1061 | def as_int(self, key): 1062 | """ 1063 | A convenience method which coerces the specified value to an integer. 1064 | 1065 | If the value is an invalid literal for ``int``, a ``ValueError`` will 1066 | be raised. 1067 | 1068 | >>> a = ConfigObj() 1069 | >>> a['a'] = 'fish' 1070 | >>> a.as_int('a') 1071 | Traceback (most recent call last): 1072 | ValueError: invalid literal for int(): fish 1073 | >>> a['b'] = '1' 1074 | >>> a.as_int('b') 1075 | 1 1076 | >>> a['b'] = '3.2' 1077 | >>> a.as_int('b') 1078 | Traceback (most recent call last): 1079 | ValueError: invalid literal for int(): 3.2 1080 | """ 1081 | return int(self[key]) 1082 | 1083 | 1084 | def as_float(self, key): 1085 | """ 1086 | A convenience method which coerces the specified value to a float. 1087 | 1088 | If the value is an invalid literal for ``float``, a ``ValueError`` will 1089 | be raised. 1090 | 1091 | >>> a = ConfigObj() 1092 | >>> a['a'] = 'fish' 1093 | >>> a.as_float('a') 1094 | Traceback (most recent call last): 1095 | ValueError: invalid literal for float(): fish 1096 | >>> a['b'] = '1' 1097 | >>> a.as_float('b') 1098 | 1.0 1099 | >>> a['b'] = '3.2' 1100 | >>> a.as_float('b') 1101 | 3.2000000000000002 1102 | """ 1103 | return float(self[key]) 1104 | 1105 | 1106 | def restore_default(self, key): 1107 | """ 1108 | Restore (and return) default value for the specified key. 1109 | 1110 | This method will only work for a ConfigObj that was created 1111 | with a configspec and has been validated. 1112 | 1113 | If there is no default value for this key, ``KeyError`` is raised. 1114 | """ 1115 | default = self.default_values[key] 1116 | dict.__setitem__(self, key, default) 1117 | if key not in self.defaults: 1118 | self.defaults.append(key) 1119 | return default 1120 | 1121 | 1122 | def restore_defaults(self): 1123 | """ 1124 | Recursively restore default values to all members 1125 | that have them. 1126 | 1127 | This method will only work for a ConfigObj that was created 1128 | with a configspec and has been validated. 1129 | 1130 | It doesn't delete or modify entries without default values. 1131 | """ 1132 | for key in self.default_values: 1133 | self.restore_default(key) 1134 | 1135 | for section in self.sections: 1136 | self[section].restore_defaults() 1137 | 1138 | 1139 | class ConfigObj(Section): 1140 | """An object to read, create, and write config files.""" 1141 | 1142 | _keyword = re.compile(r'''^ # line start 1143 | (\s*) # indentation 1144 | ( # keyword 1145 | (?:".*?")| # double quotes 1146 | (?:'.*?')| # single quotes 1147 | (?:[^'"=].*?) # no quotes 1148 | ) 1149 | \s*=\s* # divider 1150 | (.*) # value (including list values and comments) 1151 | $ # line end 1152 | ''', 1153 | re.VERBOSE) 1154 | 1155 | _sectionmarker = re.compile(r'''^ 1156 | (\s*) # 1: indentation 1157 | ((?:\[\s*)+) # 2: section marker open 1158 | ( # 3: section name open 1159 | (?:"\s*\S.*?\s*")| # at least one non-space with double quotes 1160 | (?:'\s*\S.*?\s*')| # at least one non-space with single quotes 1161 | (?:[^'"\s].*?) # at least one non-space unquoted 1162 | ) # section name close 1163 | ((?:\s*\])+) # 4: section marker close 1164 | \s*(\#.*)? # 5: optional comment 1165 | $''', 1166 | re.VERBOSE) 1167 | 1168 | # this regexp pulls list values out as a single string 1169 | # or single values and comments 1170 | # FIXME: this regex adds a '' to the end of comma terminated lists 1171 | # workaround in ``_handle_value`` 1172 | _valueexp = re.compile(r'''^ 1173 | (?: 1174 | (?: 1175 | ( 1176 | (?: 1177 | (?: 1178 | (?:".*?")| # double quotes 1179 | (?:'.*?')| # single quotes 1180 | (?:[^'",\#][^,\#]*?) # unquoted 1181 | ) 1182 | \s*,\s* # comma 1183 | )* # match all list items ending in a comma (if any) 1184 | ) 1185 | ( 1186 | (?:".*?")| # double quotes 1187 | (?:'.*?')| # single quotes 1188 | (?:[^'",\#\s][^,]*?)| # unquoted 1189 | (?:(? 1: 1346 | msg = "Parsing failed with several errors.\nFirst error %s" % info 1347 | error = ConfigObjError(msg) 1348 | else: 1349 | error = self._errors[0] 1350 | # set the errors attribute; it's a list of tuples: 1351 | # (error_type, message, line_number) 1352 | error.errors = self._errors 1353 | # set the config attribute 1354 | error.config = self 1355 | raise error 1356 | # delete private attributes 1357 | del self._errors 1358 | 1359 | if configspec is None: 1360 | self.configspec = None 1361 | else: 1362 | self._handle_configspec(configspec) 1363 | 1364 | 1365 | def _initialise(self, options=None): 1366 | if options is None: 1367 | options = OPTION_DEFAULTS 1368 | 1369 | # initialise a few variables 1370 | self.filename = None 1371 | self._errors = [] 1372 | self.raise_errors = options['raise_errors'] 1373 | self.interpolation = options['interpolation'] 1374 | self.list_values = options['list_values'] 1375 | self.create_empty = options['create_empty'] 1376 | self.file_error = options['file_error'] 1377 | self.stringify = options['stringify'] 1378 | self.indent_type = options['indent_type'] 1379 | self.encoding = options['encoding'] 1380 | self.default_encoding = options['default_encoding'] 1381 | self.BOM = False 1382 | self.newlines = None 1383 | self.write_empty_values = options['write_empty_values'] 1384 | self.unrepr = options['unrepr'] 1385 | 1386 | self.initial_comment = [] 1387 | self.final_comment = [] 1388 | self.configspec = {} 1389 | 1390 | # Clear section attributes as well 1391 | Section._initialise(self) 1392 | 1393 | 1394 | def __repr__(self): 1395 | return ('ConfigObj({%s})' % 1396 | ', '.join([('%s: %s' % (repr(key), repr(self[key]))) 1397 | for key in (self.scalars + self.sections)])) 1398 | 1399 | 1400 | def _handle_bom(self, infile): 1401 | """ 1402 | Handle any BOM, and decode if necessary. 1403 | 1404 | If an encoding is specified, that *must* be used - but the BOM should 1405 | still be removed (and the BOM attribute set). 1406 | 1407 | (If the encoding is wrongly specified, then a BOM for an alternative 1408 | encoding won't be discovered or removed.) 1409 | 1410 | If an encoding is not specified, UTF8 or UTF16 BOM will be detected and 1411 | removed. The BOM attribute will be set. UTF16 will be decoded to 1412 | unicode. 1413 | 1414 | NOTE: This method must not be called with an empty ``infile``. 1415 | 1416 | Specifying the *wrong* encoding is likely to cause a 1417 | ``UnicodeDecodeError``. 1418 | 1419 | ``infile`` must always be returned as a list of lines, but may be 1420 | passed in as a single string. 1421 | """ 1422 | if ((self.encoding is not None) and 1423 | (self.encoding.lower() not in BOM_LIST)): 1424 | # No need to check for a BOM 1425 | # the encoding specified doesn't have one 1426 | # just decode 1427 | return self._decode(infile, self.encoding) 1428 | 1429 | if isinstance(infile, (list, tuple)): 1430 | line = infile[0] 1431 | else: 1432 | line = infile 1433 | if self.encoding is not None: 1434 | # encoding explicitly supplied 1435 | # And it could have an associated BOM 1436 | # TODO: if encoding is just UTF16 - we ought to check for both 1437 | # TODO: big endian and little endian versions. 1438 | enc = BOM_LIST[self.encoding.lower()] 1439 | if enc == 'utf_16': 1440 | # For UTF16 we try big endian and little endian 1441 | for BOM, (encoding, final_encoding) in BOMS.items(): 1442 | if not final_encoding: 1443 | # skip UTF8 1444 | continue 1445 | if infile.startswith(BOM): 1446 | ### BOM discovered 1447 | ##self.BOM = True 1448 | # Don't need to remove BOM 1449 | return self._decode(infile, encoding) 1450 | 1451 | # If we get this far, will *probably* raise a DecodeError 1452 | # As it doesn't appear to start with a BOM 1453 | return self._decode(infile, self.encoding) 1454 | 1455 | # Must be UTF8 1456 | BOM = BOM_SET[enc] 1457 | if not line.startswith(BOM): 1458 | return self._decode(infile, self.encoding) 1459 | 1460 | newline = line[len(BOM):] 1461 | 1462 | # BOM removed 1463 | if isinstance(infile, (list, tuple)): 1464 | infile[0] = newline 1465 | else: 1466 | infile = newline 1467 | self.BOM = True 1468 | return self._decode(infile, self.encoding) 1469 | 1470 | # No encoding specified - so we need to check for UTF8/UTF16 1471 | for BOM, (encoding, final_encoding) in BOMS.items(): 1472 | if not line.startswith(BOM): 1473 | continue 1474 | else: 1475 | # BOM discovered 1476 | self.encoding = final_encoding 1477 | if not final_encoding: 1478 | self.BOM = True 1479 | # UTF8 1480 | # remove BOM 1481 | newline = line[len(BOM):] 1482 | if isinstance(infile, (list, tuple)): 1483 | infile[0] = newline 1484 | else: 1485 | infile = newline 1486 | # UTF8 - don't decode 1487 | if isinstance(infile, StringTypes): 1488 | return infile.splitlines(True) 1489 | else: 1490 | return infile 1491 | # UTF16 - have to decode 1492 | return self._decode(infile, encoding) 1493 | 1494 | # No BOM discovered and no encoding specified, just return 1495 | if isinstance(infile, StringTypes): 1496 | # infile read from a file will be a single string 1497 | return infile.splitlines(True) 1498 | return infile 1499 | 1500 | 1501 | def _a_to_u(self, aString): 1502 | """Decode ASCII strings to unicode if a self.encoding is specified.""" 1503 | if self.encoding: 1504 | return aString.decode('ascii') 1505 | else: 1506 | return aString 1507 | 1508 | 1509 | def _decode(self, infile, encoding): 1510 | """ 1511 | Decode infile to unicode. Using the specified encoding. 1512 | 1513 | if is a string, it also needs converting to a list. 1514 | """ 1515 | if isinstance(infile, StringTypes): 1516 | # can't be unicode 1517 | # NOTE: Could raise a ``UnicodeDecodeError`` 1518 | return infile.decode(encoding).splitlines(True) 1519 | for i, line in enumerate(infile): 1520 | if not isinstance(line, unicode): 1521 | # NOTE: The isinstance test here handles mixed lists of unicode/string 1522 | # NOTE: But the decode will break on any non-string values 1523 | # NOTE: Or could raise a ``UnicodeDecodeError`` 1524 | infile[i] = line.decode(encoding) 1525 | return infile 1526 | 1527 | 1528 | def _decode_element(self, line): 1529 | """Decode element to unicode if necessary.""" 1530 | if not self.encoding: 1531 | return line 1532 | if isinstance(line, str) and self.default_encoding: 1533 | return line.decode(self.default_encoding) 1534 | return line 1535 | 1536 | 1537 | def _str(self, value): 1538 | """ 1539 | Used by ``stringify`` within validate, to turn non-string values 1540 | into strings. 1541 | """ 1542 | if not isinstance(value, StringTypes): 1543 | return str(value) 1544 | else: 1545 | return value 1546 | 1547 | 1548 | def _parse(self, infile): 1549 | """Actually parse the config file.""" 1550 | temp_list_values = self.list_values 1551 | if self.unrepr: 1552 | self.list_values = False 1553 | 1554 | comment_list = [] 1555 | done_start = False 1556 | this_section = self 1557 | maxline = len(infile) - 1 1558 | cur_index = -1 1559 | reset_comment = False 1560 | 1561 | while cur_index < maxline: 1562 | if reset_comment: 1563 | comment_list = [] 1564 | cur_index += 1 1565 | line = infile[cur_index] 1566 | sline = line.strip() 1567 | # do we have anything on the line ? 1568 | if not sline or sline.startswith('#'): 1569 | reset_comment = False 1570 | comment_list.append(line) 1571 | continue 1572 | 1573 | if not done_start: 1574 | # preserve initial comment 1575 | self.initial_comment = comment_list 1576 | comment_list = [] 1577 | done_start = True 1578 | 1579 | reset_comment = True 1580 | # first we check if it's a section marker 1581 | mat = self._sectionmarker.match(line) 1582 | if mat is not None: 1583 | # is a section line 1584 | (indent, sect_open, sect_name, sect_close, comment) = mat.groups() 1585 | if indent and (self.indent_type is None): 1586 | self.indent_type = indent 1587 | cur_depth = sect_open.count('[') 1588 | if cur_depth != sect_close.count(']'): 1589 | self._handle_error("Cannot compute the section depth at line %s.", 1590 | NestingError, infile, cur_index) 1591 | continue 1592 | 1593 | if cur_depth < this_section.depth: 1594 | # the new section is dropping back to a previous level 1595 | try: 1596 | parent = self._match_depth(this_section, 1597 | cur_depth).parent 1598 | except SyntaxError: 1599 | self._handle_error("Cannot compute nesting level at line %s.", 1600 | NestingError, infile, cur_index) 1601 | continue 1602 | elif cur_depth == this_section.depth: 1603 | # the new section is a sibling of the current section 1604 | parent = this_section.parent 1605 | elif cur_depth == this_section.depth + 1: 1606 | # the new section is a child the current section 1607 | parent = this_section 1608 | else: 1609 | self._handle_error("Section too nested at line %s.", 1610 | NestingError, infile, cur_index) 1611 | 1612 | sect_name = self._unquote(sect_name) 1613 | if parent.has_key(sect_name): 1614 | self._handle_error('Duplicate section name at line %s.', 1615 | DuplicateError, infile, cur_index) 1616 | continue 1617 | 1618 | # create the new section 1619 | this_section = Section( 1620 | parent, 1621 | cur_depth, 1622 | self, 1623 | name=sect_name) 1624 | parent[sect_name] = this_section 1625 | parent.inline_comments[sect_name] = comment 1626 | parent.comments[sect_name] = comment_list 1627 | continue 1628 | # 1629 | # it's not a section marker, 1630 | # so it should be a valid ``key = value`` line 1631 | mat = self._keyword.match(line) 1632 | if mat is None: 1633 | # it neither matched as a keyword 1634 | # or a section marker 1635 | self._handle_error( 1636 | 'Invalid line at line "%s".', 1637 | ParseError, infile, cur_index) 1638 | else: 1639 | # is a keyword value 1640 | # value will include any inline comment 1641 | (indent, key, value) = mat.groups() 1642 | if indent and (self.indent_type is None): 1643 | self.indent_type = indent 1644 | # check for a multiline value 1645 | if value[:3] in ['"""', "'''"]: 1646 | try: 1647 | (value, comment, cur_index) = self._multiline( 1648 | value, infile, cur_index, maxline) 1649 | except SyntaxError: 1650 | self._handle_error( 1651 | 'Parse error in value at line %s.', 1652 | ParseError, infile, cur_index) 1653 | continue 1654 | else: 1655 | if self.unrepr: 1656 | comment = '' 1657 | try: 1658 | value = unrepr(value) 1659 | except Exception, e: 1660 | if type(e) == UnknownType: 1661 | msg = 'Unknown name or type in value at line %s.' 1662 | else: 1663 | msg = 'Parse error in value at line %s.' 1664 | self._handle_error(msg, UnreprError, infile, 1665 | cur_index) 1666 | continue 1667 | else: 1668 | if self.unrepr: 1669 | comment = '' 1670 | try: 1671 | value = unrepr(value) 1672 | except Exception, e: 1673 | if isinstance(e, UnknownType): 1674 | msg = 'Unknown name or type in value at line %s.' 1675 | else: 1676 | msg = 'Parse error in value at line %s.' 1677 | self._handle_error(msg, UnreprError, infile, 1678 | cur_index) 1679 | continue 1680 | else: 1681 | # extract comment and lists 1682 | try: 1683 | (value, comment) = self._handle_value(value) 1684 | except SyntaxError: 1685 | self._handle_error( 1686 | 'Parse error in value at line %s.', 1687 | ParseError, infile, cur_index) 1688 | continue 1689 | # 1690 | key = self._unquote(key) 1691 | if this_section.has_key(key): 1692 | self._handle_error( 1693 | 'Duplicate keyword name at line %s.', 1694 | DuplicateError, infile, cur_index) 1695 | continue 1696 | # add the key. 1697 | # we set unrepr because if we have got this far we will never 1698 | # be creating a new section 1699 | this_section.__setitem__(key, value, unrepr=True) 1700 | this_section.inline_comments[key] = comment 1701 | this_section.comments[key] = comment_list 1702 | continue 1703 | # 1704 | if self.indent_type is None: 1705 | # no indentation used, set the type accordingly 1706 | self.indent_type = '' 1707 | 1708 | # preserve the final comment 1709 | if not self and not self.initial_comment: 1710 | self.initial_comment = comment_list 1711 | elif not reset_comment: 1712 | self.final_comment = comment_list 1713 | self.list_values = temp_list_values 1714 | 1715 | 1716 | def _match_depth(self, sect, depth): 1717 | """ 1718 | Given a section and a depth level, walk back through the sections 1719 | parents to see if the depth level matches a previous section. 1720 | 1721 | Return a reference to the right section, 1722 | or raise a SyntaxError. 1723 | """ 1724 | while depth < sect.depth: 1725 | if sect is sect.parent: 1726 | # we've reached the top level already 1727 | raise SyntaxError() 1728 | sect = sect.parent 1729 | if sect.depth == depth: 1730 | return sect 1731 | # shouldn't get here 1732 | raise SyntaxError() 1733 | 1734 | 1735 | def _handle_error(self, text, ErrorClass, infile, cur_index): 1736 | """ 1737 | Handle an error according to the error settings. 1738 | 1739 | Either raise the error or store it. 1740 | The error will have occured at ``cur_index`` 1741 | """ 1742 | line = infile[cur_index] 1743 | cur_index += 1 1744 | message = text % cur_index 1745 | error = ErrorClass(message, cur_index, line) 1746 | if self.raise_errors: 1747 | # raise the error - parsing stops here 1748 | raise error 1749 | # store the error 1750 | # reraise when parsing has finished 1751 | self._errors.append(error) 1752 | 1753 | 1754 | def _unquote(self, value): 1755 | """Return an unquoted version of a value""" 1756 | if (value[0] == value[-1]) and (value[0] in ('"', "'")): 1757 | value = value[1:-1] 1758 | return value 1759 | 1760 | 1761 | def _quote(self, value, multiline=True): 1762 | """ 1763 | Return a safely quoted version of a value. 1764 | 1765 | Raise a ConfigObjError if the value cannot be safely quoted. 1766 | If multiline is ``True`` (default) then use triple quotes 1767 | if necessary. 1768 | 1769 | Don't quote values that don't need it. 1770 | Recursively quote members of a list and return a comma joined list. 1771 | Multiline is ``False`` for lists. 1772 | Obey list syntax for empty and single member lists. 1773 | 1774 | If ``list_values=False`` then the value is only quoted if it contains 1775 | a ``\n`` (is multiline) or '#'. 1776 | 1777 | If ``write_empty_values`` is set, and the value is an empty string, it 1778 | won't be quoted. 1779 | """ 1780 | if multiline and self.write_empty_values and value == '': 1781 | # Only if multiline is set, so that it is used for values not 1782 | # keys, and not values that are part of a list 1783 | return '' 1784 | 1785 | if multiline and isinstance(value, (list, tuple)): 1786 | if not value: 1787 | return ',' 1788 | elif len(value) == 1: 1789 | return self._quote(value[0], multiline=False) + ',' 1790 | return ', '.join([self._quote(val, multiline=False) 1791 | for val in value]) 1792 | if not isinstance(value, StringTypes): 1793 | if self.stringify: 1794 | value = str(value) 1795 | else: 1796 | raise TypeError('Value "%s" is not a string.' % value) 1797 | 1798 | if not value: 1799 | return '""' 1800 | 1801 | no_lists_no_quotes = not self.list_values and '\n' not in value and '#' not in value 1802 | need_triple = multiline and ((("'" in value) and ('"' in value)) or ('\n' in value )) 1803 | hash_triple_quote = multiline and not need_triple and ("'" in value) and ('"' in value) and ('#' in value) 1804 | check_for_single = (no_lists_no_quotes or not need_triple) and not hash_triple_quote 1805 | 1806 | if check_for_single: 1807 | if not self.list_values: 1808 | # we don't quote if ``list_values=False`` 1809 | quot = noquot 1810 | # for normal values either single or double quotes will do 1811 | elif '\n' in value: 1812 | # will only happen if multiline is off - e.g. '\n' in key 1813 | raise ConfigObjError('Value "%s" cannot be safely quoted.' % value) 1814 | elif ((value[0] not in wspace_plus) and 1815 | (value[-1] not in wspace_plus) and 1816 | (',' not in value)): 1817 | quot = noquot 1818 | else: 1819 | quot = self._get_single_quote(value) 1820 | else: 1821 | # if value has '\n' or "'" *and* '"', it will need triple quotes 1822 | quot = self._get_triple_quote(value) 1823 | 1824 | if quot == noquot and '#' in value and self.list_values: 1825 | quot = self._get_single_quote(value) 1826 | 1827 | return quot % value 1828 | 1829 | 1830 | def _get_single_quote(self, value): 1831 | if ("'" in value) and ('"' in value): 1832 | raise ConfigObjError('Value "%s" cannot be safely quoted.' % value) 1833 | elif '"' in value: 1834 | quot = squot 1835 | else: 1836 | quot = dquot 1837 | return quot 1838 | 1839 | 1840 | def _get_triple_quote(self, value): 1841 | if (value.find('"""') != -1) and (value.find("'''") != -1): 1842 | raise ConfigObjError('Value "%s" cannot be safely quoted.' % value) 1843 | if value.find('"""') == -1: 1844 | quot = tdquot 1845 | else: 1846 | quot = tsquot 1847 | return quot 1848 | 1849 | 1850 | def _handle_value(self, value): 1851 | """ 1852 | Given a value string, unquote, remove comment, 1853 | handle lists. (including empty and single member lists) 1854 | """ 1855 | # do we look for lists in values ? 1856 | if not self.list_values: 1857 | mat = self._nolistvalue.match(value) 1858 | if mat is None: 1859 | raise SyntaxError() 1860 | # NOTE: we don't unquote here 1861 | return mat.groups() 1862 | # 1863 | mat = self._valueexp.match(value) 1864 | if mat is None: 1865 | # the value is badly constructed, probably badly quoted, 1866 | # or an invalid list 1867 | raise SyntaxError() 1868 | (list_values, single, empty_list, comment) = mat.groups() 1869 | if (list_values == '') and (single is None): 1870 | # change this if you want to accept empty values 1871 | raise SyntaxError() 1872 | # NOTE: note there is no error handling from here if the regex 1873 | # is wrong: then incorrect values will slip through 1874 | if empty_list is not None: 1875 | # the single comma - meaning an empty list 1876 | return ([], comment) 1877 | if single is not None: 1878 | # handle empty values 1879 | if list_values and not single: 1880 | # FIXME: the '' is a workaround because our regex now matches 1881 | # '' at the end of a list if it has a trailing comma 1882 | single = None 1883 | else: 1884 | single = single or '""' 1885 | single = self._unquote(single) 1886 | if list_values == '': 1887 | # not a list value 1888 | return (single, comment) 1889 | the_list = self._listvalueexp.findall(list_values) 1890 | the_list = [self._unquote(val) for val in the_list] 1891 | if single is not None: 1892 | the_list += [single] 1893 | return (the_list, comment) 1894 | 1895 | 1896 | def _multiline(self, value, infile, cur_index, maxline): 1897 | """Extract the value, where we are in a multiline situation.""" 1898 | quot = value[:3] 1899 | newvalue = value[3:] 1900 | single_line = self._triple_quote[quot][0] 1901 | multi_line = self._triple_quote[quot][1] 1902 | mat = single_line.match(value) 1903 | if mat is not None: 1904 | retval = list(mat.groups()) 1905 | retval.append(cur_index) 1906 | return retval 1907 | elif newvalue.find(quot) != -1: 1908 | # somehow the triple quote is missing 1909 | raise SyntaxError() 1910 | # 1911 | while cur_index < maxline: 1912 | cur_index += 1 1913 | newvalue += '\n' 1914 | line = infile[cur_index] 1915 | if line.find(quot) == -1: 1916 | newvalue += line 1917 | else: 1918 | # end of multiline, process it 1919 | break 1920 | else: 1921 | # we've got to the end of the config, oops... 1922 | raise SyntaxError() 1923 | mat = multi_line.match(line) 1924 | if mat is None: 1925 | # a badly formed line 1926 | raise SyntaxError() 1927 | (value, comment) = mat.groups() 1928 | return (newvalue + value, comment, cur_index) 1929 | 1930 | 1931 | def _handle_configspec(self, configspec): 1932 | """Parse the configspec.""" 1933 | # FIXME: Should we check that the configspec was created with the 1934 | # correct settings ? (i.e. ``list_values=False``) 1935 | if not isinstance(configspec, ConfigObj): 1936 | try: 1937 | configspec = ConfigObj(configspec, 1938 | raise_errors=True, 1939 | file_error=True, 1940 | list_values=False) 1941 | except ConfigObjError, e: 1942 | # FIXME: Should these errors have a reference 1943 | # to the already parsed ConfigObj ? 1944 | raise ConfigspecError('Parsing configspec failed: %s' % e) 1945 | except IOError, e: 1946 | raise IOError('Reading configspec failed: %s' % e) 1947 | 1948 | self._set_configspec_value(configspec, self) 1949 | 1950 | 1951 | def _set_configspec_value(self, configspec, section): 1952 | """Used to recursively set configspec values.""" 1953 | if '__many__' in configspec.sections: 1954 | section.configspec['__many__'] = configspec['__many__'] 1955 | if len(configspec.sections) > 1: 1956 | # FIXME: can we supply any useful information here ? 1957 | raise RepeatSectionError() 1958 | 1959 | if hasattr(configspec, 'initial_comment'): 1960 | section._configspec_initial_comment = configspec.initial_comment 1961 | section._configspec_final_comment = configspec.final_comment 1962 | section._configspec_encoding = configspec.encoding 1963 | section._configspec_BOM = configspec.BOM 1964 | section._configspec_newlines = configspec.newlines 1965 | section._configspec_indent_type = configspec.indent_type 1966 | 1967 | for entry in configspec.scalars: 1968 | section._configspec_comments[entry] = configspec.comments[entry] 1969 | section._configspec_inline_comments[entry] = configspec.inline_comments[entry] 1970 | section.configspec[entry] = configspec[entry] 1971 | section._order.append(entry) 1972 | 1973 | for entry in configspec.sections: 1974 | if entry == '__many__': 1975 | continue 1976 | 1977 | section._cs_section_comments[entry] = configspec.comments[entry] 1978 | section._cs_section_inline_comments[entry] = configspec.inline_comments[entry] 1979 | if not section.has_key(entry): 1980 | section[entry] = {} 1981 | self._set_configspec_value(configspec[entry], section[entry]) 1982 | 1983 | 1984 | def _handle_repeat(self, section, configspec): 1985 | """Dynamically assign configspec for repeated section.""" 1986 | try: 1987 | section_keys = configspec.sections 1988 | scalar_keys = configspec.scalars 1989 | except AttributeError: 1990 | section_keys = [entry for entry in configspec 1991 | if isinstance(configspec[entry], dict)] 1992 | scalar_keys = [entry for entry in configspec 1993 | if not isinstance(configspec[entry], dict)] 1994 | 1995 | if '__many__' in section_keys and len(section_keys) > 1: 1996 | # FIXME: can we supply any useful information here ? 1997 | raise RepeatSectionError() 1998 | 1999 | scalars = {} 2000 | sections = {} 2001 | for entry in scalar_keys: 2002 | val = configspec[entry] 2003 | scalars[entry] = val 2004 | for entry in section_keys: 2005 | val = configspec[entry] 2006 | if entry == '__many__': 2007 | scalars[entry] = val 2008 | continue 2009 | sections[entry] = val 2010 | 2011 | section.configspec = scalars 2012 | for entry in sections: 2013 | if not section.has_key(entry): 2014 | section[entry] = {} 2015 | self._handle_repeat(section[entry], sections[entry]) 2016 | 2017 | 2018 | def _write_line(self, indent_string, entry, this_entry, comment): 2019 | """Write an individual line, for the write method""" 2020 | # NOTE: the calls to self._quote here handles non-StringType values. 2021 | if not self.unrepr: 2022 | val = self._decode_element(self._quote(this_entry)) 2023 | else: 2024 | val = repr(this_entry) 2025 | return '%s%s%s%s%s' % (indent_string, 2026 | self._decode_element(self._quote(entry, multiline=False)), 2027 | self._a_to_u(' = '), 2028 | val, 2029 | self._decode_element(comment)) 2030 | 2031 | 2032 | def _write_marker(self, indent_string, depth, entry, comment): 2033 | """Write a section marker line""" 2034 | return '%s%s%s%s%s' % (indent_string, 2035 | self._a_to_u('[' * depth), 2036 | self._quote(self._decode_element(entry), multiline=False), 2037 | self._a_to_u(']' * depth), 2038 | self._decode_element(comment)) 2039 | 2040 | 2041 | def _handle_comment(self, comment): 2042 | """Deal with a comment.""" 2043 | if not comment: 2044 | return '' 2045 | start = self.indent_type 2046 | if not comment.startswith('#'): 2047 | start += self._a_to_u(' # ') 2048 | return (start + comment) 2049 | 2050 | 2051 | # Public methods 2052 | 2053 | def write(self, outfile=None, section=None): 2054 | """ 2055 | Write the current ConfigObj as a file 2056 | 2057 | tekNico: FIXME: use StringIO instead of real files 2058 | 2059 | >>> filename = a.filename 2060 | >>> a.filename = 'test.ini' 2061 | >>> a.write() 2062 | >>> a.filename = filename 2063 | >>> a == ConfigObj('test.ini', raise_errors=True) 2064 | 1 2065 | """ 2066 | if self.indent_type is None: 2067 | # this can be true if initialised from a dictionary 2068 | self.indent_type = DEFAULT_INDENT_TYPE 2069 | 2070 | out = [] 2071 | cs = self._a_to_u('#') 2072 | csp = self._a_to_u('# ') 2073 | if section is None: 2074 | int_val = self.interpolation 2075 | self.interpolation = False 2076 | section = self 2077 | for line in self.initial_comment: 2078 | line = self._decode_element(line) 2079 | stripped_line = line.strip() 2080 | if stripped_line and not stripped_line.startswith(cs): 2081 | line = csp + line 2082 | out.append(line) 2083 | 2084 | indent_string = self.indent_type * section.depth 2085 | for entry in (section.scalars + section.sections): 2086 | if entry in section.defaults: 2087 | # don't write out default values 2088 | continue 2089 | for comment_line in section.comments[entry]: 2090 | comment_line = self._decode_element(comment_line.lstrip()) 2091 | if comment_line and not comment_line.startswith(cs): 2092 | comment_line = csp + comment_line 2093 | out.append(indent_string + comment_line) 2094 | this_entry = section[entry] 2095 | comment = self._handle_comment(section.inline_comments[entry]) 2096 | 2097 | if isinstance(this_entry, dict): 2098 | # a section 2099 | out.append(self._write_marker( 2100 | indent_string, 2101 | this_entry.depth, 2102 | entry, 2103 | comment)) 2104 | out.extend(self.write(section=this_entry)) 2105 | else: 2106 | out.append(self._write_line( 2107 | indent_string, 2108 | entry, 2109 | this_entry, 2110 | comment)) 2111 | 2112 | if section is self: 2113 | for line in self.final_comment: 2114 | line = self._decode_element(line) 2115 | stripped_line = line.strip() 2116 | if stripped_line and not stripped_line.startswith(cs): 2117 | line = csp + line 2118 | out.append(line) 2119 | self.interpolation = int_val 2120 | 2121 | if section is not self: 2122 | return out 2123 | 2124 | if (self.filename is None) and (outfile is None): 2125 | # output a list of lines 2126 | # might need to encode 2127 | # NOTE: This will *screw* UTF16, each line will start with the BOM 2128 | if self.encoding: 2129 | out = [l.encode(self.encoding) for l in out] 2130 | if (self.BOM and ((self.encoding is None) or 2131 | (BOM_LIST.get(self.encoding.lower()) == 'utf_8'))): 2132 | # Add the UTF8 BOM 2133 | if not out: 2134 | out.append('') 2135 | out[0] = BOM_UTF8 + out[0] 2136 | return out 2137 | 2138 | # Turn the list to a string, joined with correct newlines 2139 | newline = self.newlines or os.linesep 2140 | output = self._a_to_u(newline).join(out) 2141 | if self.encoding: 2142 | output = output.encode(self.encoding) 2143 | if self.BOM and ((self.encoding is None) or match_utf8(self.encoding)): 2144 | # Add the UTF8 BOM 2145 | output = BOM_UTF8 + output 2146 | 2147 | if not output.endswith(newline): 2148 | output += newline 2149 | if outfile is not None: 2150 | outfile.write(output) 2151 | else: 2152 | h = open(self.filename, 'wb') 2153 | h.write(output) 2154 | h.close() 2155 | 2156 | 2157 | def validate(self, validator, preserve_errors=False, copy=False, 2158 | section=None): 2159 | """ 2160 | Test the ConfigObj against a configspec. 2161 | 2162 | It uses the ``validator`` object from *validate.py*. 2163 | 2164 | To run ``validate`` on the current ConfigObj, call: :: 2165 | 2166 | test = config.validate(validator) 2167 | 2168 | (Normally having previously passed in the configspec when the ConfigObj 2169 | was created - you can dynamically assign a dictionary of checks to the 2170 | ``configspec`` attribute of a section though). 2171 | 2172 | It returns ``True`` if everything passes, or a dictionary of 2173 | pass/fails (True/False). If every member of a subsection passes, it 2174 | will just have the value ``True``. (It also returns ``False`` if all 2175 | members fail). 2176 | 2177 | In addition, it converts the values from strings to their native 2178 | types if their checks pass (and ``stringify`` is set). 2179 | 2180 | If ``preserve_errors`` is ``True`` (``False`` is default) then instead 2181 | of a marking a fail with a ``False``, it will preserve the actual 2182 | exception object. This can contain info about the reason for failure. 2183 | For example the ``VdtValueTooSmallError`` indicates that the value 2184 | supplied was too small. If a value (or section) is missing it will 2185 | still be marked as ``False``. 2186 | 2187 | You must have the validate module to use ``preserve_errors=True``. 2188 | 2189 | You can then use the ``flatten_errors`` function to turn your nested 2190 | results dictionary into a flattened list of failures - useful for 2191 | displaying meaningful error messages. 2192 | """ 2193 | if section is None: 2194 | if self.configspec is None: 2195 | raise ValueError('No configspec supplied.') 2196 | if preserve_errors: 2197 | # We do this once to remove a top level dependency on the validate module 2198 | # Which makes importing configobj faster 2199 | from validate import VdtMissingValue 2200 | self._vdtMissingValue = VdtMissingValue 2201 | section = self 2202 | # 2203 | spec_section = section.configspec 2204 | if copy and hasattr(section, '_configspec_initial_comment'): 2205 | section.initial_comment = section._configspec_initial_comment 2206 | section.final_comment = section._configspec_final_comment 2207 | section.encoding = section._configspec_encoding 2208 | section.BOM = section._configspec_BOM 2209 | section.newlines = section._configspec_newlines 2210 | section.indent_type = section._configspec_indent_type 2211 | 2212 | if '__many__' in section.configspec: 2213 | many = spec_section['__many__'] 2214 | # dynamically assign the configspecs 2215 | # for the sections below 2216 | for entry in section.sections: 2217 | self._handle_repeat(section[entry], many) 2218 | # 2219 | out = {} 2220 | ret_true = True 2221 | ret_false = True 2222 | order = [k for k in section._order if k in spec_section] 2223 | order += [k for k in spec_section if k not in order] 2224 | for entry in order: 2225 | if entry == '__many__': 2226 | continue 2227 | if (not entry in section.scalars) or (entry in section.defaults): 2228 | # missing entries 2229 | # or entries from defaults 2230 | missing = True 2231 | val = None 2232 | if copy and not entry in section.scalars: 2233 | # copy comments 2234 | section.comments[entry] = ( 2235 | section._configspec_comments.get(entry, [])) 2236 | section.inline_comments[entry] = ( 2237 | section._configspec_inline_comments.get(entry, '')) 2238 | # 2239 | else: 2240 | missing = False 2241 | val = section[entry] 2242 | try: 2243 | check = validator.check(spec_section[entry], 2244 | val, 2245 | missing=missing 2246 | ) 2247 | except validator.baseErrorClass, e: 2248 | if not preserve_errors or isinstance(e, self._vdtMissingValue): 2249 | out[entry] = False 2250 | else: 2251 | # preserve the error 2252 | out[entry] = e 2253 | ret_false = False 2254 | ret_true = False 2255 | else: 2256 | try: 2257 | section.default_values.pop(entry, None) 2258 | except AttributeError: 2259 | # For Python 2.2 compatibility 2260 | try: 2261 | del section.default_values[entry] 2262 | except KeyError: 2263 | pass 2264 | 2265 | if hasattr(validator, 'get_default_value'): 2266 | try: 2267 | section.default_values[entry] = validator.get_default_value(spec_section[entry]) 2268 | except KeyError: 2269 | # No default 2270 | pass 2271 | 2272 | ret_false = False 2273 | out[entry] = True 2274 | if self.stringify or missing: 2275 | # if we are doing type conversion 2276 | # or the value is a supplied default 2277 | if not self.stringify: 2278 | if isinstance(check, (list, tuple)): 2279 | # preserve lists 2280 | check = [self._str(item) for item in check] 2281 | elif missing and check is None: 2282 | # convert the None from a default to a '' 2283 | check = '' 2284 | else: 2285 | check = self._str(check) 2286 | if (check != val) or missing: 2287 | section[entry] = check 2288 | if not copy and missing and entry not in section.defaults: 2289 | section.defaults.append(entry) 2290 | # Missing sections will have been created as empty ones when the 2291 | # configspec was read. 2292 | for entry in section.sections: 2293 | # FIXME: this means DEFAULT is not copied in copy mode 2294 | if section is self and entry == 'DEFAULT': 2295 | continue 2296 | if copy: 2297 | section.comments[entry] = section._cs_section_comments.get(entry, []) 2298 | section.inline_comments[entry] = section._cs_section_inline_comments.get(entry, '') 2299 | check = self.validate(validator, preserve_errors=preserve_errors, copy=copy, section=section[entry]) 2300 | out[entry] = check 2301 | if check == False: 2302 | ret_true = False 2303 | elif check == True: 2304 | ret_false = False 2305 | else: 2306 | ret_true = False 2307 | ret_false = False 2308 | # 2309 | if ret_true: 2310 | return True 2311 | elif ret_false: 2312 | return False 2313 | return out 2314 | 2315 | 2316 | def reset(self): 2317 | """Clear ConfigObj instance and restore to 'freshly created' state.""" 2318 | self.clear() 2319 | self._initialise() 2320 | # FIXME: Should be done by '_initialise', but ConfigObj constructor (and reload) 2321 | # requires an empty dictionary 2322 | self.configspec = None 2323 | # Just to be sure ;-) 2324 | self._original_configspec = None 2325 | 2326 | 2327 | def reload(self): 2328 | """ 2329 | Reload a ConfigObj from file. 2330 | 2331 | This method raises a ``ReloadError`` if the ConfigObj doesn't have 2332 | a filename attribute pointing to a file. 2333 | """ 2334 | if not isinstance(self.filename, StringTypes): 2335 | raise ReloadError() 2336 | 2337 | filename = self.filename 2338 | current_options = {} 2339 | for entry in OPTION_DEFAULTS: 2340 | if entry == 'configspec': 2341 | continue 2342 | current_options[entry] = getattr(self, entry) 2343 | 2344 | configspec = self._original_configspec 2345 | current_options['configspec'] = configspec 2346 | 2347 | self.clear() 2348 | self._initialise(current_options) 2349 | self._load(filename, configspec) 2350 | 2351 | 2352 | 2353 | class SimpleVal(object): 2354 | """ 2355 | A simple validator. 2356 | Can be used to check that all members expected are present. 2357 | 2358 | To use it, provide a configspec with all your members in (the value given 2359 | will be ignored). Pass an instance of ``SimpleVal`` to the ``validate`` 2360 | method of your ``ConfigObj``. ``validate`` will return ``True`` if all 2361 | members are present, or a dictionary with True/False meaning 2362 | present/missing. (Whole missing sections will be replaced with ``False``) 2363 | """ 2364 | 2365 | def __init__(self): 2366 | self.baseErrorClass = ConfigObjError 2367 | 2368 | def check(self, check, member, missing=False): 2369 | """A dummy check method, always returns the value unchanged.""" 2370 | if missing: 2371 | raise self.baseErrorClass() 2372 | return member 2373 | 2374 | 2375 | # Check / processing functions for options 2376 | def flatten_errors(cfg, res, levels=None, results=None): 2377 | """ 2378 | An example function that will turn a nested dictionary of results 2379 | (as returned by ``ConfigObj.validate``) into a flat list. 2380 | 2381 | ``cfg`` is the ConfigObj instance being checked, ``res`` is the results 2382 | dictionary returned by ``validate``. 2383 | 2384 | (This is a recursive function, so you shouldn't use the ``levels`` or 2385 | ``results`` arguments - they are used by the function. 2386 | 2387 | Returns a list of keys that failed. Each member of the list is a tuple : 2388 | :: 2389 | 2390 | ([list of sections...], key, result) 2391 | 2392 | If ``validate`` was called with ``preserve_errors=False`` (the default) 2393 | then ``result`` will always be ``False``. 2394 | 2395 | *list of sections* is a flattened list of sections that the key was found 2396 | in. 2397 | 2398 | If the section was missing then key will be ``None``. 2399 | 2400 | If the value (or section) was missing then ``result`` will be ``False``. 2401 | 2402 | If ``validate`` was called with ``preserve_errors=True`` and a value 2403 | was present, but failed the check, then ``result`` will be the exception 2404 | object returned. You can use this as a string that describes the failure. 2405 | 2406 | For example *The value "3" is of the wrong type*. 2407 | 2408 | >>> import validate 2409 | >>> vtor = validate.Validator() 2410 | >>> my_ini = ''' 2411 | ... option1 = True 2412 | ... [section1] 2413 | ... option1 = True 2414 | ... [section2] 2415 | ... another_option = Probably 2416 | ... [section3] 2417 | ... another_option = True 2418 | ... [[section3b]] 2419 | ... value = 3 2420 | ... value2 = a 2421 | ... value3 = 11 2422 | ... ''' 2423 | >>> my_cfg = ''' 2424 | ... option1 = boolean() 2425 | ... option2 = boolean() 2426 | ... option3 = boolean(default=Bad_value) 2427 | ... [section1] 2428 | ... option1 = boolean() 2429 | ... option2 = boolean() 2430 | ... option3 = boolean(default=Bad_value) 2431 | ... [section2] 2432 | ... another_option = boolean() 2433 | ... [section3] 2434 | ... another_option = boolean() 2435 | ... [[section3b]] 2436 | ... value = integer 2437 | ... value2 = integer 2438 | ... value3 = integer(0, 10) 2439 | ... [[[section3b-sub]]] 2440 | ... value = string 2441 | ... [section4] 2442 | ... another_option = boolean() 2443 | ... ''' 2444 | >>> cs = my_cfg.split('\\n') 2445 | >>> ini = my_ini.split('\\n') 2446 | >>> cfg = ConfigObj(ini, configspec=cs) 2447 | >>> res = cfg.validate(vtor, preserve_errors=True) 2448 | >>> errors = [] 2449 | >>> for entry in flatten_errors(cfg, res): 2450 | ... section_list, key, error = entry 2451 | ... section_list.insert(0, '[root]') 2452 | ... if key is not None: 2453 | ... section_list.append(key) 2454 | ... else: 2455 | ... section_list.append('[missing]') 2456 | ... section_string = ', '.join(section_list) 2457 | ... errors.append((section_string, ' = ', error)) 2458 | >>> errors.sort() 2459 | >>> for entry in errors: 2460 | ... print entry[0], entry[1], (entry[2] or 0) 2461 | [root], option2 = 0 2462 | [root], option3 = the value "Bad_value" is of the wrong type. 2463 | [root], section1, option2 = 0 2464 | [root], section1, option3 = the value "Bad_value" is of the wrong type. 2465 | [root], section2, another_option = the value "Probably" is of the wrong type. 2466 | [root], section3, section3b, section3b-sub, [missing] = 0 2467 | [root], section3, section3b, value2 = the value "a" is of the wrong type. 2468 | [root], section3, section3b, value3 = the value "11" is too big. 2469 | [root], section4, [missing] = 0 2470 | """ 2471 | if levels is None: 2472 | # first time called 2473 | levels = [] 2474 | results = [] 2475 | if res is True: 2476 | return results 2477 | if res is False: 2478 | results.append((levels[:], None, False)) 2479 | if levels: 2480 | levels.pop() 2481 | return results 2482 | for (key, val) in res.items(): 2483 | if val == True: 2484 | continue 2485 | if isinstance(cfg.get(key), dict): 2486 | # Go down one level 2487 | levels.append(key) 2488 | flatten_errors(cfg[key], val, levels, results) 2489 | continue 2490 | results.append((levels[:], key, val)) 2491 | # 2492 | # Go up one level 2493 | if levels: 2494 | levels.pop() 2495 | # 2496 | return results 2497 | 2498 | 2499 | """*A programming language is a medium of expression.* - Paul Graham""" 2500 | -------------------------------------------------------------------------------- /Release 2.0/configobj.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tachang/EyeFiServer/4c473e76bcf97472393773ffe352c9078ec1bf39/Release 2.0/configobj.pyc -------------------------------------------------------------------------------- /Release 2.0/pictures/CIMG1859.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tachang/EyeFiServer/4c473e76bcf97472393773ffe352c9078ec1bf39/Release 2.0/pictures/CIMG1859.JPG -------------------------------------------------------------------------------- /Release 2.0/pictures/CIMG1860.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tachang/EyeFiServer/4c473e76bcf97472393773ffe352c9078ec1bf39/Release 2.0/pictures/CIMG1860.JPG -------------------------------------------------------------------------------- /Release 2.0/pictures/CIMG1861.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tachang/EyeFiServer/4c473e76bcf97472393773ffe352c9078ec1bf39/Release 2.0/pictures/CIMG1861.JPG -------------------------------------------------------------------------------- /Release 2.0/pictures/Thumbs.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tachang/EyeFiServer/4c473e76bcf97472393773ffe352c9078ec1bf39/Release 2.0/pictures/Thumbs.db -------------------------------------------------------------------------------- /eyefi-config.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | 4 | log = open('requestMessage.log', 'wb') 5 | 6 | requestMessageFilename = "D:\\EyeFi\\reqm" 7 | requestCounterFilename = "/media/CASIO-DSC/EyeFi/reqc" 8 | responseMessageFilename = "/media/CASIO-DSC/EyeFi/rspm" 9 | 10 | counter = 1 11 | previousRequestMessage = "" 12 | while(True): 13 | 14 | requestMessageFile = open(requestMessageFilename, "r") 15 | 16 | 17 | requestMessage = requestMessageFile.read(16) 18 | 19 | if(previousRequestMessage != requestMessage): 20 | message = "" 21 | for char in requestMessage: 22 | message = message + "," + str(hex(ord(char))) 23 | 24 | log.write(str(counter) + ": " + message + "\n") 25 | previousRequestMessage = requestMessage 26 | log.flush() 27 | 28 | requestMessageFile.close() 29 | counter = counter + 1 30 | 31 | 32 | # message = "l".ljust(16384,"\x00") 33 | 34 | 35 | -------------------------------------------------------------------------------- /rebootEyeFi.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | 4 | requestMessageFilename = "D:\\EyeFi\\REQM" 5 | requestMessageFile = open(requestMessageFilename, "w") 6 | 7 | message = "\x62".ljust(16384,"\x00") 8 | requestMessageFile.write(message) 9 | requestMessageFile.flush() 10 | requestMessageFile.close() 11 | 12 | print "Issued Eye-Fi reboot command" --------------------------------------------------------------------------------