# protocol.py -- Shared parts of the git protocols
# Copyright (C) 2008 John Carr <john.carr@unrouted.co.uk>
# Copyright (C) 2008-2012 Jelmer Vernooij <jelmer@jelmer.uk>
#
# Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
# General Public License as public by the Free Software Foundation; version 2.0
# or (at your option) any later version. You can redistribute it and/or
# modify it under the terms of either of these two licenses.
#
# 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.
#
# You should have received a copy of the licenses; if not, see
# <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
# and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
# License, Version 2.0.
#
"""Generic functions for talking the git smart server protocol."""
from io import BytesIO
from os import (
SEEK_END,
)
import socket
import dulwich
from dulwich.errors import (
HangupException,
GitProtocolError,
)
TCP_GIT_PORT = 9418
ZERO_SHA = b"0" * 40
SINGLE_ACK = 0
MULTI_ACK = 1
MULTI_ACK_DETAILED = 2
# pack data
SIDE_BAND_CHANNEL_DATA = 1
# progress messages
SIDE_BAND_CHANNEL_PROGRESS = 2
# fatal error message just before stream aborts
SIDE_BAND_CHANNEL_FATAL = 3
CAPABILITY_DEEPEN_SINCE = b'deepen-since'
CAPABILITY_DEEPEN_NOT = b'deepen-not'
CAPABILITY_DEEPEN_RELATIVE = b'deepen-relative'
CAPABILITY_DELETE_REFS = b'delete-refs'
CAPABILITY_INCLUDE_TAG = b'include-tag'
CAPABILITY_MULTI_ACK = b'multi_ack'
CAPABILITY_MULTI_ACK_DETAILED = b'multi_ack_detailed'
CAPABILITY_NO_DONE = b'no-done'
CAPABILITY_NO_PROGRESS = b'no-progress'
CAPABILITY_OFS_DELTA = b'ofs-delta'
CAPABILITY_QUIET = b'quiet'
CAPABILITY_REPORT_STATUS = b'report-status'
CAPABILITY_SHALLOW = b'shallow'
CAPABILITY_SIDE_BAND = b'side-band'
CAPABILITY_SIDE_BAND_64K = b'side-band-64k'
CAPABILITY_THIN_PACK = b'thin-pack'
CAPABILITY_AGENT = b'agent'
CAPABILITY_SYMREF = b'symref'
# Magic ref that is used to attach capabilities to when
# there are no refs. Should always be ste to ZERO_SHA.
CAPABILITIES_REF = b'capabilities^{}'
COMMON_CAPABILITIES = [
CAPABILITY_OFS_DELTA,
CAPABILITY_SIDE_BAND,
CAPABILITY_SIDE_BAND_64K,
CAPABILITY_AGENT,
CAPABILITY_NO_PROGRESS]
KNOWN_UPLOAD_CAPABILITIES = set(COMMON_CAPABILITIES + [
CAPABILITY_THIN_PACK,
CAPABILITY_MULTI_ACK,
CAPABILITY_MULTI_ACK_DETAILED,
CAPABILITY_INCLUDE_TAG,
CAPABILITY_DEEPEN_SINCE,
CAPABILITY_SYMREF,
CAPABILITY_SHALLOW,
CAPABILITY_DEEPEN_NOT,
CAPABILITY_DEEPEN_RELATIVE,
])
KNOWN_RECEIVE_CAPABILITIES = set(COMMON_CAPABILITIES + [
CAPABILITY_REPORT_STATUS])
def agent_string():
return ('dulwich/%d.%d.%d' % dulwich.__version__).encode('ascii')
def capability_agent():
return CAPABILITY_AGENT + b'=' + agent_string()
def capability_symref(from_ref, to_ref):
return CAPABILITY_SYMREF + b'=' + from_ref + b':' + to_ref
def extract_capability_names(capabilities):
return set(parse_capability(c)[0] for c in capabilities)
def parse_capability(capability):
parts = capability.split(b'=', 1)
if len(parts) == 1:
return (parts[0], None)
return tuple(parts)
def symref_capabilities(symrefs):
return [capability_symref(*k) for k in symrefs]
COMMAND_DEEPEN = b'deepen'
COMMAND_SHALLOW = b'shallow'
COMMAND_UNSHALLOW = b'unshallow'
COMMAND_DONE = b'done'
COMMAND_WANT = b'want'
COMMAND_HAVE = b'have'
class ProtocolFile(object):
"""A dummy file for network ops that expect file-like objects."""
def __init__(self, read, write):
self.read = read
self.write = write
def tell(self):
pass
def close(self):
pass
def pkt_line(data):
"""Wrap data in a pkt-line.
:param data: The data to wrap, as a str or None.
:return: The data prefixed with its length in pkt-line format; if data was
None, returns the flush-pkt ('0000').
"""
if data is None:
return b'0000'
return ('%04x' % (len(data) + 4)).encode('ascii') + data
class Protocol(object):
"""Class for interacting with a remote git process over the wire.
Parts of the git wire protocol use 'pkt-lines' to communicate. A pkt-line
consists of the length of the line as a 4-byte hex string, followed by the
payload data. The length includes the 4-byte header. The special line
'0000' indicates the end of a section of input and is called a 'flush-pkt'.
For details on the pkt-line format, see the cgit distribution:
Documentation/technical/protocol-common.txt
"""
def __init__(self, read, write, close=None, report_activity=None):
self.read = read
self.write = write
self._close = close
self.report_activity = report_activity
self._readahead = None
def close(self):
if self._close:
self._close()
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.close()
def read_pkt_line(self):
"""Reads a pkt-line from the remote git process.
This method may read from the readahead buffer; see unread_pkt_line.
:return: The next string from the stream, without the length prefix, or
None for a flush-pkt ('0000').
"""
if self._readahead is None:
read = self.read
else:
read = self._readahead.read
self._readahead = None
try:
sizestr = read(4)
if not sizestr:
raise HangupException()
size = int(sizestr, 16)
if size == 0:
if self.report_activity:
self.report_activity(4, 'read')
return None
if self.report_activity:
self.report_activity(size, 'read')
pkt_contents = read(size-4)
except socket.error as e:
raise GitProtocolError(e)
else:
if len(pkt_contents) + 4 != size:
raise GitProtocolError(
'Length of pkt read %04x does not match length prefix %04x'
% (len(pkt_contents) + 4, size))
return pkt_contents
def eof(self):
"""Test whether the protocol stream has reached EOF.
Note that this refers to the actual stream EOF and not just a
flush-pkt.
:return: True if the stream is at EOF, False otherwise.
"""
try:
next_line = self.read_pkt_line()
except HangupException:
return True
self.unread_pkt_line(next_line)
return False
def unread_pkt_line(self, data):
"""Unread a single line of data into the readahead buffer.
This method can be used to unread a single pkt-line into a fixed
readahead buffer.
:param data: The data to unread, without the length prefix.
:raise ValueError: If more than one pkt-line is unread.
"""
if self._readahead is not None:
raise ValueError('Attempted to unread multiple pkt-lines.')
self._readahead = BytesIO(pkt_line(data))
def read_pkt_seq(self):
"""Read a sequence of pkt-lines from the remote git process.
:return: Yields each line of data up to but not including the next
flush-pkt.
"""
pkt = self.read_pkt_line()
while pkt:
yield pkt
pkt = self.read_pkt_line()
def write_pkt_line(self, line):
"""Sends a pkt-line to the remote git process.
:param line: A string containing the data to send, without the length
prefix.
"""
try:
line = pkt_line(line)
self.write(line)
if self.report_activity:
self.report_activity(len(line), 'write')
except socket.error as e:
raise GitProtocolError(e)
def write_file(self):
"""Return a writable file-like object for this protocol."""
class ProtocolFile(object):
def __init__(self, proto):
self._proto = proto
self._offset = 0
def write(self, data):
self._proto.write(data)
self._offset += len(data)
def tell(self):
return self._offset
def close(self):
pass
return ProtocolFile(self)
def write_sideband(self, channel, blob):
"""Write multiplexed data to the sideband.
:param channel: An int specifying the channel to write to.
:param blob: A blob of data (as a string) to send on this channel.
"""
# a pktline can be a max of 65520. a sideband line can therefore be
# 65520-5 = 65515
# WTF: Why have the len in ASCII, but the channel in binary.
while blob:
self.write_pkt_line(bytes(bytearray([channel])) + blob[:65515])
blob = blob[65515:]
def send_cmd(self, cmd, *args):
"""Send a command and some arguments to a git server.
Only used for the TCP git protocol (git://).
:param cmd: The remote service to access.
:param args: List of arguments to send to remove service.
"""
self.write_pkt_line(cmd + b" " + b"".join([(a + b"\0") for a in args]))
def read_cmd(self):
"""Read a command and some arguments from the git client
Only used for the TCP git protocol (git://).
:return: A tuple of (command, [list of arguments]).
"""
line = self.read_pkt_line()
splice_at = line.find(b" ")
cmd, args = line[:splice_at], line[splice_at+1:]
assert args[-1:] == b"\x00"
return cmd, args[:-1].split(b"\0")
_RBUFSIZE = 8192 # Default read buffer size.
class ReceivableProtocol(Protocol):
"""Variant of Protocol that allows reading up to a size without blocking.
This class has a recv() method that behaves like socket.recv() in addition
to a read() method.
If you want to read n bytes from the wire and block until exactly n bytes
(or EOF) are read, use read(n). If you want to read at most n bytes from
the wire but don't care if you get less, use recv(n). Note that recv(n)
will still block until at least one byte is read.
"""
Loading ...