├── 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"
--------------------------------------------------------------------------------