Posted: Dec 7, 2008 by ron scheckelhoff -- rscheckelhoff-at-fourcalorieservers-dot-com @ Datazygte, Inc
Building the "file-2-server" Extension
The author found himself in need of a convenient way to upload photos to a personal family site, and determined he would do this without dancing around browser security issues. It was decided to create a browser extension.1
Plugins enjoy full reign in terms of security issues (after all, plugins run as native code), and extensions run as javascript, with less reign but easier implementation. Except for big applications most contemporary browser enhancements are being accomplished with javascript extension technology, rather than plugins developed in C++.
An extension project is fairly simple to implement, but as with many things in the software world, there is a substantial amount of "environment setup" work that needs to happen before anything else. The easy way to set up the project is to use an online wizard.
It is nice to have help with some of the typing required to set up a project. Ted at Ted.mielczarek.org/code/mozilla/extensionwiz/ has supplied a nice online wizard to do just that.
(Ted also makes his wizard available via a link on the developer.mozilla.org1,2 site)
Run the wizard, and you will have the dozen
or so project files nicely populated for you based upon user input supplied to the wizard. The first few lines and
the last few lines in the following code were created by the wizard:
// // File: overlay.js // /* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is file2server. * * The Initial Developer of the Original Code is * Datazygte, Inc * Portions created by the Initial Developer are Copyright (C) 2008 * the Initial Developer. All Rights Reserved. * * Contributor(s) : rscheckelhoff * * ***** END LICENSE BLOCK ***** */
var file2server = {
onLoad: function() {
this.initialized = true;
this.strings = document.getElementById("file2server-strings");
document.getElementById("contentAreaContextMenu")
.addEventListener("popupshowing", function(e) { this.showContextMenu(e); }, false);
},
showContextMenu: function(event) {
document.getElementById("context-file2server").hidden = gContextMenu.onImage;
},
onMenuItemCommand: function(e) {
// Custom user extension code starts here ------- >
try
{
// Set up and show a file picker (info at: https://developer.mozilla.org/en/NsIFilePicker
var nsIFilePicker = Components.interfaces.nsIFilePicker;
var result;
var intBytes = 0;
var intFileSent = 0;
var MAX_TIMEOUT = 5;
var MAX_FILE_SIZE = 2500000;
var fp = Components.classes["@mozilla.org/filepicker;1"]
.createInstance(nsIFilePicker);
if (fp !=null)
{
fp.init(window, "Select a File", nsIFilePicker.modeOpen);
result = fp.show();
}
if (result == nsIFilePicker.returnOK)
{
var ios = Components.classes["@mozilla.org/network/io-service;1"]
.getService(Components.interfaces.nsIIOService);
var inputStream = Components.classes["@mozilla.org/network/file-input-stream;1"]
.createInstance(Components.interfaces.nsIFileInputStream);
var byteStream = Components.classes["@mozilla.org/binaryinputstream;1"]
.createInstance(Components.interfaces.nsIBinaryInputStream);
var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"].
getService(Components.interfaces.nsIPromptService);
if ((ios != null) && (inputStream != null) && (byteStream != null) && (promptService != null))
{
var url = ios.newFileURI(fp.file);
var imageFile = url.QueryInterface(Components.interfaces.nsIFileURL).file;
inputStream.init(imageFile, -1, -1, false);
byteStream.setInputStream(inputStream);
intBytes = byteStream.available();
if (intBytes > 0)
{
if (intBytes < MAX_FILE_SIZE)
{
var bytes = byteStream.readBytes(intBytes); // could make async func or return here to make sure we have it all
var bytes64 = window.btoa(bytes); // encode in base64 since binary seems to send AJAX object awol
httpRequest = new XMLHttpRequest(); // Use the so-called AJAX object
// Detect completion event
httpRequest.onreadystatechange = function(aevt){
if (httpRequest.readyState == 4)
{
if (httpRequest.status == 200)
intFileSent = 1;
}
};
httpRequest.open("POST", "http://photos.scheckelhoffs.org:8000/cgi-bin/getphotofile.py", true);
httpRequest.setRequestHeader("Content-Length", intBytes);
httpRequest.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
httpRequest.setRequestHeader('Connection','close');
httpRequest.send(bytes64);
i = 0;
while (1)
{
if (intFileSent == 1)
{
promptService.alert(null, "File2Server Info", "The transfer is complete.");
break;
}
else
if (i > MAX_TIMEOUT)
{
promptService.alert(null, "file2server Info", "Sorry, the transfer failed.");
break;
}
promptService.alert(null, "file2server Info", "Waiting for [sent ok] signal ...");
i++;
}
delete httpRequest;
}
else
promptService.alert(null, "file2server Info", "File was too big.");
}
else
promptService.alert(null, "file2server Info", "File was zero size.");
delete byteStream;
delete inputStream;
}
}
this.close();
}
catch ( e )
{
promptService.alert(null, "file2server info", "Exception occurred. Verify that file was sent!");
}
},
onToolbarButtonCommand: function(e) {
file2server.onMenuItemCommand(e);
}
};
window.addEventListener("load", function(e) { file2server.onLoad(e); }, false);
//
// End of File
//
For my purposes, the overlay.js file was really the only file that required the addition
of any code. The code, as it is, has been packed into one somewhat largish function for this little extension,
and while it could be broken out into a few more functions for cleanliness sake, I have not elected to do so.
I think that the simplicity of the extension is fairly obvious, and it demonstrates several oft-used methods for
extension development. The simplicity of this extension is directly due to the fact that the file2server functionality is so simple:
(Click on the tools menu item, select a file,
and click "OK" to (hopefully) send the file)
For more graphics intensive extensions, the user may be adding additional .xul files, each for main windows and
for dialogs. The xul language seems relatively straight forward: https://developer.mozilla.org/en/XUL_Tutorial
Link to the xpi extension file:
http://www.fourcalorieservers.com/servers/file2server.xpi
Note that the xpi file (like all extensions) is just an ordinary zip file. After the xpi "zip" file has been unzipped into a temporary extraction directory, the bulk of the work code is to be found in the overlay.js file that
resides within the content subdirectory of the extraction directory.
On the receiving end ...
The newly created extension (running on the client browser) needs to work with something on the back-end
(the server) to receive the photos. The following
Python3 script was quickly assembled to intercept the files. Note it is extremely
rudimentary, and must be fleshed out with a number of features and enhancements before it
would be ready for serious work:
#!/usr/bin/env python # # *** What is it? *** # # This is a very simple script demonstrating the processing of file # uploads from the "file2server" extension. This script is intended only # to demonstrate a sample of the most rudimentary "post " processing # code, is provided on an "as is" basis, and is not in any way warranted # (implied or otherwise) against any defect(s). # # There may be security issues and/or other issues (bugs) for which a # resolution remains to be completed. # # The author(s) of this code may not be held liable for any damages # whatsoever, however suffered, from the use of or the inability to # use the code, or it's derivatives, on any theory of liability. # # The script has been contributed by Ron Scheckelhoff and is owned # and copyrighted by Datazygte, Inc. All rights reserved.
# built in modules we will use
import sys
import base64
import os
import string
import time
import random
import re
# socketServer
from SocketServer import TCPServer as tcpsServer
from SocketServer import StreamRequestHandler as stmRequest
import select
#
# This class does all the work
#
class tcpHandler(stmRequest):
# Timeout value for network latency, chunk size
self.TIMEOUT = 5
self.CHUNK = 2048
#
# This method represents a VERY simpleton way to grab the incoming client data
#
def getData(self, filnos):
clientinputline=""
selread = [filnos]
selwrite = []
selx = []
self.ii = 0
while (1):
(rselread, rselwrite, rselx) = select.select(selread, selwrite, selx, self.TIMEOUT)
if (len(rselread) > 0):
line = self.request.recv(self.CHUNK)
if (self.ii == 0):
lstPartition = line.split("\r\n\r\n")
# break off the header (we want what is after)
if (lstPartition != None):
if (len(lstPartition) > 1):
line = lstPartition[1]
else:
return ""
else:
return ""
self.ii = self.ii + 1
if (len(line) > 0):
clientinputline += line
self.intTotal+=len(line)
else:
break
else:
break
return clientinputline
#
# Most of the functionality is here, as the TCPServer class makes this
# the destination for incoming TCP/IP traffic
#
def handle(self):
clientinputline = ""
self.intTotal = 0;
i = 0
clientinputline = self.getData(self.request.fileno())
# acknowledgement to return to client
self.OPS = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nserver: Sample\r\n\r\n\n"
self.OPS_FAIL = "HTTP/1.1 204 No Content\r\nContent-Type: text/html\r\nserver: Sample\r\n\r\n\n"
if (len(clientinputline) != 0):
print "Accepted Connection ...\n "
data = base64.b64decode(clientinputline)
# Save incoming - in real program, this could be database call
try:
objFile = open("/home/lindsay/testsave.img", 'wb')
objFile.write(data)
objFile.flush()
objFile.close()
except IOError, e:
print u"Error saving data ...\n %d bytes " % self.intTotal
self.wfile.write(self.OPS_FAIL)
else:
print u"Data received ...\n %d bytes " % self.intTotal
self.wfile.write(self.OPS)
else:
print u"No data received ...\n"
self.wfile.write(self.OPS_FAIL)
def mainfunc():
# This function will instantiate the socket handler
# Obviously this one is hard-coded for my lan network
addr = ("10.0.1.44", 8000)
tServer = tcpsServer(addr, tcpHandler)
tServer.serve_forever()
# call the main function
mainfunc()
About the Code
The script (shown above) eliminated my need to use a web server like the FourCalorieServers/HTTP server or Apache to do the post processing. While I have had speedy results with this setup, when using file sizes up to the maximum limit (somewhat arbitrarily set at 2.5 Mbytes), one thing that really puts the socket library performance in the tank is the avahi-daemon. I always disable it when using a socket library driven application.
The timeout value for the select.select call is somewhat arbitrarily set at five seconds. As it is written, after five seconds the script will give up, but this behaviour can be easily changed, if the usage will be aimed at a high latency network.
For an example that demonstrates how one might create a database "back end" connection in script to replace the single test
file (obviously good for only one file upload)
in the code shown in the (above) example, see :
http://www.fourcalorieservers.com/servers/tips/ibmdb2inst5.html
A few enhancements could be added, including some code to make a real determination of the appropriate result status to return to the
client. Since the current client (extension) checks only for status = "200 ok", other return scenarios require
a change to the extension code.
Soap for the Ajax
Notice the base64 encoding/decoding. Ain't Python3 grand? The browser used for this test does not support an AJAX component that adequately handles binary data. Thus, the data (png images are quite binary) must be textualized via base64. The extension does this with the window.btoa method, and so we decode it in script with the base64.b64decode method. Like I said, ain't Python3 grand?
The processing method does assume that the incoming data is base64 encoded. An embellishment might add code to detect that indeed the incoming data stream represents base64 data.
Newer browser versions include an AJAX component that has an expanded set of methods, including a "sendbinary" method that
should negate
the need to swap back and forth between binary and base64. This author has yet to try the new version of the component.
The script engine ships with an ever-growing composition of included modules, and some of the code in the example may require the
use of modules distributed with version 2.52 (or later). This site is not affiliated with Python in any way. More info is available at http://www.python.org
Wouldn't a GUI be nice?
While we're at it, wouldn't it be nice to add a slick-looking X-GUI application for handling the incoming files on the server end? (There's all those years working with MS-Windows showing in me again.) For an idea about using script to build such a thing, see:
http://www.fourcalorieservers.com/servers/simpleapp.html
1 Note: Firefox and Mozilla are trademarks belonging to the Mozilla Foundation
2 Note: this site has nothing to do (officially) with Firefox, and they do not endorse this
site in any way whatsoever. For information directly from the source, go
to http://www.mozilla.org .
3 Note: this site has nothing to do (officially) with Python and Python.org, and they do not endorse this
site in any way whatsoever. For information directly from the source, go
to http://www.python.org .
Contact: Ron Scheckelhoff -- Email suggestions to: rscheckelhoff@.com
( )