2020-04-06 19:49:49 +00:00
package router
2019-04-06 05:20:26 +00:00
import (
2019-04-07 00:32:35 +00:00
"bufio"
2020-04-04 20:08:10 +00:00
"crypto/sha256"
2020-12-16 05:56:53 +00:00
"emperror.dev/errors"
2020-04-04 20:08:10 +00:00
"encoding/hex"
2020-12-25 21:32:41 +00:00
"encoding/json"
"fmt"
2020-09-04 03:29:53 +00:00
"github.com/apex/log"
2020-04-06 19:49:49 +00:00
"github.com/gin-gonic/gin"
2020-12-25 21:32:41 +00:00
"github.com/juju/ratelimit"
2020-04-04 22:15:49 +00:00
"github.com/mholt/archiver/v3"
2020-12-25 21:32:41 +00:00
"github.com/mitchellh/colorstring"
2020-04-04 05:17:26 +00:00
"github.com/pterodactyl/wings/api"
2019-12-17 05:43:07 +00:00
"github.com/pterodactyl/wings/config"
2019-11-16 23:10:53 +00:00
"github.com/pterodactyl/wings/installer"
2020-04-06 19:49:49 +00:00
"github.com/pterodactyl/wings/router/tokens"
2019-04-06 05:20:26 +00:00
"github.com/pterodactyl/wings/server"
2020-12-25 21:32:41 +00:00
"github.com/pterodactyl/wings/system"
2019-04-06 05:55:48 +00:00
"io"
2019-04-06 05:20:26 +00:00
"net/http"
2019-04-06 05:55:48 +00:00
"os"
2020-04-04 20:08:10 +00:00
"path/filepath"
2019-04-06 19:27:44 +00:00
"strconv"
2019-04-06 17:49:31 +00:00
"strings"
2020-12-25 21:32:41 +00:00
"sync/atomic"
"time"
2019-04-06 05:20:26 +00:00
)
2020-12-25 21:32:41 +00:00
// Number of ticks in the progress bar
const ticks = 25
// 100% / number of ticks = percentage represented by each tick
const tickPercentage = 100 / ticks
type downloadProgress struct {
size int64
progress int64
}
// Data passed over to initiate a server transfer.
type serverTransferRequest struct {
ServerID string ` binding:"required" json:"server_id" `
URL string ` binding:"required" json:"url" `
Token string ` binding:"required" json:"token" `
Server json . RawMessage ` json:"server" `
}
// Returns the archive for a server so that it can be transfered to a new node.
2020-04-06 19:49:49 +00:00
func getServerArchive ( c * gin . Context ) {
auth := strings . SplitN ( c . GetHeader ( "Authorization" ) , " " , 2 )
2020-04-04 06:51:35 +00:00
if len ( auth ) != 2 || auth [ 0 ] != "Bearer" {
2020-04-06 19:49:49 +00:00
c . Header ( "WWW-Authenticate" , "Bearer" )
c . AbortWithStatusJSON ( http . StatusUnauthorized , gin . H {
"error" : "The required authorization heads were not present in the request." ,
} )
2020-04-04 06:51:35 +00:00
return
}
2020-04-06 19:49:49 +00:00
token := tokens . TransferPayload { }
2020-04-06 20:39:33 +00:00
if err := tokens . ParseToken ( [ ] byte ( auth [ 1 ] ) , & token ) ; err != nil {
2020-12-16 05:08:00 +00:00
NewTrackedError ( err ) . Abort ( c )
2020-04-04 06:51:35 +00:00
return
}
2020-12-25 21:32:41 +00:00
s := ExtractServer ( c )
if token . Subject != s . Id ( ) {
2020-04-06 19:49:49 +00:00
c . AbortWithStatusJSON ( http . StatusForbidden , gin . H {
2020-12-25 21:32:41 +00:00
"error" : "Missing required token subject, or subject is not valid for the requested server." ,
2020-04-06 19:49:49 +00:00
} )
2020-04-04 06:51:35 +00:00
return
}
2020-04-04 05:17:26 +00:00
st , err := s . Archiver . Stat ( )
if err != nil {
2020-11-08 23:15:39 +00:00
if ! errors . Is ( err , os . ErrNotExist ) {
2020-12-25 21:32:41 +00:00
WithError ( c , err )
2020-04-04 05:17:26 +00:00
return
}
2020-04-06 19:49:49 +00:00
c . AbortWithStatus ( http . StatusNotFound )
2020-04-04 05:17:26 +00:00
return
}
checksum , err := s . Archiver . Checksum ( )
if err != nil {
2020-12-16 05:08:00 +00:00
NewServerError ( err , s ) . SetMessage ( "failed to calculate checksum" ) . Abort ( c )
2020-04-04 05:17:26 +00:00
return
}
2020-09-27 19:24:08 +00:00
file , err := os . Open ( s . Archiver . Path ( ) )
2020-04-04 05:17:26 +00:00
if err != nil {
2020-12-25 21:32:41 +00:00
WithError ( c , err )
2020-04-04 05:17:26 +00:00
return
}
defer file . Close ( )
2020-04-06 19:49:49 +00:00
c . Header ( "X-Checksum" , checksum )
c . Header ( "X-Mime-Type" , st . Mimetype )
c . Header ( "Content-Length" , strconv . Itoa ( int ( st . Info . Size ( ) ) ) )
2020-09-27 19:24:08 +00:00
c . Header ( "Content-Disposition" , "attachment; filename=" + s . Archiver . Name ( ) )
2020-04-06 19:49:49 +00:00
c . Header ( "Content-Type" , "application/octet-stream" )
2020-04-04 05:17:26 +00:00
2020-04-06 19:49:49 +00:00
bufio . NewReader ( file ) . WriteTo ( c . Writer )
2020-04-04 05:17:26 +00:00
}
2020-04-06 19:49:49 +00:00
func postServerArchive ( c * gin . Context ) {
2020-12-25 21:32:41 +00:00
s := ExtractServer ( c )
2020-04-06 19:49:49 +00:00
2020-09-04 03:29:53 +00:00
go func ( s * server . Server ) {
2020-11-29 19:31:54 +00:00
r := api . New ( )
2020-12-12 00:24:35 +00:00
l := log . WithField ( "server" , s . Id ( ) )
2020-11-29 19:31:54 +00:00
2020-12-25 21:32:41 +00:00
// This function automatically adds the Source Node prefix and Timestamp to the log
// output before sending it over the websocket.
sendTransferLog := func ( data string ) {
output := colorstring . Color ( fmt . Sprintf ( "[yellow][bold]%s [Pterodactyl Transfer System] [Source Node]:[default] %s" , time . Now ( ) . Format ( time . RFC1123 ) , data ) )
s . Events ( ) . Publish ( server . TransferLogsEvent , output )
}
s . Events ( ) . Publish ( server . TransferStatusEvent , "starting" )
sendTransferLog ( "Attempting to archive server..." )
hasError := true
defer func ( ) {
if ! hasError {
return
}
s . Events ( ) . Publish ( server . TransferStatusEvent , "failure" )
2020-11-29 19:31:54 +00:00
2020-12-25 21:32:41 +00:00
sendTransferLog ( "Attempting to notify panel of archive failure.." )
2020-11-29 19:31:54 +00:00
if err := r . SendArchiveStatus ( s . Id ( ) , false ) ; err != nil {
if ! api . IsRequestError ( err ) {
2020-12-25 21:32:41 +00:00
sendTransferLog ( "Failed to notify panel of archive failure: " + err . Error ( ) )
2020-12-12 00:24:35 +00:00
l . WithField ( "error" , err ) . Error ( "failed to notify panel of failed archive status" )
2020-11-29 19:31:54 +00:00
return
}
2020-12-25 21:32:41 +00:00
sendTransferLog ( "Panel returned an error while notifying it of a failed archive: " + err . Error ( ) )
2020-12-12 00:24:35 +00:00
l . WithField ( "error" , err . Error ( ) ) . Error ( "panel returned an error when notifying it of a failed archive status" )
2020-11-29 19:31:54 +00:00
return
}
2020-12-25 21:32:41 +00:00
sendTransferLog ( "Successfully notified panel of failed archive status" )
2020-12-12 00:24:35 +00:00
l . Info ( "successfully notified panel of failed archive status" )
2020-12-25 21:32:41 +00:00
} ( )
// Mark the server as transferring to prevent problems.
s . SetTransferring ( true )
// Ensure the server is offline. Sometimes a "No such container" error gets through
// which means the server is already stopped. We can ignore that.
if err := s . Environment . WaitForStop ( 60 , false ) ; err != nil && ! strings . Contains ( strings . ToLower ( err . Error ( ) ) , "no such container" ) {
sendTransferLog ( "Failed to stop server, aborting transfer.." )
l . WithField ( "error" , err ) . Error ( "failed to stop server" )
return
}
// Attempt to get an archive of the server.
if err := s . Archiver . Archive ( ) ; err != nil {
sendTransferLog ( "An error occurred while archiving the server: " + err . Error ( ) )
l . WithField ( "error" , err ) . Error ( "failed to get transfer archive for server" )
2020-04-06 19:49:49 +00:00
return
}
2020-12-25 21:32:41 +00:00
sendTransferLog ( "Successfully created archive, attempting to notify panel.." )
2020-12-12 00:24:35 +00:00
l . Info ( "successfully created server transfer archive, notifying panel.." )
2020-04-06 19:49:49 +00:00
2020-11-29 19:31:54 +00:00
if err := r . SendArchiveStatus ( s . Id ( ) , true ) ; err != nil {
2020-10-31 17:04:20 +00:00
if ! api . IsRequestError ( err ) {
2020-12-25 21:32:41 +00:00
sendTransferLog ( "Failed to notify panel of archive success: " + err . Error ( ) )
2020-12-12 00:24:35 +00:00
l . WithField ( "error" , err ) . Error ( "failed to notify panel of successful archive status" )
2020-04-06 19:49:49 +00:00
return
}
2020-12-25 21:32:41 +00:00
sendTransferLog ( "Panel returned an error while notifying it of a successful archive: " + err . Error ( ) )
2020-12-12 00:24:35 +00:00
l . WithField ( "error" , err . Error ( ) ) . Error ( "panel returned an error when notifying it of a successful archive status" )
2020-04-06 19:49:49 +00:00
return
}
2020-12-25 21:32:41 +00:00
hasError = false
// This log may not be displayed by the client due to the status event being sent before or at the same time.
sendTransferLog ( "Successfully notified panel of successful archive status" )
2020-12-12 00:24:35 +00:00
l . Info ( "successfully notified panel of successful transfer archive status" )
2020-12-25 21:32:41 +00:00
s . Events ( ) . Publish ( server . TransferStatusEvent , "archived" )
2020-04-06 19:49:49 +00:00
} ( s )
c . Status ( http . StatusAccepted )
}
2020-12-25 21:32:41 +00:00
func ( w * downloadProgress ) Write ( v [ ] byte ) ( int , error ) {
n := len ( v )
atomic . AddInt64 ( & w . progress , int64 ( n ) )
return n , nil
}
// Log helper function to attach all errors and info output to a consistently formatted
// log string for easier querying.
func ( str serverTransferRequest ) log ( ) * log . Entry {
return log . WithField ( "subsystem" , "transfers" ) . WithField ( "server_id" , str . ServerID )
}
// Downloads an archive from the machine that the server currently lives on.
func ( str serverTransferRequest ) downloadArchive ( ) ( * http . Response , error ) {
client := http . Client { Timeout : 0 }
req , err := http . NewRequest ( http . MethodGet , str . URL , nil )
if err != nil {
return nil , err
2020-12-12 00:24:35 +00:00
}
2020-12-25 21:32:41 +00:00
req . Header . Set ( "Authorization" , str . Token )
res , err := client . Do ( req )
if err != nil {
return nil , err
}
return res , nil
}
2020-04-04 20:08:10 +00:00
2020-12-25 21:32:41 +00:00
// Returns the path to the local archive on the system.
func ( str serverTransferRequest ) path ( ) string {
return filepath . Join ( config . Get ( ) . System . ArchiveDirectory , str . ServerID + ".tar.gz" )
}
2020-04-04 20:08:10 +00:00
2020-12-25 21:32:41 +00:00
// Creates the archive location on this machine by first checking that the required file
// does not already exist. If it does exist, the file is deleted and then re-created as
// an empty file.
func ( str serverTransferRequest ) createArchiveFile ( ) ( * os . File , error ) {
p := str . path ( )
if _ , err := os . Stat ( p ) ; err != nil {
if ! os . IsNotExist ( err ) {
return nil , err
}
} else if err := os . Remove ( p ) ; err != nil {
return nil , err
}
return os . Create ( p )
}
2020-04-04 20:08:10 +00:00
2020-12-25 21:32:41 +00:00
// Deletes the archive from the local filesystem. This is executed as a deferred function.
func ( str serverTransferRequest ) removeArchivePath ( ) {
p := str . path ( )
str . log ( ) . Debug ( "deleting temporary transfer archive" )
if err := os . Remove ( p ) ; err != nil && ! os . IsNotExist ( err ) {
str . log ( ) . WithField ( "path" , p ) . WithField ( "error" , err ) . Error ( "failed to delete temporary transfer archive file" )
return
}
str . log ( ) . Debug ( "deleted temporary transfer archive successfully" )
}
2020-04-05 00:27:31 +00:00
2020-12-25 21:32:41 +00:00
// Verifies that the SHA-256 checksum of the file on the local filesystem matches the
// expected value from the transfer request. The string value returned is the computed
// checksum on the system.
func ( str serverTransferRequest ) verifyChecksum ( matches string ) ( bool , string , error ) {
file , err := os . Open ( str . path ( ) )
if err != nil {
return false , "" , err
}
defer file . Close ( )
hash := sha256 . New ( )
buf := make ( [ ] byte , 1024 * 4 )
if _ , err := io . CopyBuffer ( hash , file , buf ) ; err != nil {
return false , "" , err
}
checksum := hex . EncodeToString ( hash . Sum ( nil ) )
return checksum == matches , checksum , nil
}
2020-04-05 00:27:31 +00:00
2020-12-25 21:32:41 +00:00
// Sends a notification to the Panel letting it know what the status of this transfer is.
func ( str serverTransferRequest ) sendTransferStatus ( successful bool ) error {
lg := str . log ( ) . WithField ( "transfer_successful" , successful )
lg . Info ( "notifying Panel of server transfer state" )
if err := api . New ( ) . SendTransferStatus ( str . ServerID , successful ) ; err != nil {
lg . WithField ( "error" , err ) . Error ( "error notifying panel of transfer state" )
return err
}
lg . Debug ( "notified panel of transfer state" )
return nil
}
// Initiates a transfer between two nodes for a server by downloading an archive from the
// remote node and then applying the server details to this machine.
func postTransfer ( c * gin . Context ) {
var data serverTransferRequest
if err := c . BindJSON ( & data ) ; err != nil {
return
}
2020-04-05 00:27:31 +00:00
2020-12-25 21:32:41 +00:00
data . log ( ) . Info ( "handling incoming server transfer request" )
go func ( data * serverTransferRequest ) {
hasError := true
defer func ( ) {
_ = data . sendTransferStatus ( ! hasError )
2020-04-05 00:27:31 +00:00
} ( )
2020-12-25 21:32:41 +00:00
// Create a new server installer. This will only configure the environment and not
// run the installer scripts.
i , err := installer . New ( data . Server )
2020-04-04 20:08:10 +00:00
if err != nil {
2020-12-25 21:32:41 +00:00
data . log ( ) . WithField ( "error" , err ) . Error ( "failed to validate received server data" )
2020-04-04 20:08:10 +00:00
return
}
2020-12-25 21:32:41 +00:00
// This function automatically adds the Target Node prefix and Timestamp to the log output before sending it
// over the websocket.
sendTransferLog := func ( data string ) {
output := colorstring . Color ( fmt . Sprintf ( "[yellow][bold]%s [Pterodactyl Transfer System] [Target Node]:[default] %s" , time . Now ( ) . Format ( time . RFC1123 ) , data ) )
i . Server ( ) . Events ( ) . Publish ( server . TransferLogsEvent , output )
2020-04-04 20:08:10 +00:00
}
2020-12-25 21:32:41 +00:00
// Mark the server as transferring to prevent problems later on during the process and
// then push the server into the global server collection for this instance.
i . Server ( ) . SetTransferring ( true )
server . GetServers ( ) . Add ( i . Server ( ) )
defer func ( s * server . Server ) {
// In the event that this transfer call fails, remove the server from the global
// server tracking so that we don't have a dangling instance.
if hasError {
sendTransferLog ( "Server transfer failed, check Wings logs for additional information." )
s . Events ( ) . Publish ( server . TransferStatusEvent , "failure" )
server . GetServers ( ) . Remove ( func ( s2 * server . Server ) bool {
return s . Id ( ) == s2 . Id ( )
} )
} else {
i . Server ( ) . SetTransferring ( false )
i . Server ( ) . Events ( ) . Publish ( server . TransferStatusEvent , "success" )
2020-04-04 20:08:10 +00:00
}
2020-12-25 21:32:41 +00:00
} ( i . Server ( ) )
2020-04-04 20:08:10 +00:00
2020-12-25 21:32:41 +00:00
data . log ( ) . Info ( "downloading server archive from current server node" )
sendTransferLog ( "Received incoming transfer from Panel, attempting to download archive from source node..." )
res , err := data . downloadArchive ( )
if err != nil {
sendTransferLog ( "Failed to retrieve server archive from remote node: " + err . Error ( ) )
data . log ( ) . WithField ( "error" , err ) . Error ( "failed to download archive for server transfer" )
2020-04-04 20:08:10 +00:00
return
}
2020-12-25 21:32:41 +00:00
defer res . Body . Close ( )
if res . StatusCode != 200 {
data . log ( ) . WithField ( "error" , err ) . WithField ( "status" , res . StatusCode ) . Error ( "unexpected error response from transfer endpoint" )
2020-12-12 00:24:35 +00:00
return
2020-04-05 00:27:31 +00:00
}
2020-12-25 21:32:41 +00:00
size := res . ContentLength
if size == 0 {
data . log ( ) . WithField ( "error" , err ) . Error ( "recieved an archive response with Content-Length of 0" )
2020-04-04 20:08:10 +00:00
return
}
2020-12-25 21:32:41 +00:00
sendTransferLog ( "Got server archive response from remote node. (Content-Length: " + strconv . Itoa ( int ( size ) ) + ")" )
sendTransferLog ( "Creating local archive file..." )
file , err := data . createArchiveFile ( )
2020-04-04 20:08:10 +00:00
if err != nil {
2020-12-25 21:32:41 +00:00
data . log ( ) . WithField ( "error" , err ) . Error ( "failed to create archive file on local filesystem" )
2020-04-04 20:08:10 +00:00
return
}
2020-12-25 21:32:41 +00:00
sendTransferLog ( "Writing archive to disk..." )
data . log ( ) . Info ( "writing transfer archive to disk.." )
2020-09-04 03:29:53 +00:00
2020-12-25 21:32:41 +00:00
// Copy the file.
progress := & downloadProgress { size : size }
ticker := time . NewTicker ( 3 * time . Second )
go func ( progress * downloadProgress , t * time . Ticker ) {
for range ticker . C {
// p = 100 (Downloaded)
// size = 1000 (Content-Length)
// p / size = 0.1
// * 100 = 10% (Multiply by 100 to get a percentage of the download)
// 10% / tickPercentage = (10% / (100 / 25)) (Divide by tick percentage to get the number of ticks)
// 2.5 (Number of ticks as a float64)
// 2 (convert to an integer)
p := atomic . LoadInt64 ( & progress . progress )
// We have to cast these numbers to float in order to get a float result from the division.
width := ( ( float64 ( p ) / float64 ( size ) ) * 100 ) / tickPercentage
bar := strings . Repeat ( "=" , int ( width ) ) + strings . Repeat ( " " , ticks - int ( width ) )
sendTransferLog ( "Downloading [" + bar + "] " + system . FormatBytes ( p ) + " / " + system . FormatBytes ( progress . size ) )
2020-11-29 00:00:52 +00:00
}
2020-12-25 21:32:41 +00:00
} ( progress , ticker )
2020-11-29 00:00:52 +00:00
2020-12-25 21:32:41 +00:00
var reader io . Reader = res . Body
downloadLimit := float64 ( config . Get ( ) . System . Transfers . DownloadLimit ) * 1024 * 1024
if downloadLimit > 0 {
// Wrap the body with a reader that is limited to the defined download limit speed.
reader = ratelimit . Reader ( res . Body , ratelimit . NewBucketWithRate ( downloadLimit , int64 ( downloadLimit ) ) )
2020-04-04 20:08:10 +00:00
}
2020-12-25 21:32:41 +00:00
buf := make ( [ ] byte , 1024 * 4 )
if _ , err := io . CopyBuffer ( file , io . TeeReader ( reader , progress ) , buf ) ; err != nil {
ticker . Stop ( )
sendTransferLog ( "Failed while writing archive file to disk: " + err . Error ( ) )
data . log ( ) . WithField ( "error" , err ) . Error ( "failed to copy archive file to disk" )
2020-04-04 20:08:10 +00:00
return
}
2020-12-25 21:32:41 +00:00
ticker . Stop ( )
2020-04-04 20:08:10 +00:00
2020-12-25 21:32:41 +00:00
// Show 100% completion.
humanSize := system . FormatBytes ( progress . size )
sendTransferLog ( "Downloading [" + strings . Repeat ( "=" , ticks ) + "] " + humanSize + " / " + humanSize )
2020-04-04 20:08:10 +00:00
2020-04-04 22:15:49 +00:00
if err := file . Close ( ) ; err != nil {
2020-12-25 21:32:41 +00:00
data . log ( ) . WithField ( "error" , err ) . Error ( "unable to close archive file on local filesystem" )
2020-04-04 22:15:49 +00:00
return
}
2020-12-25 21:32:41 +00:00
data . log ( ) . Info ( "finished writing transfer archive to disk" )
sendTransferLog ( "Successfully wrote archive to disk." )
// Whenever the transfer fails or succeeds, delete the temporary transfer archive that
// was created on the disk.
defer data . removeArchivePath ( )
sendTransferLog ( "Verifying checksum of downloaded archive..." )
data . log ( ) . Info ( "computing checksum of downloaded archive file" )
expected := res . Header . Get ( "X-Checksum" )
if matches , computed , err := data . verifyChecksum ( expected ) ; err != nil {
data . log ( ) . WithField ( "error" , err ) . Error ( "encountered an error while calculating local filesystem archive checksum" )
2020-04-04 22:15:49 +00:00
return
2020-12-25 21:32:41 +00:00
} else if ! matches {
sendTransferLog ( "@@@@@ CHECKSUM VERIFICATION FAILED @@@@@" )
sendTransferLog ( " - Source Checksum: " + expected )
sendTransferLog ( " - Computed Checksum: " + computed )
data . log ( ) . WithField ( "expected_sum" , expected ) . WithField ( "computed_checksum" , computed ) . Error ( "checksum mismatch when verifying integrity of local archive" )
2020-04-04 22:15:49 +00:00
return
}
2020-12-25 21:32:41 +00:00
// Create the server's environment.
sendTransferLog ( "Creating server environment, this could take a while.." )
data . log ( ) . Info ( "creating server environment" )
2020-09-13 04:48:04 +00:00
if err := i . Server ( ) . CreateEnvironment ( ) ; err != nil {
2020-12-25 21:32:41 +00:00
data . log ( ) . WithField ( "error" , err ) . Error ( "failed to create server environment" )
2020-09-13 04:48:04 +00:00
return
}
2020-04-04 22:15:49 +00:00
2020-12-25 21:32:41 +00:00
sendTransferLog ( "Server environment has been created, extracting transfer archive.." )
data . log ( ) . Info ( "server environment configured, extracting transfer archive" )
if err := archiver . NewTarGz ( ) . Unarchive ( data . path ( ) , i . Server ( ) . Filesystem ( ) . Path ( ) ) ; err != nil {
// Unarchiving failed, delete the server's data directory.
if err := os . RemoveAll ( i . Server ( ) . Filesystem ( ) . Path ( ) ) ; err != nil && ! os . IsNotExist ( err ) {
data . log ( ) . WithField ( "error" , err ) . Warn ( "failed to delete local server files directory" )
}
data . log ( ) . WithField ( "error" , err ) . Error ( "failed to extract server archive" )
2020-04-06 20:39:33 +00:00
return
}
2020-04-04 22:15:49 +00:00
2020-04-06 20:39:33 +00:00
// We mark the process as being successful here as if we fail to send a transfer success,
// then a transfer failure won't probably be successful either.
//
// It may be useful to retry sending the transfer success every so often just in case of a small
// hiccup or the fix of whatever error causing the success request to fail.
hasError = false
2020-12-25 21:32:41 +00:00
data . log ( ) . Info ( "archive transfered successfully, notifying panel of status" )
sendTransferLog ( "Archive transfered successfully." )
} ( & data )
2020-04-04 05:17:26 +00:00
2020-04-06 19:49:49 +00:00
c . Status ( http . StatusAccepted )
2019-04-06 05:55:48 +00:00
}