def send(self, request): """Send given request on the socket, support delayed emission.""" msg = request.getBytesStream() msglen = len(msg) outmsg('# SENDING ({0}) =====>'.format(msglen)) # here we use the not-so real format (special bytes are not # replaced in str(), only in getBytesStream()) Tools.print_message(six.text_type(request), cleanup=True) try: self._socket_send(msg) except socket.error as errmsg: outmsg('#<====ABORTED COMMUNICATION WHILE' ' SENDING {0}\n#{1}'.format(six.text_type(msg), errmsg)) return while request.is_delayed: msg = request.getDelayedOutput() msglen = len(msg) outmsg('# SENDING Delayed ({0}) =====>'.format(msglen)) # hopefully we do not use strange bytes in delayed chunks for now Tools.print_message(six.text_type(msg), cleanup=True) try: self._socket_send(msg) except socket.error as errmsg: outmsg('#<====ABORTED COMMUNICATION WHILE' ' SENDING (delayed) ' '{0}\r\n#{1}'.format(six.text_type(msg), errmsg)) return
def __str__(self): out = "" if self.error: if not self.valid: out = "****INVALID {0} <".format(self.name) else: out = "****BAD {0} <".format(self.name) for error, value in Tools.iteritems(self.errors): out += " {0};".format(error) out += ">****\n" out += " [{0} 1st line]\n{1}".format(self.short_name, self.first_line) out += " [{0} Headers]\n".format(self.short_name) for header in self.headers: out += "{0}".format(header) if self.chunked: out += " [{0} Chunks] ({1})\n".format(self.short_name, len(self.chunks)) for chunk in self.chunks: out += "{0}".format(chunk) bodylen = len(self.body) if (bodylen > 1000): body = self.body[0:1000] + b'( to be continued...)' else: body = self.body out += " [{0} Body] (size {1})\n{2}".format(self.short_name, bodylen, body) out += "\n ++++++++++++++++++++++++++++++++++++++\n" return out
def __str__(self): out = '' if self.error: if self.ERROR_MAYBE_09 in self.errors: out += "**(raw):{0}**\n".format(self.raw) if not self.valid: out += "**INVALID FIRST LINE <" else: out += "**BAD FIRST LINE <" for error, value in Tools.iteritems(self.errors): out += " {0};".format(error) out += ">**\n" out += "{0}[{1}]{2} ".format(self.prefix, self.method, self.method_sep) if self.has_absolute_uri: out += "<[{0}][{1}]>".format(self.proto, self.domain) out += "[{0}]".format(self.location) if self.has_args: out += "?[{0}]".format(self.query_string) out += "{0} HTTP/[{1}][{2}][{3}]".format( self.url_sep, self.version_major, self.version_sep, self.version_minor) out += " {0}[{1}]\n".format( self.suffix, self.eof) return out
def merge_value(self, other_h): """merge this header value with another header (multiline headers) Long after the end of parsing the header, parsing the next line, and ops_fold multiline header was detected. This headers is now coming back with this one the be merged as the value part. We need to catch errors on the value header and mix it with the current one. """ if self.value == u'': if other_h.value_prefix != u'': self.value_prefix = u'{0}{1}{2}'.format(self.value_prefix, self.value_suffix, other_h.value_prefix) self.value = u'{0}{1}'.format(other_h.value_prefix, other_h.value) else: self.value = u'{0}{1}{2}{3}'.format(self.value, self.value_suffix, other_h.value_prefix, other_h.value) self.suffix = other_h.value_suffix self.eof = other_h.eof for error, value in Tools.iteritems(other_h.errors): if error != self.ERROR_MULTILINE_OPTIONAL: self.setError(error, critical=False) self.setError(self.ERROR_WAS_MULTILINE_OPTIONAL, critical=False) if not other_h.valid: self.valid = False
def __str__(self): out = '' if self.error: if sys.version_info[0] < 3: # python2 out += "**(raw):{0}**".format( self.raw.replace('\r', '\\r').replace('\n', '\\n')) else: out += "**(raw):{0}**".format(self.raw) if not self.valid: out += "**INVALID CHUNK <" else: out += "**BAD CHUNK <" for error, value in Tools.iteritems(self.errors): out += " {0};".format(error) out += ">**\n" if self.is_last_chunk: out += "[LAST CHUNK] " if self.size == b'': out += '--unkown size--' else: if self.has_trailer: out += "[{0} ({1})] ;[{2}] [{3}]\n".format( self.size, int(self.size, 16), self.trailer, self.eof) else: out += "[{0} ({1})] [{2}]\n".format( self.size, self.real_size, self.eof) return out
def test_3010_method_separator(self): "Test various characters after the METHOD." self.real_test = "{0}_{1}".format(inspect.stack()[0][3], Tools.show_chars(self.separator)) self.req.set_method_sep(self.separator) # for RP mode: self.transmission_map = { 'GET{0} '.format(self.separator): self.STATUS_TRANSMITTED_CRAP, 'GET{0}'.format(self.separator): self.STATUS_TRANSMITTED_EXACT, 'GET ': self.STATUS_TRANSMITTED_CRAP, } # FIXME: may be an err400 in http 0.9 # GETXXXfoo HTTP/1.1 # method url noproto # sending err400 in 0.9 is not plainly wrong # playing with the method keyword we can expect err 400 # for every bad character... self._add_default_status_map(valid=self.valid_method_suffix, always_allow_rejected=True) # Local adjustments self.status_map[self.STATUS_TRANSMITTED] = self.GRAVITY_MINOR self.status_map[self.STATUS_TRANSMITTED_EXACT] = self.GRAVITY_MINOR self._end_1st_line_query()
def test_3013_line_prefix(self): "Some characters before the query..." self.real_test = "{0}_{1}".format( inspect.stack()[0][3], Tools.show_chars(self.separator)) self.setGravity(BaseTest.GRAVITY_MINOR) self.req.set_first_line_prefix(self.separator) # for RP mode: self.transmission_map = { '{0}GET'.format( self.separator): self.STATUS_TRANSMITTED_EXACT, ' GET': self.STATUS_TRANSMITTED_CRAP, } self._add_default_status_map( valid=self.valid_prefix, always_allow_rejected=self.can_be_rejected ) # local changes self.status_map[self.STATUS_TRANSMITTED_EXACT] = self.GRAVITY_MINOR self.status_map[self.STATUS_TRANSMITTED] = self.GRAVITY_MINOR self._end_1st_line_query()
def test_3010_method_separator(self): "Test various characters after the METHOD." self.real_test = "{0}_{1}".format( inspect.stack()[0][3], Tools.show_chars(self.separator)) self.req.set_method_sep(self.separator) # for RP mode: self.transmission_map = { 'GET{0} '.format( self.separator): self.STATUS_TRANSMITTED_CRAP, 'GET{0}'.format( self.separator): self.STATUS_TRANSMITTED_EXACT, 'GET ': self.STATUS_TRANSMITTED_CRAP, } # FIXME: may be an err400 in http 0.9 # GETXXXfoo HTTP/1.1 # method url noproto # sending err400 in 0.9 is not plainly wrong # playing with the method keyword we can expect err 400 # for every bad character... self._add_default_status_map(valid=self.valid_method_suffix, always_allow_rejected=True) # Local adjustments self.status_map[self.STATUS_TRANSMITTED] = self.GRAVITY_MINOR self.status_map[self.STATUS_TRANSMITTED_EXACT] = self.GRAVITY_MINOR self._end_1st_line_query()
def __str__(self): out = "" if not self.valid: out = "******INVALID {0} STREAM <".format(self.name) for error, value in Tools.iteritems(self.errors): out += " {0};".format(error) out += ">******\n" out += "-{0} {1}-".format(self.count, self.name) for resp in self.messages: out += "\n---\n{0}\n---".format(resp) return out
def test_3023_crcrcrlf_line_prefix(self): self.separator = u'\r\r\r\r\r\n' self.real_test = "{0}_{1}".format(inspect.stack()[0][3], Tools.show_chars(self.separator)) self.setGravity(BaseTest.GRAVITY_MINOR) self.req.set_first_line_prefix(self.separator) # for RP mode: self.transmission_map = { '{0}GET'.format(self.separator): self.STATUS_TRANSMITTED_EXACT, ' GET': self.STATUS_TRANSMITTED_CRAP, } self._add_default_status_map(valid=True, always_allow_rejected=True) self._end_1st_line_query(http09_allowed=False)
def __str__(self): out = '' if self.error: if self.ERROR_MAYBE_09 in self.errors: out += "**(raw):{0}**".format(self.raw) if not not self.valid: out += "**INVALID FIRST LINE <" else: out += "**BAD FIRST LINE <" for error, value in Tools.iteritems(self.errors): out += " {0};".format(error) out += ">**\n" out += "HTTP/[{0}][{1}][{2}] [{3}] [{4}] [{5}]\n".format( self.version_major, self.version_sep, self.version_minor, self.code, self.value, self.eof) return out
def test_3023_crcrcrlf_line_prefix(self): self.separator = u'\r\r\r\r\r\n' self.real_test = "{0}_{1}".format( inspect.stack()[0][3], Tools.show_chars(self.separator)) self.setGravity(BaseTest.GRAVITY_MINOR) self.req.set_first_line_prefix(self.separator) # for RP mode: self.transmission_map = { '{0}GET'.format( self.separator): self.STATUS_TRANSMITTED_EXACT, ' GET': self.STATUS_TRANSMITTED_CRAP, } self._add_default_status_map( valid=True, always_allow_rejected=True) self._end_1st_line_query(http09_allowed=False)
def test_3014_line_suffix(self): "Let's add some garbage after the protocol." self.real_test = "{0}_{1}".format(inspect.stack()[0][3], Tools.show_chars(self.separator)) self.setGravity(BaseTest.GRAVITY_WARNING) self.req.set_first_line_suffix(self.separator) # for RP mode: self.transmission_map = { ' HTTP/1.0{0}\r\n'.format(self.separator): self.STATUS_TRANSMITTED_EXACT, ' HTTP/1.1{0}\r\n'.format(self.separator): self.STATUS_TRANSMITTED_EXACT, ' HTTP/1.1 \r\n': self.STATUS_TRANSMITTED_CRAP, } self._add_default_status_map(valid=False) self._end_1st_line_query(http09_allowed=False)
def _end_1st_line_query(self, responses=None, http09_allowed=False, expected_number=1, status_map=None): if responses is None: responses = self.send_queries() self.analysis(responses, http09_allowed=http09_allowed, regular_expected=False) self.adjust_status_by_map(status_map) allowed = [] for status_elt, status_gravity in Tools.iteritems(self.status_map): if status_gravity is self.GRAVITY_OK: allowed.append(status_elt) self.assertIn(self.status, allowed)
def __str__(self): out = '' if self.error: if not self.valid: out += "**INVALID HEADER <" else: out += "**BAD HEADER <" for error, value in Tools.iteritems(self.errors): out += " {0};".format(error) out += ">**\n" out += "{0}[{1}] {2}[{3}]{4} [{5}] {6}[{7}]\n".format( self.header_prefix, self.header, self.separator_prefix, self.separator, self.value_prefix, self.value, self.value_suffix, self.eof) return out
def test_3016_line_suffix_with_char_H(self): "Nginx, for example, likes the H character" self.real_test = "{0}_{1}".format(inspect.stack()[0][3], Tools.show_chars(self.separator)) self.setGravity(BaseTest.GRAVITY_WARNING) self.req.set_first_line_suffix(self.separator + u'H') # for RP mode: self.transmission_map = { ' HTTP/1.0{0}H\r\n'.format(self.separator): self.STATUS_TRANSMITTED_EXACT, ' HTTP/1.1{0}H\r\n'.format(self.separator): self.STATUS_TRANSMITTED_EXACT, 'HTTP/1.1{0}H H'.format(self.separator): self.STATUS_TRANSMITTED_CRAP, 'HTTP/1.1 H\r\n': self.STATUS_TRANSMITTED_CRAP, } self._add_default_status_map(valid=False) self._end_1st_line_query(http09_allowed=False)
def test_3014_line_suffix(self): "Let's add some garbage after the protocol." self.real_test = "{0}_{1}".format( inspect.stack()[0][3], Tools.show_chars(self.separator)) self.setGravity(BaseTest.GRAVITY_WARNING) self.req.set_first_line_suffix(self.separator) # for RP mode: self.transmission_map = { ' HTTP/1.0{0}\r\n'.format( self.separator): self.STATUS_TRANSMITTED_EXACT, ' HTTP/1.1{0}\r\n'.format( self.separator): self.STATUS_TRANSMITTED_EXACT, ' HTTP/1.1 \r\n': self.STATUS_TRANSMITTED_CRAP, } self._add_default_status_map( valid=False) self._end_1st_line_query(http09_allowed=False)
def test_3017_line_suffix_with_double_HTTP11(self): "Ending first line with two times the protocol" self.real_test = "{0}_{1}".format(inspect.stack()[0][3], Tools.show_chars(self.separator)) self.req.set_first_line_suffix(self.separator + u'HTTP/1.1') # for RP mode: self.transmission_map = { 'HTTP/1.1{0}HTTP/1.1\r\n'.format(self.separator): self.STATUS_TRANSMITTED_EXACT, 'HTTP/1.0{0}HTTP/1.0\r\n'.format(self.separator): self.STATUS_TRANSMITTED_CRAP, 'HTTP/1.0{0}HTTP/1.1\r\n'.format(self.separator): self.STATUS_TRANSMITTED_CRAP, 'HTTP/1.1{0}HTTP/1.0\r\n'.format(self.separator): self.STATUS_TRANSMITTED_CRAP, } self._add_default_status_map(valid=False) self._end_1st_line_query(http09_allowed=False)
def _analyze_invalid_echo_queries(self, stream_mode=False): "Having a backend received query which is invalid" if self.transmission_zone is not None: if stream_mode: zone = self.backend_queries else: if self.transmission_zone is Tools.ZONE_FIRST_LINE: query = self.backend_queries[0] zone = query.first_line.raw inmsg('# zone to analyze:.') inmsg(str(zone)) if self.transmission_zone is Tools.ZONE_HEADERS: query = self.backend_queries[0] zone = b'' for header in query.headers: zone += header.raw inmsg('# zone to analyze:.') inmsg(str(zone)) if self.transmission_zone is Tools.ZONE_CHUNK_SIZE: query = self.backend_queries[0] zone = b'' if query.chunked: for chunk in query.chunks: zone += chunk.raw inmsg('# zone to analyze:.') inmsg(str(zone)) if self.transmission_map: for proof, status in Tools.iteritems(self.transmission_map): # here we manipulate bytes strings if isinstance(proof, six.string_types): # we do not use special non-ascii chars internally proof = proof.encode('ascii') if proof in zone: inmsg('# found one transmission proof,' ' status to : {0}'.format(status)) inmsg(repr(proof)) self.setStatus(status) return True self.setStatus(self.STATUS_TRANSMITTED)
def test_3011_location_separator(self): "After the query string, valid separator or a forbidden char?" self.real_test = "{0}_{1}".format(inspect.stack()[0][3], Tools.show_chars(self.separator)) self.setGravity(BaseTest.GRAVITY_CRITICAL) self.req.set_location_sep(self.separator) # for RP mode: self.transmission_map = { '{0}HTTP/1.1 H'.format(self.separator): self.STATUS_TRANSMITTED, ' HTTP/1.1 H'.format(self.separator): self.STATUS_TRANSMITTED_CRAP, ' HTTP/1.0 H'.format(self.separator): self.STATUS_TRANSMITTED_CRAP, '{0} H'.format(self.separator): self.STATUS_TRANSMITTED_CRAP, '{0}HTTP/1.1\r\n'.format(self.separator): self.STATUS_TRANSMITTED_EXACT, } # RFC states that some of the separators, like FF, HTAB, VT, CR # are allowed, but ... well, .., we should allow rejection on most # of theses things (that should not be an error) self._add_default_status_map( http09_allowed=self.valid_09_location, valid=self.valid_location, always_allow_rejected=self.can_be_rejected) # Local adjustments if self.separator in [Tools.BEL, Tools.BS]: # better to reject, but this transmission is done the right # way if you do not consider theses chars as spaces self.status_map[self.STATUS_TRANSMITTED] = self.GRAVITY_MINOR self.status_map[self.STATUS_TRANSMITTED_EXACT] = self.GRAVITY_MINOR else: self.status_map[self.STATUS_TRANSMITTED] = self.GRAVITY_WARNING self.status_map[ self.STATUS_TRANSMITTED_EXACT] = self.GRAVITY_WARNING self._end_1st_line_query(http09_allowed=self.valid_09_location)
def test_3011_location_separator(self): "After the query string, valid separator or a forbidden char?" self.real_test = "{0}_{1}".format( inspect.stack()[0][3], Tools.show_chars(self.separator)) self.setGravity(BaseTest.GRAVITY_CRITICAL) self.req.set_location_sep(self.separator) # for RP mode: self.transmission_map = { '{0}HTTP/1.1 H'.format( self.separator): self.STATUS_TRANSMITTED, ' HTTP/1.1 H'.format( self.separator): self.STATUS_TRANSMITTED_CRAP, ' HTTP/1.0 H'.format( self.separator): self.STATUS_TRANSMITTED_CRAP, '{0} H'.format(self.separator): self.STATUS_TRANSMITTED_CRAP, '{0}HTTP/1.1\r\n'.format( self.separator): self.STATUS_TRANSMITTED_EXACT, } # RFC states that some of the separators, like FF, HTAB, VT, CR # are allowed, but ... well, .., we should allow rejection on most # of theses things (that should not be an error) self._add_default_status_map( http09_allowed=self.valid_09_location, valid=self.valid_location, always_allow_rejected=self.can_be_rejected) # Local adjustments if self.separator in [Tools.BEL, Tools.BS]: # better to reject, but this transmission is done the right # way if you do not consider theses chars as spaces self.status_map[self.STATUS_TRANSMITTED] = self.GRAVITY_MINOR self.status_map[self.STATUS_TRANSMITTED_EXACT] = self.GRAVITY_MINOR else: self.status_map[self.STATUS_TRANSMITTED] = self.GRAVITY_WARNING self.status_map[ self.STATUS_TRANSMITTED_EXACT] = self.GRAVITY_WARNING self._end_1st_line_query(http09_allowed=self.valid_09_location)
def test_3018_location_separator_and_extra_proto(self): "After the query, valid separator or a forbidden char? Proto repeated." self.real_test = "{0}_{1}".format( inspect.stack()[0][3], Tools.show_chars(self.separator)) self.setGravity(BaseTest.GRAVITY_MINOR) self.req.add_argument('last', 'marker') self.req.set_location_sep(self.separator) self.req.set_http_version(major=1, minor=1, force=True) self.req.set_first_line_suffix(u' HTTP/1.1') # for RP mode: self.transmission_map = { '{0}HTTP/1.1 H'.format( self.separator): self.STATUS_TRANSMITTED_EXACT, ' HTTP/1.1 H'.format( self.separator): self.STATUS_TRANSMITTED_CRAP, ' HTTP/1.0 H'.format( self.separator): self.STATUS_TRANSMITTED_CRAP, 'last=marker HTTP/1.0\r\n'.format( self.separator): self.STATUS_TRANSMITTED_CRAP, 'last=marker HTTP/1.1\r\n'.format( self.separator): self.STATUS_TRANSMITTED_CRAP, } self._add_default_status_map( valid=self.valid_location) # Local adjustments if self.separator in [Tools.BEL, Tools.BS]: # better to reject, but this transmission is done the right # way if you do not consider theses chars as spaces self.status_map[self.STATUS_TRANSMITTED] = self.GRAVITY_MINOR # local changes self.status_map[self.STATUS_TRANSMITTED_EXACT] = self.GRAVITY_MINOR self.status_map[self.STATUS_09DOWNGRADE] = self.GRAVITY_CRITICAL self.status_map[self.STATUS_09OK] = self.GRAVITY_CRITICAL self._end_1st_line_query()
def test_3013_line_prefix(self): "Some characters before the query..." self.real_test = "{0}_{1}".format(inspect.stack()[0][3], Tools.show_chars(self.separator)) self.setGravity(BaseTest.GRAVITY_MINOR) self.req.set_first_line_prefix(self.separator) # for RP mode: self.transmission_map = { '{0}GET'.format(self.separator): self.STATUS_TRANSMITTED_EXACT, ' GET': self.STATUS_TRANSMITTED_CRAP, } self._add_default_status_map( valid=self.valid_prefix, always_allow_rejected=self.can_be_rejected) # local changes self.status_map[self.STATUS_TRANSMITTED_EXACT] = self.GRAVITY_MINOR self.status_map[self.STATUS_TRANSMITTED] = self.GRAVITY_MINOR self._end_1st_line_query()
def test_3016_line_suffix_with_char_H(self): "Nginx, for example, likes the H character" self.real_test = "{0}_{1}".format( inspect.stack()[0][3], Tools.show_chars(self.separator)) self.setGravity(BaseTest.GRAVITY_WARNING) self.req.set_first_line_suffix(self.separator + u'H') # for RP mode: self.transmission_map = { ' HTTP/1.0{0}H\r\n'.format( self.separator): self.STATUS_TRANSMITTED_EXACT, ' HTTP/1.1{0}H\r\n'.format( self.separator): self.STATUS_TRANSMITTED_EXACT, 'HTTP/1.1{0}H H'.format( self.separator): self.STATUS_TRANSMITTED_CRAP, 'HTTP/1.1 H\r\n': self.STATUS_TRANSMITTED_CRAP, } self._add_default_status_map( valid=False) self._end_1st_line_query(http09_allowed=False)
def test_3017_line_suffix_with_double_HTTP11(self): "Ending first line with two times the protocol" self.real_test = "{0}_{1}".format( inspect.stack()[0][3], Tools.show_chars(self.separator)) self.req.set_first_line_suffix(self.separator + u'HTTP/1.1') # for RP mode: self.transmission_map = { 'HTTP/1.1{0}HTTP/1.1\r\n'.format( self.separator): self.STATUS_TRANSMITTED_EXACT, 'HTTP/1.0{0}HTTP/1.0\r\n'.format( self.separator): self.STATUS_TRANSMITTED_CRAP, 'HTTP/1.0{0}HTTP/1.1\r\n'.format( self.separator): self.STATUS_TRANSMITTED_CRAP, 'HTTP/1.1{0}HTTP/1.0\r\n'.format( self.separator): self.STATUS_TRANSMITTED_CRAP, } self._add_default_status_map( valid=False) self._end_1st_line_query(http09_allowed=False)
def test_3018_location_separator_and_extra_proto(self): "After the query, valid separator or a forbidden char? Proto repeated." self.real_test = "{0}_{1}".format(inspect.stack()[0][3], Tools.show_chars(self.separator)) self.setGravity(BaseTest.GRAVITY_MINOR) self.req.add_argument('last', 'marker') self.req.set_location_sep(self.separator) self.req.set_http_version(major=1, minor=1, force=True) self.req.set_first_line_suffix(u' HTTP/1.1') # for RP mode: self.transmission_map = { '{0}HTTP/1.1 H'.format(self.separator): self.STATUS_TRANSMITTED_EXACT, ' HTTP/1.1 H'.format(self.separator): self.STATUS_TRANSMITTED_CRAP, ' HTTP/1.0 H'.format(self.separator): self.STATUS_TRANSMITTED_CRAP, 'last=marker HTTP/1.0\r\n'.format(self.separator): self.STATUS_TRANSMITTED_CRAP, 'last=marker HTTP/1.1\r\n'.format(self.separator): self.STATUS_TRANSMITTED_CRAP, } self._add_default_status_map(valid=self.valid_location) # Local adjustments if self.separator in [Tools.BEL, Tools.BS]: # better to reject, but this transmission is done the right # way if you do not consider theses chars as spaces self.status_map[self.STATUS_TRANSMITTED] = self.GRAVITY_MINOR # local changes self.status_map[self.STATUS_TRANSMITTED_EXACT] = self.GRAVITY_MINOR self.status_map[self.STATUS_09DOWNGRADE] = self.GRAVITY_CRITICAL self.status_map[self.STATUS_09OK] = self.GRAVITY_CRITICAL self._end_1st_line_query()
def __str__(self): out = '' if self.error: if self.ERROR_MAYBE_09 in self.errors: out += "**(raw):{0}**\n".format(self.raw) if not self.valid: out += "**INVALID FIRST LINE <" else: out += "**BAD FIRST LINE <" for error, value in Tools.iteritems(self.errors): out += " {0};".format(error) out += ">**\n" out += "{0}[{1}]{2} ".format(self.prefix, self.method, self.method_sep) if self.has_absolute_uri: out += "<[{0}][{1}]>".format(self.proto, self.domain) out += "[{0}]".format(self.location) if self.has_args: out += "?[{0}]".format(self.query_string) out += "{0} HTTP/[{1}][{2}][{3}]".format(self.url_sep, self.version_major, self.version_sep, self.version_minor) out += " {0}[{1}]\n".format(self.suffix, self.eof) return out