blob: 4b1a60ee30b778201b5dff22a01b2a7f91d4276a [file] [log] [blame]
#!/usr/bin/python
# Copyright 2011 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# pylint:disable=anomalous-backslash-in-string
# pylint:disable=g-docstring-has-escape
"""Protocol helpers for multiline sh-style-quoted blocks.
Blocks are formatted as lines, separated by newline characters, each
containing one or more quoted words. The block ends at the first line
containing zero words.
For example:
this is a line
so "is this
stuff"
this 'also' is a "'line'"
this\ is\ one\ word
""
still going
Parses as follows:
[['this', 'is', 'a', 'line'],
['so', 'is this\nstuff'],
['this', 'also', 'is', 'a', "'line'"],
['this is one word'],
[''],
['still', 'going']]
The net result is a human-friendly protocol that can be used for many
purposes.
"""
__author__ = 'apenwarr@google.com (Avery Pennarun)'
import bup.shquote
import mainloop
def _HasQuotes(s):
return '\\' in s or '"' in s or "'" in s
class QuotedBlockProtocol(object):
"""Implement the QuotedBlock protocol.
You should call GotData() every time you receive incoming data.
Calls a callback function with an array of lines at the end of each block.
The callback function returns a list that is then quoted and returned
from GotData().
Try using QuotedBlockStreamer for a wrapper that ties this into a mainloop.
"""
def __init__(self, handle_lines_func):
"""Initialize a QuotedBlockProtocol instance.
Args:
handle_lines_func: called as handle_lines_func(lines) at the end of
each incoming block. Returns a list of lines to send back.
"""
self.handle_lines_func = handle_lines_func
self.partial_line = ''
self.lines = []
def GotData(self, data):
"""Call this method every time you receive incoming bytes.
It will call the handle_lines_func at the end of a block. When this
function returns non-None, it will be an encoded block string you should
send back to the remote end.
This function knows how to handle lines that contain a quoted newline
character. It merges the two lines into a single one and then calls
self.GotLine().
Args:
data: a string of bytes you received from the remote.
Returns:
None or a string that should be returned to the remote.
"""
line = self.partial_line + data
word = None
if _HasQuotes(line):
_, word = bup.shquote.unfinished_word(line)
if word:
self.partial_line = line
else:
self.partial_line = ''
try:
return self.GotLine(line.decode('utf-8'))
except UnicodeDecodeError as e:
print 'GotData: error decoding "%s", %s' % (line, e)
return None
def GotLine(self, line):
"""Call this method every time you receive a parseable line of data.
Most of the time you will call GotData() instead. Only use this method
if you're absolutely sure the line does not have any unfinished quoted
sections.
Args:
line: a parseable string of bytes you received from the remote.
Returns:
None or a string that should be returned to the remote.
"""
if line.strip():
# a new line in this block
if _HasQuotes(line):
parts = bup.shquote.quotesplit(line)
self.lines.append([word for unused_offset, word in parts])
else:
self.lines.append(line.split())
else:
# blank line means end of block
lines = self.lines
self.lines = []
result = self.handle_lines_func(lines)
if result is None:
return None
else:
return self.RenderBlock(result)
def RenderBlock(self, lines):
"""Quote the given lines array back into a parseable string."""
out = []
lines = lines or []
for line in lines:
line = [unicode(word) for word in line]
out.append(bup.shquote.quotify_list(line).encode('utf-8') + '\r\n')
out.append('\r\n')
return ''.join(out)
class QuotedBlockStreamer(object):
"""A simple helper that can be used as the callback to MainLoop.Listen.
Derive from this class and override ProcessBlock() to change how you want
to interpret and respond to blocks. The listener will automatically
accept incoming connections, and ProcessBlock() will be called
automatically for each full block received from the remote. It should
return the lines that should be sent back to the remote. We send back
the lines automatically using tornado.IOStream.
Example:
loop = mainloop.MainLoop()
loop.ListenUnix('/tmp/cwmpd.sock', QuotedBlockStreamer)
loop.Start()
"""
def __init__(self, sock, address):
"""Initialize a QuotedBlockStreamer.
Args:
sock: the socket provided by MainLoop.Listen
address: the address provided by MainLoop.Listen
"""
self.sock = sock
self.address = address
qb = QuotedBlockProtocol(self.ProcessBlock)
mainloop.LineReader(sock, address, qb.GotData)
def ProcessBlock(self, lines):
"""Redefine this function to respond to incoming requests how you want."""
print 'lines: %r' % (lines,)
return [['RESPONSE:']] + lines + [['EOR']]
def main():
loop = mainloop.MainLoop()
loop.ListenInet6(('', 12999), QuotedBlockStreamer)
loop.ListenUnix('/tmp/cwmpd.sock', QuotedBlockStreamer)
print 'hello'
loop.Start()
if __name__ == '__main__':
main()