From d5025b3fa0aac5d0b636532a86d6ed705e55e9a3 Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Mon, 22 Jan 2024 17:17:20 -0600 Subject: [PATCH 01/18] Fix broken URI behaviors (#3140) * Fix the issues with URI's behavior and add tests for the various issues reported on GitHub * Fix Sendable correctness in the various content coder subsystems * Tweak a few timeouts to reduce test runtime (reduced by 2/3) * Use app.startup() rather than app.start() in async contexts in tests * Minor README updates * Add Mastodon link to replace old Twitter one * Add missing image alt text --- README.md | 25 +- .../Content/ContainerGetPathExecutor.swift | 2 +- Sources/Vapor/Content/ContentCoders.swift | 8 +- Sources/Vapor/Content/ContentContainer.swift | 4 +- .../Vapor/Content/JSONCoders+Content.swift | 4 +- Sources/Vapor/Content/PlaintextDecoder.swift | 4 +- Sources/Vapor/Content/PlaintextEncoder.swift | 9 +- Sources/Vapor/Content/URLQueryCoders.swift | 8 +- Sources/Vapor/Content/URLQueryContainer.swift | 4 +- .../Multipart/FormDataDecoder+Content.swift | 2 +- .../Multipart/FormDataEncoder+Content.swift | 10 +- .../URLEncodedFormDecoder.swift | 423 ++++++++++-------- .../URLEncodedFormEncoder.swift | 10 +- Sources/Vapor/Utilities/URI.swift | 109 +++-- Tests/AsyncTests/AsyncCommandsTests.swift | 4 +- Tests/AsyncTests/AsyncRequestTests.swift | 14 +- Tests/VaporTests/ClientTests.swift | 4 +- Tests/VaporTests/ContentTests.swift | 45 +- Tests/VaporTests/RouteTests.swift | 18 + Tests/VaporTests/ServerTests.swift | 8 +- Tests/VaporTests/URITests.swift | 119 +++-- 21 files changed, 486 insertions(+), 348 deletions(-) diff --git a/README.md b/README.md index 51e7fbebba..66df03a6ff 100644 --- a/README.md +++ b/README.md @@ -5,23 +5,26 @@

- - Documentation + + Documentation - Team Chat + Team Chat - MIT License + MIT License - - Continuous Integration + + Continuous Integration + + + Code Coverage - Swift 5.7 + Swift 5.7+ - - Twitter + + Mastodon

@@ -33,11 +36,11 @@ Take a look at some of the [awesome stuff](https://github.com/Cellane/awesome-va ### 💧 Community -Join the welcoming community of fellow Vapor developers on [Discord](http://vapor.team). +Join the welcoming community of fellow Vapor developers on [Discord](https://vapor.team). ### 🚀 Contributing -To contribute a **feature or idea** to Vapor, [create an issue](https://github.com/vapor/vapor/issues/new) explaining your idea or bring it up on [Discord](http://vapor.team). +To contribute a **feature or idea** to Vapor, [create an issue](https://github.com/vapor/vapor/issues/new) explaining your idea or bring it up on [Discord](https://vapor.team). If you find a **bug**, please [create an issue](https://github.com/vapor/vapor/issues/new). diff --git a/Sources/Vapor/Content/ContainerGetPathExecutor.swift b/Sources/Vapor/Content/ContainerGetPathExecutor.swift index 3b776442b3..6ff619b740 100644 --- a/Sources/Vapor/Content/ContainerGetPathExecutor.swift +++ b/Sources/Vapor/Content/ContainerGetPathExecutor.swift @@ -2,7 +2,7 @@ internal struct ContainerGetPathExecutor: Decodable { let result: D - static func userInfo(for keyPath: [CodingKey]) -> [CodingUserInfoKey: Any] { + static func userInfo(for keyPath: [CodingKey]) -> [CodingUserInfoKey: Sendable] { [.containerGetKeypath: keyPath] } diff --git a/Sources/Vapor/Content/ContentCoders.swift b/Sources/Vapor/Content/ContentCoders.swift index 0cd0c40534..509044d57b 100644 --- a/Sources/Vapor/Content/ContentCoders.swift +++ b/Sources/Vapor/Content/ContentCoders.swift @@ -21,7 +21,7 @@ public protocol ContentEncoder { /// /// For legacy API compatibility reasons, the default protocol conformance for this method forwards it to the legacy /// encode method. - func encode(_ encodable: E, to body: inout ByteBuffer, headers: inout HTTPHeaders, userInfo: [CodingUserInfoKey: Any]) throws + func encode(_ encodable: E, to body: inout ByteBuffer, headers: inout HTTPHeaders, userInfo: [CodingUserInfoKey: Sendable]) throws where E: Encodable } @@ -42,12 +42,12 @@ public protocol ContentDecoder { /// /// For legacy API compatibility reasons, the default protocol conformance for this method forwards it to the legacy /// decode method. - func decode(_ decodable: D.Type, from body: ByteBuffer, headers: HTTPHeaders, userInfo: [CodingUserInfoKey: Any]) throws -> D + func decode(_ decodable: D.Type, from body: ByteBuffer, headers: HTTPHeaders, userInfo: [CodingUserInfoKey: Sendable]) throws -> D where D: Decodable } extension ContentEncoder { - public func encode(_ encodable: E, to body: inout ByteBuffer, headers: inout HTTPHeaders, userInfo: [CodingUserInfoKey: Any]) throws + public func encode(_ encodable: E, to body: inout ByteBuffer, headers: inout HTTPHeaders, userInfo: [CodingUserInfoKey: Sendable]) throws where E: Encodable { try self.encode(encodable, to: &body, headers: &headers) @@ -55,7 +55,7 @@ extension ContentEncoder { } extension ContentDecoder { - public func decode(_ decodable: D.Type, from body: ByteBuffer, headers: HTTPHeaders, userInfo: [CodingUserInfoKey: Any]) throws -> D + public func decode(_ decodable: D.Type, from body: ByteBuffer, headers: HTTPHeaders, userInfo: [CodingUserInfoKey: Sendable]) throws -> D where D: Decodable { try self.decode(decodable, from: body, headers: headers) diff --git a/Sources/Vapor/Content/ContentContainer.swift b/Sources/Vapor/Content/ContentContainer.swift index 3c730418c7..e0016be3c0 100644 --- a/Sources/Vapor/Content/ContentContainer.swift +++ b/Sources/Vapor/Content/ContentContainer.swift @@ -131,12 +131,12 @@ extension ContentContainer { /// Injects coder userInfo into a ``ContentDecoder`` so we don't have to add passthroughs to ``ContentContainer``. fileprivate struct ForwardingContentDecoder: ContentDecoder { - let base: ContentDecoder, info: [CodingUserInfoKey: Any] + let base: ContentDecoder, info: [CodingUserInfoKey: Sendable] func decode(_: D.Type, from body: ByteBuffer, headers: HTTPHeaders) throws -> D { try self.base.decode(D.self, from: body, headers: headers, userInfo: self.info) } - func decode(_: D.Type, from body: ByteBuffer, headers: HTTPHeaders, userInfo: [CodingUserInfoKey: Any]) throws -> D { + func decode(_: D.Type, from body: ByteBuffer, headers: HTTPHeaders, userInfo: [CodingUserInfoKey: Sendable]) throws -> D { try self.base.decode(D.self, from: body, headers: headers, userInfo: userInfo.merging(self.info) { $1 }) } } diff --git a/Sources/Vapor/Content/JSONCoders+Content.swift b/Sources/Vapor/Content/JSONCoders+Content.swift index e0f034b2f3..0350c09636 100644 --- a/Sources/Vapor/Content/JSONCoders+Content.swift +++ b/Sources/Vapor/Content/JSONCoders+Content.swift @@ -9,7 +9,7 @@ extension JSONEncoder: ContentEncoder { try self.encode(encodable, to: &body, headers: &headers, userInfo: [:]) } - public func encode(_ encodable: E, to body: inout ByteBuffer, headers: inout HTTPHeaders, userInfo: [CodingUserInfoKey: Any]) throws + public func encode(_ encodable: E, to body: inout ByteBuffer, headers: inout HTTPHeaders, userInfo: [CodingUserInfoKey: Sendable]) throws where E: Encodable { headers.contentType = .json @@ -36,7 +36,7 @@ extension JSONDecoder: ContentDecoder { try self.decode(D.self, from: body, headers: headers, userInfo: [:]) } - public func decode(_ decodable: D.Type, from body: ByteBuffer, headers: HTTPHeaders, userInfo: [CodingUserInfoKey: Any]) throws -> D + public func decode(_ decodable: D.Type, from body: ByteBuffer, headers: HTTPHeaders, userInfo: [CodingUserInfoKey: Sendable]) throws -> D where D: Decodable { let data = body.getData(at: body.readerIndex, length: body.readableBytes) ?? Data() diff --git a/Sources/Vapor/Content/PlaintextDecoder.swift b/Sources/Vapor/Content/PlaintextDecoder.swift index 0e0c7a1411..c6521adb0d 100644 --- a/Sources/Vapor/Content/PlaintextDecoder.swift +++ b/Sources/Vapor/Content/PlaintextDecoder.swift @@ -13,7 +13,7 @@ public struct PlaintextDecoder: ContentDecoder { } /// `ContentDecoder` conformance. - public func decode(_ decodable: D.Type, from body: ByteBuffer, headers: HTTPHeaders, userInfo: [CodingUserInfoKey: Any]) throws -> D + public func decode(_ decodable: D.Type, from body: ByteBuffer, headers: HTTPHeaders, userInfo: [CodingUserInfoKey: Sendable]) throws -> D where D : Decodable { let string = body.getString(at: body.readerIndex, length: body.readableBytes) @@ -29,7 +29,7 @@ private final class _PlaintextDecoder: Decoder, SingleValueDecodingContainer { let userInfo: [CodingUserInfoKey: Any] let plaintext: String? - init(plaintext: String?, userInfo: [CodingUserInfoKey: Any] = [:]) { + init(plaintext: String?, userInfo: [CodingUserInfoKey: Sendable] = [:]) { self.plaintext = plaintext self.userInfo = userInfo } diff --git a/Sources/Vapor/Content/PlaintextEncoder.swift b/Sources/Vapor/Content/PlaintextEncoder.swift index eb567e99c8..266e82ab91 100644 --- a/Sources/Vapor/Content/PlaintextEncoder.swift +++ b/Sources/Vapor/Content/PlaintextEncoder.swift @@ -26,12 +26,12 @@ public struct PlaintextEncoder: ContentEncoder { try self.encode(encodable, to: &body, headers: &headers, userInfo: [:]) } - public func encode(_ encodable: E, to body: inout ByteBuffer, headers: inout HTTPHeaders, userInfo: [CodingUserInfoKey: Any]) throws + public func encode(_ encodable: E, to body: inout ByteBuffer, headers: inout HTTPHeaders, userInfo: [CodingUserInfoKey: Sendable]) throws where E: Encodable { let actualEncoder: _PlaintextEncoder if !userInfo.isEmpty { // Changing a coder's userInfo is a thread-unsafe mutation, operate on a copy - actualEncoder = _PlaintextEncoder(userInfo: self.encoder.userInfo.merging(userInfo) { $1 }) + actualEncoder = _PlaintextEncoder(userInfo: self.encoder.userInfoSendable.merging(userInfo) { $1 }) } else { actualEncoder = self.encoder } @@ -51,10 +51,11 @@ public struct PlaintextEncoder: ContentEncoder { private final class _PlaintextEncoder: Encoder, SingleValueEncodingContainer { public var codingPath: [CodingKey] = [] - public var userInfo: [CodingUserInfoKey: Any] + fileprivate var userInfoSendable: [CodingUserInfoKey: Sendable] + public var userInfo: [CodingUserInfoKey: Any] { self.userInfoSendable } public var plaintext: String? - public init(userInfo: [CodingUserInfoKey: Any] = [:]) { self.userInfo = userInfo } + public init(userInfo: [CodingUserInfoKey: Sendable] = [:]) { self.userInfoSendable = userInfo } public func container(keyedBy type: Key.Type) -> KeyedEncodingContainer { .init(FailureEncoder()) } public func unkeyedContainer() -> UnkeyedEncodingContainer { FailureEncoder() } diff --git a/Sources/Vapor/Content/URLQueryCoders.swift b/Sources/Vapor/Content/URLQueryCoders.swift index 18ee7e7a13..bf995a258f 100644 --- a/Sources/Vapor/Content/URLQueryCoders.swift +++ b/Sources/Vapor/Content/URLQueryCoders.swift @@ -2,7 +2,7 @@ public protocol URLQueryDecoder { func decode(_ decodable: D.Type, from url: URI) throws -> D where D: Decodable - func decode(_ decodable: D.Type, from url: URI, userInfo: [CodingUserInfoKey: Any]) throws -> D + func decode(_ decodable: D.Type, from url: URI, userInfo: [CodingUserInfoKey: Sendable]) throws -> D where D: Decodable } @@ -10,12 +10,12 @@ public protocol URLQueryEncoder { func encode(_ encodable: E, to url: inout URI) throws where E: Encodable - func encode(_ encodable: E, to url: inout URI, userInfo: [CodingUserInfoKey: Any]) throws + func encode(_ encodable: E, to url: inout URI, userInfo: [CodingUserInfoKey: Sendable]) throws where E: Encodable } extension URLQueryEncoder { - public func encode(_ encodable: E, to url: inout URI, userInfo: [CodingUserInfoKey: Any]) throws + public func encode(_ encodable: E, to url: inout URI, userInfo: [CodingUserInfoKey: Sendable]) throws where E: Encodable { try self.encode(encodable, to: &url) @@ -23,7 +23,7 @@ extension URLQueryEncoder { } extension URLQueryDecoder { - public func decode(_ decodable: D.Type, from url: URI, userInfo: [CodingUserInfoKey: Any]) throws -> D + public func decode(_ decodable: D.Type, from url: URI, userInfo: [CodingUserInfoKey: Sendable]) throws -> D where D: Decodable { try self.decode(decodable, from: url) diff --git a/Sources/Vapor/Content/URLQueryContainer.swift b/Sources/Vapor/Content/URLQueryContainer.swift index c96e89efdd..b50f32e984 100644 --- a/Sources/Vapor/Content/URLQueryContainer.swift +++ b/Sources/Vapor/Content/URLQueryContainer.swift @@ -98,10 +98,10 @@ extension URLQueryContainer { /// Injects coder userInfo into a ``URLQueryDecoder`` so we don't have to add passthroughs to ``URLQueryContainer``. fileprivate struct ForwardingURLQueryDecoder: URLQueryDecoder { - let base: URLQueryDecoder, info: [CodingUserInfoKey: Any] + let base: URLQueryDecoder, info: [CodingUserInfoKey: Sendable] func decode(_: D.Type, from url: URI) throws -> D { try self.base.decode(D.self, from: url, userInfo: self.info) } - func decode(_: D.Type, from url: URI, userInfo: [CodingUserInfoKey: Any]) throws -> D { + func decode(_: D.Type, from url: URI, userInfo: [CodingUserInfoKey: Sendable]) throws -> D { try self.base.decode(D.self, from: url, userInfo: userInfo.merging(self.info) { $1 }) } } diff --git a/Sources/Vapor/Multipart/FormDataDecoder+Content.swift b/Sources/Vapor/Multipart/FormDataDecoder+Content.swift index 5e41e2b815..99fc707e28 100644 --- a/Sources/Vapor/Multipart/FormDataDecoder+Content.swift +++ b/Sources/Vapor/Multipart/FormDataDecoder+Content.swift @@ -9,7 +9,7 @@ extension FormDataDecoder: ContentDecoder { try self.decode(D.self, from: body, headers: headers, userInfo: [:]) } - public func decode(_ decodable: D.Type, from body: ByteBuffer, headers: HTTPHeaders, userInfo: [CodingUserInfoKey: Any]) throws -> D + public func decode(_ decodable: D.Type, from body: ByteBuffer, headers: HTTPHeaders, userInfo: [CodingUserInfoKey: Sendable]) throws -> D where D: Decodable { guard let boundary = headers.contentType?.parameters["boundary"] else { diff --git a/Sources/Vapor/Multipart/FormDataEncoder+Content.swift b/Sources/Vapor/Multipart/FormDataEncoder+Content.swift index 2e4c49364d..70db701b95 100644 --- a/Sources/Vapor/Multipart/FormDataEncoder+Content.swift +++ b/Sources/Vapor/Multipart/FormDataEncoder+Content.swift @@ -3,19 +3,17 @@ import NIOHTTP1 import NIOCore extension FormDataEncoder: ContentEncoder { - public func encode(_ encodable: E, to body: inout ByteBuffer, headers: inout HTTPHeaders) throws - where E: Encodable - { + public func encode(_ encodable: E, to body: inout ByteBuffer, headers: inout HTTPHeaders) throws { try self.encode(encodable, to: &body, headers: &headers, userInfo: [:]) } - public func encode(_ encodable: E, to body: inout ByteBuffer, headers: inout HTTPHeaders, userInfo: [CodingUserInfoKey: Any]) throws - where E: Encodable - { + public func encode(_ encodable: E, to body: inout ByteBuffer, headers: inout HTTPHeaders, userInfo: [CodingUserInfoKey: Sendable]) throws { let boundary = "----vaporBoundary\(randomBoundaryData())" + headers.contentType = HTTPMediaType(type: "multipart", subType: "form-data", parameters: ["boundary": boundary]) if !userInfo.isEmpty { var actualEncoder = self // Changing a coder's userInfo is a thread-unsafe mutation, operate on a copy + actualEncoder.userInfo.merge(userInfo) { $1 } return try actualEncoder.encode(encodable, boundary: boundary, into: &body) } else { diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift index 41c9c75125..a54e81dc5c 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift @@ -2,50 +2,59 @@ import NIOCore import Foundation import NIOHTTP1 -/// Decodes instances of `Decodable` types from `application/x-www-form-urlencoded` `Data`. +/// Decodes instances of `Decodable` types from `application/x-www-form-urlencoded` data. /// -/// print(data) // "name=Vapor&age=3" -/// let user = try URLEncodedFormDecoder().decode(User.self, from: data) -/// print(user) // User +/// ```swift +/// print(data) // "name=Vapor&age=3" +/// let user = try URLEncodedFormDecoder().decode(User.self, from: data) +/// print(user) // User +/// ``` /// /// URL-encoded forms are commonly used by websites to send form data via POST requests. This encoding is relatively -/// efficient for small amounts of data but must be percent-encoded. `multipart/form-data` is more efficient for sending -/// large data blobs like files. +/// efficient for small amounts of data but must be percent-encoded. `multipart/form-data` is more efficient for +/// sending larger data blobs like files, and `application/json` encoding has become increasingly common. /// -/// See [Mozilla's](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST) docs for more information about -/// url-encoded forms. +/// See [the offical WhatWG URL standard](https://url.spec.whatwg.org/#application/x-www-form-urlencoded) for more +/// information about the "URL-encoded WWW form" format. public struct URLEncodedFormDecoder: ContentDecoder, URLQueryDecoder { - /// Used to capture URLForm Coding Configuration used for decoding + /// Ecapsulates configuration options for URL-encoded form decoding. public struct Configuration { /// Supported date formats public enum DateDecodingStrategy { - /// Seconds since 1 January 1970 00:00:00 UTC (Unix Timestamp) + /// Decodes integer or floating-point values expressed as seconds since the UNIX + /// epoch (`1970-01-01 00:00:00.000Z`). case secondsSince1970 - /// ISO 8601 formatted date + + /// Decodes ISO-8601 formatted date strings. case iso8601 - /// Using custom callback + + /// Invokes a custom callback to decode values when a date is requested. case custom((Decoder) throws -> Date) } let boolFlags: Bool let arraySeparators: [Character] let dateDecodingStrategy: DateDecodingStrategy - let userInfo: [CodingUserInfoKey: Any] + let userInfo: [CodingUserInfoKey: Sendable] - /// Creates a new `URLEncodedFormCodingConfiguration`. - /// - parameters: - /// - boolFlags: Set to `true` allows you to parse `flag1&flag2` as boolean variables - /// where object with variable `flag1` and `flag2` would decode to `true` - /// or `false` depending on if the value was present or not. If this flag is set to - /// true, it will always resolve for an optional `Bool`. - /// - arraySeparators: Uses these characters to decode arrays. If set to `,`, `arr=v1,v2` would - /// populate a key named `arr` of type `Array` to be decoded as `["v1", "v2"]` - /// - dateDecodingStrategy: Date format used to decode a date. Date formats are tried in the order provided + /// Creates a new ``URLEncodedFormDecoder/Configuration``. + /// + /// - Parameters: + /// - boolFlags: When `true`, form data such as `flag1&flag2` will be interpreted as boolean flags, where + /// the resulting value is true if the flag name exists and false if it does not. When `false`, such data + /// is interpreted as keys having no values. + /// - arraySeparators: A set of characters to be treated as value separators for array values. For example, + /// using the default of `[",", "|"]`, both `arr=v1,v2` and `arr=v1|v2` are decoded as an array named `arr` + /// with the two values `v1` and `v2`. + /// - dateDecodingStrategy: The ``URLEncodedFormDecoder/Configuration/DateDecodingStrategy`` to use for + /// date decoding. + /// - userInfo: Additional and/or overriding user info keys for the underlying `Decoder` (you probably + /// don't need this). public init( boolFlags: Bool = true, arraySeparators: [Character] = [",", "|"], dateDecodingStrategy: DateDecodingStrategy = .secondsSince1970, - userInfo: [CodingUserInfoKey: Any] = [:] + userInfo: [CodingUserInfoKey: Sendable] = [:] ) { self.boolFlags = boolFlags self.arraySeparators = arraySeparators @@ -54,87 +63,117 @@ public struct URLEncodedFormDecoder: ContentDecoder, URLQueryDecoder { } } - - /// The underlying `URLEncodedFormEncodedParser` + /// The underlying ``URLEncodedFormParser``. private let parser: URLEncodedFormParser + /// The decoder's configuration. private let configuration: Configuration - /// Create a new `URLEncodedFormDecoder`. Can be configured by using the global `ContentConfiguration` class + /// Create a new ``URLEncodedFormDecoder``. /// - /// ContentConfiguration.global.use(urlDecoder: URLEncodedFormDecoder(bracketsAsArray: true, flagsAsBool: true, arraySeparator: nil)) + /// Typically configured via the global ``ContentConfiguration`` class: /// - /// - parameters: - /// - configuration: Defines how decoding is done see `URLEncodedFormCodingConfig` for more information - public init( - configuration: Configuration = .init() - ) { + /// ```swift + /// ContentConfiguration.global.use(urlDecoder: URLEncodedFormDecoder( + /// bracketsAsArray: true, + /// flagsAsBool: true, + /// arraySeparator: nil + /// )) + /// ``` + /// + /// - Parameter configuration: A ``URLEncodedFormDecoder/Configuration`` specifying the decoder's behavior. + public init(configuration: Configuration = .init()) { self.parser = URLEncodedFormParser() self.configuration = configuration } - /// ``ContentDecoder`` conformance. - public func decode(_ decodable: D.Type, from body: ByteBuffer, headers: HTTPHeaders) throws -> D - where D: Decodable - { + // See `ContentDecoder.decode(_:from:headers:)`. + public func decode(_: D.Type, from body: ByteBuffer, headers: HTTPHeaders) throws -> D { try self.decode(D.self, from: body, headers: headers, userInfo: [:]) } - /// ``ContentDecoder`` conformance. - public func decode(_ decodable: D.Type, from body: ByteBuffer, headers: HTTPHeaders, userInfo: [CodingUserInfoKey: Any]) throws -> D - where D: Decodable - { + // See `ContentDecoder.decode(_:from:headers:userInfo:)`. + public func decode(_: D.Type, from body: ByteBuffer, headers: HTTPHeaders, userInfo: [CodingUserInfoKey: Sendable]) throws -> D { guard headers.contentType == .urlEncodedForm else { throw Abort(.unsupportedMediaType) } + let string = body.getString(at: body.readerIndex, length: body.readableBytes) ?? "" + return try self.decode(D.self, from: string, userInfo: userInfo) } - /// Decodes the URL's query string to the type provided - /// - /// let ziz = try URLEncodedFormDecoder().decode(Pet.self, from: "name=Ziz&type=cat") - /// - /// - Parameters: - /// - decodable: Type to decode to - /// - url: ``URI`` to read the query string from - public func decode(_ decodable: D.Type, from url: URI) throws -> D where D : Decodable { + // See `URLQueryDecoder.decode(_:from:)`. + public func decode(_: D.Type, from url: URI) throws -> D { try self.decode(D.self, from: url, userInfo: [:]) } - /// Decodes the URL's query string to the type provided + // See `URLQueryDecoder.decode(_:from:userInfo:)`. + public func decode(_: D.Type, from url: URI, userInfo: [CodingUserInfoKey: Sendable]) throws -> D { + try self.decode(D.self, from: url.query ?? "", userInfo: userInfo) + } + + /// Decodes an instance of the supplied `Decodable` type from a `String`. /// - /// let ziz = try URLEncodedFormDecoder().decode(Pet.self, from: "name=Ziz&type=cat") + /// ```swift + /// print(data) // "name=Vapor&age=3" + /// let user = try URLEncodedFormDecoder().decode(User.self, from: data) + /// print(user) // User + /// ``` /// /// - Parameters: - /// - decodable: Type to decode to - /// - url: ``URI`` to read the query string from - /// - userInfo: Overrides the default coder user info - public func decode(_ decodable: D.Type, from url: URI, userInfo: [CodingUserInfoKey: Any]) throws -> D where D : Decodable { - try self.decode(D.self, from: url.query ?? "", userInfo: userInfo) + /// - decodable: A `Decodable` type `D` to decode. + /// - string: String to decode a `D` from. + /// - Returns: An instance of `D`. + /// - Throws: Any error that may occur while attempting to decode the specified type. + public func decode(_: D.Type, from string: String) throws -> D { + /// This overload did not previously exist; instead, the much more obvious approach of defaulting the + /// `userInfo` argument of ``decode(_:from:userInfo:)-6h3y5`` was taken. Unfortunately, this resulted + /// in the compiler calling ``decode(_:from:)-7fve9`` via ``URI``'s conformance to + /// `ExpressibleByStringInterpolation` preferentially when a caller did not provide their own user info (so, + /// always). This, completely accidentally, did the "right thing" in the past thanks to a quirk of the + /// ancient and badly broken C-based URI parser. That parser no longer being in use, it is now necessary to + /// provide the explicit overload to convince the compiler to do the right thing. (`@_disfavoredOverload` was + /// considered and rejected as an alternative option - using it caused an infinite loop between + /// ``decode(_:from:userInfo:)-893nd`` and ``URLQueryDecoder/decode(_:from:)`` when built on Linux. + /// + /// None of this, of course, was in any way whatsoever confusing in the slightest. Indeed, Tanner's choice to + /// makie ``URI`` `ExpressibleByStringInterpolation` (and, for that matter, `ExpressibleByStringLiteral`) + /// back in 2019 was unquestionably just, just a truly _awesome_ and _inspired_ design decision 🤥. + try self.decode(D.self, from: string, userInfo: [:]) } - /// Decodes an instance of the supplied ``Decodable`` type from a ``String``. + /// Decodes an instance of the supplied `Decodable` type from a `String`. /// - /// print(data) // "name=Vapor&age=3" - /// let user = try URLEncodedFormDecoder().decode(User.self, from: data) - /// print(user) // User + /// ```swift + /// print(data) // "name=Vapor&age=3" + /// let user = try URLEncodedFormDecoder().decode(User.self, from: data, userInfo: [...]) + /// print(user) // User + /// ``` /// /// - Parameters: - /// - decodable: Generic ``Decodable`` type (``D``) to decode. - /// - string: String to decode a ``D`` from. - /// - userInfo: Overrides the default coder user info - /// - returns: An instance of the `Decodable` type (``D``). - /// - throws: Any error that may occur while attempting to decode the specified type. - public func decode(_ decodable: D.Type, from string: String, userInfo: [CodingUserInfoKey: Any] = [:]) throws -> D where D : Decodable { - let parsedData = try self.parser.parse(string) + /// - decodable: A `Decodable` type `D` to decode. + /// - string: String to decode a `D` from. + /// - userInfo: Overrides and/or augments the default coder user info. + /// - Returns: An instance of `D`. + /// - Throws: Any error that may occur while attempting to decode the specified type. + public func decode(_: D.Type, from string: String, userInfo: [CodingUserInfoKey: Sendable]) throws -> D { let configuration: URLEncodedFormDecoder.Configuration + if !userInfo.isEmpty { // Changing a coder's userInfo is a thread-unsafe mutation, operate on a copy - configuration = .init(boolFlags: self.configuration.boolFlags, arraySeparators: self.configuration.arraySeparators, dateDecodingStrategy: self.configuration.dateDecodingStrategy, userInfo: self.configuration.userInfo.merging(userInfo) { $1 }) + configuration = .init( + boolFlags: self.configuration.boolFlags, + arraySeparators: self.configuration.arraySeparators, + dateDecodingStrategy: self.configuration.dateDecodingStrategy, + userInfo: self.configuration.userInfo.merging(userInfo) { $1 } + ) } else { configuration = self.configuration } + + let parsedData = try self.parser.parse(string) let decoder = _Decoder(data: parsedData, codingPath: [], configuration: configuration) + return try D(from: decoder) } } @@ -144,38 +183,36 @@ public struct URLEncodedFormDecoder: ContentDecoder, URLQueryDecoder { /// Private `Decoder`. See `URLEncodedFormDecoder` for public decoder. private struct _Decoder: Decoder { var data: URLEncodedFormData - var codingPath: [CodingKey] var configuration: URLEncodedFormDecoder.Configuration - /// See `Decoder` + // See `Decoder.codingPath` + var codingPath: [CodingKey] + + // See `Decoder.userInfo` var userInfo: [CodingUserInfoKey: Any] { self.configuration.userInfo } - /// Creates a new `_URLEncodedFormDecoder`. + /// Creates a new `_Decoder`. init(data: URLEncodedFormData, codingPath: [CodingKey], configuration: URLEncodedFormDecoder.Configuration) { self.data = data self.codingPath = codingPath self.configuration = configuration } - func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer - where Key: CodingKey - { - return KeyedDecodingContainer(KeyedContainer( - data: data, + func container(keyedBy: Key.Type) throws -> KeyedDecodingContainer { + .init(KeyedContainer( + data: self.data, codingPath: self.codingPath, - configuration: configuration + configuration: self.configuration )) } - struct KeyedContainer: KeyedDecodingContainerProtocol - where Key: CodingKey - { + struct KeyedContainer: KeyedDecodingContainerProtocol { let data: URLEncodedFormData var codingPath: [CodingKey] var configuration: URLEncodedFormDecoder.Configuration var allKeys: [Key] { - return self.data.children.keys.compactMap { Key(stringValue: String($0)) } + self.data.children.keys.compactMap { Key(stringValue: String($0)) } } init( @@ -189,83 +226,80 @@ private struct _Decoder: Decoder { } func contains(_ key: Key) -> Bool { - return self.data.children[key.stringValue] != nil + self.data.children[key.stringValue] != nil } func decodeNil(forKey key: Key) throws -> Bool { - return self.data.children[key.stringValue] == nil + self.data.children[key.stringValue] == nil } - private func decodeDate(forKey key: Key) throws -> Date { - //If we are trying to decode a required array, we might not have decoded a child, but we should still try to decode an empty array - let child = self.data.children[key.stringValue] ?? [] - return try configuration.decodeDate(from: child, codingPath: self.codingPath, forKey: key) + private func decodeDate(forKey key: Key, child: URLEncodedFormData) throws -> Date { + try configuration.decodeDate(from: child, codingPath: self.codingPath, forKey: key) } - func decode(_ type: T.Type, forKey key: Key) throws -> T where T: Decodable { - //Check if we received a date. We need the decode with the appropriate format - guard !(T.self is Date.Type) else { - return try decodeDate(forKey: key) as! T - } - //If we are trying to decode a required array, we might not have decoded a child, but we should still try to decode an empty array + func decode(_: T.Type, forKey key: Key) throws -> T { + // If we are trying to decode a required array, we might not have decoded a child, but we should + // still try to decode an empty array let child = self.data.children[key.stringValue] ?? [] - if let convertible = T.self as? URLQueryFragmentConvertible.Type { - guard let value = child.values.last else { - if self.configuration.boolFlags { - //If no values found see if we are decoding a boolean - if let _ = T.self as? Bool.Type { - return self.data.values.contains(.urlDecoded(key.stringValue)) as! T - } + + // If decoding a date, we need to apply the configured date decoding strategy. + if T.self is Date.Type { + return try self.decodeDate(forKey: key, child: child) as! T + } else if let convertible = T.self as? URLQueryFragmentConvertible.Type { + switch child.values.last { + case let value?: + guard let result = convertible.init(urlQueryFragmentValue: value) else { + throw DecodingError.typeMismatch(T.self, at: self.codingPath + [key]) } - throw DecodingError.valueNotFound(T.self, at: self.codingPath + [key]) - } - if let result = convertible.init(urlQueryFragmentValue: value) { return result as! T - } else { - throw DecodingError.typeMismatch(T.self, at: self.codingPath + [key]) + case nil where self.configuration.boolFlags && T.self is Bool.Type: + // If there's no value, but flags are enabled and a Bool was requested, treat it as a flag. + return self.data.values.contains(.urlDecoded(key.stringValue)) as! T + default: + throw DecodingError.valueNotFound(T.self, at: self.codingPath + [key]) } } else { - let decoder = _Decoder(data: child, codingPath: self.codingPath + [key], configuration: configuration) - return try T(from: decoder) + let decoder = _Decoder(data: child, codingPath: self.codingPath + [key], configuration: self.configuration) + + return try T.init(from: decoder) } } - func nestedContainer( - keyedBy type: NestedKey.Type, - forKey key: Key - ) throws -> KeyedDecodingContainer - where NestedKey: CodingKey - { - let child = self.data.children[key.stringValue] ?? [] - - return KeyedDecodingContainer(KeyedContainer(data: child, codingPath: self.codingPath + [key], configuration: configuration)) + func nestedContainer(keyedBy: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer { + .init(KeyedContainer( + data: self.data.children[key.stringValue] ?? [], + codingPath: self.codingPath + [key], + configuration: self.configuration + )) } func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer { - let child = self.data.children[key.stringValue] ?? [] - - return try UnkeyedContainer( - data: child, + try UnkeyedContainer( + data: self.data.children[key.stringValue] ?? [], codingPath: self.codingPath + [key], - configuration: configuration + configuration: self.configuration ) } func superDecoder() throws -> Decoder { - let child = self.data.children["super"] ?? [] - - return _Decoder(data: child, codingPath: self.codingPath + [BasicCodingKey.key("super")], configuration: self.configuration) + _Decoder( + data: self.data.children["super"] ?? [], + codingPath: self.codingPath + [BasicCodingKey.key("super")], + configuration: self.configuration + ) } func superDecoder(forKey key: Key) throws -> Decoder { - let child = self.data.children[key.stringValue] ?? [] - - return _Decoder(data: child, codingPath: self.codingPath + [key], configuration: self.configuration) + _Decoder( + data: self.data.children[key.stringValue] ?? [], + codingPath: self.codingPath + [key], + configuration: self.configuration + ) } } func unkeyedContainer() throws -> UnkeyedDecodingContainer { - return try UnkeyedContainer(data: data, codingPath: codingPath, configuration: configuration) + try UnkeyedContainer(data: self.data, codingPath: self.codingPath, configuration: self.configuration) } struct UnkeyedContainer: UnkeyedDecodingContainer { @@ -276,19 +310,22 @@ private struct _Decoder: Decoder { var allChildKeysAreNumbers: Bool var count: Int? { - // Did we get an array with arr[0]=a&arr[1]=b indexing? if self.allChildKeysAreNumbers { + // Did we get an array with arr[0]=a&arr[1]=b indexing? return data.children.count + } else { + // No, we got an array with arr[]=a&arr[]=b or arr=a&arr=b + return self.values.count } - // No we got an array with arr[]=a&arr[]=b or arr=a&arr=b - return self.values.count } + var isAtEnd: Bool { guard let count = self.count else { return true } return currentIndex >= count } + var currentIndex: Int init( @@ -300,70 +337,73 @@ private struct _Decoder: Decoder { self.codingPath = codingPath self.configuration = configuration self.currentIndex = 0 - // Did we get an array with arr[0]=a&arr[1]=b indexing? - // Cache this result - self.allChildKeysAreNumbers = data.children.count > 0 && data.allChildKeysAreSequentialIntegers + // Did we get an array with arr[0]=a&arr[1]=b indexing? Cache the result. + self.allChildKeysAreNumbers = !data.children.isEmpty && data.allChildKeysAreSequentialIntegers - if allChildKeysAreNumbers { + if self.allChildKeysAreNumbers { self.values = data.values } else { - // No we got an array with arr[]=a&arr[]=b or arr=a&arr=b + // No, we got an array with arr[]=a&arr[]=b or arr=a&arr=b var values = data.values - // empty brackets turn into empty strings! + + // Empty brackets turn into empty strings if let valuesInBracket = data.children[""] { - values = values + valuesInBracket.values + values += valuesInBracket.values } - // parse out any character separated array values - self.values = try values.flatMap { value in - try value.asUrlEncoded() - .split(omittingEmptySubsequences: false, - whereSeparator: configuration.arraySeparators.contains) - .map { (ss: Substring) in - URLQueryFragment.urlEncoded(String(ss)) - } + // Parse out any character-separated array values + self.values = try values.flatMap { + try $0.asUrlEncoded() + .split(omittingEmptySubsequences: false, whereSeparator: configuration.arraySeparators.contains) + .map { .urlEncoded(.init($0)) } } } } func decodeNil() throws -> Bool { - return false + false } - mutating func decode(_ type: T.Type) throws -> T where T: Decodable { + mutating func decode(_: T.Type) throws -> T { defer { self.currentIndex += 1 } + if self.allChildKeysAreNumbers { + // We can force-unwrap because we already checked data.allChildKeysAreNumbers in the initializer. let childData = self.data.children[String(self.currentIndex)]! - //We can force an unwrap because in the constructor - // we checked data.allChildKeysAreNumbers let decoder = _Decoder( data: childData, codingPath: self.codingPath + [BasicCodingKey.index(self.currentIndex)], configuration: self.configuration ) + return try T(from: decoder) } else { let value = self.values[self.currentIndex] - // Check if we received a date. We need the decode with the appropriate format. - guard !(T.self is Date.Type) else { - return try self.configuration.decodeDate(from: value, codingPath: self.codingPath, forKey: BasicCodingKey.index(self.currentIndex)) as! T - } - if let convertible = T.self as? URLQueryFragmentConvertible.Type { - if let result = convertible.init(urlQueryFragmentValue: value) { - return result as! T - } else { + if T.self is Date.Type { + return try self.configuration.decodeDate( + from: value, + codingPath: self.codingPath, + forKey: BasicCodingKey.index(self.currentIndex) + ) as! T + } else if let convertible = T.self as? URLQueryFragmentConvertible.Type { + guard let result = convertible.init(urlQueryFragmentValue: value) else { throw DecodingError.typeMismatch(T.self, at: self.codingPath + [BasicCodingKey.index(self.currentIndex)]) } + return result as! T } else { - //We need to pass in the value to be decoded - let decoder = _Decoder(data: URLEncodedFormData(values: [value]), codingPath: self.codingPath + [BasicCodingKey.index(self.currentIndex)], configuration: self.configuration) - return try T(from: decoder) + let decoder = _Decoder( + data: URLEncodedFormData(values: [value]), + codingPath: self.codingPath + [BasicCodingKey.index(self.currentIndex)], + configuration: self.configuration + ) + + return try T.init(from: decoder) } } } - mutating func nestedContainer(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer where NestedKey: CodingKey { + mutating func nestedContainer(keyedBy: NestedKey.Type) throws -> KeyedDecodingContainer { throw DecodingError.typeMismatch([String: Decodable].self, at: self.codingPath + [BasicCodingKey.index(self.currentIndex)]) } @@ -373,13 +413,19 @@ private struct _Decoder: Decoder { mutating func superDecoder() throws -> Decoder { defer { self.currentIndex += 1 } + let data = self.allChildKeysAreNumbers ? self.data.children[self.currentIndex.description]! : .init(values: [self.values[self.currentIndex]]) - return _Decoder(data: data, codingPath: self.codingPath + [BasicCodingKey.index(self.currentIndex)], configuration: self.configuration) + + return _Decoder( + data: data, + codingPath: self.codingPath + [BasicCodingKey.index(self.currentIndex)], + configuration: self.configuration + ) } } func singleValueContainer() throws -> SingleValueDecodingContainer { - return SingleValueContainer(data: self.data, codingPath: self.codingPath, configuration: self.configuration) + SingleValueContainer(data: self.data, codingPath: self.codingPath, configuration: self.configuration) } struct SingleValueContainer: SingleValueDecodingContainer { @@ -401,22 +447,25 @@ private struct _Decoder: Decoder { self.data.values.isEmpty } - func decode(_ type: T.Type) throws -> T where T: Decodable { - // Check if we received a date. We need the decode with the appropriate format. - guard !(T.self is Date.Type) else { + func decode(_: T.Type) throws -> T { + if T.self is Date.Type { return try self.configuration.decodeDate(from: self.data, codingPath: self.codingPath, forKey: nil) as! T - } - if let convertible = T.self as? URLQueryFragmentConvertible.Type { - guard let value = self.data.values.last else { + } else if let convertible = T.self as? URLQueryFragmentConvertible.Type { + guard let value = self.data.values.last else { throw DecodingError.valueNotFound(T.self, at: self.codingPath) } - if let result = convertible.init(urlQueryFragmentValue: value) { - return result as! T - } else { + guard let result = convertible.init(urlQueryFragmentValue: value) else { throw DecodingError.typeMismatch(T.self, at: self.codingPath) } + + return result as! T } else { - let decoder = _Decoder(data: self.data, codingPath: self.codingPath, configuration: self.configuration) + let decoder = _Decoder( + data: self.data, + codingPath: self.codingPath, + configuration: self.configuration + ) + return try T(from: decoder) } } @@ -426,26 +475,28 @@ private struct _Decoder: Decoder { private extension URLEncodedFormDecoder.Configuration { func decodeDate(from data: URLEncodedFormData, codingPath: [CodingKey], forKey key: CodingKey?) throws -> Date { let newCodingPath = codingPath + (key.map { [$0] } ?? []) - switch dateDecodingStrategy { + + switch self.dateDecodingStrategy { case .secondsSince1970: guard let value = data.values.last else { throw DecodingError.valueNotFound(Date.self, at: newCodingPath) } - if let result = Date.init(urlQueryFragmentValue: value) { - return result - } else { + guard let result = Date(urlQueryFragmentValue: value) else { throw DecodingError.typeMismatch(Date.self, at: newCodingPath) } + + return result case .iso8601: let decoder = _Decoder(data: data, codingPath: newCodingPath, configuration: self) let container = try decoder.singleValueContainer() - if let date = ISO8601DateFormatter.threadSpecific.date(from: try container.decode(String.self)) { - return date - } else { - throw DecodingError.dataCorrupted(.init(codingPath: newCodingPath, debugDescription: "Unable to decode date. Expecting ISO8601 formatted date")) + + guard let date = ISO8601DateFormatter.threadSpecific.date(from: try container.decode(String.self)) else { + throw DecodingError.dataCorrupted(.init(codingPath: newCodingPath, debugDescription: "Unable to decode ISO-8601 date.")) } + return date case .custom(let callback): let decoder = _Decoder(data: data, codingPath: newCodingPath, configuration: self) + return try callback(decoder) } } @@ -457,20 +508,22 @@ private extension URLEncodedFormDecoder.Configuration { private extension DecodingError { static func typeMismatch(_ type: Any.Type, at path: [CodingKey]) -> DecodingError { - let pathString = path.map { $0.stringValue }.joined(separator: ".") + let pathString = path.map(\.stringValue).joined(separator: ".") let context = DecodingError.Context( codingPath: path, debugDescription: "Data found at '\(pathString)' was not \(type)" ) - return Swift.DecodingError.typeMismatch(type, context) + + return .typeMismatch(type, context) } static func valueNotFound(_ type: Any.Type, at path: [CodingKey]) -> DecodingError { - let pathString = path.map { $0.stringValue }.joined(separator: ".") + let pathString = path.map(\.stringValue).joined(separator: ".") let context = DecodingError.Context( codingPath: path, debugDescription: "No \(type) was found at '\(pathString)'" ) - return Swift.DecodingError.valueNotFound(type, context) + + return .valueNotFound(type, context) } } diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift index 2a1ac07447..c132754276 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift @@ -43,7 +43,7 @@ public struct URLEncodedFormEncoder: ContentEncoder, URLQueryEncoder { /// Specified array encoding. public var arrayEncoding: ArrayEncoding public var dateEncodingStrategy: DateEncodingStrategy - public var userInfo: [CodingUserInfoKey: Any] + public var userInfo: [CodingUserInfoKey: Sendable] /// Creates a new `Configuration`. /// @@ -53,7 +53,7 @@ public struct URLEncodedFormEncoder: ContentEncoder, URLQueryEncoder { public init( arrayEncoding: ArrayEncoding = .bracket, dateEncodingStrategy: DateEncodingStrategy = .secondsSince1970, - userInfo: [CodingUserInfoKey: Any] = [:] + userInfo: [CodingUserInfoKey: Sendable] = [:] ) { self.arrayEncoding = arrayEncoding self.dateEncodingStrategy = dateEncodingStrategy @@ -81,7 +81,7 @@ public struct URLEncodedFormEncoder: ContentEncoder, URLQueryEncoder { } /// ``ContentEncoder`` conformance. - public func encode(_ encodable: E, to body: inout ByteBuffer, headers: inout HTTPHeaders, userInfo: [CodingUserInfoKey: Any]) throws + public func encode(_ encodable: E, to body: inout ByteBuffer, headers: inout HTTPHeaders, userInfo: [CodingUserInfoKey: Sendable]) throws where E: Encodable { headers.contentType = .urlEncodedForm @@ -96,7 +96,7 @@ public struct URLEncodedFormEncoder: ContentEncoder, URLQueryEncoder { } /// ``URLQueryEncoder`` conformance. - public func encode(_ encodable: E, to url: inout URI, userInfo: [CodingUserInfoKey: Any]) throws + public func encode(_ encodable: E, to url: inout URI, userInfo: [CodingUserInfoKey: Sendable]) throws where E: Encodable { url.query = try self.encode(encodable, userInfo: userInfo) @@ -113,7 +113,7 @@ public struct URLEncodedFormEncoder: ContentEncoder, URLQueryEncoder { /// - userInfo: Overrides the default coder user info. /// - Returns: Encoded ``String`` /// - Throws: Any error that may occur while attempting to encode the specified type. - public func encode(_ encodable: E, userInfo: [CodingUserInfoKey: Any] = [:]) throws -> String + public func encode(_ encodable: E, userInfo: [CodingUserInfoKey: Sendable] = [:]) throws -> String where E: Encodable { var configuration = self.configuration // Changing a coder's userInfo is a thread-unsafe mutation, operate on a copy diff --git a/Sources/Vapor/Utilities/URI.swift b/Sources/Vapor/Utilities/URI.swift index 42e1b01335..ba206bb6e6 100644 --- a/Sources/Vapor/Utilities/URI.swift +++ b/Sources/Vapor/Utilities/URI.swift @@ -4,6 +4,8 @@ import struct Foundation.URLComponents #endif +// MARK: - URI + /// A type for constructing and manipulating (most) Uniform Resource Indicators. /// /// > Warning: This is **NOT** the same as Foundation's [`URL`] type! @@ -29,17 +31,25 @@ import struct Foundation.URLComponents /// [`swift-foundation`]: https://github.com/apple/swift-foundation /// [`URL`]: https://developer.apple.com/documentation/foundation/url /// [`URLComponents`]: https://developer.apple.com/documentation/foundation/urlcomponents -public struct URI: Sendable, ExpressibleByStringInterpolation, CustomStringConvertible { +public struct URI: CustomStringConvertible, ExpressibleByStringInterpolation, Hashable, Codable, Sendable { private var components: URLComponents? + public init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + let string = try container.decode(String.self) + + self.init(string: string) + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(self.string) + } + public init(string: String = "/") { self.components = URL(string: string).flatMap { .init(url: $0, resolvingAgainstBaseURL: true) } } - public var description: String { - self.string - } - public init( scheme: String?, userinfo: String?, @@ -92,7 +102,11 @@ public struct URI: Sendable, ExpressibleByStringInterpolation, CustomStringConve if scheme.value == nil, userinfo == nil, host == nil, port == nil, query == nil, fragment == nil { // If only a path is given, treat it as a string to parse. (This behavior is awful, but must be kept for compatibility.) - components = URL(string: path).flatMap { .init(url: $0, resolvingAgainstBaseURL: true) } + // In order to do this in a fully compatible way (where in this case "compatible" means "being stuck with + // systematic misuse of both the URI type and concept"), we must collapse any non-zero number of + // leading `/` characters into a single character (thus breaking the ability to parse what is otherwise a + // valid URI format according to spec) to avoid weird routing misbehaviors. + components = URL(string: "/\(path.drop(while: { $0 == "/" }))").flatMap { .init(url: $0, resolvingAgainstBaseURL: true) } } else { // N.B.: We perform percent encoding manually and unconditionally on each non-nil component because the // behavior of URLComponents is completely different on Linux than on macOS for inputs which are already @@ -131,30 +145,26 @@ public struct URI: Sendable, ExpressibleByStringInterpolation, CustomStringConve self.components = components } - public init(stringLiteral value: String) { - self.init(string: value) - } - public var scheme: String? { get { self.components?.scheme } set { self.components?.scheme = newValue } } public var userinfo: String? { - get { self.components?.user.map { "\($0)\(self.components?.password.map { ":\($0)" } ?? "")" } } + get { self.components?.percentEncodedUser.map { "\($0)\(self.components?.percentEncodedPassword.map { ":\($0)" } ?? "")" } } set { if let userinfoData = newValue?.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false) { - self.components?.user = .init(userinfoData[0]) - self.components?.password = userinfoData.count > 1 ? .init(userinfoData[1]) : nil + self.components?.percentEncodedUser = .init(userinfoData[0]) + self.components?.percentEncodedPassword = userinfoData.count > 1 ? .init(userinfoData[1]) : nil } else { - self.components?.user = nil + self.components?.percentEncodedUser = nil } } } public var host: String? { - get { self.components?.host } - set { self.components?.host = newValue } + get { self.components?.percentEncodedHost } + set { self.components?.percentEncodedHost = newValue } } public var port: Int? { @@ -163,18 +173,18 @@ public struct URI: Sendable, ExpressibleByStringInterpolation, CustomStringConve } public var path: String { - get { self.components?.path ?? "/" } - set { self.components?.path = newValue } + get { self.components?.percentEncodedPath.replacingOccurrences(of: "%3B", with: ";", options: .literal) ?? "/" } + set { self.components?.percentEncodedPath = newValue.withAllowedUrlDelimitersEncoded } } public var query: String? { - get { self.components?.query } - set { self.components?.query = newValue } + get { self.components?.percentEncodedQuery } + set { self.components?.percentEncodedQuery = newValue?.withAllowedUrlDelimitersEncoded } } public var fragment: String? { - get { self.components?.fragment } - set { self.components?.fragment = newValue } + get { self.components?.percentEncodedFragment } + set { self.components?.percentEncodedFragment = newValue?.withAllowedUrlDelimitersEncoded } } public var string: String { @@ -186,14 +196,25 @@ public struct URI: Sendable, ExpressibleByStringInterpolation, CustomStringConve #endif } + // See `ExpressibleByStringInterpolation.init(stringLiteral:)`. + public init(stringLiteral value: String) { + self.init(string: value) + } + + // See `CustomStringConvertible.description`. + public var description: String { + self.string + } } +// MARK: - URI.Scheme + extension URI { /// A URI scheme, as defined by [RFC 3986 § 3.1] and [RFC 7595]. /// /// [RFC 3986 § 3.1]: https://datatracker.ietf.org/doc/html/rfc3986#section-3.1 /// [RGC 7595]: https://datatracker.ietf.org/doc/html/rfc7595 - public struct Scheme { + public struct Scheme: CustomStringConvertible, ExpressibleByStringInterpolation, Hashable, Codable, Sendable { /// The string representation of the scheme. public let value: String? @@ -250,21 +271,39 @@ extension URI { public static let httpsUnixDomainSocket: Self = "https+unix" // MARK: End of "well-known" schemes - + + // See `ExpressibleByStringInterpolation.init(stringLiteral:)`. + public init(stringLiteral value: String) { self.init(value) } + + // See `CustomStringConvertible.description`. + public var description: String { self.value ?? "" } } } -extension URI.Scheme: ExpressibleByStringInterpolation { - // See `ExpressibleByStringInterpolation.init(stringLiteral:)`. - public init(stringLiteral value: String) { self.init(value) } -} +// MARK: - Utilities -extension URI.Scheme: CustomStringConvertible { - // See `CustomStringConvertible.description`. - public var description: String { self.value ?? "" } +extension StringProtocol { + /// Apply percent-encoding to any unencoded instances of `[` and `]` in the string + /// + /// The `[` and `]` characters are considered "general delimiters" by [RFC 3986 § 2.2], and thus + /// part of the "reserved" set. As such, Foundation's URL handling logic rejects them if they + /// appear unencoded when setting a "percent-encoded" component. However, in practice neither + /// character presents any possible ambiguity in parsing unless it appears as part of the "authority" + /// component, and they are often used unencoded in paths. They appear even more commonly as "array" + /// syntax in query strings. As such, we need to sidestep Foundation's complaints by manually encoding + /// them when they show up. + /// + /// > Note: Fortunately, we don't have to perform the corresponding decoding when going in the other + /// > direction, as it will be taken care of by standard percent encoding logic. If this were not the + /// > case, doing this with 100% correctness would require a nontrivial amount of shadow state tracking. + /// + /// [RFC 3986 § 2.2]: https://datatracker.ietf.org/doc/html/rfc3986#section-2.2 + fileprivate var withAllowedUrlDelimitersEncoded: String { + self.replacingOccurrences(of: "[", with: "%5B", options: .literal) + .replacingOccurrences(of: "]", with: "%5D", options: .literal) + } } -extension URI.Scheme: Sendable {} - extension CharacterSet { /// The set of characters allowed in a URI scheme, as per [RFC 3986 § 3.1]. /// @@ -272,7 +311,7 @@ extension CharacterSet { fileprivate static var urlSchemeAllowed: Self { // Intersect the alphanumeric set plus additional characters with the host-allowed set to ensure // we get only ASCII codepoints in the result. - Self.urlHostAllowed.intersection(Self.alphanumerics.union(.init(charactersIn: "+-."))) + .urlHostAllowed.intersection(.alphanumerics.union(.init(charactersIn: "+-."))) } /// The set of characters allowed in a URI path, as per [RFC 3986 § 3.3]. @@ -282,10 +321,6 @@ extension CharacterSet { /// /// [RFC 3986 § 3.3]: https://datatracker.ietf.org/doc/html/rfc3986#section-3.3 fileprivate static var urlCorrectPathAllowed: Self { - #if canImport(Darwin) - .urlPathAllowed - #else .urlPathAllowed.union(.init(charactersIn: ";")) - #endif } } diff --git a/Tests/AsyncTests/AsyncCommandsTests.swift b/Tests/AsyncTests/AsyncCommandsTests.swift index e9e7948e23..3abb914563 100644 --- a/Tests/AsyncTests/AsyncCommandsTests.swift +++ b/Tests/AsyncTests/AsyncCommandsTests.swift @@ -2,7 +2,7 @@ import XCTest import Vapor final class AsyncCommandsTests: XCTestCase { - func testAsyncCommands() throws { + func testAsyncCommands() async throws { let app = Application(.testing) defer { app.shutdown() } @@ -10,7 +10,7 @@ final class AsyncCommandsTests: XCTestCase { app.environment.arguments = ["vapor", "foo", "bar"] - XCTAssertNoThrow(try app.start()) + try await app.startup() XCTAssertTrue(app.storage[TestStorageKey.self] ?? false) } diff --git a/Tests/AsyncTests/AsyncRequestTests.swift b/Tests/AsyncTests/AsyncRequestTests.swift index 2260349c54..bbf9a09541 100644 --- a/Tests/AsyncTests/AsyncRequestTests.swift +++ b/Tests/AsyncTests/AsyncRequestTests.swift @@ -46,7 +46,7 @@ final class AsyncRequestTests: XCTestCase { } app.environment.arguments = ["serve"] - XCTAssertNoThrow(try app.start()) + try await app.startup() XCTAssertNotNil(app.http.server.shared.localAddress) guard let localAddress = app.http.server.shared.localAddress, @@ -79,7 +79,7 @@ final class AsyncRequestTests: XCTestCase { } app.environment.arguments = ["serve"] - XCTAssertNoThrow(try app.start()) + try await app.startup() XCTAssertNotNil(app.http.server.shared.localAddress) guard let localAddress = app.http.server.shared.localAddress, @@ -94,10 +94,10 @@ final class AsyncRequestTests: XCTestCase { var request = HTTPClientRequest(url: "http://\(ip):\(port)/hello") request.method = .POST request.body = .stream(oneMB.async, length: .known(oneMB.count)) - let response = try await app.http.client.shared.execute(request, timeout: .seconds(5)) - - XCTAssertGreaterThan(bytesTheServerRead.load(ordering: .relaxed), 0) - XCTAssertEqual(response.status, .internalServerError) + if let response = try? await app.http.client.shared.execute(request, timeout: .seconds(5)) { + XCTAssertGreaterThan(bytesTheServerRead.load(ordering: .relaxed), 0) + XCTAssertEqual(response.status, .internalServerError) + } } // TODO: Re-enable once it reliably works and doesn't cause issues with trying to shut the application down @@ -140,7 +140,7 @@ final class AsyncRequestTests: XCTestCase { } app.environment.arguments = ["serve"] - XCTAssertNoThrow(try app.start()) + try await app.startup() XCTAssertNotNil(app.http.server.shared.localAddress) guard let localAddress = app.http.server.shared.localAddress, diff --git a/Tests/VaporTests/ClientTests.swift b/Tests/VaporTests/ClientTests.swift index 9d90a9b558..66ef4b7fa0 100644 --- a/Tests/VaporTests/ClientTests.swift +++ b/Tests/VaporTests/ClientTests.swift @@ -156,8 +156,8 @@ final class ClientTests: XCTestCase { defer { app.shutdown() } try app.boot() - XCTAssertNoThrow(try app.client.get("http://localhost:\(remoteAppPort!)/json") { $0.timeout = .seconds(2) }.wait()) - XCTAssertThrowsError(try app.client.get("http://localhost:\(remoteAppPort!)/stalling") { $0.timeout = .seconds(2) }.wait()) { + XCTAssertNoThrow(try app.client.get("http://localhost:\(remoteAppPort!)/json") { $0.timeout = .seconds(1) }.wait()) + XCTAssertThrowsError(try app.client.get("http://localhost:\(remoteAppPort!)/stalling") { $0.timeout = .seconds(1) }.wait()) { XCTAssertTrue(type(of: $0) == HTTPClientError.self, "\(type(of: $0)) is not a \(HTTPClientError.self)") XCTAssertEqual($0 as? HTTPClientError, .deadlineExceeded) } diff --git a/Tests/VaporTests/ContentTests.swift b/Tests/VaporTests/ContentTests.swift index 5c3c3a439d..c17c3943d7 100644 --- a/Tests/VaporTests/ContentTests.swift +++ b/Tests/VaporTests/ContentTests.swift @@ -55,7 +55,7 @@ final class ContentTests: XCTestCase { let request = Request( application: app, collectedBody: .init(string: complexJSON), - on: app.eventLoopGroup.next() + on: app.eventLoopGroup.any() ) request.headers.contentType = .json try XCTAssertEqual(request.content.get(at: "batters", "batter", 1, "type"), "Chocolate") @@ -500,7 +500,7 @@ final class ContentTests: XCTestCase { let request = Request( application: app, collectedBody: .init(string:""), - on: EmbeddedEventLoop() + on: app.eventLoopGroup.any() ) request.url.query = "name=before+decode" request.headers.contentType = .json @@ -511,11 +511,44 @@ final class ContentTests: XCTestCase { XCTAssertEqual(request.url.query, "name=new%20name") } + /// https://github.com/vapor/vapor/issues/3135 + func testDecodePercentEncodedQuery() throws { + let app = Application() + defer { app.shutdown() } + + let request = Request( + application: app, + collectedBody: .init(string: ""), + on: app.eventLoopGroup.any() + ) + request.url = .init(string: "/?name=value%20has%201%25%20of%20its%20percents") + request.headers.contentType = .urlEncodedForm + + XCTAssertEqual(try request.query.get(String.self, at: "name"), "value has 1% of its percents") + } + + /// https://github.com/vapor/vapor/issues/3133 + func testEncodePercentEncodedQuery() throws { + let app = Application() + defer { app.shutdown() } + + struct Foo: Content { + var status: String + } + + var request = ClientRequest(url: .init(scheme: "https", host: "example.com", path: "/api")) + try request.query.encode(Foo(status: + "⬆️ taylorswift just released swift-mongodb v0.10.1 – use BSON and MongoDB in pure Swift\n\nhttps://swiftpackageindex.com/tayloraswift/swift-mongodb#releases" + )) + + XCTAssertEqual(request.url.string, "https://example.com/api?status=%E2%AC%86%EF%B8%8F%20taylorswift%20just%20released%20swift-mongodb%20v0.10.1%20%E2%80%93%20use%20BSON%20and%20MongoDB%20in%20pure%20Swift%0A%0Ahttps://swiftpackageindex.com/tayloraswift/swift-mongodb%23releases") + } + func testSnakeCaseCodingKeyError() throws { let app = Application() defer { app.shutdown() } - let req = Request(application: app, on: app.eventLoopGroup.next()) + let req = Request(application: app, on: app.eventLoopGroup.any()) try req.content.encode([ "title": "The title" ], as: .json) @@ -546,7 +579,7 @@ final class ContentTests: XCTestCase { url: URI(string: "https://vapor.codes"), headersNoUpdate: ["Content-Type": "application/json"], collectedBody: ByteBuffer(string: #"{"badJson: "Key doesn't have a trailing quote"}"#), - on: app.eventLoopGroup.next() + on: app.eventLoopGroup.any() ) struct DecodeModel: Content { @@ -564,7 +597,7 @@ final class ContentTests: XCTestCase { let app = Application() defer { app.shutdown() } - let req = Request(application: app, on: app.eventLoopGroup.next()) + let req = Request(application: app, on: app.eventLoopGroup.any()) try req.content.encode([ "items": ["1"] ], as: .json) @@ -593,7 +626,7 @@ final class ContentTests: XCTestCase { let app = Application() defer { app.shutdown() } - let req = Request(application: app, on: app.eventLoopGroup.next()) + let req = Request(application: app, on: app.eventLoopGroup.any()) try req.content.encode([ "item": [ "title": "The title" diff --git a/Tests/VaporTests/RouteTests.swift b/Tests/VaporTests/RouteTests.swift index ff74eccb98..cfbf1c1ae9 100644 --- a/Tests/VaporTests/RouteTests.swift +++ b/Tests/VaporTests/RouteTests.swift @@ -429,4 +429,22 @@ final class RouteTests: XCTestCase { XCTAssertEqual(res.status.code, 500) } } + + // https://github.com/vapor/vapor/issues/3137 + func testDoubleSlashRouteAccess() throws { + let app = Application(.testing) + defer { app.shutdown() } + + app.get("foo", "bar", "buz") { req -> String in + try req.query.get(at: "v") + } + + try app.testable().test(.GET, "/foo/bar/buz?v=M%26M") { res in + XCTAssertEqual(res.status, .ok) + XCTAssertEqual(res.body.string, #"M&M"#) + }.test(.GET, "//foo/bar/buz?v=M%26M") { res in + XCTAssertEqual(res.status, .ok) + XCTAssertEqual(res.body.string, #"M&M"#) + } + } } diff --git a/Tests/VaporTests/ServerTests.swift b/Tests/VaporTests/ServerTests.swift index 275df4c0c7..e1f91aa503 100644 --- a/Tests/VaporTests/ServerTests.swift +++ b/Tests/VaporTests/ServerTests.swift @@ -42,16 +42,14 @@ final class ServerTests: XCTestCase { let app = Application(env) defer { app.shutdown() } - app.get("foo") { req in - return "bar" - } + app.get("foo") { _ in "bar" } try app.start() - let res = try app.client.get(.init(scheme: .httpUnixDomainSocket, host: socketPath, path: "/foo")).wait() + let res = try app.client.get(.init(scheme: .httpUnixDomainSocket, host: socketPath, path: "/foo")) { $0.timeout = .milliseconds(500) }.wait() XCTAssertEqual(res.body?.string, "bar") // no server should be bound to the port despite one being set on the configuration. - XCTAssertThrowsError(try app.client.get("http://127.0.0.1:8080/foo").wait()) + XCTAssertThrowsError(try app.client.get("http://127.0.0.1:8080/foo") { $0.timeout = .milliseconds(500) }.wait()) } func testIncompatibleStartupOptions() throws { diff --git a/Tests/VaporTests/URITests.swift b/Tests/VaporTests/URITests.swift index dcdeeab499..fb973c0cb4 100644 --- a/Tests/VaporTests/URITests.swift +++ b/Tests/VaporTests/URITests.swift @@ -4,29 +4,6 @@ import Vapor import NIOCore import Algorithms -@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) -extension RangeReplaceableCollection where Self.SubSequence == Substring, Self: StringProtocol { - #if compiler(>=5.9) - #if hasFeature(BareSlashRegexLiterals) - private static var percentEncodingPattern: Regex { /(?:%\p{AHex}{2})+/ } - #else - private static var percentEncodingPattern: Regex { try! Regex("(?:%\\p{AHex}{2})+") } - #endif - #else - private static var percentEncodingPattern: Regex { try! Regex("(?:%\\p{AHex}{2})+") } - #endif - - /// Foundation's `String.removingPercentEncoding` property is very unforgiving; `nil` is returned - /// for any kind of failure whatsoever. This is just a version that gracefully ignores invalid - /// sequences whenever possible (which is almost always). - var safelyUrlDecoded: Self { - self.replacing( - Self.percentEncodingPattern, - with: { Self(decoding: $0.0.split(separator: "%").map { .init($0, radix: 16)! }, as: UTF8.self) } - ) - } -} - @available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) func XCTAssertURIComponents( scheme: @autoclosure () throws -> URI.Scheme?, @@ -65,23 +42,30 @@ func XCTAssertURIComponents( _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line ) { do { - let scheme = try scheme(), userinfo = try userinfo(), host = try host(), port = try port(), + let scheme = try scheme(), rawuserinfo = try userinfo(), host = try host(), port = try port(), path = try path(), query = try query(), fragment = try fragment() - let uri = URI(scheme: scheme, userinfo: userinfo, host: host, port: port, path: path, query: query, fragment: fragment) + let uri = URI(scheme: scheme, userinfo: rawuserinfo, host: host, port: port, path: path, query: query, fragment: fragment) + + let userinfo = rawuserinfo.map { + !$0.contains(":") ? $0 : + $0.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false).enumerated() + .map { $1.addingPercentEncoding(withAllowedCharacters: $0 == 0 ? .urlUserAllowed : .urlPasswordAllowed)! } + .joined(separator: ":") + } - // All components should be identical to their input counterparts, sans percent encoding. - XCTAssertEqual(uri.scheme, scheme?.safelyUrlDecoded, "(scheme) \(message())", file: file, line: line) - XCTAssertEqual(uri.userinfo, userinfo?.safelyUrlDecoded, "(userinfo) \(message())", file: file, line: line) - XCTAssertEqual(uri.host, host?.safelyUrlDecoded, "(host) \(message())", file: file, line: line) - XCTAssertEqual(uri.port, port, "(port) \(message())", file: file, line: line) - XCTAssertEqual(uri.path, "/\(path.safelyUrlDecoded.trimmingPrefix("/"))", "(path) \(message())", file: file, line: line) - XCTAssertEqual(uri.query, query?.safelyUrlDecoded, "(query) \(message())", file: file, line: line) - XCTAssertEqual(uri.fragment, fragment?.safelyUrlDecoded, "(fragment) \(message())", file: file, line: line) + // All components should be identical to their input counterparts with percent encoding. + XCTAssertEqual(uri.scheme, scheme, "(scheme) \(message())", file: file, line: line) + XCTAssertEqual(uri.userinfo, userinfo, "(userinfo) \(message())", file: file, line: line) + XCTAssertEqual(uri.host, host?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed), "(host) \(message())", file: file, line: line) + XCTAssertEqual(uri.port, port, "(port) \(message())", file: file, line: line) + XCTAssertEqual(uri.path, path.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed), "(path) \(message())", file: file, line: line) + XCTAssertEqual(uri.query, query?.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), "(query) \(message())", file: file, line: line) + XCTAssertEqual(uri.fragment, fragment?.addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed), "(fragment) \(message())", file: file, line: line) // The URI's generated string should match the expected input. - XCTAssertEqual(uri.string, try expected(), "(string) \(message())", file: file, line: line) + XCTAssertEqual(uri.string, try expected(), "(string) \(message())", file: file, line: line) } catch { - XCTAssertEqual(try { throw error }(), false, message(), file: file, line: line) + XCTAssertEqual(try { throw error }(), false, message(), file: file, line: line) } } @@ -103,20 +87,20 @@ func XCTAssertURIString( let uri = URI(string: string) // Each component should match its expected value. - XCTAssertEqual(uri.scheme, try scheme()?.safelyUrlDecoded, "(scheme) \(message())", file: file, line: line) - XCTAssertEqual(uri.userinfo, try userinfo()?.safelyUrlDecoded, "(userinfo) \(message())", file: file, line: line) - XCTAssertEqual(uri.host, try host()?.safelyUrlDecoded, "(host) \(message())", file: file, line: line) - XCTAssertEqual(uri.port, try port(), "(port) \(message())", file: file, line: line) - XCTAssertEqual(uri.path, try path().safelyUrlDecoded, "(path) \(message())", file: file, line: line) - XCTAssertEqual(uri.query, try query()?.safelyUrlDecoded, "(query) \(message())", file: file, line: line) - XCTAssertEqual(uri.fragment, try fragment()?.safelyUrlDecoded, "(fragment) \(message())", file: file, line: line) + XCTAssertEqual(uri.scheme, try scheme(), "(scheme) \(message())", file: file, line: line) + XCTAssertEqual(uri.userinfo, try userinfo(), "(userinfo) \(message())", file: file, line: line) + XCTAssertEqual(uri.host, try host(), "(host) \(message())", file: file, line: line) + XCTAssertEqual(uri.port, try port(), "(port) \(message())", file: file, line: line) + XCTAssertEqual(uri.path, try path(), "(path) \(message())", file: file, line: line) + XCTAssertEqual(uri.query, try query(), "(query) \(message())", file: file, line: line) + XCTAssertEqual(uri.fragment, try fragment(), "(fragment) \(message())", file: file, line: line) // The URI's generated string should come out identical to the input string, unless explicitly stated otherwise. if try exact() { - XCTAssertEqual(uri.string, string, "(string) \(message())", file: file, line: line) + XCTAssertEqual(uri.string, string, "(string) \(message())", file: file, line: line) } } catch { - XCTAssertEqual(try { throw error }(), false, message(), file: file, line: line) + XCTAssertEqual(try { throw error }(), false, message(), file: file, line: line) } } @@ -190,19 +174,19 @@ final class URITests: XCTestCase { // N.B.: This test previously asserted that the resulting string did _not_ start with the `//` "authority" // prefix. Again, according to RFC 3986, this was always semantically incorrect. XCTAssertURIComponents( - host: "host", port: 1, path: "test", query: "query", fragment: "fragment", + host: "host", port: 1, path: "/test", query: "query", fragment: "fragment", generate: "//host:1/test?query#fragment" ) XCTAssertURIComponents( - scheme: .httpUnixDomainSocket, host: "/path", path: "test", + scheme: .httpUnixDomainSocket, host: "/path", path: "/test", generate: "http+unix://%2Fpath/test" ) XCTAssertURIComponents( - scheme: .httpUnixDomainSocket, host: "/path", path: "test", fragment: "fragment", + scheme: .httpUnixDomainSocket, host: "/path", path: "/test", fragment: "fragment", generate: "http+unix://%2Fpath/test#fragment" ) XCTAssertURIComponents( - scheme: .httpUnixDomainSocket, host: "/path", path: "test", query: "query", fragment: "fragment", + scheme: .httpUnixDomainSocket, host: "/path", path: "/test", query: "query", fragment: "fragment", generate: "http+unix://%2Fpath/test?query#fragment" ) } @@ -216,10 +200,25 @@ final class URITests: XCTestCase { let zeros = String(repeating: "0", count: 65_512) let untrustedInput = "[https://vapor.codes.somewhere-else.test:](https://vapor.codes.somewhere-else.test/\(zeros)443)[\(zeros)](https://vapor.codes.somewhere-else.test/\(zeros)443)[443](https://vapor.codes.somewhere-else.test/\(zeros)443)" + let readableInAssertionOutput = untrustedInput + .replacingOccurrences(of: zeros, with: "00...00") + .addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)! + let uri = URI(string: untrustedInput) + + XCTAssertNil(uri.scheme) + XCTAssertNil(uri.userinfo) + XCTAssertNil(uri.host) + XCTAssertNil(uri.port) + XCTAssertNil(uri.query) + XCTAssertNil(uri.fragment) if #available(macOS 14, iOS 17, watchOS 10, tvOS 17, *) { - XCTAssertURIString(untrustedInput, hasHost: nil, hasPath: untrustedInput, hasEqualString: false) + // TODO: It is not clear why the "encode the first colon as %3A but none of the others" behavior appears, and why only on Darwin + XCTAssertEqual( + uri.path.replacingOccurrences(of: zeros, with: "00...00").replacing("%3A", with: ":", maxReplacements: 1), + readableInAssertionOutput.replacing("%3A", with: ":", maxReplacements: 1) + ) } else { - XCTAssertURIString(untrustedInput, hasHost: nil, hasPath: "/", hasEqualString: false) + XCTAssertEqual(uri.path, "/") } } @@ -289,27 +288,27 @@ final class URITests: XCTestCase { hasScheme: "scheme", hasUserinfo: "user:pass", hasHost: "host", hasPort: 1, hasPath: "/path/path2/file.html;params", hasQuery: "query", hasFragment: "fragment", hasEqualString: false ) - XCTAssertURIString("http://test.com/a%20space", hasScheme: "http", hasHost: "test.com", hasPath: "/a space") - XCTAssertURIString("http://test.com/aBrace%7B", hasScheme: "http", hasHost: "test.com", hasPath: "/aBrace{") - XCTAssertURIString("http://test.com/aJ%4a", hasScheme: "http", hasHost: "test.com", hasPath: "/aJJ") - XCTAssertURIString("file:///%3F", hasScheme: "file", hasPath: "/?") - XCTAssertURIString("file:///%78", hasScheme: "file", hasPath: "/x") + XCTAssertURIString("http://test.com/a%20space", hasScheme: "http", hasHost: "test.com", hasPath: "/a%20space") + XCTAssertURIString("http://test.com/aBrace%7B", hasScheme: "http", hasHost: "test.com", hasPath: "/aBrace%7B") + XCTAssertURIString("http://test.com/aJ%4a", hasScheme: "http", hasHost: "test.com", hasPath: "/aJ%4a") + XCTAssertURIString("file:///%3F", hasScheme: "file", hasPath: "/%3F") + XCTAssertURIString("file:///%78", hasScheme: "file", hasPath: "/%78") XCTAssertURIString("file:///?", hasScheme: "file", hasPath: "/", hasQuery: "") XCTAssertURIString("file:///&", hasScheme: "file", hasPath: "/&") XCTAssertURIString("file:///x", hasScheme: "file", hasPath: "/x") - XCTAssertURIString("http:///%3F", hasScheme: "http", hasPath: "/?") - XCTAssertURIString("http:///%78", hasScheme: "http", hasPath: "/x") + XCTAssertURIString("http:///%3F", hasScheme: "http", hasPath: "/%3F") + XCTAssertURIString("http:///%78", hasScheme: "http", hasPath: "/%78") XCTAssertURIString("http:///?", hasScheme: "http", hasPath: "/", hasQuery: "") XCTAssertURIString("http:///&", hasScheme: "http", hasPath: "/&") XCTAssertURIString("http:///x", hasScheme: "http", hasPath: "/x") - XCTAssertURIString("glorb:///%3F", hasScheme: "glorb", hasPath: "/?") - XCTAssertURIString("glorb:///%78", hasScheme: "glorb", hasPath: "/x") + XCTAssertURIString("glorb:///%3F", hasScheme: "glorb", hasPath: "/%3F") + XCTAssertURIString("glorb:///%78", hasScheme: "glorb", hasPath: "/%78") XCTAssertURIString("glorb:///?", hasScheme: "glorb", hasPath: "/", hasQuery: "") XCTAssertURIString("glorb:///&", hasScheme: "glorb", hasPath: "/&") XCTAssertURIString("glorb:///x", hasScheme: "glorb", hasPath: "/x") XCTAssertURIString("uahsfcncvuhrtgvnahr", hasHost: nil, hasPath: "uahsfcncvuhrtgvnahr") XCTAssertURIString("http://[fe80::20a:27ff:feae:8b9e]/", hasScheme: "http", hasHost: "[fe80::20a:27ff:feae:8b9e]", hasPath: "/") - XCTAssertURIString("http://[fe80::20a:27ff:feae:8b9e%25en0]/", hasScheme: "http", hasHost: "[fe80::20a:27ff:feae:8b9e%en0]", hasPath: "/") + XCTAssertURIString("http://[fe80::20a:27ff:feae:8b9e%25en0]/", hasScheme: "http", hasHost: "[fe80::20a:27ff:feae:8b9e%25en0]", hasPath: "/") XCTAssertURIString("http://host.com/foo/bar/../index.html", hasScheme: "http", hasHost: "host.com", hasPath: "/foo/bar/../index.html") XCTAssertURIString("http://host.com/foo/bar/./index.html", hasScheme: "http", hasHost: "host.com", hasPath: "/foo/bar/./index.html") XCTAssertURIString("http:/cgi-bin/Count.cgi?ft=0", hasScheme: "http", hasHost: nil, hasPath: "/cgi-bin/Count.cgi", hasQuery: "ft=0") From 4942d74e8493fc918ed6144c835c8a0e6affd4f4 Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Wed, 24 Jan 2024 06:20:23 -0600 Subject: [PATCH 02/18] Fix URI handling with multiple slashes and variable components. (#3143) * Fix URI handling when at least one component is a variable. * Use #filePath instead of #file in tests. --- .../Server/HTTPServerRequestDecoder.swift | 14 +++++++- Sources/Vapor/Utilities/URI.swift | 17 ++++++--- Sources/XCTVapor/XCTApplication.swift | 20 +++++------ Sources/XCTVapor/XCTHTTPResponse.swift | 17 ++++----- Tests/AsyncTests/AsyncPasswordTests.swift | 8 ++--- Tests/VaporTests/DotEnvTests.swift | 2 +- Tests/VaporTests/EnvironmentSecretTests.swift | 6 ++-- Tests/VaporTests/ErrorTests.swift | 2 +- Tests/VaporTests/FileTests.swift | 36 +++++++++---------- Tests/VaporTests/PasswordTests.swift | 8 ++--- Tests/VaporTests/RouteTests.swift | 31 ++++++++++++---- Tests/VaporTests/ValidationTests.swift | 6 ++-- 12 files changed, 100 insertions(+), 67 deletions(-) diff --git a/Sources/Vapor/HTTP/Server/HTTPServerRequestDecoder.swift b/Sources/Vapor/HTTP/Server/HTTPServerRequestDecoder.swift index dc7f3a7b67..14c2b136d9 100644 --- a/Sources/Vapor/HTTP/Server/HTTPServerRequestDecoder.swift +++ b/Sources/Vapor/HTTP/Server/HTTPServerRequestDecoder.swift @@ -38,10 +38,22 @@ final class HTTPServerRequestDecoder: ChannelDuplexHandler, RemovableChannelHand case .head(let head): switch self.requestState { case .ready: + /// Note: It is critical that `URI.init(path:)` is used here, _NOT_ `URI.init(string:)`. The following + /// example illustrates why: + /// + /// let uri1 = URI(string: "//foo/bar?a#b"), uri2 = URI(path: "//foo/bar?a#b") + /// + /// print(uri1.host, uri1.path, uri1.query, uri1.fragment) + /// // Optional(foo) /bar a b + /// print(uri2.host, uri2.path, uri2.query, uri2.fragment) + /// // nil /foo/bar a b + /// + /// The latter parse has the correct semantics for an HTTP request's URL (which, in the absence of an + /// accompanying scheme, should never have a host); the former follows strict RFC 3986 rules. let request = Request( application: self.application, method: head.method, - url: .init(string: head.uri), + url: .init(path: head.uri), version: head.version, headersNoUpdate: head.headers, remoteAddress: context.channel.remoteAddress, diff --git a/Sources/Vapor/Utilities/URI.swift b/Sources/Vapor/Utilities/URI.swift index ba206bb6e6..1ee37cf016 100644 --- a/Sources/Vapor/Utilities/URI.swift +++ b/Sources/Vapor/Utilities/URI.swift @@ -46,6 +46,9 @@ public struct URI: CustomStringConvertible, ExpressibleByStringInterpolation, Ha try container.encode(self.string) } + /// Create a ``URI`` by parsing a given string according to the semantics of [RFC 3986]. + /// + /// [RFC 3986]: https://datatracker.ietf.org/doc/html/rfc3986 public init(string: String = "/") { self.components = URL(string: string).flatMap { .init(url: $0, resolvingAgainstBaseURL: true) } } @@ -83,11 +86,15 @@ public struct URI: CustomStringConvertible, ExpressibleByStringInterpolation, Ha /// Percent encoding is added to each component (if necessary) automatically. There is currently no /// way to change this behavior; use `URLComponents` instead if this is insufficient. /// - /// > Warning: For backwards compatibility reasons, if the `path` component is specified in isolation - /// > (e.g. all other components are `nil`), the path is parsed as if by the ``init(string:)`` initializer. - /// - /// > Important: If the `path` does not begin with a `/`, one is prepended. This occurs even if the path - /// > is specified in isolation (as described above). + /// > Important: For backwards compatibility reasons, if the `path` component is specified in isolation + /// > (e.g. all other components are `nil`), the path is parsed as if by the ``init(string:)`` initializer, + /// > _EXCEPT_ that if the path begins with `//`, it will be treated as beginning with `/` instead, thus + /// > parsing the first path component as part of the path rather than as a host component. These semantics + /// > are suitable for parsing URI-like strings which are known to lack an authority component, such as the + /// > URI part of the first line of an HTTP request. + /// > + /// > In all cases, a `/` is prepended to the path if it does not begin with one, irrespective of whether or + /// > not the path has been specified in isolation as described above. public init( scheme: Scheme = .init(), userinfo: String?, diff --git a/Sources/XCTVapor/XCTApplication.swift b/Sources/XCTVapor/XCTApplication.swift index ff38039e23..e8ff4b0449 100644 --- a/Sources/XCTVapor/XCTApplication.swift +++ b/Sources/XCTVapor/XCTApplication.swift @@ -128,7 +128,7 @@ extension XCTApplicationTester { _ path: String, headers: HTTPHeaders = [:], body: ByteBuffer? = nil, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line, afterResponse: (XCTHTTPResponse) async throws -> () ) async throws -> XCTApplicationTester { @@ -150,7 +150,7 @@ extension XCTApplicationTester { _ path: String, headers: HTTPHeaders = [:], body: ByteBuffer? = nil, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line, afterResponse: (XCTHTTPResponse) throws -> () ) throws -> XCTApplicationTester { @@ -172,7 +172,7 @@ extension XCTApplicationTester { _ path: String, headers: HTTPHeaders = [:], body: ByteBuffer? = nil, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line, beforeRequest: (inout XCTHTTPRequest) async throws -> () = { _ in }, afterResponse: (XCTHTTPResponse) async throws -> () = { _ in } @@ -188,7 +188,7 @@ extension XCTApplicationTester { let response = try self.performTest(request: request) try await afterResponse(response) } catch { - XCTFail("\(error)", file: (file), line: line) + XCTFail("\(error)", file: file, line: line) throw error } return self @@ -200,7 +200,7 @@ extension XCTApplicationTester { _ path: String, headers: HTTPHeaders = [:], body: ByteBuffer? = nil, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line, beforeRequest: (inout XCTHTTPRequest) throws -> () = { _ in }, afterResponse: (XCTHTTPResponse) throws -> () = { _ in } @@ -216,7 +216,7 @@ extension XCTApplicationTester { let response = try self.performTest(request: request) try afterResponse(response) } catch { - XCTFail("\(error)", file: (file), line: line) + XCTFail("\(error)", file: file, line: line) throw error } return self @@ -227,7 +227,7 @@ extension XCTApplicationTester { _ path: String, headers: HTTPHeaders = [:], body: ByteBuffer? = nil, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line, beforeRequest: (inout XCTHTTPRequest) async throws -> () = { _ in } ) async throws -> XCTHTTPResponse { @@ -241,7 +241,7 @@ extension XCTApplicationTester { do { return try self.performTest(request: request) } catch { - XCTFail("\(error)", file: (file), line: line) + XCTFail("\(error)", file: file, line: line) throw error } } @@ -251,7 +251,7 @@ extension XCTApplicationTester { _ path: String, headers: HTTPHeaders = [:], body: ByteBuffer? = nil, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line, beforeRequest: (inout XCTHTTPRequest) throws -> () = { _ in } ) throws -> XCTHTTPResponse { @@ -265,7 +265,7 @@ extension XCTApplicationTester { do { return try self.performTest(request: request) } catch { - XCTFail("\(error)", file: (file), line: line) + XCTFail("\(error)", file: file, line: line) throw error } } diff --git a/Sources/XCTVapor/XCTHTTPResponse.swift b/Sources/XCTVapor/XCTHTTPResponse.swift index 9406b95489..c3d435a677 100644 --- a/Sources/XCTVapor/XCTHTTPResponse.swift +++ b/Sources/XCTVapor/XCTHTTPResponse.swift @@ -47,15 +47,12 @@ extension Response.Body { public func XCTAssertContent( _ type: D.Type, _ res: XCTHTTPResponse, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line, _ closure: (D) throws -> () -) - rethrows - where D: Decodable -{ +) rethrows where D: Decodable { guard let contentType = res.headers.contentType else { - XCTFail("response does not contain content type", file: (file), line: line) + XCTFail("response does not contain content type", file: file, line: line) return } @@ -65,14 +62,14 @@ public func XCTAssertContent( let decoder = try ContentConfiguration.global.requireDecoder(for: contentType) content = try decoder.decode(D.self, from: res.body, headers: res.headers) } catch { - XCTFail("could not decode body: \(error)", file: (file), line: line) + XCTFail("could not decode body: \(error)", file: file, line: line) return } try closure(content) } -public func XCTAssertContains(_ haystack: String?, _ needle: String?, file: StaticString = #file, line: UInt = #line) { +public func XCTAssertContains(_ haystack: String?, _ needle: String?, file: StaticString = #filePath, line: UInt = #line) { let file = (file) switch (haystack, needle) { case (.some(let haystack), .some(let needle)): @@ -86,11 +83,11 @@ public func XCTAssertContains(_ haystack: String?, _ needle: String?, file: Stat } } -public func XCTAssertEqualJSON(_ data: String?, _ test: T, file: StaticString = #file, line: UInt = #line) +public func XCTAssertEqualJSON(_ data: String?, _ test: T, file: StaticString = #filePath, line: UInt = #line) where T: Codable & Equatable { guard let data = data else { - XCTFail("nil does not equal \(test)", file: (file), line: line) + XCTFail("nil does not equal \(test)", file: file, line: line) return } do { diff --git a/Tests/AsyncTests/AsyncPasswordTests.swift b/Tests/AsyncTests/AsyncPasswordTests.swift index cbed3ecdec..cbb5106c9f 100644 --- a/Tests/AsyncTests/AsyncPasswordTests.swift +++ b/Tests/AsyncTests/AsyncPasswordTests.swift @@ -59,7 +59,7 @@ final class AsyncPasswordTests: XCTestCase { private func assertAsyncApplicationPasswordVerifies( _ provider: Application.Passwords.Provider, on app: Application, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) async throws { app.passwords.use(provider) @@ -72,13 +72,13 @@ final class AsyncPasswordTests: XCTestCase { .async(on: app.threadPool, hopTo: app.eventLoopGroup.next()) .verify("vapor", created: asyncHash) - XCTAssertTrue(asyncVerifiy, file: (file), line: line) + XCTAssertTrue(asyncVerifiy, file: file, line: line) } private func assertAsyncRequestPasswordVerifies( _ provider: Application.Passwords.Provider, on app: Application, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) throws { app.passwords.use(provider) @@ -90,7 +90,7 @@ final class AsyncPasswordTests: XCTestCase { } try app.test(.GET, "test", afterResponse: { res in - XCTAssertEqual(res.body.string, "true", file: (file), line: line) + XCTAssertEqual(res.body.string, "true", file: file, line: line) }) } } diff --git a/Tests/VaporTests/DotEnvTests.swift b/Tests/VaporTests/DotEnvTests.swift index b205ab13a1..8cb59a01a5 100644 --- a/Tests/VaporTests/DotEnvTests.swift +++ b/Tests/VaporTests/DotEnvTests.swift @@ -10,7 +10,7 @@ final class DotEnvTests: XCTestCase { let pool = NIOThreadPool(numberOfThreads: 1) pool.start() let fileio = NonBlockingFileIO(threadPool: pool) - let folder = #file.split(separator: "/").dropLast().joined(separator: "/") + let folder = #filePath.split(separator: "/").dropLast().joined(separator: "/") let path = "/" + folder + "/Utilities/test.env" let file = try DotEnvFile.read(path: path, fileio: fileio, on: elg.next()).wait() let test = file.lines.map { $0.description }.joined(separator: "\n") diff --git a/Tests/VaporTests/EnvironmentSecretTests.swift b/Tests/VaporTests/EnvironmentSecretTests.swift index 35e1a650f5..5278fe739c 100644 --- a/Tests/VaporTests/EnvironmentSecretTests.swift +++ b/Tests/VaporTests/EnvironmentSecretTests.swift @@ -4,7 +4,7 @@ import XCTest final class EnvironmentSecretTests: XCTestCase { func testNonExistingSecretFile() throws { - let folder = #file.split(separator: "/").dropLast().joined(separator: "/") + let folder = #filePath.split(separator: "/").dropLast().joined(separator: "/") let path = "/" + folder + "/Utilities/non-existing-secret" let app = Application(.testing) @@ -16,7 +16,7 @@ final class EnvironmentSecretTests: XCTestCase { } func testExistingSecretFile() throws { - let folder = #file.split(separator: "/").dropLast().joined(separator: "/") + let folder = #filePath.split(separator: "/").dropLast().joined(separator: "/") let path = "/" + folder + "/Utilities/my-secret-env-content" let app = Application(.testing) @@ -28,7 +28,7 @@ final class EnvironmentSecretTests: XCTestCase { } func testExistingSecretFileFromEnvironmentKey() throws { - let folder = #file.split(separator: "/").dropLast().joined(separator: "/") + let folder = #filePath.split(separator: "/").dropLast().joined(separator: "/") let path = "/" + folder + "/Utilities/my-secret-env-content" let key = "MY_ENVIRONMENT_SECRET" diff --git a/Tests/VaporTests/ErrorTests.swift b/Tests/VaporTests/ErrorTests.swift index 7a4e67ba3c..bd2b3eb91b 100644 --- a/Tests/VaporTests/ErrorTests.swift +++ b/Tests/VaporTests/ErrorTests.swift @@ -99,7 +99,7 @@ final class ErrorTests: XCTestCase { func XCTAssertContains( _ haystack: String?, _ needle: String, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) { let file = (file) diff --git a/Tests/VaporTests/FileTests.swift b/Tests/VaporTests/FileTests.swift index 7f70f2f3d2..c089588b74 100644 --- a/Tests/VaporTests/FileTests.swift +++ b/Tests/VaporTests/FileTests.swift @@ -10,7 +10,7 @@ final class FileTests: XCTestCase { defer { app.shutdown() } app.get("file-stream") { req in - return req.fileio.streamFile(at: #file) { result in + return req.fileio.streamFile(at: #filePath) { result in do { try result.get() } catch { @@ -31,7 +31,7 @@ final class FileTests: XCTestCase { defer { app.shutdown() } app.get("file-stream") { req in - return req.fileio.streamFile(at: #file) + return req.fileio.streamFile(at: #filePath) } var headers = HTTPHeaders() @@ -72,7 +72,7 @@ final class FileTests: XCTestCase { defer { app.shutdown() } app.get("file-stream") { req in - return req.fileio.streamFile(at: #file) { result in + return req.fileio.streamFile(at: #filePath) { result in do { try result.get() } catch { @@ -103,7 +103,7 @@ final class FileTests: XCTestCase { defer { app.shutdown() } app.get("file-stream") { req in - return req.fileio.streamFile(at: #file) { result in + return req.fileio.streamFile(at: #filePath) { result in do { try result.get() } catch { @@ -134,7 +134,7 @@ final class FileTests: XCTestCase { defer { app.shutdown() } app.get("file-stream") { req in - return req.fileio.streamFile(at: #file) { result in + return req.fileio.streamFile(at: #filePath) { result in do { try result.get() } catch { @@ -165,7 +165,7 @@ final class FileTests: XCTestCase { defer { app.shutdown() } app.get("file-stream") { req in - return req.fileio.streamFile(at: #file) { result in + return req.fileio.streamFile(at: #filePath) { result in do { try result.get() } catch { @@ -192,7 +192,7 @@ final class FileTests: XCTestCase { defer { app.shutdown() } app.get("file-stream") { req in - return req.fileio.streamFile(at: #file) { result in + return req.fileio.streamFile(at: #filePath) { result in do { try result.get() } catch { @@ -218,7 +218,7 @@ final class FileTests: XCTestCase { defer { app.shutdown() } app.get("file-stream") { req in - return req.fileio.streamFile(at: #file) { result in + return req.fileio.streamFile(at: #filePath) { result in do { try result.get() } catch { @@ -244,7 +244,7 @@ final class FileTests: XCTestCase { defer { app.shutdown() } app.get("file-stream") { req in - return req.fileio.streamFile(at: #file) { result in + return req.fileio.streamFile(at: #filePath) { result in do { try result.get() } catch { @@ -285,7 +285,7 @@ final class FileTests: XCTestCase { let app = Application(.testing) defer { app.shutdown() } - let path = #file.split(separator: "/").dropLast().joined(separator: "/") + let path = #filePath.split(separator: "/").dropLast().joined(separator: "/") app.middleware.use(FileMiddleware(publicDirectory: "/" + path)) try app.test(.GET, "/Utilities/foo%20bar.html") { res in @@ -298,7 +298,7 @@ final class FileTests: XCTestCase { let app = Application(.testing) defer { app.shutdown() } - let path = #file.split(separator: "/").dropLast().joined(separator: "/") + let path = #filePath.split(separator: "/").dropLast().joined(separator: "/") app.middleware.use(FileMiddleware(publicDirectory: "/" + path)) try app.test(.GET, "%2e%2e/VaporTests/Utilities/foo.txt") { res in @@ -313,7 +313,7 @@ final class FileTests: XCTestCase { let app = Application(.testing) defer { app.shutdown() } - let path = #file.split(separator: "/").dropLast().joined(separator: "/") + let path = #filePath.split(separator: "/").dropLast().joined(separator: "/") app.middleware.use(FileMiddleware(publicDirectory: "/" + path, defaultFile: "index.html")) try app.test(.GET, "Utilities/") { res in @@ -329,7 +329,7 @@ final class FileTests: XCTestCase { let app = Application(.testing) defer { app.shutdown() } - let path = #file.split(separator: "/").dropLast().joined(separator: "/") + let path = #filePath.split(separator: "/").dropLast().joined(separator: "/") app.middleware.use(FileMiddleware(publicDirectory: "/" + path, defaultFile: "/Utilities/index.html")) try app.test(.GET, "Utilities/") { res in @@ -345,7 +345,7 @@ final class FileTests: XCTestCase { let app = Application(.testing) defer { app.shutdown() } - let path = #file.split(separator: "/").dropLast().joined(separator: "/") + let path = #filePath.split(separator: "/").dropLast().joined(separator: "/") app.middleware.use(FileMiddleware(publicDirectory: "/" + path)) try app.test(.GET, "Utilities/") { res in @@ -357,7 +357,7 @@ final class FileTests: XCTestCase { let app = Application(.testing) defer { app.shutdown() } - let path = #file.split(separator: "/").dropLast().joined(separator: "/") + let path = #filePath.split(separator: "/").dropLast().joined(separator: "/") app.middleware.use( FileMiddleware( publicDirectory: "/" + path, @@ -377,7 +377,7 @@ final class FileTests: XCTestCase { let app = Application(.testing) defer { app.shutdown() } - let path = #file.split(separator: "/").dropLast().joined(separator: "/") + let path = #filePath.split(separator: "/").dropLast().joined(separator: "/") app.middleware.use( FileMiddleware( publicDirectory: "/" + path, @@ -402,7 +402,7 @@ final class FileTests: XCTestCase { let app = Application(.testing) defer { app.shutdown() } - let path = #file.split(separator: "/").dropLast().joined(separator: "/") + let path = #filePath.split(separator: "/").dropLast().joined(separator: "/") app.middleware.use( FileMiddleware( publicDirectory: "/" + path, @@ -424,7 +424,7 @@ final class FileTests: XCTestCase { defer { app.shutdown() } app.get("file-stream") { req in - return req.fileio.streamFile(at: #file) + return req.fileio.streamFile(at: #filePath) } var headers = HTTPHeaders() diff --git a/Tests/VaporTests/PasswordTests.swift b/Tests/VaporTests/PasswordTests.swift index b3f171b08e..0ae3c1444a 100644 --- a/Tests/VaporTests/PasswordTests.swift +++ b/Tests/VaporTests/PasswordTests.swift @@ -85,7 +85,7 @@ final class PasswordTests: XCTestCase { private func assertAsyncApplicationPasswordVerifies( _ provider: Application.Passwords.Provider, on app: Application, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) throws { app.passwords.use(provider) @@ -100,13 +100,13 @@ final class PasswordTests: XCTestCase { .verify("vapor", created: asyncHash) .wait() - XCTAssertTrue(asyncVerifiy, file: (file), line: line) + XCTAssertTrue(asyncVerifiy, file: file, line: line) } private func assertAsyncRequestPasswordVerifies( _ provider: Application.Passwords.Provider, on app: Application, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) throws { app.passwords.use(provider) @@ -125,7 +125,7 @@ final class PasswordTests: XCTestCase { } try app.test(.GET, "test", afterResponse: { res in - XCTAssertEqual(res.body.string, "true", file: (file), line: line) + XCTAssertEqual(res.body.string, "true", file: file, line: line) }) } } diff --git a/Tests/VaporTests/RouteTests.swift b/Tests/VaporTests/RouteTests.swift index cfbf1c1ae9..bf28872016 100644 --- a/Tests/VaporTests/RouteTests.swift +++ b/Tests/VaporTests/RouteTests.swift @@ -431,20 +431,39 @@ final class RouteTests: XCTestCase { } // https://github.com/vapor/vapor/issues/3137 + // https://github.com/vapor/vapor/issues/3142 func testDoubleSlashRouteAccess() throws { let app = Application(.testing) defer { app.shutdown() } - app.get("foo", "bar", "buz") { req -> String in - try req.query.get(at: "v") + app.get(":foo", ":bar", "buz") { req -> String in + "\(try req.parameters.require("foo"))\(try req.parameters.require("bar"))" } - try app.testable().test(.GET, "/foo/bar/buz?v=M%26M") { res in + try app.testable(method: .running(port: 0)).test(.GET, "/foop/barp/buz") { res in XCTAssertEqual(res.status, .ok) - XCTAssertEqual(res.body.string, #"M&M"#) - }.test(.GET, "//foo/bar/buz?v=M%26M") { res in + XCTAssertEqual(res.body.string, "foopbarp") + }.test(.GET, "//foop/barp/buz") { res in XCTAssertEqual(res.status, .ok) - XCTAssertEqual(res.body.string, #"M&M"#) + XCTAssertEqual(res.body.string, "foopbarp") + }.test(.GET, "//foop//barp/buz") { res in + XCTAssertEqual(res.status, .ok) + XCTAssertEqual(res.body.string, "foopbarp") + }.test(.GET, "//foop//barp//buz") { res in + XCTAssertEqual(res.status, .ok) + XCTAssertEqual(res.body.string, "foopbarp") + }.test(.GET, "/foop//barp/buz") { res in + XCTAssertEqual(res.status, .ok) + XCTAssertEqual(res.body.string, "foopbarp") + }.test(.GET, "/foop//barp//buz") { res in + XCTAssertEqual(res.status, .ok) + XCTAssertEqual(res.body.string, "foopbarp") + }.test(.GET, "/foop/barp//buz") { res in + XCTAssertEqual(res.status, .ok) + XCTAssertEqual(res.body.string, "foopbarp") + }.test(.GET, "//foop/barp//buz") { res in + XCTAssertEqual(res.status, .ok) + XCTAssertEqual(res.body.string, "foopbarp") } } } diff --git a/Tests/VaporTests/ValidationTests.swift b/Tests/VaporTests/ValidationTests.swift index 3491cf0f35..c12d31c81d 100644 --- a/Tests/VaporTests/ValidationTests.swift +++ b/Tests/VaporTests/ValidationTests.swift @@ -846,10 +846,9 @@ private func assert( _ data: T, fails validator: Validator, _ description: String, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) { - let file = (file) let result = validator.validate(data) XCTAssert(result.isFailure, result.successDescription ?? "n/a", file: file, line: line) XCTAssertEqual(description, result.failureDescription ?? "n/a", file: file, line: line) @@ -858,10 +857,9 @@ private func assert( private func assert( _ data: T, passes validator: Validator, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) { - let file = (file) let result = validator.validate(data) XCTAssert(!result.isFailure, result.failureDescription ?? "n/a", file: file, line: line) } From 823f20cf213f58295647cb0849c4cfb7fc6b82b9 Mon Sep 17 00:00:00 2001 From: Tim Condon <0xTim@users.noreply.github.com> Date: Thu, 25 Jan 2024 17:10:13 +0000 Subject: [PATCH 03/18] Merge Async Tests (#3141) Merge the async tests as we start to push people towards those APIs --- Package.swift | 4 - Package@swift-5.9.swift | 11 -- .../AsyncAuthTests.swift | 0 .../AsyncCacheTests.swift | 0 .../AsyncClientTests.swift | 2 +- .../AsyncCommandsTests.swift | 0 .../AsyncMiddlewareTests.swift | 9 ++ .../AsyncPasswordTests.swift | 0 .../AsyncRequestTests.swift | 0 .../AsyncRouteTests.swift | 0 .../AsyncSessionTests.swift | 58 +++++++ .../AsyncWebSocketTests.swift | 0 Tests/VaporTests/CacheTests.swift | 43 ----- Tests/VaporTests/ClientTests.swift | 153 ------------------ 14 files changed, 68 insertions(+), 212 deletions(-) rename Tests/{AsyncTests => VaporTests}/AsyncAuthTests.swift (100%) rename Tests/{AsyncTests => VaporTests}/AsyncCacheTests.swift (100%) rename Tests/{AsyncTests => VaporTests}/AsyncClientTests.swift (99%) rename Tests/{AsyncTests => VaporTests}/AsyncCommandsTests.swift (100%) rename Tests/{AsyncTests => VaporTests}/AsyncMiddlewareTests.swift (89%) rename Tests/{AsyncTests => VaporTests}/AsyncPasswordTests.swift (100%) rename Tests/{AsyncTests => VaporTests}/AsyncRequestTests.swift (100%) rename Tests/{AsyncTests => VaporTests}/AsyncRouteTests.swift (100%) rename Tests/{AsyncTests => VaporTests}/AsyncSessionTests.swift (57%) rename Tests/{AsyncTests => VaporTests}/AsyncWebSocketTests.swift (100%) diff --git a/Package.swift b/Package.swift index a93cfa97da..65c0007bd5 100644 --- a/Package.swift +++ b/Package.swift @@ -120,9 +120,5 @@ let package = Package( .copy("Utilities/expired.crt"), .copy("Utilities/expired.key"), ]), - .testTarget(name: "AsyncTests", dependencies: [ - .product(name: "NIOTestUtils", package: "swift-nio"), - .target(name: "XCTVapor"), - ]), ] ) diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift index 65e020b7e2..81f565c5ed 100644 --- a/Package@swift-5.9.swift +++ b/Package@swift-5.9.swift @@ -133,16 +133,5 @@ let package = Package( .enableExperimentalFeature("StrictConcurrency=complete"), ] ), - .testTarget( - name: "AsyncTests", - dependencies: [ - .product(name: "NIOTestUtils", package: "swift-nio"), - .target(name: "XCTVapor"), - ], - swiftSettings: [ - .enableUpcomingFeature("BareSlashRegexLiterals"), - .enableExperimentalFeature("StrictConcurrency=complete"), - ] - ), ] ) diff --git a/Tests/AsyncTests/AsyncAuthTests.swift b/Tests/VaporTests/AsyncAuthTests.swift similarity index 100% rename from Tests/AsyncTests/AsyncAuthTests.swift rename to Tests/VaporTests/AsyncAuthTests.swift diff --git a/Tests/AsyncTests/AsyncCacheTests.swift b/Tests/VaporTests/AsyncCacheTests.swift similarity index 100% rename from Tests/AsyncTests/AsyncCacheTests.swift rename to Tests/VaporTests/AsyncCacheTests.swift diff --git a/Tests/AsyncTests/AsyncClientTests.swift b/Tests/VaporTests/AsyncClientTests.swift similarity index 99% rename from Tests/AsyncTests/AsyncClientTests.swift rename to Tests/VaporTests/AsyncClientTests.swift index 8608860589..9f02952a56 100644 --- a/Tests/AsyncTests/AsyncClientTests.swift +++ b/Tests/VaporTests/AsyncClientTests.swift @@ -241,7 +241,7 @@ extension Application.Clients.Provider { } -private final class TestLogHandler: LogHandler { +final class TestLogHandler: LogHandler { subscript(metadataKey key: String) -> Logger.Metadata.Value? { get { self.metadata[key] } diff --git a/Tests/AsyncTests/AsyncCommandsTests.swift b/Tests/VaporTests/AsyncCommandsTests.swift similarity index 100% rename from Tests/AsyncTests/AsyncCommandsTests.swift rename to Tests/VaporTests/AsyncCommandsTests.swift diff --git a/Tests/AsyncTests/AsyncMiddlewareTests.swift b/Tests/VaporTests/AsyncMiddlewareTests.swift similarity index 89% rename from Tests/AsyncTests/AsyncMiddlewareTests.swift rename to Tests/VaporTests/AsyncMiddlewareTests.swift index 570df44a67..be605ebf9f 100644 --- a/Tests/AsyncTests/AsyncMiddlewareTests.swift +++ b/Tests/VaporTests/AsyncMiddlewareTests.swift @@ -106,4 +106,13 @@ final class AsyncMiddlewareTests: XCTestCase { XCTAssertEqual(res.headers[.accessControlAllowHeaders], [""]) } } + + func testFileMiddlewareFromBundleInvalidPublicDirectory() { + XCTAssertThrowsError(try FileMiddleware(bundle: .module, publicDirectory: "/totally-real/folder")) { error in + guard let error = error as? FileMiddleware.BundleSetupError else { + return XCTFail("Error should be of type FileMiddleware.SetupError") + } + XCTAssertEqual(error, .publicDirectoryIsNotAFolder) + } + } } diff --git a/Tests/AsyncTests/AsyncPasswordTests.swift b/Tests/VaporTests/AsyncPasswordTests.swift similarity index 100% rename from Tests/AsyncTests/AsyncPasswordTests.swift rename to Tests/VaporTests/AsyncPasswordTests.swift diff --git a/Tests/AsyncTests/AsyncRequestTests.swift b/Tests/VaporTests/AsyncRequestTests.swift similarity index 100% rename from Tests/AsyncTests/AsyncRequestTests.swift rename to Tests/VaporTests/AsyncRequestTests.swift diff --git a/Tests/AsyncTests/AsyncRouteTests.swift b/Tests/VaporTests/AsyncRouteTests.swift similarity index 100% rename from Tests/AsyncTests/AsyncRouteTests.swift rename to Tests/VaporTests/AsyncRouteTests.swift diff --git a/Tests/AsyncTests/AsyncSessionTests.swift b/Tests/VaporTests/AsyncSessionTests.swift similarity index 57% rename from Tests/AsyncTests/AsyncSessionTests.swift rename to Tests/VaporTests/AsyncSessionTests.swift index dcf53558e0..281230ca86 100644 --- a/Tests/AsyncTests/AsyncSessionTests.swift +++ b/Tests/VaporTests/AsyncSessionTests.swift @@ -81,4 +81,62 @@ final class AsyncSessionTests: XCTestCase { ]) } } + + func testInvalidCookie() throws { + let app = Application(.testing) + defer { app.shutdown() } + + // Configure sessions. + app.sessions.use(.memory) + app.middleware.use(app.sessions.middleware) + + // Adds data to the request session. + app.get("set") { req -> HTTPStatus in + req.session.data["foo"] = "bar" + return .ok + } + + // Fetches data from the request session. + app.get("get") { req -> String in + guard let foo = req.session.data["foo"] else { + throw Abort(.badRequest) + } + return foo + } + + + // Test accessing session with no cookie. + try app.test(.GET, "get") { res in + XCTAssertEqual(res.status, .badRequest) + } + + // Test setting session with invalid cookie. + var newCookie: HTTPCookies.Value? + try app.test(.GET, "set", beforeRequest: { req in + req.headers.cookie = ["vapor-session": "foo"] + }, afterResponse: { res in + // We should get a new cookie back. + newCookie = res.headers.setCookie?["vapor-session"] + XCTAssertNotNil(newCookie) + // That is not the same as the invalid cookie we sent. + XCTAssertNotEqual(newCookie?.string, "foo") + XCTAssertEqual(res.status, .ok) + }) + + // Test accessing newly created session. + try app.test(.GET, "get", beforeRequest: { req in + // Pass cookie from previous request. + req.headers.cookie = ["vapor-session": newCookie!] + }, afterResponse: { res in + // Session access should be successful. + XCTAssertEqual(res.body.string, "bar") + XCTAssertEqual(res.status, .ok) + }) + } + + func testCookieQuotes() throws { + var headers = HTTPHeaders() + headers.replaceOrAdd(name: .cookie, value: #"foo= "+cookie/value" "#) + XCTAssertEqual(headers.cookie?["foo"]?.string, "+cookie/value") + } } diff --git a/Tests/AsyncTests/AsyncWebSocketTests.swift b/Tests/VaporTests/AsyncWebSocketTests.swift similarity index 100% rename from Tests/AsyncTests/AsyncWebSocketTests.swift rename to Tests/VaporTests/AsyncWebSocketTests.swift diff --git a/Tests/VaporTests/CacheTests.swift b/Tests/VaporTests/CacheTests.swift index e97fa30de2..b3a621ed22 100644 --- a/Tests/VaporTests/CacheTests.swift +++ b/Tests/VaporTests/CacheTests.swift @@ -33,46 +33,3 @@ final class CacheTests: XCTestCase { try XCTAssertEqual(app.cache.get("foo").wait(), "bar") } } - -extension Application.Caches.Provider { - static var foo: Self { - .init { $0.caches.use { FooCache(on: $0.eventLoopGroup.next()) } } - } -} - -// Always returns "bar" for key "foo". -// That's all... -struct FooCache: Cache { - let eventLoop: EventLoop - init(on eventLoop: EventLoop) { - self.eventLoop = eventLoop - } - - func get(_ key: String, as type: T.Type) -> EventLoopFuture - where T : Decodable - { - let value: T? - if key == "foo" { - value = "bar" as? T - } else { - value = nil - } - return self.eventLoop.makeSucceededFuture(value) - } - - func set(_ key: String, to value: T?) -> EventLoopFuture where T : Encodable { - return self.eventLoop.makeSucceededFuture(()) - } - - func setToNil(_ key: String) -> EventLoopFuture { - return self.eventLoop.makeSucceededFuture(()) - } - - func set(_ key: String, to value: ExpressibleByNilLiteral?) -> EventLoopFuture { - return self.eventLoop.makeSucceededFuture(()) - } - - func `for`(_ request: Request) -> FooCache { - return self - } -} diff --git a/Tests/VaporTests/ClientTests.swift b/Tests/VaporTests/ClientTests.swift index 66ef4b7fa0..121112e268 100644 --- a/Tests/VaporTests/ClientTests.swift +++ b/Tests/VaporTests/ClientTests.swift @@ -249,156 +249,3 @@ final class ClientTests: XCTestCase { XCTAssertNotNil(metadata["ahc-request-id"]) } } - -final class CustomClient: Client, Sendable { - var eventLoop: EventLoop { - EmbeddedEventLoop() - } - let _requests: NIOLockedValueBox<[ClientRequest]> - var requests: [ClientRequest] { - get { - self._requests.withLockedValue { $0 } - } - } - - init() { - self._requests = .init([]) - } - - func send(_ request: ClientRequest) -> EventLoopFuture { - self._requests.withLockedValue { $0.append(request) } - return self.eventLoop.makeSucceededFuture(ClientResponse()) - } - - func delegating(to eventLoop: EventLoop) -> Client { - self - } -} - -extension Application { - struct CustomClientKey: StorageKey { - typealias Value = CustomClient - } - - var customClient: CustomClient { - if let existing = self.storage[CustomClientKey.self] { - return existing - } else { - let new = CustomClient() - self.storage[CustomClientKey.self] = new - return new - } - } -} - -extension Application.Clients.Provider { - static var custom: Self { - .init { - $0.clients.use { $0.customClient } - } - } -} - - -private final class TestLogHandler: LogHandler { - - subscript(metadataKey key: String) -> Logger.Metadata.Value? { - get { self.metadata[key] } - set { self.metadata[key] = newValue } - } - - var metadata: Logger.Metadata { - get { - self._metadata.withLockedValue { $0 } - } - set { - self._metadata.withLockedValue { $0 = newValue } - } - } - - var logLevel: Logger.Level { - get { - _logLevel - } - set { - // We don't use this anywhere - } - } - - var messages: [Logger.Message] { - get { - self._messages.withLockedValue { $0 } - } - set { - self._messages.withLockedValue { $0 = newValue } - } - } - - let _logLevel: Logger.Level - let _metadata: NIOLockedValueBox - let _messages: NIOLockedValueBox<[Logger.Message]> - - var logger: Logger { - .init(label: "test") { label in - self - } - } - - init() { - self._metadata = .init([:]) - self._logLevel = .trace - self._messages = .init([]) - } - - func log( - level: Logger.Level, - message: Logger.Message, - metadata: Logger.Metadata?, - source: String, - file: String, - function: String, - line: UInt - ) { - self._messages.withLockedValue { $0.append(message) } - } - - func read() -> [String] { - self._messages.withLockedValue { - let copy = $0 - $0 = [] - return copy.map(\.description) - } - } - - func getMetadata() -> Logger.Metadata { - self._metadata.withLockedValue { $0 } - } -} - - -struct SomeJSON: Content { - let vapor: SomeNestedJSON - - init() { - vapor = SomeNestedJSON(name: "The Vapor Project", age: 7, repos: [ - VaporRepoJSON(name: "WebsocketKit", url: "https://github.com/vapor/websocket-kit"), - VaporRepoJSON(name: "PostgresNIO", url: "https://github.com/vapor/postgres-nio") - ]) - } -} - -struct SomeNestedJSON: Content { - let name: String - let age: Int - let repos: [VaporRepoJSON] -} - -struct VaporRepoJSON: Content { - let name: String - let url: String -} - -struct AnythingResponse: Content { - var headers: [String: String] - var json: [String: String] -} From fa44af085089d88fd8bc49bf0e820ec8ca708e83 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Feb 2024 00:06:48 +0000 Subject: [PATCH 04/18] Bump the dependencies group with 1 update (#3148) --- .github/workflows/sponsors.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sponsors.yml b/.github/workflows/sponsors.yml index 3a6f3c352d..897995bcfc 100644 --- a/.github/workflows/sponsors.yml +++ b/.github/workflows/sponsors.yml @@ -26,7 +26,7 @@ jobs: organization: true - name: Commit and create PR - uses: peter-evans/create-pull-request@v5 + uses: peter-evans/create-pull-request@v6 with: token: ${{ steps.get-token.outputs.token }} commit-message: "[skip ci] Update README with new sponsor" From 664a06346c5429025d3ffdfa63b93978a8cdd096 Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Wed, 7 Feb 2024 08:02:20 -0600 Subject: [PATCH 05/18] Fix handling of "flag" URL query params (#3151) Fix handling of "flag" URL query params (e.g. `/foo?bar&baz`). Fix a Sendable warning. --- .../Content/ContainerGetPathExecutor.swift | 12 ++++++++-- Sources/Vapor/Utilities/RFC1123.swift | 10 +++++++- Tests/VaporTests/QueryTests.swift | 23 +++++++++++++++++++ 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/Sources/Vapor/Content/ContainerGetPathExecutor.swift b/Sources/Vapor/Content/ContainerGetPathExecutor.swift index 6ff619b740..7615161499 100644 --- a/Sources/Vapor/Content/ContainerGetPathExecutor.swift +++ b/Sources/Vapor/Content/ContainerGetPathExecutor.swift @@ -11,13 +11,21 @@ internal struct ContainerGetPathExecutor: Decodable { throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "Container getter couldn't find keypath to fetch (broken Decoder?)")) } - self.result = try keypath.reduce(decoder) { + let lastDecoder = try keypath.dropLast().reduce(decoder) { if let index = $1.intValue { return try $0.unkeyedContainer(startingAt: index)._unsafe_inplace_superDecoder() } else { return try $0.container(keyedBy: BasicCodingKey.self).superDecoder(forKey: .key($1.stringValue)) } - }.singleValueContainer().decode(D.self) + } + if let index = keypath.last?.intValue { + var container = try lastDecoder.unkeyedContainer(startingAt: index) + self.result = try container.decode(D.self) + } else if let key = keypath.last?.stringValue { + self.result = try lastDecoder.container(keyedBy: BasicCodingKey.self).decode(D.self, forKey: .key(key)) + } else { + self.result = try lastDecoder.singleValueContainer().decode(D.self) + } } } diff --git a/Sources/Vapor/Utilities/RFC1123.swift b/Sources/Vapor/Utilities/RFC1123.swift index 63cad187aa..eb4156b4b8 100644 --- a/Sources/Vapor/Utilities/RFC1123.swift +++ b/Sources/Vapor/Utilities/RFC1123.swift @@ -1,5 +1,13 @@ -#if canImport(Glibc) && swift(>=5.10) +#if swift(>=5.10) +#if canImport(Darwin) +@preconcurrency import Darwin +#elseif canImport(Glibc) @preconcurrency import Glibc +#elseif canImport(Musl) +@preconcurrency import Musl +#elseif canImport(WinSDK) +@preconcurrency import WinSDK +#endif #endif import Foundation import NIOPosix diff --git a/Tests/VaporTests/QueryTests.swift b/Tests/VaporTests/QueryTests.swift index 273339b534..48b5af0a7c 100644 --- a/Tests/VaporTests/QueryTests.swift +++ b/Tests/VaporTests/QueryTests.swift @@ -300,4 +300,27 @@ final class QueryTests: XCTestCase { XCTAssertThrowsError(try req.query.get(Int?.self, at: "page")) } } + + func testValuelessParamGet() throws { + let app = Application() + defer { app.shutdown() } + let req = Request( + application: app, + method: .GET, + url: URI(string: "/"), + on: app.eventLoopGroup.next() + ) + + req.url = .init(path: "/foo?bar") + XCTAssertTrue(try req.query.get(Bool.self, at: "bar")) + + req.url = .init(path: "/foo?bar&baz=bop") + XCTAssertTrue(try req.query.get(Bool.self, at: "bar")) + + req.url = .init(path: "/foo") + XCTAssertFalse(try req.query.get(Bool.self, at: "bar")) + + req.url = .init(path: "/foo?baz=bop") + XCTAssertFalse(try req.query.get(Bool.self, at: "bar")) + } } From 9da9d14f43bc1b32b384f0b4eb231b8a5a851dee Mon Sep 17 00:00:00 2001 From: Tim Condon <0xTim@users.noreply.github.com> Date: Wed, 14 Feb 2024 11:34:56 +0000 Subject: [PATCH 06/18] Fix issue when client disconnects midway through a stream (#3102) Use a lock instead of loop bound to fix crash on deinit Co-authored-by: Gwynne Raskind --- Sources/Vapor/Request/Request+BodyStream.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/Vapor/Request/Request+BodyStream.swift b/Sources/Vapor/Request/Request+BodyStream.swift index 204dab202a..09300f46fd 100644 --- a/Sources/Vapor/Request/Request+BodyStream.swift +++ b/Sources/Vapor/Request/Request+BodyStream.swift @@ -16,13 +16,13 @@ extension Request { var buffer: [(BodyStreamResult, EventLoopPromise?)] } - private let isClosed: NIOLoopBoundBox + private let isClosed: NIOLockedValueBox private let handlerBuffer: NIOLoopBoundBox private let allocator: ByteBufferAllocator init(on eventLoop: EventLoop, byteBufferAllocator: ByteBufferAllocator) { self.eventLoop = eventLoop - self.isClosed = .init(false, eventLoop: eventLoop) + self.isClosed = .init(false) self.handlerBuffer = .init(.init(handler: nil, buffer: []), eventLoop: eventLoop) self.allocator = byteBufferAllocator } @@ -60,7 +60,7 @@ extension Request { private func write0(_ chunk: BodyStreamResult, promise: EventLoopPromise?) { switch chunk { case .end, .error: - self.isClosed.value = true + self.isClosed.withLockedValue { $0 = true } case .buffer: break } @@ -101,7 +101,7 @@ extension Request { } deinit { - assert(self.isClosed.value, "Request.BodyStream deinitialized before closing.") + assert(self.isClosed.withLockedValue { $0 }, "Request.BodyStream deinitialized before closing.") } } } From 3a7da193a2937472b252b8db210897e7abf37b47 Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Tue, 20 Feb 2024 05:51:27 -0800 Subject: [PATCH 07/18] Allow `HTTPServer`'s configuration to be dynamically updatable (#3132) * Updated HTTPServer's configuration to be dynamically updatable Fixes #3130 * Added a check for a specific thrown error in updating-config tests * Updated HTTPServer comments to be uniform --- .../HTTP/Server/Application+HTTP+Server.swift | 18 +++- Sources/Vapor/HTTP/Server/HTTPServer.swift | 89 ++++++++++++------- Tests/VaporTests/ServerTests.swift | 86 +++++++++++++++++- 3 files changed, 160 insertions(+), 33 deletions(-) diff --git a/Sources/Vapor/HTTP/Server/Application+HTTP+Server.swift b/Sources/Vapor/HTTP/Server/Application+HTTP+Server.swift index fe34ead842..3261b40edb 100644 --- a/Sources/Vapor/HTTP/Server/Application+HTTP+Server.swift +++ b/Sources/Vapor/HTTP/Server/Application+HTTP+Server.swift @@ -33,6 +33,18 @@ extension Application.HTTP { typealias Value = HTTPServer } + /// The configuration for the HTTP server. + /// + /// Although the configuration can be changed after the server has started, a warning will be logged + /// and the configuration will be discarded if an option will no longer be considered. + /// + /// These include the following properties, which are only read once when the server starts: + /// - ``HTTPServer/Configuration-swift.struct/address`` + /// - ``HTTPServer/Configuration-swift.struct/hostname`` + /// - ``HTTPServer/Configuration-swift.struct/port`` + /// - ``HTTPServer/Configuration-swift.struct/backlog`` + /// - ``HTTPServer/Configuration-swift.struct/reuseAddress`` + /// - ``HTTPServer/Configuration-swift.struct/tcpNoDelay`` public var configuration: HTTPServer.Configuration { get { self.application.storage[ConfigurationKey.self] ?? .init( @@ -40,8 +52,10 @@ extension Application.HTTP { ) } nonmutating set { - if self.application.storage.contains(Key.self) { - self.application.logger.warning("Cannot modify server configuration after server has been used.") + /// If a server is available, configure it directly, otherwise cache a configuration instance + /// here to be used until the server is instantiated. + if let server = self.application.storage[Key.self] { + server.configuration = newValue } else { self.application.storage[ConfigurationKey.self] = newValue } diff --git a/Sources/Vapor/HTTP/Server/HTTPServer.swift b/Sources/Vapor/HTTP/Server/HTTPServer.swift index 135fa752fa..f9a9927984 100644 --- a/Sources/Vapor/HTTP/Server/HTTPServer.swift +++ b/Sources/Vapor/HTTP/Server/HTTPServer.swift @@ -237,11 +237,32 @@ public final class HTTPServer: Server, Sendable { return connection.channel.closeFuture } + /// The configuration for the HTTP server. + /// + /// Many properties of the configuration may be changed both before and after the server has been started. + /// + /// However, a warning will be logged and the configuration will be discarded if an option could not be + /// changed after the server has started. These include the following properties, which are only read + /// once when the server starts: + /// - ``Configuration-swift.struct/address`` + /// - ``Configuration-swift.struct/hostname`` + /// - ``Configuration-swift.struct/port`` + /// - ``Configuration-swift.struct/backlog`` + /// - ``Configuration-swift.struct/reuseAddress`` + /// - ``Configuration-swift.struct/tcpNoDelay`` public var configuration: Configuration { get { _configuration.withLockedValue { $0 } } set { - guard !didStart.withLockedValue({ $0 }) else { - _configuration.withLockedValue({ $0 }).logger.warning("Cannot modify server configuration after server has been started.") + let oldValue = _configuration.withLockedValue { $0 } + + let canBeUpdatedDynamically = + oldValue.address == newValue.address + && oldValue.backlog == newValue.backlog + && oldValue.reuseAddress == newValue.reuseAddress + && oldValue.tcpNoDelay == newValue.tcpNoDelay + + guard canBeUpdatedDynamically || !didStart.withLockedValue({ $0 }) else { + oldValue.logger.warning("Cannot modify server configuration after server has been started.") return } self.application.storage[Application.HTTP.Server.ConfigurationKey.self] = newValue @@ -276,15 +297,18 @@ public final class HTTPServer: Server, Sendable { var configuration = self.configuration switch address { - case .none: // use the configuration as is + case .none: + /// Use the configuration as is. break - case .hostname(let hostname, let port): // override the hostname, port, neither, or both + case .hostname(let hostname, let port): + /// Override the hostname, port, neither, or both. configuration.address = .hostname(hostname ?? configuration.hostname, port: port ?? configuration.port) - case .unixDomainSocket: // override the socket path + case .unixDomainSocket: + /// Override the socket path. configuration.address = address! } - // print starting message + /// Print starting message. let scheme = configuration.tlsConfiguration == nil ? "http" : "https" let addressDescription: String switch configuration.address { @@ -296,10 +320,11 @@ public final class HTTPServer: Server, Sendable { self.configuration.logger.notice("Server starting on \(addressDescription)") - // start the actual HTTPServer + /// Start the actual `HTTPServer`. try self.connection.withLockedValue { $0 = try HTTPServerConnection.start( application: self.application, + server: self, responder: self.responder, configuration: configuration, on: self.eventLoopGroup @@ -341,26 +366,30 @@ private final class HTTPServerConnection: Sendable { static func start( application: Application, + server: HTTPServer, responder: Responder, configuration: HTTPServer.Configuration, on eventLoopGroup: EventLoopGroup ) -> EventLoopFuture { let quiesce = ServerQuiescingHelper(group: eventLoopGroup) let bootstrap = ServerBootstrap(group: eventLoopGroup) - // Specify backlog and enable SO_REUSEADDR for the server itself + /// Specify backlog and enable `SO_REUSEADDR` for the server itself. .serverChannelOption(ChannelOptions.backlog, value: Int32(configuration.backlog)) .serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: configuration.reuseAddress ? SocketOptionValue(1) : SocketOptionValue(0)) - // Set handlers that are applied to the Server's channel + /// Set handlers that are applied to the Server's channel. .serverChannelInitializer { channel in channel.pipeline.addHandler(quiesce.makeServerChannelHandler(channel: channel)) } - // Set the handlers that are applied to the accepted Channels - .childChannelInitializer { [unowned application] channel in - // add TLS handlers if configured + /// Set the handlers that are applied to the accepted Channels. + .childChannelInitializer { [unowned application, unowned server] channel in + /// Copy the most up-to-date configuration. + let configuration = server.configuration + + /// Add TLS handlers if configured. if var tlsConfiguration = configuration.tlsConfiguration { - // prioritize http/2 + /// Prioritize http/2 if supported. if configuration.supportVersions.contains(.two) { tlsConfiguration.applicationProtocols.append("h2") } @@ -408,7 +437,7 @@ private final class HTTPServerConnection: Sendable { } } - // Enable TCP_NODELAY and SO_REUSEADDR for the accepted Channels + /// Enable `TCP_NODELAY` and `SO_REUSEADDR` for the accepted Channels. .childChannelOption(ChannelOptions.socket(IPPROTO_TCP, TCP_NODELAY), value: configuration.tcpNoDelay ? SocketOptionValue(1) : SocketOptionValue(0)) .childChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: configuration.reuseAddress ? SocketOptionValue(1) : SocketOptionValue(0)) .childChannelOption(ChannelOptions.maxMessagesPerRead, value: 1) @@ -468,31 +497,31 @@ extension ChannelPipeline { responder: Responder, configuration: HTTPServer.Configuration ) -> EventLoopFuture { - // create server pipeline array + /// Create server pipeline array. var handlers: [ChannelHandler] = [] let http2 = HTTP2FramePayloadToHTTP1ServerCodec() handlers.append(http2) - // add NIO -> HTTP request decoder + /// Add NIO → HTTP request decoder. let serverReqDecoder = HTTPServerRequestDecoder( application: application ) handlers.append(serverReqDecoder) - // add NIO -> HTTP response encoder + /// Add NIO → HTTP response encoder. let serverResEncoder = HTTPServerResponseEncoder( serverHeader: configuration.serverName, dateCache: .eventLoop(self.eventLoop) ) handlers.append(serverResEncoder) - // add server request -> response delegate + /// Add server request → response delegate. let handler = HTTPServerHandler(responder: responder, logger: application.logger) handlers.append(handler) return self.addHandlers(handlers).flatMap { - // close the connection in case of any errors + /// Close the connection in case of any errors. self.addHandler(NIOCloseOnErrorHandler()) } } @@ -502,24 +531,24 @@ extension ChannelPipeline { responder: Responder, configuration: HTTPServer.Configuration ) -> EventLoopFuture { - // create server pipeline array + /// Create server pipeline array. var handlers: [RemovableChannelHandler] = [] - // configure HTTP/1 - // add http parsing and serializing + /// Configure HTTP/1: + /// Add http parsing and serializing. let httpResEncoder = HTTPResponseEncoder() let httpReqDecoder = ByteToMessageHandler(HTTPRequestDecoder( leftOverBytesStrategy: .forwardBytes )) handlers += [httpResEncoder, httpReqDecoder] - // add pipelining support if configured + /// Add pipelining support if configured. if configuration.supportPipelining { let pipelineHandler = HTTPServerPipelineHandler() handlers.append(pipelineHandler) } - // add response compressor if configured + /// Add response compressor if configured. switch configuration.responseCompression.storage { case .enabled(let initialByteBufferCapacity): let responseCompressionHandler = HTTPResponseCompressor( @@ -530,7 +559,7 @@ extension ChannelPipeline { break } - // add request decompressor if configured + /// Add request decompressor if configured. switch configuration.requestDecompression.storage { case .enabled(let limit): let requestDecompressionHandler = NIOHTTPRequestDecompressor( @@ -541,22 +570,22 @@ extension ChannelPipeline { break } - // add NIO -> HTTP response encoder + /// Add NIO → HTTP response encoder. let serverResEncoder = HTTPServerResponseEncoder( serverHeader: configuration.serverName, dateCache: .eventLoop(self.eventLoop) ) handlers.append(serverResEncoder) - // add NIO -> HTTP request decoder + /// Add NIO → HTTP request decoder. let serverReqDecoder = HTTPServerRequestDecoder( application: application ) handlers.append(serverReqDecoder) - // add server request -> response delegate + /// Add server request → response delegate. let handler = HTTPServerHandler(responder: responder, logger: application.logger) - // add HTTP upgrade handler + /// Add HTTP upgrade handler. let upgrader = HTTPServerUpgradeHandler( httpRequestDecoder: httpReqDecoder, httpHandlers: handlers + [handler] @@ -566,7 +595,7 @@ extension ChannelPipeline { handlers.append(handler) return self.addHandlers(handlers).flatMap { - // close the connection in case of any errors + /// Close the connection in case of any errors. self.addHandler(NIOCloseOnErrorHandler()) } } diff --git a/Tests/VaporTests/ServerTests.swift b/Tests/VaporTests/ServerTests.swift index e1f91aa503..d4173031c5 100644 --- a/Tests/VaporTests/ServerTests.swift +++ b/Tests/VaporTests/ServerTests.swift @@ -931,7 +931,6 @@ final class ServerTests: XCTestCase { // This lies and accepts the above cert, which has actually expired. XCTAssertEqual(peerCerts, [cert]) successPromise.succeed(.certificateVerified) - } // We need to disable verification on the client, because the cert we're using has expired, and we want to @@ -967,6 +966,91 @@ final class ServerTests: XCTestCase { XCTAssertEqual(a.body, ByteBuffer(string: "world")) } + func testCanChangeConfigurationDynamically() throws { + guard let clientCertPath = Bundle.module.url(forResource: "expired", withExtension: "crt"), + let clientKeyPath = Bundle.module.url(forResource: "expired", withExtension: "key") else { + XCTFail("Cannot load expired cert and associated key") + return + } + + let cert = try NIOSSLCertificate(file: clientCertPath.path, format: .pem) + let key = try NIOSSLPrivateKey(file: clientKeyPath.path, format: .pem) + + let app = Application(.testing) + + app.http.server.configuration.hostname = "127.0.0.1" + app.http.server.configuration.port = 0 + app.http.server.configuration.serverName = "Old" + + /// We need to disable verification on the client, because the cert we're using has expired + var clientConfig = TLSConfiguration.makeClientConfiguration() + clientConfig.certificateVerification = .none + clientConfig.certificateChain = [.certificate(cert)] + clientConfig.privateKey = .privateKey(key) + app.http.client.configuration.tlsConfiguration = clientConfig + app.http.client.configuration.maximumUsesPerConnection = 1 + + app.environment.arguments = ["serve"] + + app.get("hello") { req in + "world" + } + + defer { app.shutdown() } + try app.start() + + XCTAssertNotNil(app.http.server.shared.localAddress) + guard let localAddress = app.http.server.shared.localAddress, + let ip = localAddress.ipAddress, + let port = localAddress.port else { + XCTFail("couldn't get ip/port from \(app.http.server.shared.localAddress.debugDescription)") + return + } + + /// Make a regular request + let a = try app.http.client.shared.execute( + request: try HTTPClient.Request( + url: "http://\(ip):\(port)/hello", + method: .GET + ) + ).wait() + XCTAssertEqual(a.headers[.server], ["Old"]) + XCTAssertEqual(a.body, ByteBuffer(string: "world")) + + /// Configure server name without stopping the server + app.http.server.configuration.serverName = "New" + /// Configure TLS without stopping the server + var serverConfig = TLSConfiguration.makeServerConfiguration(certificateChain: [.certificate(cert)], privateKey: .privateKey(key)) + serverConfig.certificateVerification = .noHostnameVerification + + app.http.server.configuration.tlsConfiguration = serverConfig + app.http.server.configuration.customCertificateVerifyCallback = { peerCerts, successPromise in + /// This lies and accepts the above cert, which has actually expired. + XCTAssertEqual(peerCerts, [cert]) + successPromise.succeed(.certificateVerified) + } + + /// Make a TLS request this time around + let b = try app.http.client.shared.execute( + request: try HTTPClient.Request( + url: "https://\(ip):\(port)/hello", + method: .GET + ) + ).wait() + XCTAssertEqual(b.headers[.server], ["New"]) + XCTAssertEqual(b.body, ByteBuffer(string: "world")) + + /// Non-TLS request should now fail + XCTAssertThrowsError(try app.http.client.shared.execute( + request: try HTTPClient.Request( + url: "http://\(ip):\(port)/hello", + method: .GET + ) + ).wait()) { error in + XCTAssertEqual(error as? HTTPClientError, HTTPClientError.remoteConnectionClosed) + } + } + override class func setUp() { XCTAssertTrue(isLoggingConfigured) } From 11cdb29614a5c7f8c5289f3c97b3398c3d89b395 Mon Sep 17 00:00:00 2001 From: Iceman Date: Wed, 20 Mar 2024 22:18:57 +0900 Subject: [PATCH 08/18] Fix some Sendable warnings on 5.10 (#3158) --- Sources/Vapor/HTTP/Headers/HTTPHeaders+Connection.swift | 2 +- Sources/Vapor/HTTP/Headers/HTTPHeaders+ContentDisposition.swift | 2 +- Sources/Vapor/HTTP/Headers/HTTPHeaders+Link.swift | 2 +- Sources/Vapor/HTTP/Headers/HTTPHeaders+Name.swift | 2 +- Sources/Vapor/Utilities/BaseN.swift | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/Vapor/HTTP/Headers/HTTPHeaders+Connection.swift b/Sources/Vapor/HTTP/Headers/HTTPHeaders+Connection.swift index bc62027f3c..6dfc2625de 100644 --- a/Sources/Vapor/HTTP/Headers/HTTPHeaders+Connection.swift +++ b/Sources/Vapor/HTTP/Headers/HTTPHeaders+Connection.swift @@ -1,7 +1,7 @@ import NIOHTTP1 extension HTTPHeaders { - public struct Connection: ExpressibleByStringLiteral, Equatable { + public struct Connection: ExpressibleByStringLiteral, Equatable, Sendable { public static let close: Self = "close" public static let keepAlive: Self = "keep-alive" diff --git a/Sources/Vapor/HTTP/Headers/HTTPHeaders+ContentDisposition.swift b/Sources/Vapor/HTTP/Headers/HTTPHeaders+ContentDisposition.swift index 8e507ae430..bac04490bf 100644 --- a/Sources/Vapor/HTTP/Headers/HTTPHeaders+ContentDisposition.swift +++ b/Sources/Vapor/HTTP/Headers/HTTPHeaders+ContentDisposition.swift @@ -20,7 +20,7 @@ extension HTTPHeaders { } public struct ContentDisposition { - public struct Value: Equatable { + public struct Value: Equatable, Sendable { public static let inline = Value(string: "inline") public static let attachment = Value(string: "attachment") public static let formData = Value(string: "form-data") diff --git a/Sources/Vapor/HTTP/Headers/HTTPHeaders+Link.swift b/Sources/Vapor/HTTP/Headers/HTTPHeaders+Link.swift index 82b312db9f..d14ed81755 100644 --- a/Sources/Vapor/HTTP/Headers/HTTPHeaders+Link.swift +++ b/Sources/Vapor/HTTP/Headers/HTTPHeaders+Link.swift @@ -21,7 +21,7 @@ extension HTTPHeaders { // TODO: Support multiple relations in a single `rel` attribute, as permitted by spec. public struct Link { /// See https://www.iana.org/assignments/link-relations/link-relations.xhtml - public struct Relation: RawRepresentable, Hashable { + public struct Relation: RawRepresentable, Hashable, Sendable { public static let about = Relation("about") public static let alternate = Relation("alternate") public static let appendix = Relation("appendix") diff --git a/Sources/Vapor/HTTP/Headers/HTTPHeaders+Name.swift b/Sources/Vapor/HTTP/Headers/HTTPHeaders+Name.swift index bd9a669b50..e88fdcbdb3 100644 --- a/Sources/Vapor/HTTP/Headers/HTTPHeaders+Name.swift +++ b/Sources/Vapor/HTTP/Headers/HTTPHeaders+Name.swift @@ -2,7 +2,7 @@ import NIOHTTP1 extension HTTPHeaders { /// Type used for the name of a HTTP header in the `HTTPHeaders` storage. - public struct Name: Codable, Hashable, Equatable, CustomStringConvertible, ExpressibleByStringLiteral { + public struct Name: Codable, Hashable, Equatable, CustomStringConvertible, ExpressibleByStringLiteral, Sendable { /// See `Hashable` public func hash(into hasher: inout Hasher) { self.lowercased.hash(into: &hasher) diff --git a/Sources/Vapor/Utilities/BaseN.swift b/Sources/Vapor/Utilities/BaseN.swift index 9d46927f95..e7847223be 100644 --- a/Sources/Vapor/Utilities/BaseN.swift +++ b/Sources/Vapor/Utilities/BaseN.swift @@ -7,7 +7,7 @@ import Algorithms import struct Foundation.Data -public struct BaseNEncoding { +public struct BaseNEncoding: Sendable { /// For a given base and count, calculate the number of values needed to encode the given count of bytes. @inlinable internal static func sizeEnc(for bits: Int, count: Int) -> Int { From 8409c3c296e7f8965df14e906dbcd44eb68f54a9 Mon Sep 17 00:00:00 2001 From: Takumi Muraishi Date: Mon, 8 Apr 2024 22:03:48 +0900 Subject: [PATCH 09/18] Fix typos across the codebase (#3162) * fix docs in .github * fix typo in Authentication * fix typo in Concurrency * fix typo in Content * fix typos logger label * fix typo caseInsensitive * fix typo in HTTP * fix typo in Request * fix typo in URLEncodedForm * fix typo in Utilities * fix typo in Validation * fix some typos in Tests * Revert "fix typo caseInsensitive" This reverts commit 0950033073ca1140bc68ac47088dad02f358b2b5. --- .github/contributing.md | 2 +- .github/maintainers.md | 2 +- Sources/Vapor/Authentication/AuthenticationCache.swift | 2 +- Sources/Vapor/Concurrency/RequestBody+Concurrency.swift | 4 ++-- Sources/Vapor/Content/ContentCoders.swift | 4 ++-- Sources/Vapor/Content/PlaintextEncoder.swift | 2 +- Sources/Vapor/Deprecations/DotEnvFile+load.swift | 4 ++-- Sources/Vapor/HTTP/Headers/HTTPCookies.swift | 4 ++-- Sources/Vapor/Request/Request.swift | 2 +- Sources/Vapor/URLEncodedForm/URLEncodedFormSerializer.swift | 2 +- Sources/Vapor/Utilities/BaseN.swift | 4 ++-- Sources/Vapor/Utilities/DotEnv.swift | 4 ++-- Sources/Vapor/Validation/RangeResult.swift | 2 +- Sources/Vapor/Validation/Validators/Range.swift | 2 +- Tests/VaporTests/AsyncMiddlewareTests.swift | 2 +- Tests/VaporTests/AsyncPasswordTests.swift | 4 ++-- Tests/VaporTests/AsyncRequestTests.swift | 6 +++--- Tests/VaporTests/MiddlewareTests.swift | 2 +- Tests/VaporTests/PasswordTests.swift | 4 ++-- Tests/VaporTests/RequestTests.swift | 2 +- Tests/VaporTests/Utilities/CapturingMetricsSystem.swift | 2 +- 21 files changed, 31 insertions(+), 31 deletions(-) diff --git a/.github/contributing.md b/.github/contributing.md index 7e24b5968d..28e34309c7 100644 --- a/.github/contributing.md +++ b/.github/contributing.md @@ -84,7 +84,7 @@ The release title should be concise description. For example: - ✅ Add case-insensitive routing - ✅ Fix `routes` command symbol usage -The release titles should use sentence capitialization and not be too verbose. They should also use present tense. +The release titles should use sentence capitalization and not be too verbose. They should also use present tense. - ❌ Fix `routes` Command Symbol Usage - ❌ Add new method on RouteBuilder called `caseInsensitive` which can be used to enable case-insensitive routing diff --git a/.github/maintainers.md b/.github/maintainers.md index 03c86cf348..89600f7ea2 100644 --- a/.github/maintainers.md +++ b/.github/maintainers.md @@ -6,7 +6,7 @@ maintainers will determine if you are a good fit and either accept and merge the to be taken in order to be accepted. ## Perks -- Special "Maintainer" role in Discord with purple highlght. +- Special "Maintainer" role in Discord with purple highlight. - Write access to maintained repo (including approve and merge permissions). - Invite to "Maintainers" team on GitHub. - Opportunity to actively participate in deciding on new features in package. diff --git a/Sources/Vapor/Authentication/AuthenticationCache.swift b/Sources/Vapor/Authentication/AuthenticationCache.swift index e52a42f37f..dce9829490 100755 --- a/Sources/Vapor/Authentication/AuthenticationCache.swift +++ b/Sources/Vapor/Authentication/AuthenticationCache.swift @@ -104,7 +104,7 @@ extension Request.Authentication { // multiple places. But given how Vapor and its users use Authentication this should almost never // occur and it was decided the trade-off was acceptable // As the name implies, the usage of this is unsafe because it disables the sendable checking of the -// compiler and does not add any synchronisation. +// compiler and does not add any synchronization. @usableFromInline internal struct UnsafeAuthenticationBox: @unchecked Sendable { @usableFromInline diff --git a/Sources/Vapor/Concurrency/RequestBody+Concurrency.swift b/Sources/Vapor/Concurrency/RequestBody+Concurrency.swift index 5f2b0778fb..b4b9a21a89 100644 --- a/Sources/Vapor/Concurrency/RequestBody+Concurrency.swift +++ b/Sources/Vapor/Concurrency/RequestBody+Concurrency.swift @@ -120,7 +120,7 @@ extension Request.Body: AsyncSequence { /// Generates an `AsyncIterator` to stream the body’s content as /// `ByteBuffer` sequences. This implementation supports backpressure using /// `NIOAsyncSequenceProducerBackPressureStrategies` - /// - Returns: `AsyncIterator` containing the `Requeset.Body` as a + /// - Returns: `AsyncIterator` containing the `Request.Body` as a /// `ByteBuffer` sequence public func makeAsyncIterator() -> AsyncIterator { let delegate = AsyncSequenceDelegate(eventLoop: request.eventLoop) @@ -159,7 +159,7 @@ extension Request.Body: AsyncSequence { // return the future that we will fulfill eventually. return promise.futureResult case .produceMore: - // We can produce more immidately. Return a succeeded future. + // We can produce more immediately. Return a succeeded future. return request.eventLoop.makeSucceededVoidFuture() } case .error(let error): diff --git a/Sources/Vapor/Content/ContentCoders.swift b/Sources/Vapor/Content/ContentCoders.swift index 509044d57b..2ba3aa5883 100644 --- a/Sources/Vapor/Content/ContentCoders.swift +++ b/Sources/Vapor/Content/ContentCoders.swift @@ -27,7 +27,7 @@ public protocol ContentEncoder { /// Conform a type to this protocol to make it usable for decoding data via Vapor's ``ContentConfiguration`` system. public protocol ContentDecoder { - /// Legacy "decode object" method. The provided ``NIOCore/ByteBuffer`` should be decoded as a vaule of the given + /// Legacy "decode object" method. The provided ``NIOCore/ByteBuffer`` should be decoded as a value of the given /// type, optionally guided by the provided ``NIOHTTP1/HTTPHeaders``. /// /// Most decoders should implement this method by simply forwarding it to the decoder userInfo-aware version below, @@ -36,7 +36,7 @@ public protocol ContentDecoder { func decode(_ decodable: D.Type, from body: ByteBuffer, headers: HTTPHeaders) throws -> D where D: Decodable - /// "Decode object" method. The provided ``NIOCore/ByteBuffer`` should be decoded as a vaule of the given type, + /// "Decode object" method. The provided ``NIOCore/ByteBuffer`` should be decoded as a value of the given type, /// optionally guided by the provided ``NIOHTTP1/HTTPHeaders``. The provided ``userInfo`` dictionary must be /// forwarded to the underlying ``Swift/Decoder`` used to perform the decoding operation. /// diff --git a/Sources/Vapor/Content/PlaintextEncoder.swift b/Sources/Vapor/Content/PlaintextEncoder.swift index 266e82ab91..d2b27c6a0c 100644 --- a/Sources/Vapor/Content/PlaintextEncoder.swift +++ b/Sources/Vapor/Content/PlaintextEncoder.swift @@ -98,7 +98,7 @@ private final class _PlaintextEncoder: Encoder, SingleValueEncodingContainer { } } - /// This ridiculosity is a workaround for the inability of encoders to throw errors in various places. It's still better than fatalError()ing. + /// This ridiculously is a workaround for the inability of encoders to throw errors in various places. It's still better than fatalError()ing. struct FailureEncoder: Encoder, KeyedEncodingContainerProtocol, UnkeyedEncodingContainer, SingleValueEncodingContainer { let codingPath = [CodingKey](), userInfo = [CodingUserInfoKey: Any](), count = 0 var error: EncodingError { .invalidValue((), .init(codingPath: [], debugDescription: "Plaintext encoding does not support nesting.")) } diff --git a/Sources/Vapor/Deprecations/DotEnvFile+load.swift b/Sources/Vapor/Deprecations/DotEnvFile+load.swift index 774d963d42..4602ba50e2 100644 --- a/Sources/Vapor/Deprecations/DotEnvFile+load.swift +++ b/Sources/Vapor/Deprecations/DotEnvFile+load.swift @@ -19,7 +19,7 @@ extension DotEnvFile { public static func load( for environment: Environment = .development, on eventLoopGroupProvider: Application.EventLoopGroupProvider = .singleton, - logger: Logger = Logger(label: "dot-env-loggger") + logger: Logger = Logger(label: "dot-env-logger") ) { let threadPool = NIOThreadPool(numberOfThreads: 1) threadPool.start() @@ -51,7 +51,7 @@ extension DotEnvFile { public static func load( path: String, on eventLoopGroupProvider: Application.EventLoopGroupProvider = .singleton, - logger: Logger = Logger(label: "dot-env-loggger") + logger: Logger = Logger(label: "dot-env-logger") ) { let threadPool = NIOThreadPool(numberOfThreads: 1) threadPool.start() diff --git a/Sources/Vapor/HTTP/Headers/HTTPCookies.swift b/Sources/Vapor/HTTP/Headers/HTTPCookies.swift index aca7a28860..1a287e5008 100644 --- a/Sources/Vapor/HTTP/Headers/HTTPCookies.swift +++ b/Sources/Vapor/HTTP/Headers/HTTPCookies.swift @@ -193,7 +193,7 @@ public struct HTTPCookies: ExpressibleByDictionaryLiteral, Sendable { // MARK: Methods - /// Seriaizes an `HTTPCookie` to a `String`. + /// Serializes an `HTTPCookie` to a `String`. public func serialize(name: String) -> String { var serialized = "\(name)=\(self.string)" @@ -264,7 +264,7 @@ public struct HTTPCookies: ExpressibleByDictionaryLiteral, Sendable { // MARK: Serialize - /// Seriaizes the `Cookies` for a `Request` + /// Serializes the `Cookies` for a `Request` var cookieHeader: String? { guard !self.cookies.isEmpty else { return nil diff --git a/Sources/Vapor/Request/Request.swift b/Sources/Vapor/Request/Request.swift index 8d9f340777..0969648bf1 100644 --- a/Sources/Vapor/Request/Request.swift +++ b/Sources/Vapor/Request/Request.swift @@ -57,7 +57,7 @@ public final class Request: CustomStringConvertible, Sendable { /// A unique ID for the request. /// /// The request identifier is set to value of the `X-Request-Id` header when present, or to a - /// uniquelly generated value otherwise. + /// uniquely generated value otherwise. public let id: String // MARK: Metadata diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormSerializer.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormSerializer.swift index 2a0676b222..d35ac517dd 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormSerializer.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormSerializer.swift @@ -53,7 +53,7 @@ extension Array where Element == CodingKey { } } -// MARK: Utilties +// MARK: Utilities extension String { /// Prepares a `String` for inclusion in form-urlencoded data. diff --git a/Sources/Vapor/Utilities/BaseN.swift b/Sources/Vapor/Utilities/BaseN.swift index e7847223be..2243206b1f 100644 --- a/Sources/Vapor/Utilities/BaseN.swift +++ b/Sources/Vapor/Utilities/BaseN.swift @@ -12,7 +12,7 @@ public struct BaseNEncoding: Sendable { @inlinable internal static func sizeEnc(for bits: Int, count: Int) -> Int { let outputs = 8 >> bits.trailingZeroBitCount, inputs = bits >> bits.trailingZeroBitCount // number of output values per input bytes - return ((count * outputs - 1) / inputs) + 1 // Integer divsion rounding away from zero + return ((count * outputs - 1) / inputs) + 1 // Integer division rounding away from zero } /// For a given base and count, calculate the number of bytes encoded by a given count of values. Does not @@ -101,7 +101,7 @@ public struct BaseNEncoding: Sendable { internal struct BreakLoopError: Error { @inlinable internal init() {} } // N.B.: The values used for the invalid and padding byte representations are not arbitrarily chosen; they are - // intended to be used in optimized versions of the algorithm to quickly distinguish the vairous cases. + // intended to be used in optimized versions of the algorithm to quickly distinguish the various cases. @usableFromInline internal static var invalidByte: UInt8 { 0b1111_1111 } // All bits set diff --git a/Sources/Vapor/Utilities/DotEnv.swift b/Sources/Vapor/Utilities/DotEnv.swift index 01fcdf8518..942db35a5c 100644 --- a/Sources/Vapor/Utilities/DotEnv.swift +++ b/Sources/Vapor/Utilities/DotEnv.swift @@ -46,7 +46,7 @@ public struct DotEnvFile: Sendable { for environment: Environment = .development, on eventLoopGroupProvider: Application.EventLoopGroupProvider = .singleton, fileio: NonBlockingFileIO, - logger: Logger = Logger(label: "dot-env-loggger") + logger: Logger = Logger(label: "dot-env-logger") ) { let eventLoopGroup: EventLoopGroup @@ -93,7 +93,7 @@ public struct DotEnvFile: Sendable { path: String, on eventLoopGroupProvider: Application.EventLoopGroupProvider = .singleton, fileio: NonBlockingFileIO, - logger: Logger = Logger(label: "dot-env-loggger") + logger: Logger = Logger(label: "dot-env-logger") ) { let eventLoopGroup: EventLoopGroup diff --git a/Sources/Vapor/Validation/RangeResult.swift b/Sources/Vapor/Validation/RangeResult.swift index 3c484797c3..f39364337c 100644 --- a/Sources/Vapor/Validation/RangeResult.swift +++ b/Sources/Vapor/Validation/RangeResult.swift @@ -57,7 +57,7 @@ public enum RangeResult: Equatable where T: Comparable { // any other case is either not comparable (e.g. comparing Float.nan with anything is always false) // or it should never happen because all static methods on `Validator` that can make // the count and range validators all result in at least a minimum or a maximum or both. - // The thrown error needs to be handlded at a higher level then. + // The thrown error needs to be handled at a higher level then. throw RangeResultError.notComparable } } diff --git a/Sources/Vapor/Validation/Validators/Range.swift b/Sources/Vapor/Validation/Validators/Range.swift index 0f17dbd3ce..b3caa0edae 100644 --- a/Sources/Vapor/Validation/Validators/Range.swift +++ b/Sources/Vapor/Validation/Validators/Range.swift @@ -59,7 +59,7 @@ extension Validator { // TODO: Remove when keypaths are `Sendable` /// ``UnsafeMutableTransferBox`` can be used to make non-`Sendable` values `Sendable` and mutable. /// It can be used to capture local mutable values in a `@Sendable` closure and mutate them from within the closure. -/// As the name implies, the usage of this is unsafe because it disables the sendable checking of the compiler and does not add any synchronisation. +/// As the name implies, the usage of this is unsafe because it disables the sendable checking of the compiler and does not add any synchronization. @usableFromInline final class UnsafeMutableTransferBox { @usableFromInline diff --git a/Tests/VaporTests/AsyncMiddlewareTests.swift b/Tests/VaporTests/AsyncMiddlewareTests.swift index be605ebf9f..7ead30eb31 100644 --- a/Tests/VaporTests/AsyncMiddlewareTests.swift +++ b/Tests/VaporTests/AsyncMiddlewareTests.swift @@ -88,7 +88,7 @@ final class AsyncMiddlewareTests: XCTestCase { } } - func testCORSMiddlewareNoVariationByRequstOriginAllowed() throws { + func testCORSMiddlewareNoVariationByRequestOriginAllowed() throws { let app = Application(.testing) defer { app.shutdown() } diff --git a/Tests/VaporTests/AsyncPasswordTests.swift b/Tests/VaporTests/AsyncPasswordTests.swift index cbb5106c9f..8a09d59b74 100644 --- a/Tests/VaporTests/AsyncPasswordTests.swift +++ b/Tests/VaporTests/AsyncPasswordTests.swift @@ -68,11 +68,11 @@ final class AsyncPasswordTests: XCTestCase { .async(on: app.threadPool, hopTo: app.eventLoopGroup.next()) .hash("vapor") - let asyncVerifiy = try await app.password + let asyncVerify = try await app.password .async(on: app.threadPool, hopTo: app.eventLoopGroup.next()) .verify("vapor", created: asyncHash) - XCTAssertTrue(asyncVerifiy, file: file, line: line) + XCTAssertTrue(asyncVerify, file: file, line: line) } private func assertAsyncRequestPasswordVerifies( diff --git a/Tests/VaporTests/AsyncRequestTests.swift b/Tests/VaporTests/AsyncRequestTests.swift index bbf9a09541..4e251ba247 100644 --- a/Tests/VaporTests/AsyncRequestTests.swift +++ b/Tests/VaporTests/AsyncRequestTests.swift @@ -35,13 +35,13 @@ final class AsyncRequestTests: XCTestCase { let testValue = String.randomDigits() app.on(.POST, "stream", body: .stream) { req in - var recievedBuffer = ByteBuffer() + var receivedBuffer = ByteBuffer() for try await part in req.body { XCTAssertNotNil(part) var part = part - recievedBuffer.writeBuffer(&part) + receivedBuffer.writeBuffer(&part) } - let string = String(buffer: recievedBuffer) + let string = String(buffer: receivedBuffer) return string } diff --git a/Tests/VaporTests/MiddlewareTests.swift b/Tests/VaporTests/MiddlewareTests.swift index b2c5c56a4d..80fef5776b 100644 --- a/Tests/VaporTests/MiddlewareTests.swift +++ b/Tests/VaporTests/MiddlewareTests.swift @@ -92,7 +92,7 @@ final class MiddlewareTests: XCTestCase { } } - func testCORSMiddlewareNoVariationByRequstOriginAllowed() throws { + func testCORSMiddlewareNoVariationByRequestOriginAllowed() throws { let app = Application(.testing) defer { app.shutdown() } diff --git a/Tests/VaporTests/PasswordTests.swift b/Tests/VaporTests/PasswordTests.swift index 0ae3c1444a..d28918f935 100644 --- a/Tests/VaporTests/PasswordTests.swift +++ b/Tests/VaporTests/PasswordTests.swift @@ -95,12 +95,12 @@ final class PasswordTests: XCTestCase { .hash("vapor") .wait() - let asyncVerifiy = try app.password + let asyncVerify = try app.password .async(on: app.threadPool, hopTo: app.eventLoopGroup.next()) .verify("vapor", created: asyncHash) .wait() - XCTAssertTrue(asyncVerifiy, file: file, line: line) + XCTAssertTrue(asyncVerify, file: file, line: line) } private func assertAsyncRequestPasswordVerifies( diff --git a/Tests/VaporTests/RequestTests.swift b/Tests/VaporTests/RequestTests.swift index 0499fb4cae..90796da554 100644 --- a/Tests/VaporTests/RequestTests.swift +++ b/Tests/VaporTests/RequestTests.swift @@ -75,7 +75,7 @@ final class RequestTests: XCTestCase { } } - func testRequestPeerAddressRemoteAddres() throws { + func testRequestPeerAddressRemoteAddress() throws { let app = Application(.testing) defer { app.shutdown() } diff --git a/Tests/VaporTests/Utilities/CapturingMetricsSystem.swift b/Tests/VaporTests/Utilities/CapturingMetricsSystem.swift index baccaa9244..d8c54983c7 100644 --- a/Tests/VaporTests/Utilities/CapturingMetricsSystem.swift +++ b/Tests/VaporTests/Utilities/CapturingMetricsSystem.swift @@ -156,7 +156,7 @@ internal final class TestTimer: TimerHandler, Equatable, @unchecked Sendable { } } - func retriveValueInPreferredUnit(atIndex i: Int) -> Double { + func retrieveValueInPreferredUnit(atIndex i: Int) -> Double { return self.lock.withLock { let value = values[i].1 guard let displayUnit = self.displayUnit else { From 096c51992b1c32695c0e17329d1eeda45e026a9d Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Wed, 17 Apr 2024 22:39:13 -0500 Subject: [PATCH 10/18] Don't set ignore status for SIGTERM and SIGINT on Linux (#3174) --- Sources/Vapor/Commands/ServeCommand.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/Vapor/Commands/ServeCommand.swift b/Sources/Vapor/Commands/ServeCommand.swift index a089fd3c98..2947bfe635 100644 --- a/Sources/Vapor/Commands/ServeCommand.swift +++ b/Sources/Vapor/Commands/ServeCommand.swift @@ -85,6 +85,11 @@ public final class ServeCommand: Command, Sendable { // setup signal sources for shutdown let signalQueue = DispatchQueue(label: "codes.vapor.server.shutdown") func makeSignalSource(_ code: Int32) { + #if canImport(Darwin) + /// https://github.com/swift-server/swift-service-lifecycle/blob/main/Sources/UnixSignals/UnixSignalsSequence.swift#L77-L82 + signal(code, SIG_IGN) + #endif + let source = DispatchSource.makeSignalSource(signal: code, queue: signalQueue) source.setEventHandler { print() // clear ^C @@ -92,7 +97,6 @@ public final class ServeCommand: Command, Sendable { } source.resume() box.signalSources.append(source) - signal(code, SIG_IGN) } makeSignalSource(SIGTERM) makeSignalSource(SIGINT) From 71dff4f3f889553b4525b53376dcb2c5c27ad993 Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Fri, 19 Apr 2024 14:15:12 -0700 Subject: [PATCH 11/18] HTTP2 Response Compression/Request Decompression (#3126) * Fixed an issue where HTTP2 didn't support response compression and request recompression Fixes #3125 * Added exhaustive tests for request and response compression across both HTTP 1.1 and HTTP 2 --- Sources/Vapor/HTTP/Server/HTTPServer.swift | 22 ++ Tests/VaporTests/ServerTests.swift | 364 ++++++++++++++++++++- 2 files changed, 383 insertions(+), 3 deletions(-) diff --git a/Sources/Vapor/HTTP/Server/HTTPServer.swift b/Sources/Vapor/HTTP/Server/HTTPServer.swift index f9a9927984..949e1cac76 100644 --- a/Sources/Vapor/HTTP/Server/HTTPServer.swift +++ b/Sources/Vapor/HTTP/Server/HTTPServer.swift @@ -503,6 +503,28 @@ extension ChannelPipeline { let http2 = HTTP2FramePayloadToHTTP1ServerCodec() handlers.append(http2) + /// Add response compressor if configured. + switch configuration.responseCompression.storage { + case .enabled(let initialByteBufferCapacity): + let responseCompressionHandler = HTTPResponseCompressor( + initialByteBufferCapacity: initialByteBufferCapacity + ) + handlers.append(responseCompressionHandler) + case .disabled: + break + } + + /// Add request decompressor if configured. + switch configuration.requestDecompression.storage { + case .enabled(let limit): + let requestDecompressionHandler = NIOHTTPRequestDecompressor( + limit: limit + ) + handlers.append(requestDecompressionHandler) + case .disabled: + break + } + /// Add NIO → HTTP request decoder. let serverReqDecoder = HTTPServerRequestDecoder( application: application diff --git a/Tests/VaporTests/ServerTests.swift b/Tests/VaporTests/ServerTests.swift index d4173031c5..bcf7a0a7d0 100644 --- a/Tests/VaporTests/ServerTests.swift +++ b/Tests/VaporTests/ServerTests.swift @@ -264,7 +264,7 @@ final class ServerTests: XCTestCase { struct Nothing: Codable {} XCTAssertNoThrow(try JSONDecoder().decode(Nothing.self, from: body)) } else { - XCTFail() + XCTFail("Missing response.body") } } @@ -277,9 +277,9 @@ final class ServerTests: XCTestCase { let smallBody = ByteBuffer(base64String: "H4sIAAAAAAAAE/NIzcnJ11Eozy/KSVEEAObG5usNAAAA")! // "Hello, world!" let bigBody = ByteBuffer(base64String: "H4sIAAAAAAAAE/NIzcnJ11HILU3OgBBJmenpqUUK5flFOSkKJRmJeQpJqWn5RamKAICcGhUqAAAA")! // "Hello, much much bigger world than before!" - // Max out at the smaller payload (.size is of compressed data) + // Max out at the smaller payload (.size is of uncompressed data) app.http.server.configuration.requestDecompression = .enabled( - limit: .size(smallBody.readableBytes) + limit: .size(smallOrigString.utf8.count) ) app.post("gzip") { $0.body.string ?? "" } @@ -314,6 +314,364 @@ final class ServerTests: XCTestCase { } } + func testHTTP1RequestDecompression() async throws { + let compressiblePayload = #"{"compressed": ["key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value"]}"# + /// To regenerate, copy the above and run `% pbpaste | gzip | base64`. To verify, run `% pbpaste | base64 -d | gzip -d` instead. + let compressedPayload = ByteBuffer(base64String: "H4sIANRAImYAA6tWSs7PLShKLS5OTVGyUohWyk6tBNJKZYk5palKOgqj/FH+KH+UP8of5RPmx9YCAMfjVAhQBgAA")! + + let app = Application(.testing) + defer { app.shutdown() } + + app.http.server.configuration.hostname = "127.0.0.1" + app.http.server.configuration.port = 0 + + app.http.server.configuration.supportVersions = [.one] + app.http.server.configuration.requestDecompression = .disabled + + /// Make sure the client doesn't keep the server open by re-using the connection. + app.http.client.configuration.maximumUsesPerConnection = 1 + + struct TestResponse: Content { + var content: ByteBuffer? + var contentLength: Int + } + + app.on(.POST, "compressed", body: .collect(maxSize: "1mb")) { request async throws in + let contentLength = request.headers.first(name: .contentLength).flatMap { Int($0) } + let contents = try await request.body.collect().get() + return TestResponse( + content: contents, + contentLength: contentLength ?? 0 + ) + } + + try app.server.start() + defer { app.server.shutdown() } + + XCTAssertNotNil(app.http.server.shared.localAddress) + guard let localAddress = app.http.server.shared.localAddress, + let port = localAddress.port else { + XCTFail("couldn't get ip/port from \(app.http.server.shared.localAddress.debugDescription)") + return + } + + let unsupportedNoncompressedResponse = try await app.client.post("http://localhost:\(port)/compressed") { request in + request.body = compressedPayload + } + + if let body = unsupportedNoncompressedResponse.body { + let decodedResponse = try JSONDecoder().decode(TestResponse.self, from: body) + XCTAssertEqual(decodedResponse.content, compressedPayload) + XCTAssertEqual(decodedResponse.contentLength, compressedPayload.readableBytes) + } else { + XCTFail("Missing unsupportedNoncompressedResponse.body") + } + + // TODO: The server should probably reject this? + let unsupportedCompressedResponse = try await app.client.post("http://localhost:\(port)/compressed") { request in + request.headers.replaceOrAdd(name: .contentEncoding, value: "gzip") + request.body = compressedPayload + } + + if let body = unsupportedCompressedResponse.body { + let decodedResponse = try JSONDecoder().decode(TestResponse.self, from: body) + XCTAssertEqual(decodedResponse.content, compressedPayload) + XCTAssertEqual(decodedResponse.contentLength, compressedPayload.readableBytes) + } else { + XCTFail("Missing unsupportedCompressedResponse.body") + } + + app.http.server.configuration.requestDecompression = .enabled(limit: .size(compressiblePayload.utf8.count)) + + let supportedUncompressedResponse = try await app.client.post("http://localhost:\(port)/compressed") { request in + request.body = compressedPayload + } + + if let body = supportedUncompressedResponse.body { + let decodedResponse = try JSONDecoder().decode(TestResponse.self, from: body) + XCTAssertEqual(decodedResponse.content, compressedPayload) + XCTAssertEqual(decodedResponse.contentLength, compressedPayload.readableBytes) + } else { + XCTFail("Missing supportedUncompressedResponse.body") + } + + let supportedCompressedResponse = try await app.client.post("http://localhost:\(port)/compressed") { request in + request.headers.replaceOrAdd(name: .contentEncoding, value: "gzip") + request.body = compressedPayload + } + + if let body = supportedCompressedResponse.body { + let decodedResponse = try JSONDecoder().decode(TestResponse.self, from: body) + XCTAssertEqual(decodedResponse.content, ByteBuffer(string: compressiblePayload)) + XCTAssertEqual(decodedResponse.contentLength, compressedPayload.readableBytes) + } else { + XCTFail("Missing supportedCompressedResponse.body") + } + } + + func testHTTP2RequestDecompression() async throws { + guard let clientCertPath = Bundle.module.url(forResource: "expired", withExtension: "crt"), + let clientKeyPath = Bundle.module.url(forResource: "expired", withExtension: "key") else { + XCTFail("Cannot load expired cert and associated key") + return + } + + let cert = try NIOSSLCertificate(file: clientCertPath.path, format: .pem) + let key = try NIOSSLPrivateKey(file: clientKeyPath.path, format: .pem) + + let compressiblePayload = #"{"compressed": ["key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value"]}"# + /// To regenerate, copy the above and run `% pbpaste | gzip | base64`. To verify, run `% pbpaste | base64 -d | gzip -d` instead. + let compressedPayload = ByteBuffer(base64String: "H4sIANRAImYAA6tWSs7PLShKLS5OTVGyUohWyk6tBNJKZYk5palKOgqj/FH+KH+UP8of5RPmx9YCAMfjVAhQBgAA")! + + let app = Application(.testing) + defer { app.shutdown() } + + app.http.server.configuration.hostname = "127.0.0.1" + app.http.server.configuration.port = 0 + + var serverConfig = TLSConfiguration.makeServerConfiguration(certificateChain: [.certificate(cert)], privateKey: .privateKey(key)) + serverConfig.certificateVerification = .noHostnameVerification + + app.http.server.configuration.tlsConfiguration = serverConfig + app.http.server.configuration.customCertificateVerifyCallback = { peerCerts, successPromise in + /// This lies and accepts the above cert, which has actually expired. + XCTAssertEqual(peerCerts, [cert]) + successPromise.succeed(.certificateVerified) + } + app.http.server.configuration.supportVersions = [.two] + app.http.server.configuration.requestDecompression = .disabled + + /// We need to disable verification on the client, because the cert we're using has expired + var clientConfig = TLSConfiguration.makeClientConfiguration() + clientConfig.certificateVerification = .none + clientConfig.certificateChain = [.certificate(cert)] + clientConfig.privateKey = .privateKey(key) + app.http.client.configuration.tlsConfiguration = clientConfig + + /// Make sure the client doesn't keep the server open by re-using the connection. + app.http.client.configuration.maximumUsesPerConnection = 1 + + struct TestResponse: Content { + var content: ByteBuffer? + var contentLength: Int + } + + app.post("compressed") { request async throws in + let contentLength = request.headers.first(name: .contentLength) + let contents = try await request.body.collect().get() + return TestResponse( + content: contents, + contentLength: contentLength.flatMap { Int($0) } ?? 0 + ) + } + + try app.server.start() + defer { app.server.shutdown() } + + XCTAssertNotNil(app.http.server.shared.localAddress) + guard let localAddress = app.http.server.shared.localAddress, + let port = localAddress.port else { + XCTFail("couldn't get ip/port from \(app.http.server.shared.localAddress.debugDescription)") + return + } + + let unsupportedNoncompressedResponse = try await app.client.post("https://localhost:\(port)/compressed") { request in + request.body = compressedPayload + } + + if let body = unsupportedNoncompressedResponse.body { + let decodedResponse = try JSONDecoder().decode(TestResponse.self, from: body) + XCTAssertEqual(decodedResponse.content, compressedPayload) + XCTAssertEqual(decodedResponse.contentLength, compressedPayload.readableBytes) + } else { + XCTFail("Missing unsupportedNoncompressedResponse.body") + } + + // TODO: The server should probably reject this? + let unsupportedCompressedResponse = try await app.client.post("https://localhost:\(port)/compressed") { request in + request.headers.replaceOrAdd(name: .contentEncoding, value: "gzip") + request.body = compressedPayload + } + + if let body = unsupportedCompressedResponse.body { + let decodedResponse = try JSONDecoder().decode(TestResponse.self, from: body) + XCTAssertEqual(decodedResponse.content, compressedPayload) + XCTAssertEqual(decodedResponse.contentLength, compressedPayload.readableBytes) + } else { + XCTFail("Missing unsupportedCompressedResponse.body") + } + + app.http.server.configuration.requestDecompression = .enabled(limit: .size(compressiblePayload.utf8.count)) + + let supportedUncompressedResponse = try await app.client.post("https://localhost:\(port)/compressed") { request in + request.body = compressedPayload + } + + if let body = supportedUncompressedResponse.body { + let decodedResponse = try JSONDecoder().decode(TestResponse.self, from: body) + XCTAssertEqual(decodedResponse.content, compressedPayload) + XCTAssertEqual(decodedResponse.contentLength, compressedPayload.readableBytes) + } else { + XCTFail("Missing supportedUncompressedResponse.body") + } + + let supportedCompressedResponse = try await app.client.post("https://localhost:\(port)/compressed") { request in + request.headers.replaceOrAdd(name: .contentEncoding, value: "gzip") + request.body = compressedPayload + } + + if let body = supportedCompressedResponse.body { + let decodedResponse = try JSONDecoder().decode(TestResponse.self, from: body) + XCTAssertEqual(decodedResponse.content, ByteBuffer(string: compressiblePayload)) + XCTAssertEqual(decodedResponse.contentLength, compressedPayload.readableBytes) + } else { + XCTFail("Missing supportedCompressedResponse.body") + } + } + + func testHTTP1ResponseDecompression() async throws { + let compressiblePayload = #"{"compressed": ["key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value"]}"# + + let app = Application(.testing) + defer { app.shutdown() } + + app.http.server.configuration.hostname = "127.0.0.1" + app.http.server.configuration.port = 0 + + app.http.server.configuration.supportVersions = [.one] + app.http.server.configuration.responseCompression = .disabled + + /// Make sure the client doesn't keep the server open by re-using the connection. + app.http.client.configuration.maximumUsesPerConnection = 1 + app.http.client.configuration.decompression = .enabled(limit: .none) + + app.get("compressed") { _ in compressiblePayload } + + try app.server.start() + defer { app.server.shutdown() } + + XCTAssertNotNil(app.http.server.shared.localAddress) + guard let localAddress = app.http.server.shared.localAddress, + let port = localAddress.port else { + XCTFail("couldn't get ip/port from \(app.http.server.shared.localAddress.debugDescription)") + return + } + + let unsupportedNoncompressedResponse = try await app.client.get("http://localhost:\(port)/compressed") { request in + request.headers.remove(name: .acceptEncoding) + } + XCTAssertNotEqual(unsupportedNoncompressedResponse.headers.first(name: .contentEncoding), "gzip") + XCTAssertEqual(unsupportedNoncompressedResponse.headers.first(name: .contentLength), "\(compressiblePayload.count)") + XCTAssertEqual(unsupportedNoncompressedResponse.body?.string, compressiblePayload) + + let unsupportedCompressedResponse = try await app.client.get("http://localhost:\(port)/compressed") { request in + request.headers.replaceOrAdd(name: .acceptEncoding, value: "gzip") + } + XCTAssertNotEqual(unsupportedCompressedResponse.headers.first(name: .contentEncoding), "gzip") + XCTAssertEqual(unsupportedCompressedResponse.headers.first(name: .contentLength), "\(compressiblePayload.count)") + XCTAssertEqual(unsupportedCompressedResponse.body?.string, compressiblePayload) + + app.http.server.configuration.responseCompression = .enabled + + let supportedUncompressedResponse = try await app.client.get("http://localhost:\(port)/compressed") { request in + request.headers.remove(name: .acceptEncoding) + } + XCTAssertNotEqual(supportedUncompressedResponse.headers.first(name: .contentEncoding), "gzip") + XCTAssertNotEqual(supportedUncompressedResponse.headers.first(name: .contentLength), "\(compressiblePayload.count)") + XCTAssertEqual(supportedUncompressedResponse.body?.string, compressiblePayload) + + let supportedCompressedResponse = try await app.client.get("http://localhost:\(port)/compressed") { request in + request.headers.replaceOrAdd(name: .acceptEncoding, value: "gzip") + } + XCTAssertEqual(supportedCompressedResponse.headers.first(name: .contentEncoding), "gzip") + XCTAssertNotEqual(supportedCompressedResponse.headers.first(name: .contentLength), "\(compressiblePayload.count)") + XCTAssertEqual(supportedCompressedResponse.body?.string, compressiblePayload) + } + + func testHTTP2ResponseDecompression() async throws { + guard let clientCertPath = Bundle.module.url(forResource: "expired", withExtension: "crt"), + let clientKeyPath = Bundle.module.url(forResource: "expired", withExtension: "key") else { + XCTFail("Cannot load expired cert and associated key") + return + } + + let cert = try NIOSSLCertificate(file: clientCertPath.path, format: .pem) + let key = try NIOSSLPrivateKey(file: clientKeyPath.path, format: .pem) + + let compressiblePayload = #"{"compressed": ["key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value", "key": "value"]}"# + + let app = Application(.testing) + defer { app.shutdown() } + + app.http.server.configuration.hostname = "127.0.0.1" + app.http.server.configuration.port = 0 + + var serverConfig = TLSConfiguration.makeServerConfiguration(certificateChain: [.certificate(cert)], privateKey: .privateKey(key)) + serverConfig.certificateVerification = .noHostnameVerification + + app.http.server.configuration.tlsConfiguration = serverConfig + app.http.server.configuration.customCertificateVerifyCallback = { peerCerts, successPromise in + /// This lies and accepts the above cert, which has actually expired. + XCTAssertEqual(peerCerts, [cert]) + successPromise.succeed(.certificateVerified) + } + app.http.server.configuration.supportVersions = [.two] + app.http.server.configuration.responseCompression = .disabled + + /// We need to disable verification on the client, because the cert we're using has expired + var clientConfig = TLSConfiguration.makeClientConfiguration() + clientConfig.certificateVerification = .none + clientConfig.certificateChain = [.certificate(cert)] + clientConfig.privateKey = .privateKey(key) + app.http.client.configuration.tlsConfiguration = clientConfig + + app.http.client.configuration.decompression = .enabled(limit: .none) + /// Make sure the client doesn't keep the server open by re-using the connection. + app.http.client.configuration.maximumUsesPerConnection = 1 + + app.get("compressed") { _ in compressiblePayload } + + try app.server.start() + defer { app.server.shutdown() } + + XCTAssertNotNil(app.http.server.shared.localAddress) + guard let localAddress = app.http.server.shared.localAddress, + let port = localAddress.port else { + XCTFail("couldn't get ip/port from \(app.http.server.shared.localAddress.debugDescription)") + return + } + + let unsupportedNoncompressedResponse = try await app.client.get("https://localhost:\(port)/compressed") { request in + request.headers.remove(name: .acceptEncoding) + } + XCTAssertNotEqual(unsupportedNoncompressedResponse.headers.first(name: .contentEncoding), "gzip") + XCTAssertEqual(unsupportedNoncompressedResponse.headers.first(name: .contentLength), "\(compressiblePayload.count)") + XCTAssertEqual(unsupportedNoncompressedResponse.body?.string, compressiblePayload) + + let unsupportedCompressedResponse = try await app.client.get("https://localhost:\(port)/compressed") { request in + request.headers.replaceOrAdd(name: .acceptEncoding, value: "gzip") + } + XCTAssertNotEqual(unsupportedCompressedResponse.headers.first(name: .contentEncoding), "gzip") + XCTAssertEqual(unsupportedCompressedResponse.headers.first(name: .contentLength), "\(compressiblePayload.count)") + XCTAssertEqual(unsupportedCompressedResponse.body?.string, compressiblePayload) + + app.http.server.configuration.responseCompression = .enabled + + let supportedUncompressedResponse = try await app.client.get("https://localhost:\(port)/compressed") { request in + request.headers.remove(name: .acceptEncoding) + } + XCTAssertNotEqual(supportedUncompressedResponse.headers.first(name: .contentEncoding), "gzip") + XCTAssertNotEqual(supportedUncompressedResponse.headers.first(name: .contentLength), "\(compressiblePayload.count)") + XCTAssertEqual(supportedUncompressedResponse.body?.string, compressiblePayload) + + let supportedCompressedResponse = try await app.client.get("https://localhost:\(port)/compressed") { request in + request.headers.replaceOrAdd(name: .acceptEncoding, value: "gzip") + } + XCTAssertEqual(supportedCompressedResponse.headers.first(name: .contentEncoding), "gzip") + XCTAssertNotEqual(supportedCompressedResponse.headers.first(name: .contentLength), "\(compressiblePayload.count)") + XCTAssertEqual(supportedCompressedResponse.body?.string, compressiblePayload) + } + func testRequestBodyStreamGetsFinalisedEvenIfClientAbandonsConnection() throws { let app = Application(.testing) app.http.server.configuration.hostname = "127.0.0.1" From 0fb4f1dd8b11fa7a8d4f6bab6d020844b4fc3251 Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Sun, 21 Apr 2024 03:29:21 -0700 Subject: [PATCH 12/18] Enabled Request Decompression By Default (#3175) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Updated request decompression to be enabled by default * Updated the default request decompression ratio limit to be 25× --- Sources/Vapor/HTTP/Server/HTTPServer.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/Vapor/HTTP/Server/HTTPServer.swift b/Sources/Vapor/HTTP/Server/HTTPServer.swift index 949e1cac76..7309b7df21 100644 --- a/Sources/Vapor/HTTP/Server/HTTPServer.swift +++ b/Sources/Vapor/HTTP/Server/HTTPServer.swift @@ -119,7 +119,7 @@ public final class HTTPServer: Server, Sendable { /// Enables decompression with default configuration. public static var enabled: Self { - .enabled(limit: .ratio(10)) + .enabled(limit: .ratio(25)) } /// Enables decompression with custom configuration. @@ -168,7 +168,7 @@ public final class HTTPServer: Server, Sendable { reuseAddress: Bool = true, tcpNoDelay: Bool = true, responseCompression: CompressionConfiguration = .disabled, - requestDecompression: DecompressionConfiguration = .disabled, + requestDecompression: DecompressionConfiguration = .enabled, supportPipelining: Bool = true, supportVersions: Set? = nil, tlsConfiguration: TLSConfiguration? = nil, @@ -200,7 +200,7 @@ public final class HTTPServer: Server, Sendable { reuseAddress: Bool = true, tcpNoDelay: Bool = true, responseCompression: CompressionConfiguration = .disabled, - requestDecompression: DecompressionConfiguration = .disabled, + requestDecompression: DecompressionConfiguration = .enabled, supportPipelining: Bool = true, supportVersions: Set? = nil, tlsConfiguration: TLSConfiguration? = nil, From 526a000bb082a261566b6adea3c1a1ddbed98a19 Mon Sep 17 00:00:00 2001 From: Linus <42340116+linus-hologram@users.noreply.github.com> Date: Sun, 21 Apr 2024 13:17:25 +0200 Subject: [PATCH 13/18] Advanced ETag Comparison now supported (#3015) * Update FileIO.swift sha-256 digest set when file is written to system * Introduced new streamFile method Introduced new streamFile method which allows for advancedETag comparison. Deprecated the old one. * Updated Unit Tests Updates to remove deprecated warnings by using the new streamFile() method. Also removed some other deprecation warnings. * Added more documentation * Removed unnecessary CryptoKit import * added closure return types * incorporated first round of @0xTim's feedback * added test cases to account for advanced/simple etags * Incorporated PR comments - adjusted faulty comment - access storage directly to avoid concurrent overwrites of the entire storage * Added test for legacy streamFile * Deprecated method to silence warnings * Warning fixes * following @gwynne's style advice :) --- .../Headers/HTTPHeaders+ContentRange.swift | 6 +- Sources/Vapor/Middleware/FileMiddleware.swift | 21 ++- Sources/Vapor/Utilities/FileIO.swift | 153 +++++++++++++++++- Tests/VaporTests/FileTests.swift | 111 ++++++++++--- 4 files changed, 260 insertions(+), 31 deletions(-) diff --git a/Sources/Vapor/HTTP/Headers/HTTPHeaders+ContentRange.swift b/Sources/Vapor/HTTP/Headers/HTTPHeaders+ContentRange.swift index 3130786111..493c26b082 100644 --- a/Sources/Vapor/HTTP/Headers/HTTPHeaders+ContentRange.swift +++ b/Sources/Vapor/HTTP/Headers/HTTPHeaders+ContentRange.swift @@ -5,7 +5,7 @@ extension HTTPHeaders { /// The unit in which `ContentRange`s and `Range`s are specified. This is usually `bytes`. /// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Range - public enum RangeUnit: Equatable { + public enum RangeUnit: Sendable, Equatable { case bytes case custom(value: String) @@ -21,7 +21,7 @@ extension HTTPHeaders { /// Represents the HTTP `Range` request header. /// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range - public struct Range: Equatable { + public struct Range: Sendable, Equatable { public let unit: RangeUnit public let ranges: [HTTPHeaders.Range.Value] @@ -134,7 +134,7 @@ extension HTTPHeaders.Range { /// Represents one value of the `Range` request header. /// /// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range - public enum Value: Equatable { + public enum Value: Sendable, Equatable { ///Integer with single trailing dash, e.g. `25-` case start(value: Int) ///Integer with single leading dash, e.g. `-25` diff --git a/Sources/Vapor/Middleware/FileMiddleware.swift b/Sources/Vapor/Middleware/FileMiddleware.swift index 9fc959bce2..d1c4b6867e 100644 --- a/Sources/Vapor/Middleware/FileMiddleware.swift +++ b/Sources/Vapor/Middleware/FileMiddleware.swift @@ -9,7 +9,8 @@ public final class FileMiddleware: Middleware { private let publicDirectory: String private let defaultFile: String? private let directoryAction: DirectoryAction - + private let advancedETagComparison: Bool + public struct BundleSetupError: Equatable, Error { /// The description of this error. @@ -22,6 +23,15 @@ public final class FileMiddleware: Middleware { public static let publicDirectoryIsNotAFolder: Self = .init(description: "Cannot find any actual folder for the given Public Directory") } + struct ETagHashes: StorageKey { + public typealias Value = [String: FileHash] + + public struct FileHash { + let lastModified: Date + let digestHex: String + } + } + /// Creates a new `FileMiddleware`. /// /// - parameters: @@ -29,10 +39,12 @@ public final class FileMiddleware: Middleware { /// - defaultFile: The name of the default file to look for and serve if a request hits any public directory. Starting with `/` implies /// an absolute path from the public directory root. If `nil`, no default files are served. /// - directoryAction: Determines the action to take when the request doesn't have a trailing slash but matches a directory. - public init(publicDirectory: String, defaultFile: String? = nil, directoryAction: DirectoryAction = .none) { + /// - advancedETagComparison: The method used when ETags are generated. If true, a byte-by-byte hash is created (and cached), otherwise a simple comparison based on the file's last modified date and size. + public init(publicDirectory: String, defaultFile: String? = nil, directoryAction: DirectoryAction = .none, advancedETagComparison: Bool = true) { self.publicDirectory = publicDirectory.addTrailingSlash() self.defaultFile = defaultFile self.directoryAction = directoryAction + self.advancedETagComparison = advancedETagComparison } public func respond(to request: Request, chainingTo next: Responder) -> EventLoopFuture { @@ -88,10 +100,9 @@ public final class FileMiddleware: Middleware { return next.respond(to: request) } } - + // stream the file - let res = request.fileio.streamFile(at: absPath) - return request.eventLoop.makeSucceededFuture(res) + return request.fileio.streamFile(at: absPath, advancedETagComparison: advancedETagComparison) } /// Creates a new `FileMiddleware` for a server contained in an Xcode Project. diff --git a/Sources/Vapor/Utilities/FileIO.swift b/Sources/Vapor/Utilities/FileIO.swift index 73c9a6fed7..7a3820989f 100644 --- a/Sources/Vapor/Utilities/FileIO.swift +++ b/Sources/Vapor/Utilities/FileIO.swift @@ -3,6 +3,7 @@ import NIOCore import NIOHTTP1 import NIOPosix import Logging +import Crypto import NIOConcurrencyHelpers extension Request { @@ -122,6 +123,7 @@ public struct FileIO: Sendable { /// - mediaType: HTTPMediaType, if not specified, will be created from file extension. /// - onCompleted: Closure to be run on completion of stream. /// - returns: A `200 OK` response containing the file stream and appropriate headers. + @available(*, deprecated, message: "Use the new `streamFile` method which returns EventLoopFuture") @preconcurrency public func streamFile( at path: String, chunkSize: Int = NonBlockingFileIO.defaultChunkSize, @@ -157,7 +159,7 @@ public struct FileIO: Sendable { // Respond with lastModified header headers.lastModified = HTTPHeaders.LastModified(value: modifiedAt) - + // Generate ETag value, "HEX value of last modified date" + "-" + "file size" let fileETag = "\"\(modifiedAt.timeIntervalSince1970)-\(fileSize)\"" headers.replaceOrAdd(name: .eTag, value: fileETag) @@ -218,10 +220,137 @@ public struct FileIO: Sendable { onCompleted(result) } }, count: byteCount, byteBufferAllocator: request.byteBufferAllocator) - + return response } + /// Generates a chunked `Response` for the specified file. This method respects values in + /// the `"ETag"` header and is capable of responding `304 Not Modified` if the file in question + /// has not been modified since last served. If `advancedETagComparison` is set to true, + /// the response will have its ETag field set to a byte-by-byte hash of the requested file. If set to false, a simple ETag consisting of the last modified date and file size + /// will be used. This method will also set the `"Content-Type"` header + /// automatically if an appropriate `MediaType` can be found for the file's suffix. + /// + /// router.get("file-stream") { req in + /// return req.fileio.streamFile(at: "/path/to/file.txt") + /// } + /// + /// - parameters: + /// - path: Path to file on the disk. + /// - chunkSize: Maximum size for the file data chunks. + /// - mediaType: HTTPMediaType, if not specified, will be created from file extension. + /// - advancedETagComparison: The method used when ETags are generated. If true, a byte-by-byte hash is created (and cached), otherwise a simple comparison based on the file's last modified date and size. + /// - onCompleted: Closure to be run on completion of stream. + /// - returns: A `200 OK` response containing the file stream and appropriate headers. + public func streamFile( + at path: String, + chunkSize: Int = NonBlockingFileIO.defaultChunkSize, + mediaType: HTTPMediaType? = nil, + advancedETagComparison: Bool, + onCompleted: @escaping @Sendable (Result) -> () = { _ in } + ) -> EventLoopFuture { + // Get file attributes for this file. + guard + let attributes = try? FileManager.default.attributesOfItem(atPath: path), + let modifiedAt = attributes[.modificationDate] as? Date, + let fileSize = (attributes[.size] as? NSNumber)?.intValue + else { + return request.eventLoop.makeSucceededFuture(Response(status: .internalServerError)) + } + + let contentRange: HTTPHeaders.Range? + if let rangeFromHeaders = request.headers.range { + if rangeFromHeaders.unit == .bytes && rangeFromHeaders.ranges.count == 1 { + contentRange = rangeFromHeaders + } else { + contentRange = nil + } + } else if request.headers.contains(name: .range) { + // Range header was supplied but could not be parsed i.e. it was invalid + request.logger.debug("Range header was provided in request but was invalid") + let response = Response(status: .badRequest) + return request.eventLoop.makeSucceededFuture(response) + } else { + contentRange = nil + } + + var eTagFuture: EventLoopFuture + + if advancedETagComparison { + eTagFuture = generateETagHash(path: path, lastModified: modifiedAt) + } else { + // Generate ETag value, "last modified date in epoch time" + "-" + "file size" + eTagFuture = request.eventLoop.makeSucceededFuture("\"\(modifiedAt.timeIntervalSince1970)-\(fileSize)\"") + } + + return eTagFuture.map { fileETag in + // Create empty headers array. + var headers: HTTPHeaders = [:] + + // Respond with lastModified header + headers.lastModified = HTTPHeaders.LastModified(value: modifiedAt) + + headers.replaceOrAdd(name: .eTag, value: fileETag) + + // Check if file has been cached already and return NotModified response if the etags match + if fileETag == request.headers.first(name: .ifNoneMatch) { + // Per RFC 9110 here: https://www.rfc-editor.org/rfc/rfc9110.html#status.304 + // and here: https://www.rfc-editor.org/rfc/rfc9110.html#name-content-encoding + // A 304 response MUST include the ETag header and a Content-Length header matching what the original resource's content length would have been were this a 200 response. + headers.replaceOrAdd(name: .contentLength, value: fileSize.description) + return Response(status: .notModified, version: .http1_1, headersNoUpdate: headers, body: .empty) + } + + // Create the HTTP response. + let response = Response(status: .ok, headers: headers) + let offset: Int64 + let byteCount: Int + if let contentRange = contentRange { + response.status = .partialContent + response.headers.add(name: .accept, value: contentRange.unit.serialize()) + if let firstRange = contentRange.ranges.first { + do { + let range = try firstRange.asResponseContentRange(limit: fileSize) + response.headers.contentRange = HTTPHeaders.ContentRange(unit: contentRange.unit, range: range) + (offset, byteCount) = try firstRange.asByteBufferBounds(withMaxSize: fileSize, logger: request.logger) + } catch { + let response = Response(status: .badRequest) + return response + } + } else { + offset = 0 + byteCount = fileSize + } + } else { + offset = 0 + byteCount = fileSize + } + // Set Content-Type header based on the media type + // Only set Content-Type if file not modified and returned above. + if + let fileExtension = path.components(separatedBy: ".").last, + let type = mediaType ?? HTTPMediaType.fileExtension(fileExtension) + { + response.headers.contentType = type + } + response.body = .init(stream: { stream in + self.read(path: path, fromOffset: offset, byteCount: byteCount, chunkSize: chunkSize) { chunk in + return stream.write(.buffer(chunk)) + }.whenComplete { result in + switch result { + case .failure(let error): + stream.write(.error(error), promise: nil) + case .success: + stream.write(.end, promise: nil) + } + onCompleted(result) + } + }, count: byteCount, byteBufferAllocator: request.byteBufferAllocator) + + return response + } + } + /// Private read method. `onRead` closure uses ByteBuffer and expects future return. /// There may be use in publicizing this in the future for reads that must be async. private func read( @@ -279,6 +408,26 @@ public struct FileIO: Sendable { } } } + + /// Generates a fresh ETag for a file or returns its currently cached one. + /// - Parameters: + /// - path: The file's path. + /// - lastModified: When the file was last modified. + /// - Returns: An `EventLoopFuture` which holds the ETag. + private func generateETagHash(path: String, lastModified: Date) -> EventLoopFuture { + if let hash = request.application.storage[FileMiddleware.ETagHashes.self]?[path], hash.lastModified == lastModified { + return request.eventLoop.makeSucceededFuture(hash.digestHex) + } else { + return collectFile(at: path).map { buffer in + let digest = SHA256.hash(data: buffer.readableBytesView) + + // update hash in dictionary + request.application.storage[FileMiddleware.ETagHashes.self]?[path] = FileMiddleware.ETagHashes.FileHash(lastModified: lastModified, digestHex: digest.hex) + + return digest.hex + } + } + } } extension HTTPHeaders.Range.Value { diff --git a/Tests/VaporTests/FileTests.swift b/Tests/VaporTests/FileTests.swift index c089588b74..0dd9345c63 100644 --- a/Tests/VaporTests/FileTests.swift +++ b/Tests/VaporTests/FileTests.swift @@ -3,17 +3,40 @@ import XCTest import Vapor import NIOCore import NIOHTTP1 +import Crypto final class FileTests: XCTestCase { func testStreamFile() throws { let app = Application(.testing) defer { app.shutdown() } + app.get("file-stream") { req -> EventLoopFuture in + return req.fileio.streamFile(at: #file, advancedETagComparison: true) { result in + do { + try result.get() + } catch { + XCTFail("File Stream should have succeeded") + } + } + } + + try app.testable(method: .running).test(.GET, "/file-stream") { res in + let test = "the quick brown fox" + XCTAssertNotNil(res.headers.first(name: .eTag)) + XCTAssertContains(res.body.string, test) + } + } + + @available(*, deprecated) + func testLegacyStreamFile() throws { + let app = Application(.testing) + defer { app.shutdown() } + app.get("file-stream") { req in return req.fileio.streamFile(at: #filePath) { result in do { try result.get() - } catch { + } catch { XCTFail("File Stream should have succeeded") } } @@ -30,8 +53,8 @@ final class FileTests: XCTestCase { let app = Application(.testing) defer { app.shutdown() } - app.get("file-stream") { req in - return req.fileio.streamFile(at: #filePath) + app.get("file-stream") { req -> EventLoopFuture in + return req.fileio.streamFile(at: #file, advancedETagComparison: true) } var headers = HTTPHeaders() @@ -47,13 +70,13 @@ final class FileTests: XCTestCase { let app = Application(.testing) defer { app.shutdown() } - app.get("file-stream") { req -> Response in + app.get("file-stream") { req -> EventLoopFuture in var tmpPath: String repeat { tmpPath = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString).path } while (FileManager.default.fileExists(atPath: tmpPath)) - return req.fileio.streamFile(at: tmpPath) { result in + return req.fileio.streamFile(at: tmpPath, advancedETagComparison: true) { result in do { try result.get() XCTFail("File Stream should have failed") @@ -66,13 +89,59 @@ final class FileTests: XCTestCase { XCTAssertTrue(res.body.string.isEmpty) } } + + func testAdvancedETagHeaders() throws { + let app = Application(.testing) + defer { app.shutdown() } + + app.get("file-stream") { req -> EventLoopFuture in + return req.fileio.streamFile(at: #file, advancedETagComparison: true) { result in + do { + try result.get() + } catch { + XCTFail("File Stream should have succeeded") + } + } + } + + try app.testable(method: .running).test(.GET, "/file-stream") { res in + let fileData = try Data(contentsOf: URL(fileURLWithPath: #file)) + let digest = SHA256.hash(data: fileData) + let eTag = res.headers.first(name: "etag") + XCTAssertEqual(eTag, digest.hex) + } + } + + func testSimpleETagHeaders() throws { + let app = Application(.testing) + defer { app.shutdown() } + + app.get("file-stream") { req -> EventLoopFuture in + return req.fileio.streamFile(at: #file, advancedETagComparison: false) { result in + do { + try result.get() + } catch { + XCTFail("File Stream should have succeeded") + } + } + } + + try app.testable(method: .running).test(.GET, "/file-stream") { res in + let attributes = try FileManager.default.attributesOfItem(atPath: #file) + let modifiedAt = attributes[.modificationDate] as! Date + let fileSize = (attributes[.size] as? NSNumber)!.intValue + let fileETag = "\"\(modifiedAt.timeIntervalSince1970)-\(fileSize)\"" + + XCTAssertEqual(res.headers.first(name: .eTag), fileETag) + } + } func testStreamFileContentHeaderTail() throws { let app = Application(.testing) defer { app.shutdown() } - app.get("file-stream") { req in - return req.fileio.streamFile(at: #filePath) { result in + app.get("file-stream") { req -> EventLoopFuture in + return req.fileio.streamFile(at: #file, advancedETagComparison: true) { result in do { try result.get() } catch { @@ -102,8 +171,8 @@ final class FileTests: XCTestCase { let app = Application(.testing) defer { app.shutdown() } - app.get("file-stream") { req in - return req.fileio.streamFile(at: #filePath) { result in + app.get("file-stream") { req -> EventLoopFuture in + return req.fileio.streamFile(at: #file, advancedETagComparison: true) { result in do { try result.get() } catch { @@ -111,7 +180,7 @@ final class FileTests: XCTestCase { } } } - + var headerRequest = HTTPHeaders() headerRequest.range = .init(unit: .bytes, ranges: [.start(value: 20)]) try app.testable(method: .running(port: 0)).test(.GET, "/file-stream", headers: headerRequest) { res in @@ -133,8 +202,8 @@ final class FileTests: XCTestCase { let app = Application(.testing) defer { app.shutdown() } - app.get("file-stream") { req in - return req.fileio.streamFile(at: #filePath) { result in + app.get("file-stream") { req -> EventLoopFuture in + return req.fileio.streamFile(at: #file, advancedETagComparison: true) { result in do { try result.get() } catch { @@ -165,7 +234,7 @@ final class FileTests: XCTestCase { defer { app.shutdown() } app.get("file-stream") { req in - return req.fileio.streamFile(at: #filePath) { result in + return req.fileio.streamFile(at: #file, advancedETagComparison: true) { result in do { try result.get() } catch { @@ -191,8 +260,8 @@ final class FileTests: XCTestCase { let app = Application(.testing) defer { app.shutdown() } - app.get("file-stream") { req in - return req.fileio.streamFile(at: #filePath) { result in + app.get("file-stream") { req -> EventLoopFuture in + return req.fileio.streamFile(at: #file, advancedETagComparison: true) { result in do { try result.get() } catch { @@ -217,8 +286,8 @@ final class FileTests: XCTestCase { let app = Application(.testing) defer { app.shutdown() } - app.get("file-stream") { req in - return req.fileio.streamFile(at: #filePath) { result in + app.get("file-stream") { req -> EventLoopFuture in + return req.fileio.streamFile(at: #file, advancedETagComparison: true) { result in do { try result.get() } catch { @@ -243,8 +312,8 @@ final class FileTests: XCTestCase { let app = Application(.testing) defer { app.shutdown() } - app.get("file-stream") { req in - return req.fileio.streamFile(at: #filePath) { result in + app.get("file-stream") { req -> EventLoopFuture in + return req.fileio.streamFile(at: #file, advancedETagComparison: true) { result in do { try result.get() } catch { @@ -423,8 +492,8 @@ final class FileTests: XCTestCase { let app = Application(.testing) defer { app.shutdown() } - app.get("file-stream") { req in - return req.fileio.streamFile(at: #filePath) + app.get("file-stream") { req -> EventLoopFuture in + return req.fileio.streamFile(at: #file, advancedETagComparison: true) } var headers = HTTPHeaders() From 0311f9a7eadf5117808c50dd2c7d3cfd6b95d064 Mon Sep 17 00:00:00 2001 From: Nicolas Bachschmidt Date: Tue, 23 Apr 2024 13:17:30 +0200 Subject: [PATCH 14/18] Remove HeadResponder (#3147) The HEAD method is identical to GET except that the server must not send content in the response (RFC 9110, section 9.3.2). The previous default behaviour of returning 200 OK to every HEAD request to a constant route is not standard-compliant. The new behaviour is to always forward the request to the GET route, unless the developer explicitly configured a custom HEAD route. --- .../Vapor/Responder/DefaultResponder.swift | 26 ------------------- Tests/VaporTests/RouteTests.swift | 21 +++------------ 2 files changed, 3 insertions(+), 44 deletions(-) diff --git a/Sources/Vapor/Responder/DefaultResponder.swift b/Sources/Vapor/Responder/DefaultResponder.swift index c342131261..1a2dece011 100644 --- a/Sources/Vapor/Responder/DefaultResponder.swift +++ b/Sources/Vapor/Responder/DefaultResponder.swift @@ -39,26 +39,6 @@ internal struct DefaultResponder: Responder { } } - // If the route isn't explicitly a HEAD route, - // and it's made up solely of .constant components, - // register a HEAD route with the same path - if route.method == .GET && - route.path.allSatisfy({ component in - if case .constant(_) = component { return true } - return false - }) { - let headRoute = Route( - method: .HEAD, - path: route.path, - responder: middleware.makeResponder(chainingTo: HeadResponder()), - requestType: route.requestType, - responseType: route.responseType) - - let headCachedRoute = CachedRoute(route: headRoute, responder: middleware.makeResponder(chainingTo: HeadResponder())) - - router.register(headCachedRoute, at: [.constant(HTTPMethod.HEAD.string)] + path) - } - router.register(cached, at: [.constant(route.method.string)] + path) } self.router = router @@ -155,12 +135,6 @@ internal struct DefaultResponder: Responder { } } -private struct HeadResponder: Responder { - func respond(to request: Request) -> EventLoopFuture { - request.eventLoop.makeSucceededFuture(.init(status: .ok)) - } -} - private struct NotFoundResponder: Responder { func respond(to request: Request) -> EventLoopFuture { request.eventLoop.makeFailedFuture(RouteNotFound()) diff --git a/Tests/VaporTests/RouteTests.swift b/Tests/VaporTests/RouteTests.swift index bf28872016..4ecbce263a 100644 --- a/Tests/VaporTests/RouteTests.swift +++ b/Tests/VaporTests/RouteTests.swift @@ -229,38 +229,23 @@ final class RouteTests: XCTestCase { } } - func testHeadRequestWithConstantPathReturnsOK() throws { + func testHeadRequestForwardedToGet() throws { let app = Application(.testing) defer { app.shutdown() } app.get("hello") { req -> String in - return "hi" - } - - try app.testable(method: .running(port: 0)).test(.HEAD, "/hello") { res in - XCTAssertEqual(res.status, .ok) - XCTAssertEqual(res.headers.first(name: .contentLength), "0") - XCTAssertEqual(res.body.readableBytes, 0) - } - } - - func testHeadRequestWithParameterForwardedToGet() throws { - let app = Application(.testing) - defer { app.shutdown() } - - app.get("hello", ":name") { req -> String in XCTAssertEqual(req.method, .HEAD) return "hi" } - try app.testable(method: .running(port: 0)).test(.HEAD, "/hello/joe") { res in + try app.testable(method: .running(port: 0)).test(.HEAD, "/hello") { res in XCTAssertEqual(res.status, .ok) XCTAssertEqual(res.headers.first(name: .contentLength), "2") XCTAssertEqual(res.body.readableBytes, 0) } } - func testExplicitHeadRouteHandlerOverridesGeneratedHandler() throws { + func testExplicitHeadRouteOverridesForwardingToGet() throws { let app = Application(.testing) defer { app.shutdown() } From 3e3d65b05a280aaecdd33e45f71655b5d9118a00 Mon Sep 17 00:00:00 2001 From: Linus <42340116+linus-hologram@users.noreply.github.com> Date: Wed, 24 Apr 2024 01:38:49 +0200 Subject: [PATCH 15/18] Removed streamFile deprecation + deactivated advancedETagComparison by default (#3177) * Update FileIO.swift sha-256 digest set when file is written to system * Introduced new streamFile method Introduced new streamFile method which allows for advancedETag comparison. Deprecated the old one. * Updated Unit Tests Updates to remove deprecated warnings by using the new streamFile() method. Also removed some other deprecation warnings. * Added more documentation * Removed unnecessary CryptoKit import * added closure return types * incorporated first round of @0xTim's feedback * added test cases to account for advanced/simple etags * Incorporated PR comments - adjusted faulty comment - access storage directly to avoid concurrent overwrites of the entire storage * Added test for legacy streamFile * Deprecated method to silence warnings * Warning fixes * following @gwynne's style advice :) * undone deprecation + default of new advancedEtagComparison --------- Co-authored-by: Tim Condon <0xTim@users.noreply.github.com> Co-authored-by: Gwynne Raskind --- Sources/Vapor/Middleware/FileMiddleware.swift | 2 +- Sources/Vapor/Utilities/FileIO.swift | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/Vapor/Middleware/FileMiddleware.swift b/Sources/Vapor/Middleware/FileMiddleware.swift index d1c4b6867e..f7a45b8ec7 100644 --- a/Sources/Vapor/Middleware/FileMiddleware.swift +++ b/Sources/Vapor/Middleware/FileMiddleware.swift @@ -40,7 +40,7 @@ public final class FileMiddleware: Middleware { /// an absolute path from the public directory root. If `nil`, no default files are served. /// - directoryAction: Determines the action to take when the request doesn't have a trailing slash but matches a directory. /// - advancedETagComparison: The method used when ETags are generated. If true, a byte-by-byte hash is created (and cached), otherwise a simple comparison based on the file's last modified date and size. - public init(publicDirectory: String, defaultFile: String? = nil, directoryAction: DirectoryAction = .none, advancedETagComparison: Bool = true) { + public init(publicDirectory: String, defaultFile: String? = nil, directoryAction: DirectoryAction = .none, advancedETagComparison: Bool = false) { self.publicDirectory = publicDirectory.addTrailingSlash() self.defaultFile = defaultFile self.directoryAction = directoryAction diff --git a/Sources/Vapor/Utilities/FileIO.swift b/Sources/Vapor/Utilities/FileIO.swift index 7a3820989f..426ba9f1d2 100644 --- a/Sources/Vapor/Utilities/FileIO.swift +++ b/Sources/Vapor/Utilities/FileIO.swift @@ -123,7 +123,6 @@ public struct FileIO: Sendable { /// - mediaType: HTTPMediaType, if not specified, will be created from file extension. /// - onCompleted: Closure to be run on completion of stream. /// - returns: A `200 OK` response containing the file stream and appropriate headers. - @available(*, deprecated, message: "Use the new `streamFile` method which returns EventLoopFuture") @preconcurrency public func streamFile( at path: String, chunkSize: Int = NonBlockingFileIO.defaultChunkSize, From 4c80aab7a46860920022e03ced012bca3a71b06b Mon Sep 17 00:00:00 2001 From: Tim Condon <0xTim@users.noreply.github.com> Date: Wed, 24 Apr 2024 01:03:56 +0000 Subject: [PATCH 16/18] Migrate to Async NIOFileIO APIs (#3167) * Add tests * Update NIO dependency * Migrate over to async APIs for FileIO * Add AsyncSequence-based file read method (#3170) * Add AsyncSequence-based file read method * Add `NIOFileSystem` dependency to standard manifest * Update test * Add long test file * Migrate new API to use NIOFileSystem * Fix mess up --------- Co-authored-by: Paul Toffoloni <69189821+ptoffy@users.noreply.github.com> --- Package.swift | 4 +- Package@swift-5.9.swift | 4 +- .../Concurrency/FileIO+Concurrency.swift | 50 -- Sources/Vapor/Utilities/FileIO.swift | 126 +++- Tests/VaporTests/FileTests.swift | 35 + Tests/VaporTests/Utilities/long-test-file.txt | 599 ++++++++++++++++++ 6 files changed, 755 insertions(+), 63 deletions(-) delete mode 100644 Sources/Vapor/Concurrency/FileIO+Concurrency.swift create mode 100644 Tests/VaporTests/Utilities/long-test-file.txt diff --git a/Package.swift b/Package.swift index 65c0007bd5..7db514e49a 100644 --- a/Package.swift +++ b/Package.swift @@ -34,7 +34,7 @@ let package = Package( .package(url: "https://github.com/swift-server/swift-backtrace.git", from: "1.1.1"), // Event-driven network application framework for high performance protocol servers & clients, non-blocking. - .package(url: "https://github.com/apple/swift-nio.git", from: "2.62.0"), + .package(url: "https://github.com/apple/swift-nio.git", from: "2.63.0"), // Bindings to OpenSSL-compatible libraries for TLS support in SwiftNIO .package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.8.0"), @@ -92,6 +92,8 @@ let package = Package( .product(name: "WebSocketKit", package: "websocket-kit"), .product(name: "MultipartKit", package: "multipart-kit"), .product(name: "Atomics", package: "swift-atomics"), + + .product(name: "_NIOFileSystem", package: "swift-nio"), ]), // Development diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift index 81f565c5ed..28bc42855e 100644 --- a/Package@swift-5.9.swift +++ b/Package@swift-5.9.swift @@ -31,7 +31,7 @@ let package = Package( .package(url: "https://github.com/vapor/routing-kit.git", from: "4.9.0"), // Event-driven network application framework for high performance protocol servers & clients, non-blocking. - .package(url: "https://github.com/apple/swift-nio.git", from: "2.62.0"), + .package(url: "https://github.com/apple/swift-nio.git", from: "2.63.0"), // Bindings to OpenSSL-compatible libraries for TLS support in SwiftNIO .package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.8.0"), @@ -90,6 +90,8 @@ let package = Package( .product(name: "WebSocketKit", package: "websocket-kit"), .product(name: "MultipartKit", package: "multipart-kit"), .product(name: "Atomics", package: "swift-atomics"), + + .product(name: "_NIOFileSystem", package: "swift-nio"), ], swiftSettings: [.enableExperimentalFeature("StrictConcurrency=complete")] ), diff --git a/Sources/Vapor/Concurrency/FileIO+Concurrency.swift b/Sources/Vapor/Concurrency/FileIO+Concurrency.swift deleted file mode 100644 index 4c9c41f754..0000000000 --- a/Sources/Vapor/Concurrency/FileIO+Concurrency.swift +++ /dev/null @@ -1,50 +0,0 @@ -import NIOCore - -extension FileIO { - /// Reads the contents of a file at the supplied path. - /// - /// let data = try await req.fileio.collectFile(file: "/path/to/file.txt") - /// print(data) // file data - /// - /// - parameters: - /// - path: Path to file on the disk. - /// - returns: `ByteBuffer` containing the file data. - public func collectFile(at path: String) async throws -> ByteBuffer { - return try await self.collectFile(at: path).get() - } - - /// Reads the contents of a file at the supplied path in chunks. - /// - /// try await req.fileio().readChunked(file: "/path/to/file.txt") { chunk in - /// print("chunk: \(data)") - /// } - /// - /// - parameters: - /// - path: Path to file on the disk. - /// - chunkSize: Maximum size for the file data chunks. - /// - onRead: Closure to be called sequentially for each file data chunk. - /// - returns: `Void` when the file read is finished. - // public func readFile(at path: String, chunkSize: Int = NonBlockingFileIO.defaultChunkSize, onRead: @escaping (ByteBuffer) async throws -> Void) async throws { - // // TODO - // // We should probably convert the internal private read function to async as well rather than wrapping it at this top level - // let promise = self.request.eventLoop.makePromise(of: Void.self) - // promise.completeWithTask { - // try await onRead - // } - // let closureFuture = promise.futureResult - // return try self.readFile(at: path, onRead: closureFuture).get() - // } - - /// Write the contents of buffer to a file at the supplied path. - /// - /// let data = ByteBuffer(string: "ByteBuffer") - /// try await req.fileio.writeFile(data, at: "/path/to/file.txt") - /// - /// - parameters: - /// - path: Path to file on the disk. - /// - buffer: The `ByteBuffer` to write. - /// - returns: `Void` when the file write is finished. - public func writeFile(_ buffer: ByteBuffer, at path: String) async throws { - return try await self.writeFile(buffer, at: path).get() - } -} diff --git a/Sources/Vapor/Utilities/FileIO.swift b/Sources/Vapor/Utilities/FileIO.swift index 426ba9f1d2..eeb431c116 100644 --- a/Sources/Vapor/Utilities/FileIO.swift +++ b/Sources/Vapor/Utilities/FileIO.swift @@ -1,5 +1,6 @@ import Foundation import NIOCore +import NIOFileSystem import NIOHTTP1 import NIOPosix import Logging @@ -22,21 +23,19 @@ extension Request { /// /// It can read files, both in their entirety and chunked. /// -/// let fileio = try c.make(FileIO.self) /// -/// fileio.readFile(at: "/path/to/file.txt") { chunk in +/// req.fileio.readFile(at: "/path/to/file.txt") { chunk in /// print(chunk) // part of file /// } /// -/// fileio.collectFile(at: "/path/to/file.txt").map { file in +/// req.fileio.collectFile(at: "/path/to/file.txt").map { file in /// print(file) // entire file /// } /// /// It can also create streaming HTTP responses. /// -/// let fileio = try c.make(FileIO.self) -/// router.get("file-stream") { req -> Response in -/// return fileio.streamFile(at: "/path/to/file.txt", for: req) +/// app.get("file-stream") { req -> Response in +/// return req.fileio.streamFile(at: "/path/to/file.txt", for: req) /// } /// /// Streaming file responses respect `E-Tag` headers present in the request. @@ -50,6 +49,8 @@ public struct FileIO: Sendable { /// HTTP request context. let request: Request + let fileSystem: FileSystem = .shared + /// Creates a new `FileIO`. /// /// See `Request.fileio()` to create one. @@ -61,12 +62,12 @@ public struct FileIO: Sendable { /// Reads the contents of a file at the supplied path. /// - /// let data = try req.fileio().read(file: "/path/to/file.txt").wait() + /// let data = try req.fileio.collectFile(at: "/path/to/file.txt").wait() /// print(data) // file data /// /// - parameters: /// - path: Path to file on the disk. - /// - returns: `Future` containing the file data. + /// - returns: `Future` containing the file data as a `ByteBuffer`. public func collectFile(at path: String) -> EventLoopFuture { let dataWrapper: NIOLockedValueBox = .init(self.allocator.buffer(capacity: 0)) return self.readFile(at: path) { new in @@ -78,7 +79,7 @@ public struct FileIO: Sendable { /// Reads the contents of a file at the supplied path in chunks. /// - /// try req.fileio().readChunked(file: "/path/to/file.txt") { chunk in + /// try req.fileio.readFile(at: "/path/to/file.txt") { chunk in /// print("chunk: \(data)") /// }.wait() /// @@ -113,7 +114,7 @@ public struct FileIO: Sendable { /// has not been modified since last served. This method will also set the `"Content-Type"` header /// automatically if an appropriate `MediaType` can be found for the file's suffix. /// - /// router.get("file-stream") { req in + /// app.get("file-stream") { req in /// return req.fileio.streamFile(at: "/path/to/file.txt") /// } /// @@ -383,6 +384,19 @@ public struct FileIO: Sendable { } } + /// Async version of `read(path:fromOffset:byteCount:chunkSize:onRead)` + private func read( + path: String, + fromOffset offset: Int64, + byteCount: Int + ) async throws -> ByteBuffer { + let fd = try NIOFileHandle(path: path) + defer { + try? fd.close() + } + return try await self.io.read(fileHandle: fd, fromOffset: offset, byteCount: byteCount, allocator: allocator) + } + /// Write the contents of buffer to a file at the supplied path. /// /// let data = ByteBuffer(string: "ByteBuffer") @@ -422,11 +436,101 @@ public struct FileIO: Sendable { // update hash in dictionary request.application.storage[FileMiddleware.ETagHashes.self]?[path] = FileMiddleware.ETagHashes.FileHash(lastModified: lastModified, digestHex: digest.hex) - + return digest.hex } } } + + // MARK: - Concurrency + /// Reads the contents of a file at the supplied path. + /// + /// let data = try await req.fileio.collectFile(file: "/path/to/file.txt") + /// print(data) // file data + /// + /// - parameters: + /// - path: Path to file on the disk. + /// - returns: `ByteBuffer` containing the file data. + public func collectFile(at path: String) async throws -> ByteBuffer { + guard let fileSize = try await FileSystem.shared.info(forFileAt: .init(path))?.size else { + throw Abort(.internalServerError) + } + return try await self.read(path: path, fromOffset: 0, byteCount: Int(fileSize)) + } + + /// Wrapper around `NIOFileSystem.FileChunks`. + /// This can be removed once `NIOFileSystem` reaches a stable API. + public struct FileChunks: AsyncSequence { + public typealias Element = ByteBuffer + private let fileHandle: NIOFileSystem.FileHandleProtocol + private let fileChunks: NIOFileSystem.FileChunks + + init(fileChunks: NIOFileSystem.FileChunks, fileHandle: some NIOFileSystem.FileHandleProtocol) { + self.fileChunks = fileChunks + self.fileHandle = fileHandle + } + + public struct FileChunksIterator: AsyncIteratorProtocol { + private var iterator: NIOFileSystem.FileChunks.AsyncIterator + private let fileHandle: NIOFileSystem.FileHandleProtocol + + fileprivate init(wrapping iterator: NIOFileSystem.FileChunks.AsyncIterator, fileHandle: some NIOFileSystem.FileHandleProtocol) { + self.iterator = iterator + self.fileHandle = fileHandle + } + + public mutating func next() async throws -> ByteBuffer? { + let chunk = try await iterator.next() + if chunk == nil { + try await fileHandle.close() + } + return chunk + } + } + + public func makeAsyncIterator() -> FileChunksIterator { + FileChunksIterator(wrapping: fileChunks.makeAsyncIterator(), fileHandle: fileHandle) + } + } + + /// Reads the contents of a file at the supplied path in chunks. + /// + /// for chunk in try await req.fileio.readFile(at: "/path/to/file.txt") { + /// print("chunk: \(data)") + /// } + /// + /// - parameters: + /// - path: Path to file on the disk. + /// - chunkSize: Maximum size for the file data chunks. + /// - returns: `FileChunks` containing the file data chunks. + public func readFile( + at path: String, + chunkSize: Int = NonBlockingFileIO.defaultChunkSize + ) async throws -> FileChunks { + let filePath = FilePath(path) + + let readHandle = try await fileSystem.openFile(forReadingAt: filePath) + let chunks = readHandle.readChunks(chunkLength: .bytes(Int64(chunkSize))) + + return FileChunks(fileChunks: chunks, fileHandle: readHandle) + } + + /// Write the contents of buffer to a file at the supplied path. + /// + /// let data = ByteBuffer(string: "ByteBuffer") + /// try await req.fileio.writeFile(data, at: "/path/to/file.txt") + /// + /// - parameters: + /// - path: Path to file on the disk. + /// - buffer: The `ByteBuffer` to write. + /// - returns: `Void` when the file write is finished. + public func writeFile(_ buffer: ByteBuffer, at path: String) async throws { + let fd = try NIOFileHandle(path: path, mode: .write, flags: .allowFileCreation()) + defer { + try? fd.close() + } + try await self.io.write(fileHandle: fd, buffer: buffer) + } } extension HTTPHeaders.Range.Value { diff --git a/Tests/VaporTests/FileTests.swift b/Tests/VaporTests/FileTests.swift index 0dd9345c63..47c1c1c6b2 100644 --- a/Tests/VaporTests/FileTests.swift +++ b/Tests/VaporTests/FileTests.swift @@ -537,4 +537,39 @@ final class FileTests: XCTestCase { XCTAssertEqual(res.status, .badRequest) } } + + func testAsyncFileWrite() async throws { + let app = Application(.testing) + defer { app.shutdown() } + + let request = Request(application: app, on: app.eventLoopGroup.next()) + + let data = "Hello" + let path = "/tmp/fileio_write.txt" + + try await request.fileio.writeFile(ByteBuffer(string: data), at: path) + defer { try? FileManager.default.removeItem(atPath: path) } + + let result = try String(contentsOfFile: path) + XCTAssertEqual(result, data) + } + + func testAsyncFileRead() async throws { + let app = Application(.testing) + defer { app.shutdown() } + + let request = Request(application: app, on: app.eventLoopGroup.next()) + + let path = "/" + #filePath.split(separator: "/").dropLast().joined(separator: "/") + "/Utilities/long-test-file.txt" + + let content = try String(contentsOfFile: path) + + var readContent = "" + let file = try await request.fileio.readFile(at: path, chunkSize: 16 * 1024) // 32Kb, ~5 chunks + for try await chunk in file { + readContent += String(buffer: chunk) + } + + XCTAssertEqual(readContent, content, "The content read from the file does not match the expected content.") + } } diff --git a/Tests/VaporTests/Utilities/long-test-file.txt b/Tests/VaporTests/Utilities/long-test-file.txt new file mode 100644 index 0000000000..df67018a3f --- /dev/null +++ b/Tests/VaporTests/Utilities/long-test-file.txt @@ -0,0 +1,599 @@ +Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. + +Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. + +Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. + +At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. + +Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. + +Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. + +Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. + +Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. + +At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. + +Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. + +Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. + +Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. + +Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. + +At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. + +Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. + +Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. + +Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. + +Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. + +At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. + +Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. + +Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. + +Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. + +Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. + +At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. + +Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. + +Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. + +Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. + +Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. + +At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. + +Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. + +Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. + +Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. + +Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. + +At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. + +Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. + +Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. + +Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. + +Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. + +At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. + +Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. + +Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. + +Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. + +Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. + +At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. + +Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. + +Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. + +Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. + +Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. + +At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. + +Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. + +Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. + +Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. + +Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. + +At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. + +Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. + +Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. + +Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. + +Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. + +At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. + +Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. + +Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. + +Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. + +Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. + +At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. + +Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. + +Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. + +Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. + +Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. + +At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. + +Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. + +Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. + +Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. + +Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. + +At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. + +Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. + +Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. + +Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. + +Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. + +At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. + +Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. + +Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. + +Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. + +Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. + +At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. + +Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. + +Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. + +Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. + +Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. + +At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. + +Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. + +Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. + +Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. + +Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. + +At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. + +Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. + +Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. + +Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. + +Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. + +At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. + +Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. + +Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. + +Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. + +Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. + +At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. + +Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. + +Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. + +Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. + +Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. + +At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. + +Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. + +Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. + +Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. + +Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. + +At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. + +Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. + +Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. + +Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. + +Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. + +At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. + +Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. + +Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. + +Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. + +Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. + +At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. + +Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. + +Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. + +Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. + +Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. + +At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. + +Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. + +Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. + +Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. + +Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. + +At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. + +Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. + +Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. + +Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. + +Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. + +At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. + +Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. + +Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. + +Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. + +Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. + +At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. + +Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. + +Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. + +Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. + +Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. + +At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. + +Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. + +Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. + +Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. + +Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. + +At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. + +Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. + +Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. + +Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. + +Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. + +At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. + +Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. + +Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. + +Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. + +Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. + +At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. + +Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. + +Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. + +Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. + +Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. + +At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. + +Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. + +Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. + +Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. + +Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. + +At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. + +Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. + +Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. + +Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. + +Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. + +At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. + +Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. + +Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. + +Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. + +Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. + +At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. + +Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. + +Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. + +Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. + +Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. + +At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. + +Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. + +Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. + +Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. + +Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. + +At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. + +Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. + +Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. + +Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. + +Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. + +At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. + +Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. + +Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. + +Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. + +Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. + +At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. + +Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. + +Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. + +Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. + +Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. + +At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. + +Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. + +Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. + +Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. + +Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. + +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. + +At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. From 489acd9a5ce0556f6e3afea111c844f4b8ee9ad0 Mon Sep 17 00:00:00 2001 From: Tim Condon <0xTim@users.noreply.github.com> Date: Fri, 26 Apr 2024 16:24:25 +0000 Subject: [PATCH 17/18] Update provider tests to 5.10 (#3178) --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5bb84c48f8..5ca1d9120a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,7 +23,7 @@ jobs: - provider: vapor/apns ref: 3.0.0 runs-on: ubuntu-latest - container: swift:5.9-jammy + container: swift:5.10 steps: - name: Check out Vapor uses: actions/checkout@v4 From e5cb0abdadff95ef996fafd6268f2f5f8cba6325 Mon Sep 17 00:00:00 2001 From: Michael Bisgaard Olesen Date: Fri, 26 Apr 2024 21:56:42 +0200 Subject: [PATCH 18/18] Patch configuration and log actual port on startup (#3160) * Log actual port on startup Before this change, the application ```swift let app = Application(.testing) defer { app.shutdown() } try app.server.start(hostname: nil, port: 0) defer { app.server.shutdown() } ``` would log the following message *before* starting the server: ``` [Vapor] Server starting on http://127.0.0.1:0 ``` After this change it instead logs a message like the following *after* starting the server: ``` [Vapor] Server started on [IPv4]127.0.0.1/127.0.0.1:57935 ``` * Avoid changing log format * Log both before and after starting * Make 'addressDescription' a computed property Just invoking the property twice seemed cleaner to me compared to duplicating the code block. Fixed a little indentation issue in the process I don't know if it's ever relevant, but config is now patched on all address types, including unix socket. * Remove obsolete variable * Make 'addressDescription' internal * Add test * Fix broken test --------- Co-authored-by: Tim Condon <0xTim@users.noreply.github.com> --- Sources/Vapor/HTTP/Server/HTTPServer.swift | 73 +++++++++++++--------- Tests/VaporTests/ApplicationTests.swift | 4 +- Tests/VaporTests/ServerTests.swift | 10 +++ 3 files changed, 56 insertions(+), 31 deletions(-) diff --git a/Sources/Vapor/HTTP/Server/HTTPServer.swift b/Sources/Vapor/HTTP/Server/HTTPServer.swift index 7309b7df21..8ca7e5246f 100644 --- a/Sources/Vapor/HTTP/Server/HTTPServer.swift +++ b/Sources/Vapor/HTTP/Server/HTTPServer.swift @@ -48,23 +48,34 @@ public final class HTTPServer: Server, Sendable { /// Port the server will bind to. public var port: Int { - get { - switch address { - case .hostname(_, let port): - return port ?? Self.defaultPort - default: - return Self.defaultPort - } - } - set { - switch address { - case .hostname(let hostname, _): - address = .hostname(hostname, port: newValue) - default: - address = .hostname(nil, port: newValue) - } - } - } + get { + switch address { + case .hostname(_, let port): + return port ?? Self.defaultPort + default: + return Self.defaultPort + } + } + set { + switch address { + case .hostname(let hostname, _): + address = .hostname(hostname, port: newValue) + default: + address = .hostname(nil, port: newValue) + } + } + } + + /// A human-readable description of the configured address. Used in log messages when starting server. + var addressDescription: String { + let scheme = tlsConfiguration == nil ? "http" : "https" + switch address { + case .hostname(let hostname, let port): + return "\(scheme)://\(hostname ?? Self.defaultHostname):\(port ?? Self.defaultPort)" + case .unixDomainSocket(let socketPath): + return "\(scheme)+unix: \(socketPath)" + } + } /// Listen backlog. public var backlog: Int @@ -307,19 +318,10 @@ public final class HTTPServer: Server, Sendable { /// Override the socket path. configuration.address = address! } - - /// Print starting message. - let scheme = configuration.tlsConfiguration == nil ? "http" : "https" - let addressDescription: String - switch configuration.address { - case .hostname(let hostname, let port): - addressDescription = "\(scheme)://\(hostname ?? configuration.hostname):\(port ?? configuration.port)" - case .unixDomainSocket(let socketPath): - addressDescription = "\(scheme)+unix: \(socketPath)" - } - - self.configuration.logger.notice("Server starting on \(addressDescription)") + /// Log starting message for debugging before attempting to start the server. + configuration.logger.debug("Server starting on \(configuration.addressDescription)") + /// Start the actual `HTTPServer`. try self.connection.withLockedValue { $0 = try HTTPServerConnection.start( @@ -331,6 +333,19 @@ public final class HTTPServer: Server, Sendable { ).wait() } + /// Overwrite configuration with actual address, if applicable. + /// They may differ from the provided configuation if port 0 was provided, for example. + if let localAddress = self.localAddress { + if let hostname = localAddress.hostname, let port = localAddress.port { + configuration.address = .hostname(hostname, port: port) + } else if let pathname = localAddress.pathname { + configuration.address = .unixDomainSocket(path: pathname) + } + } + + /// Log started message with the actual configuration. + configuration.logger.notice("Server started on \(configuration.addressDescription)") + self.configuration = configuration self.didStart.withLockedValue { $0 = true } } diff --git a/Tests/VaporTests/ApplicationTests.swift b/Tests/VaporTests/ApplicationTests.swift index a46cfafde4..2aaff1faf5 100644 --- a/Tests/VaporTests/ApplicationTests.swift +++ b/Tests/VaporTests/ApplicationTests.swift @@ -179,7 +179,7 @@ final class ApplicationTests: XCTestCase { XCTAssertNotNil(app.http.server.shared.localAddress) XCTAssertEqual("0.0.0.0", app.http.server.configuration.hostname) - XCTAssertEqual(0, app.http.server.configuration.port) + XCTAssertEqual(app.http.server.shared.localAddress?.port, app.http.server.configuration.port) guard let localAddress = app.http.server.shared.localAddress, localAddress.ipAddress != nil, @@ -190,7 +190,7 @@ final class ApplicationTests: XCTestCase { let response = try app.client.get("http://localhost:\(port)/hello").wait() let returnedConfig = try response.content.decode(AddressConfig.self) XCTAssertEqual(returnedConfig.hostname, "0.0.0.0") - XCTAssertEqual(returnedConfig.port, 0) + XCTAssertEqual(returnedConfig.port, port) } func testConfigurationAddressDetailsReflectedWhenProvidedThroughServeCommand() throws { diff --git a/Tests/VaporTests/ServerTests.swift b/Tests/VaporTests/ServerTests.swift index bcf7a0a7d0..4254978e97 100644 --- a/Tests/VaporTests/ServerTests.swift +++ b/Tests/VaporTests/ServerTests.swift @@ -1409,6 +1409,16 @@ final class ServerTests: XCTestCase { } } + func testConfigurationHasActualPortAfterStart() throws { + let app = Application(.testing) + app.http.server.configuration.port = 0 + defer { app.shutdown() } + try app.start() + + XCTAssertNotEqual(app.http.server.configuration.port, 0) + XCTAssertEqual(app.http.server.configuration.port, app.http.server.shared.localAddress?.port) + } + override class func setUp() { XCTAssertTrue(isLoggingConfigured) }