2022-02-07 09:42:07 +00:00
package proxy
2020-10-08 10:12:26 +00:00
import (
2020-10-20 15:26:55 +00:00
"context"
2020-11-02 11:21:34 +00:00
"fmt"
2020-10-08 10:12:26 +00:00
"io"
"net/http"
"strconv"
2021-03-08 16:46:23 +00:00
"github.com/pkg/errors"
"github.com/rs/zerolog"
2022-04-06 23:20:29 +00:00
"go.opentelemetry.io/otel/attribute"
2022-04-11 23:02:13 +00:00
"go.opentelemetry.io/otel/trace"
2021-03-08 16:46:23 +00:00
2021-07-01 18:30:26 +00:00
"github.com/cloudflare/cloudflared/carrier"
2022-04-11 16:58:18 +00:00
"github.com/cloudflare/cloudflared/cfio"
2020-10-08 10:12:26 +00:00
"github.com/cloudflare/cloudflared/connection"
2020-11-02 11:21:34 +00:00
"github.com/cloudflare/cloudflared/ingress"
2022-12-25 04:05:51 +00:00
"github.com/cloudflare/cloudflared/stream"
2022-04-06 23:20:29 +00:00
"github.com/cloudflare/cloudflared/tracing"
2020-10-08 10:12:26 +00:00
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
)
const (
2021-07-16 15:14:37 +00:00
// TagHeaderNamePrefix indicates a Cloudflared Warp Tag prefix that gets appended for warp traffic stream headers.
2021-05-15 04:49:34 +00:00
TagHeaderNamePrefix = "Cf-Warp-Tag-"
LogFieldCFRay = "cfRay"
LogFieldRule = "ingressRule"
LogFieldOriginService = "originService"
2022-06-09 12:55:26 +00:00
LogFieldFlowID = "flowID"
2023-02-22 14:52:44 +00:00
LogFieldConnIndex = "connIndex"
2022-08-16 11:21:58 +00:00
trailerHeaderName = "Trailer"
2020-10-08 10:12:26 +00:00
)
2021-07-16 15:14:37 +00:00
// Proxy represents a means to Proxy between cloudflared and the origin services.
type Proxy struct {
2022-02-11 10:49:06 +00:00
ingressRules ingress . Ingress
2021-01-17 20:22:53 +00:00
warpRouting * ingress . WarpRoutingService
2023-03-21 18:42:25 +00:00
management * ingress . ManagementService
2020-11-02 11:21:34 +00:00
tags [ ] tunnelpogs . Tag
2020-11-25 06:55:13 +00:00
log * zerolog . Logger
2020-10-08 10:12:26 +00:00
}
2021-07-16 15:14:37 +00:00
// NewOriginProxy returns a new instance of the Proxy struct.
2021-01-17 20:22:53 +00:00
func NewOriginProxy (
2022-02-11 10:49:06 +00:00
ingressRules ingress . Ingress ,
2022-06-13 16:44:27 +00:00
warpRouting ingress . WarpRoutingConfig ,
2021-01-17 20:22:53 +00:00
tags [ ] tunnelpogs . Tag ,
2021-07-16 15:14:37 +00:00
log * zerolog . Logger ,
) * Proxy {
2022-02-11 10:49:06 +00:00
proxy := & Proxy {
2020-11-02 11:21:34 +00:00
ingressRules : ingressRules ,
tags : tags ,
2020-11-25 06:55:13 +00:00
log : log ,
2020-10-08 10:12:26 +00:00
}
2022-06-13 16:44:27 +00:00
if warpRouting . Enabled {
proxy . warpRouting = ingress . NewWarpRoutingService ( warpRouting )
2022-02-11 10:49:06 +00:00
log . Info ( ) . Msgf ( "Warp-routing is enabled" )
}
return proxy
2020-10-08 10:12:26 +00:00
}
2022-09-22 14:11:59 +00:00
func ( p * Proxy ) applyIngressMiddleware ( rule * ingress . Rule , r * http . Request , w connection . ResponseWriter ) ( error , bool ) {
for _ , handler := range rule . Handlers {
result , err := handler . Handle ( r . Context ( ) , r )
if err != nil {
return errors . Wrap ( err , fmt . Sprintf ( "error while processing middleware handler %s" , handler . Name ( ) ) ) , false
}
if result . ShouldFilterRequest {
w . WriteRespHeaders ( result . StatusCode , nil )
return fmt . Errorf ( "request filtered by middleware handler (%s) due to: %s" , handler . Name ( ) , result . Reason ) , true
}
}
return nil , true
}
2021-07-16 15:14:37 +00:00
// ProxyHTTP further depends on ingress rules to establish a connection with the origin service. This may be
// a simple roundtrip or a tcp/websocket dial depending on ingres rule setup.
func ( p * Proxy ) ProxyHTTP (
w connection . ResponseWriter ,
2022-07-26 21:00:53 +00:00
tr * tracing . TracedHTTPRequest ,
2021-07-16 15:14:37 +00:00
isWebsocket bool ,
) error {
2020-10-08 10:12:26 +00:00
incrementRequests ( )
defer decrementConcurrentRequests ( )
2022-04-06 23:20:29 +00:00
req := tr . Request
2021-07-16 15:14:37 +00:00
cfRay := connection . FindCfRayHeader ( req )
lbProbe := connection . IsLBProbeRequest ( req )
2021-08-23 15:04:09 +00:00
p . appendTagHeaders ( req )
2021-01-17 20:22:53 +00:00
2022-04-11 23:02:13 +00:00
_ , ruleSpan := tr . Tracer ( ) . Start ( req . Context ( ) , "ingress_match" ,
trace . WithAttributes ( attribute . String ( "req-host" , req . Host ) ) )
2020-12-09 21:46:53 +00:00
rule , ruleNum := p . ingressRules . FindMatchingRule ( req . Host , req . URL . Path )
2021-02-05 13:01:53 +00:00
logFields := logFields {
2023-02-22 14:52:44 +00:00
cfRay : cfRay ,
lbProbe : lbProbe ,
rule : ruleNum ,
connIndex : tr . ConnIndex ,
2021-02-05 13:01:53 +00:00
}
p . logRequest ( req , logFields )
2022-04-06 23:20:29 +00:00
ruleSpan . SetAttributes ( attribute . Int ( "rule-num" , ruleNum ) )
ruleSpan . End ( )
2022-09-22 14:11:59 +00:00
if err , applied := p . applyIngressMiddleware ( rule , req , w ) ; err != nil {
if applied {
2022-10-03 09:17:11 +00:00
rule , srv := ruleField ( p . ingressRules , ruleNum )
p . logRequestError ( err , cfRay , "" , rule , srv )
2022-09-22 14:11:59 +00:00
return nil
}
return err
}
2020-11-02 11:21:34 +00:00
2021-07-01 09:29:53 +00:00
switch originProxy := rule . Service . ( type ) {
case ingress . HTTPOriginProxy :
2021-07-16 15:14:37 +00:00
if err := p . proxyHTTPRequest (
w ,
2022-04-11 19:57:50 +00:00
tr ,
2021-07-16 15:14:37 +00:00
originProxy ,
isWebsocket ,
rule . Config . DisableChunkedEncoding ,
logFields ,
) ; err != nil {
2021-05-15 04:49:34 +00:00
rule , srv := ruleField ( p . ingressRules , ruleNum )
2022-06-09 12:55:26 +00:00
p . logRequestError ( err , cfRay , "" , rule , srv )
2021-01-11 19:59:45 +00:00
return err
}
return nil
2021-07-01 09:29:53 +00:00
case ingress . StreamBasedOriginProxy :
2021-07-01 18:30:26 +00:00
dest , err := getDestFromRule ( rule , req )
if err != nil {
return err
}
2021-07-16 15:14:37 +00:00
rws := connection . NewHTTPResponseReadWriterAcker ( w , req )
2022-07-26 21:00:53 +00:00
if err := p . proxyStream ( tr . ToTracedContext ( ) , rws , dest , originProxy ) ; err != nil {
2021-07-01 09:29:53 +00:00
rule , srv := ruleField ( p . ingressRules , ruleNum )
2022-06-09 12:55:26 +00:00
p . logRequestError ( err , cfRay , "" , rule , srv )
2021-07-01 09:29:53 +00:00
return err
}
return nil
2023-03-21 18:42:25 +00:00
case ingress . HTTPLocalProxy :
originProxy . ServeHTTP ( w , req )
return nil
2021-07-01 09:29:53 +00:00
default :
return fmt . Errorf ( "Unrecognized service: %s, %t" , rule . Service , originProxy )
2021-07-01 18:30:26 +00:00
}
}
2021-02-02 18:27:50 +00:00
2021-07-16 15:14:37 +00:00
// ProxyTCP proxies to a TCP connection between the origin service and cloudflared.
func ( p * Proxy ) ProxyTCP (
ctx context . Context ,
rwa connection . ReadWriteAcker ,
req * connection . TCPRequest ,
) error {
incrementRequests ( )
defer decrementConcurrentRequests ( )
if p . warpRouting == nil {
err := errors . New ( ` cloudflared received a request from WARP client, but your configuration has disabled ingress from WARP clients. To enable this, set "warp-routing:\n\t enabled: true" in your config.yaml ` )
p . log . Error ( ) . Msg ( err . Error ( ) )
return err
2021-07-01 18:30:26 +00:00
}
2021-07-16 15:14:37 +00:00
serveCtx , cancel := context . WithCancel ( ctx )
defer cancel ( )
2022-07-26 21:00:53 +00:00
tracedCtx := tracing . NewTracedContext ( serveCtx , req . CfTraceID , p . log )
2023-02-22 14:52:44 +00:00
p . log . Debug ( ) . Str ( LogFieldFlowID , req . FlowID ) . Uint8 ( LogFieldConnIndex , req . ConnIndex ) . Msg ( "tcp proxy stream started" )
2022-06-13 16:44:27 +00:00
2022-07-26 21:00:53 +00:00
if err := p . proxyStream ( tracedCtx , rwa , req . Dest , p . warpRouting . Proxy ) ; err != nil {
2022-06-09 12:55:26 +00:00
p . logRequestError ( err , req . CFRay , req . FlowID , "" , ingress . ServiceWarpRouting )
2021-07-16 15:14:37 +00:00
return err
2021-07-01 18:30:26 +00:00
}
2021-07-16 15:14:37 +00:00
2023-02-22 14:52:44 +00:00
p . log . Debug ( ) . Str ( LogFieldFlowID , req . FlowID ) . Uint8 ( LogFieldConnIndex , req . ConnIndex ) . Msg ( "tcp proxy stream finished successfully" )
2022-06-09 12:55:26 +00:00
2021-07-16 15:14:37 +00:00
return nil
2021-01-17 20:22:53 +00:00
}
2022-02-11 10:49:06 +00:00
func ruleField ( ing ingress . Ingress , ruleNum int ) ( ruleID string , srv string ) {
2021-05-15 04:49:34 +00:00
srv = ing . Rules [ ruleNum ] . Service . String ( )
2021-04-09 21:30:14 +00:00
if ing . IsSingleRule ( ) {
2021-05-15 04:49:34 +00:00
return "" , srv
2021-04-09 21:30:14 +00:00
}
2021-05-15 04:49:34 +00:00
return fmt . Sprintf ( "%d" , ruleNum ) , srv
2021-04-09 21:30:14 +00:00
}
2021-07-16 15:14:37 +00:00
// ProxyHTTPRequest proxies requests of underlying type http and websocket to the origin service.
func ( p * Proxy ) proxyHTTPRequest (
2021-07-01 09:29:53 +00:00
w connection . ResponseWriter ,
2022-07-26 21:00:53 +00:00
tr * tracing . TracedHTTPRequest ,
2021-07-01 09:29:53 +00:00
httpService ingress . HTTPOriginProxy ,
isWebsocket bool ,
disableChunkedEncoding bool ,
2021-07-16 15:14:37 +00:00
fields logFields ,
) error {
2022-04-11 19:57:50 +00:00
roundTripReq := tr . Request
2021-07-01 09:29:53 +00:00
if isWebsocket {
2022-04-11 23:02:13 +00:00
roundTripReq = tr . Clone ( tr . Request . Context ( ) )
2021-07-01 09:29:53 +00:00
roundTripReq . Header . Set ( "Connection" , "Upgrade" )
roundTripReq . Header . Set ( "Upgrade" , "websocket" )
roundTripReq . Header . Set ( "Sec-Websocket-Version" , "13" )
roundTripReq . ContentLength = 0
roundTripReq . Body = nil
} else {
// Support for WSGI Servers by switching transfer encoding from chunked to gzip/deflate
if disableChunkedEncoding {
roundTripReq . TransferEncoding = [ ] string { "gzip" , "deflate" }
2022-04-11 19:57:50 +00:00
cLength , err := strconv . Atoi ( tr . Request . Header . Get ( "Content-Length" ) )
2021-07-01 09:29:53 +00:00
if err == nil {
roundTripReq . ContentLength = int64 ( cLength )
}
2020-10-08 10:12:26 +00:00
}
2021-07-01 09:29:53 +00:00
// Request origin to keep connection alive to improve performance
roundTripReq . Header . Set ( "Connection" , "keep-alive" )
2020-10-08 10:12:26 +00:00
}
2021-11-13 00:34:19 +00:00
// Set the User-Agent as an empty string if not provided to avoid inserting golang default UA
if roundTripReq . Header . Get ( "User-Agent" ) == "" {
roundTripReq . Header . Set ( "User-Agent" , "" )
}
2022-04-11 23:02:13 +00:00
_ , ttfbSpan := tr . Tracer ( ) . Start ( tr . Context ( ) , "ttfb_origin" )
2021-07-01 09:29:53 +00:00
resp , err := httpService . RoundTrip ( roundTripReq )
2020-10-08 10:12:26 +00:00
if err != nil {
2022-05-18 11:11:38 +00:00
tracing . EndWithErrorStatus ( ttfbSpan , err )
2022-06-17 21:39:38 +00:00
if err := roundTripReq . Context ( ) . Err ( ) ; err != nil {
return errors . Wrap ( err , "Incoming request ended abruptly" )
}
2021-05-15 04:49:34 +00:00
return errors . Wrap ( err , "Unable to reach the origin service. The service may be down or it may not be responding to traffic from cloudflared" )
2020-10-08 10:12:26 +00:00
}
2022-05-18 11:11:38 +00:00
tracing . EndWithStatusCode ( ttfbSpan , resp . StatusCode )
2020-10-08 10:12:26 +00:00
defer resp . Body . Close ( )
2022-08-16 11:21:58 +00:00
headers := make ( http . Header , len ( resp . Header ) )
// copy headers
for k , v := range resp . Header {
headers [ k ] = v
2022-05-18 10:11:48 +00:00
}
2022-04-11 19:57:50 +00:00
// Add spans to response header (if available)
2022-08-16 11:21:58 +00:00
tr . AddSpans ( headers )
2022-04-11 19:57:50 +00:00
2022-08-16 11:21:58 +00:00
err = w . WriteRespHeaders ( resp . StatusCode , headers )
2020-10-08 10:12:26 +00:00
if err != nil {
2021-02-05 13:01:53 +00:00
return errors . Wrap ( err , "Error writing response header" )
2020-10-08 10:12:26 +00:00
}
2021-07-01 09:29:53 +00:00
if resp . StatusCode == http . StatusSwitchingProtocols {
rwc , ok := resp . Body . ( io . ReadWriteCloser )
if ! ok {
return errors . New ( "internal error: unsupported connection type" )
}
defer rwc . Close ( )
eyeballStream := & bidirectionalStream {
writer : w ,
2022-04-11 19:57:50 +00:00
reader : tr . Request . Body ,
2021-07-01 09:29:53 +00:00
}
2022-12-25 04:05:51 +00:00
stream . Pipe ( eyeballStream , rwc , p . log )
2021-07-01 09:29:53 +00:00
return nil
}
2023-01-16 12:42:59 +00:00
if _ , err = cfio . Copy ( w , resp . Body ) ; err != nil {
return err
}
2022-08-16 11:21:58 +00:00
// copy trailers
copyTrailers ( w , resp )
2021-07-16 15:14:37 +00:00
2021-02-05 13:01:53 +00:00
p . logOriginResponse ( resp , fields )
return nil
2020-10-08 10:12:26 +00:00
}
2021-07-16 15:14:37 +00:00
// proxyStream proxies type TCP and other underlying types if the connection is defined as a stream oriented
// ingress rule.
func ( p * Proxy ) proxyStream (
2022-07-26 21:00:53 +00:00
tr * tracing . TracedContext ,
2021-07-16 15:14:37 +00:00
rwa connection . ReadWriteAcker ,
2021-07-01 18:30:26 +00:00
dest string ,
2021-02-02 18:27:50 +00:00
connectionProxy ingress . StreamBasedOriginProxy ,
2021-02-05 13:01:53 +00:00
) error {
2022-07-26 21:00:53 +00:00
ctx := tr . Context
2022-08-11 21:54:12 +00:00
_ , connectSpan := tr . Tracer ( ) . Start ( ctx , "stream-connect" )
2022-06-13 16:44:27 +00:00
originConn , err := connectionProxy . EstablishConnection ( ctx , dest )
2020-10-08 10:12:26 +00:00
if err != nil {
2022-07-26 21:00:53 +00:00
tracing . EndWithErrorStatus ( connectSpan , err )
2021-02-05 13:01:53 +00:00
return err
2021-02-02 18:27:50 +00:00
}
2022-07-26 21:00:53 +00:00
connectSpan . End ( )
2023-01-30 22:45:42 +00:00
defer originConn . Close ( )
2022-07-26 21:00:53 +00:00
encodedSpans := tr . GetSpans ( )
2021-07-01 18:30:26 +00:00
2022-07-26 21:00:53 +00:00
if err := rwa . AckConnection ( encodedSpans ) ; err != nil {
2021-02-05 13:01:53 +00:00
return err
2021-02-02 18:27:50 +00:00
}
2021-07-16 15:14:37 +00:00
originConn . Stream ( ctx , rwa , p . log )
2021-02-05 13:01:53 +00:00
return nil
2020-10-08 10:12:26 +00:00
}
2021-02-11 14:36:42 +00:00
type bidirectionalStream struct {
reader io . Reader
writer io . Writer
}
func ( wr * bidirectionalStream ) Read ( p [ ] byte ) ( n int , err error ) {
return wr . reader . Read ( p )
}
func ( wr * bidirectionalStream ) Write ( p [ ] byte ) ( n int , err error ) {
return wr . writer . Write ( p )
}
2021-07-16 15:14:37 +00:00
func ( p * Proxy ) appendTagHeaders ( r * http . Request ) {
2020-12-09 21:46:53 +00:00
for _ , tag := range p . tags {
2020-10-08 10:12:26 +00:00
r . Header . Add ( TagHeaderNamePrefix + tag . Name , tag . Value )
}
}
2021-02-05 13:01:53 +00:00
type logFields struct {
2023-02-22 14:52:44 +00:00
cfRay string
lbProbe bool
rule interface { }
flowID string
connIndex uint8
2021-02-05 13:01:53 +00:00
}
2022-08-16 11:21:58 +00:00
func copyTrailers ( w connection . ResponseWriter , response * http . Response ) {
for trailerHeader , trailerValues := range response . Trailer {
for _ , trailerValue := range trailerValues {
w . AddTrailer ( trailerHeader , trailerValue )
}
}
}
2021-07-16 15:14:37 +00:00
func ( p * Proxy ) logRequest ( r * http . Request , fields logFields ) {
2021-02-05 13:01:53 +00:00
if fields . cfRay != "" {
p . log . Debug ( ) . Msgf ( "CF-RAY: %s %s %s %s" , fields . cfRay , r . Method , r . URL , r . Proto )
} else if fields . lbProbe {
p . log . Debug ( ) . Msgf ( "CF-RAY: %s Load Balancer health check %s %s %s" , fields . cfRay , r . Method , r . URL , r . Proto )
2020-10-08 10:12:26 +00:00
} else {
2020-12-09 21:46:53 +00:00
p . log . Debug ( ) . Msgf ( "All requests should have a CF-RAY header. Please open a support ticket with Cloudflare. %s %s %s " , r . Method , r . URL , r . Proto )
2020-10-08 10:12:26 +00:00
}
2021-04-30 23:39:15 +00:00
p . log . Debug ( ) .
Str ( "CF-RAY" , fields . cfRay ) .
2021-05-03 21:46:43 +00:00
Str ( "Header" , fmt . Sprintf ( "%+v" , r . Header ) ) .
2021-04-30 23:39:15 +00:00
Str ( "host" , r . Host ) .
Str ( "path" , r . URL . Path ) .
Interface ( "rule" , fields . rule ) .
2023-02-22 14:52:44 +00:00
Uint8 ( LogFieldConnIndex , fields . connIndex ) .
2021-04-30 23:39:15 +00:00
Msg ( "Inbound request" )
2020-10-08 10:12:26 +00:00
if contentLen := r . ContentLength ; contentLen == - 1 {
2021-02-05 13:01:53 +00:00
p . log . Debug ( ) . Msgf ( "CF-RAY: %s Request Content length unknown" , fields . cfRay )
2020-10-08 10:12:26 +00:00
} else {
2021-02-05 13:01:53 +00:00
p . log . Debug ( ) . Msgf ( "CF-RAY: %s Request content length %d" , fields . cfRay , contentLen )
2020-10-08 10:12:26 +00:00
}
}
2021-07-16 15:14:37 +00:00
func ( p * Proxy ) logOriginResponse ( resp * http . Response , fields logFields ) {
2021-02-05 13:01:53 +00:00
responseByCode . WithLabelValues ( strconv . Itoa ( resp . StatusCode ) ) . Inc ( )
if fields . cfRay != "" {
2023-02-22 14:52:44 +00:00
p . log . Debug ( ) . Uint8 ( LogFieldConnIndex , fields . connIndex ) . Msgf ( "CF-RAY: %s Status: %s served by ingress %d" , fields . cfRay , resp . Status , fields . rule )
2021-02-05 13:01:53 +00:00
} else if fields . lbProbe {
2023-02-22 14:52:44 +00:00
p . log . Debug ( ) . Uint8 ( LogFieldConnIndex , fields . connIndex ) . Msgf ( "Response to Load Balancer health check %s" , resp . Status )
2020-10-08 10:12:26 +00:00
} else {
2023-02-22 14:52:44 +00:00
p . log . Debug ( ) . Uint8 ( LogFieldConnIndex , fields . connIndex ) . Msgf ( "Status: %s served by ingress %v" , resp . Status , fields . rule )
2020-10-08 10:12:26 +00:00
}
2023-02-22 14:52:44 +00:00
p . log . Debug ( ) . Uint8 ( LogFieldConnIndex , fields . connIndex ) . Msgf ( "CF-RAY: %s Response Headers %+v" , fields . cfRay , resp . Header )
2020-10-08 10:12:26 +00:00
2021-02-05 13:01:53 +00:00
if contentLen := resp . ContentLength ; contentLen == - 1 {
2023-02-22 14:52:44 +00:00
p . log . Debug ( ) . Uint8 ( LogFieldConnIndex , fields . connIndex ) . Msgf ( "CF-RAY: %s Response content length unknown" , fields . cfRay )
2020-10-08 10:12:26 +00:00
} else {
2023-02-22 14:52:44 +00:00
p . log . Debug ( ) . Uint8 ( LogFieldConnIndex , fields . connIndex ) . Msgf ( "CF-RAY: %s Response content length %d" , fields . cfRay , contentLen )
2020-10-08 10:12:26 +00:00
}
}
2022-06-09 12:55:26 +00:00
func ( p * Proxy ) logRequestError ( err error , cfRay string , flowID string , rule , service string ) {
2020-11-02 11:21:34 +00:00
requestErrors . Inc ( )
2021-04-09 21:30:14 +00:00
log := p . log . Error ( ) . Err ( err )
2020-11-02 11:21:34 +00:00
if cfRay != "" {
2021-04-09 21:30:14 +00:00
log = log . Str ( LogFieldCFRay , cfRay )
}
2022-06-09 12:55:26 +00:00
if flowID != "" {
log = log . Str ( LogFieldFlowID , flowID )
}
2021-04-09 21:30:14 +00:00
if rule != "" {
log = log . Str ( LogFieldRule , rule )
2020-11-02 11:21:34 +00:00
}
2021-05-15 04:49:34 +00:00
if service != "" {
log = log . Str ( LogFieldOriginService , service )
}
2021-04-09 21:30:14 +00:00
log . Msg ( "" )
2020-11-02 11:21:34 +00:00
}
2021-07-16 15:14:37 +00:00
func getDestFromRule ( rule * ingress . Rule , req * http . Request ) ( string , error ) {
switch rule . Service . String ( ) {
case ingress . ServiceBastion :
return carrier . ResolveBastionDest ( req )
default :
return rule . Service . String ( ) , nil
}
2020-10-08 10:12:26 +00:00
}