├── .dockerignore ├── .github ├── dependabot.yml └── workflows │ ├── auto_merge.yml │ └── go.yml ├── .gitignore ├── .travis.yml ├── Dockerfile ├── Dockerfile-base ├── Dockerfile-proftpd ├── Dockerfile-vsftpd ├── Makefile ├── README.md ├── Vagrantfile ├── config.toml ├── example └── webapi │ ├── webapi.go │ └── webapi_test.go ├── go.mod ├── go.sum ├── integration_test.go ├── main.go ├── misc └── server ├── pftp ├── client_handler.go ├── client_handler_test.go ├── config.go ├── context.go ├── data_handler.go ├── data_handler_test.go ├── handle_commands.go ├── handle_commands_test.go ├── logger.go ├── proxy.go ├── result.go ├── server.go ├── socket_control.go ├── tls.go └── utils.go ├── test ├── rest_server.go └── utils.go ├── tls ├── server.crt ├── server.csr └── server.key └── version /.dockerignore: -------------------------------------------------------------------------------- 1 | misc/test/data 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/auto_merge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | on: 3 | pull_request_target: 4 | 5 | jobs: 6 | automerge: 7 | runs-on: ubuntu-latest 8 | if: ${{ github.actor == 'dependabot[bot]' }} 9 | steps: 10 | - name: Dependabot metadata 11 | uses: dependabot/fetch-metadata@v1 12 | id: metadata 13 | - name: Wait for status checks 14 | uses: lewagon/wait-on-check-action@v1.3.3 15 | with: 16 | repo-token: ${{ secrets.MATZBOT_GITHUB_TOKEN }} 17 | ref: ${{ github.event.pull_request.head.sha || github.sha }} 18 | check-regexp: build* 19 | wait-interval: 30 20 | - name: Auto-merge for Dependabot PRs 21 | if: ${{ steps.metadata.outputs.update-type == 'version-update:semver-minor' || steps.metadata.outputs.update-type == 'version-update:semver-patch'}} 22 | run: gh pr merge --auto --merge "$PR_URL" 23 | env: 24 | PR_URL: ${{github.event.pull_request.html_url}} 25 | GITHUB_TOKEN: ${{ secrets.MATZBOT_GITHUB_TOKEN }} 26 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | env: 14 | GOPATH: /home/runner/go 15 | steps: 16 | - uses: actions/checkout@v2 17 | 18 | - name: setupdir 19 | run: | 20 | sudo mkdir -p misc/test/data/prouser/stor 21 | sudo mkdir -p misc/test/data/vsuser/stor 22 | sudo chmod -R 777 misc/test/data 23 | 24 | - name: Set up Go 25 | uses: actions/setup-go@v2 26 | with: 27 | go-version-file: 'go.mod' 28 | 29 | - name: Test 30 | run: make ci 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/* 2 | .vagrant 3 | pftp.pid 4 | misc/test/data 5 | pftp_bin 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: required 3 | services: 4 | - docker 5 | before_install: 6 | - sudo apt-get -qqy update 7 | - sudo mkdir -p misc/test/data/prouser/stor 8 | - sudo mkdir -p misc/test/data/vsuser/stor 9 | - sudo chmod -R 777 misc/test/data 10 | 11 | script: 12 | - GO="GO111MODULE=on go" make ci 13 | 14 | go: 15 | - 1.16.6 16 | cache: 17 | directories: 18 | - misc/test/data 19 | - /go/src/github.com 20 | notifications: 21 | slack: pepabo:CQkcN0cdvPSJvVsK2D48qYyV 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.16 2 | 3 | WORKDIR /go/src/github.com/pyama86/pftp/ 4 | 5 | COPY go.mod go.sum ./ 6 | RUN go mod download 7 | 8 | COPY pftp ./pftp 9 | COPY example ./example 10 | COPY main.go ./ 11 | 12 | RUN GOOS=linux CGO_ENABLED=0 go build -a -o pftp_bin main.go 13 | 14 | 15 | FROM alpine:latest 16 | WORKDIR /app/ 17 | 18 | COPY --from=0 /go/src/github.com/pyama86/pftp/pftp_bin ./ 19 | 20 | COPY config.toml config.toml 21 | COPY tls/ tls/ 22 | 23 | CMD ["./pftp_bin"] 24 | -------------------------------------------------------------------------------- /Dockerfile-base: -------------------------------------------------------------------------------- 1 | FROM bluefoxicy/proftpd 2 | 3 | ENV PROFTPD_PASSIVE_PORTS="31100-31150" 4 | ENV PROFTPD_AUTH_ORDER="mod_auth_pam.c* mod_auth_unix.c" 5 | ENV PROFTPD_TLS="on" 6 | ENV PROFTPD_TLS_REQUIRED="off" 7 | 8 | RUN echo "AllowForeignAddress on" >> /etc/proftpd/proftpd.conf 9 | 10 | EXPOSE 21 11 | -------------------------------------------------------------------------------- /Dockerfile-proftpd: -------------------------------------------------------------------------------- 1 | FROM bluefoxicy/proftpd 2 | 3 | ENV PROFTPD_PASSIVE_PORTS="21100-21150" 4 | ENV PROFTPD_AUTH_ORDER="mod_auth_pam.c* mod_auth_unix.c" 5 | ENV PROFTPD_DEFAULT_ROOT="/home/prouser" 6 | ENV PROFTPD_TLS="on" 7 | ENV PROFTPD_TLS_REQUIRED="off" 8 | 9 | 10 | VOLUME ["/home/prouser"] 11 | 12 | RUN echo "AllowForeignAddress on" >> /etc/proftpd/proftpd.conf 13 | RUN echo "MasqueradeAddress 127.0.0.1" >> /etc/proftpd/proftpd.conf 14 | 15 | RUN useradd prouser 16 | RUN echo 'prouser:prouser' | chpasswd 17 | 18 | EXPOSE 21 19 | -------------------------------------------------------------------------------- /Dockerfile-vsftpd: -------------------------------------------------------------------------------- 1 | FROM fauria/vsftpd 2 | ENV FTP_USER="vsuser" 3 | ENV FTP_PASS="vsuser" 4 | ENV PASV_ADDRESS="127.0.0.1" 5 | ENV PASV_MIN_PORT="11100" 6 | ENV PASV_MAX_PORT="11150" 7 | ENV LOCAL_UMASK="022" 8 | VOLUME /home/vsftpd 9 | VOLUME /var/log/vsftpd 10 | RUN echo "log_ftp_protocol=YES" >> /etc/vsftpd/vsftpd.conf 11 | EXPOSE 21 12 | 13 | CMD ["/usr/sbin/run-vsftpd.sh"] 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TEST ?= $(shell $(GO) list ./... | grep -v vendor) 2 | VERSION = $(shell cat version) 3 | REVISION = $(shell git describe --always) 4 | 5 | INFO_COLOR=\033[1;34m 6 | RESET=\033[0m 7 | BOLD=\033[1m 8 | GO ?= GO111MODULE=on go 9 | 10 | default: build 11 | ci: depsdev ftp test lint integration ## Run test and more... 12 | 13 | depsdev: ## Installing dependencies for development 14 | which staticcheck > /dev/null || $(GO) install honnef.co/go/tools/cmd/staticcheck@latest 15 | $(GO) get -u github.com/tcnksm/ghr 16 | $(GO) get github.com/mitchellh/gox 17 | 18 | test: ## Run test 19 | @echo "$(INFO_COLOR)==> $(RESET)$(BOLD)Testing$(RESET)" 20 | $(GO) test -v $(TEST) -timeout=5s -parallel=4 21 | $(GO) test -race $(TEST) 22 | 23 | vet: ## Exec $(GO) vet 24 | @echo "$(INFO_COLOR)==> $(RESET)$(BOLD)Vetting$(RESET)" 25 | $(GO) vet $(TEST) 26 | 27 | lint: ## Exec golint 28 | @echo "$(INFO_COLOR)==> $(RESET)$(BOLD)Linting$(RESET)" 29 | $(GOPATH)/bin/staticcheck ./... 30 | 31 | server: ## Run server with gin 32 | $(GO) run main.go 33 | 34 | build: ## Build as linux binary 35 | @echo "$(INFO_COLOR)==> $(RESET)$(BOLD)Building$(RESET)" 36 | $(GO) build -o pftp_bin main.go 37 | 38 | 39 | ghr: ## Upload to Github releases without token check 40 | @echo "$(INFO_COLOR)==> $(RESET)$(BOLD)Releasing for Github$(RESET)" 41 | ghr -u pyama86 v$(VERSION)-$(REVISION) pkg 42 | 43 | dist: build ## Upload to Github releases 44 | @test -z $(GITHUB_TOKEN) || test -z $(GITHUB_API) || $(MAKE) ghr 45 | 46 | help: 47 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "$(INFO_COLOR)%-30s$(RESET) %s\n", $$1, $$2}' 48 | 49 | vsftpd: vsftpd-cleanup 50 | docker build -t vsftpd-server:test -f Dockerfile-vsftpd ./ 51 | docker run -d -v "`pwd`/misc/test/data":/home/vsftpd \ 52 | -p 10021:21 -p 11100-11150:11100-11150 \ 53 | --name vsftpd --restart=always vsftpd-server:test 54 | 55 | vsftpd-cleanup: 56 | docker rm -f vsftpd | true 57 | 58 | proftpd: proftpd-cleanup 59 | docker build -t proftpd-server:test -f Dockerfile-proftpd ./ 60 | docker run -d -v "`pwd`/misc/test/data/prouser":/home/prouser \ 61 | -v "`pwd`/tls/server.crt":/etc/ssl/certs/proftpd.crt \ 62 | -v "`pwd`/tls/server.key":/etc/ssl/private/proftpd.key \ 63 | -v "`pwd`/tls/server.crt":/etc/ssl/certs/chain.crt \ 64 | -p 20021:21 -p 21100-21150:21100-21150 \ 65 | --name proftpd --restart=always proftpd-server:test 66 | proftpd-cleanup: 67 | docker rm -f proftpd | true 68 | 69 | baseftp: baseftp-cleanup 70 | docker build -t baseftp-server:test -f Dockerfile-base ./ 71 | docker run -d \ 72 | -v "`pwd`/tls/server.crt":/etc/ssl/certs/proftpd.crt \ 73 | -v "`pwd`/tls/server.key":/etc/ssl/private/proftpd.key \ 74 | -v "`pwd`/tls/server.crt":/etc/ssl/certs/chain.crt \ 75 | -p 21:21 -p 31100-31150:31100-31150 \ 76 | --name baseftp --restart=always baseftp-server:test 77 | baseftp-cleanup: 78 | docker rm -f baseftp | true 79 | 80 | ftp: baseftp vsftpd proftpd 81 | 82 | ftp-cleanup: baseftp-cleanup vsftpd-cleanup proftpd-cleanup 83 | 84 | integration: 85 | @echo "$(INFO_COLOR)==> $(RESET)$(BOLD)Integration Testing$(RESET)" 86 | ./misc/server stop || true 87 | ./misc/server start 88 | $(GO) test $(VERBOSE) -timeout=300s -integration $(TEST) $(TEST_OPTIONS) 89 | ./misc/server stop 90 | 91 | .PHONY: default dist test ftp proftpd vsftpd help ghr build server 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/pyama86/pftp.svg?branch=master)](https://travis-ci.org/pyama86/pftp) 2 | 3 | # pftp 4 | pluggable ftp proxy server 5 | 6 | # example 7 | ```go 8 | func main() { 9 | confFile := "./example.toml" 10 | 11 | ftpServer, err := pftp.NewFtpServer(confFile) 12 | if err != nil { 13 | logrus.Fatal(err) 14 | } 15 | 16 | if err := ftpServer.Start(); err != nil { 17 | logrus.Fatal(err) 18 | } 19 | } 20 | ``` 21 | 22 | ## middleware 23 | In pftp, you can hook into the ftp command and execute arbitrary processing. 24 | 25 | ### USER command example 26 | An example of changing the connection destination according to the user name. 27 | ```go 28 | func main() { 29 | ... 30 | ftpServer.Use("user", User) 31 | ... 32 | } 33 | 34 | func User(c *pftp.Context, param string) error { 35 | if param == "foo" { 36 | c.RemoteAddr = "127.0.0.1:10021" 37 | } else if param == "bar" { 38 | c.RemoteAddr = "127.0.0.1:20021" 39 | } 40 | return nil 41 | } 42 | ``` 43 | 44 | ## Require 45 | - Go 1.15 or later 46 | 47 | # author 48 | - @pyama86 49 | - @heat1024 50 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure("2") do |config| 5 | config.vm.box = "centos/7" 6 | config.vm.network :private_network, ip:"192.168.33.2" 7 | config.vm.provision "shell", inline: <<-SHELL 8 | yum -y install vsftpd 9 | sleep 3 10 | chkconfig vsftpd on | true 11 | useradd pftp | true 12 | echo -n 'pftp:pftp' | chpasswd 13 | sed -i 's/userlist_enable=YES/userlist_enable=NO/g' /etc/vsftpd/vsftpd.conf 14 | sed -i 's/tcp_wrappers=YES/tcp_wrappers=NO/g' /etc/vsftpd/vsftpd.conf 15 | if ! grep 'log_ftp_protocol=YES' '/etc/vsftpd/vsftpd.conf' >/dev/null; then 16 | echo 'log_ftp_protocol=YES' >> '/etc/vsftpd/vsftpd.conf' 17 | echo 'syslog_enable=YES' >> '/etc/vsftpd/vsftpd.conf' 18 | fi 19 | setenforce 0 20 | service vsftpd restart 21 | SHELL 22 | end 23 | -------------------------------------------------------------------------------- /config.toml: -------------------------------------------------------------------------------- 1 | max_connections = 1000 2 | idle_timeout = 120 3 | transfer_timeout = 600 4 | keepalive_time = 600 5 | remote_addr = "127.0.0.1:21" 6 | 7 | # Configure about proxy features 8 | ## Can set welcome message when first connect to pftp 9 | ## If not set, pftp will send remote_addr server's welcome message 10 | welcome_message = "sample pftp server ready" 11 | 12 | ## Send proxy protocol to origin server when user login process 13 | send_proxy_protocol = false # If true, pftp will send PROXY command to origin ftp server (default : false) 14 | 15 | ## Use data channel proxy 16 | ## Default value is false 17 | data_channel_proxy = true 18 | 19 | ## Set listen port range for data connection. 20 | ## Comment out this parameter means full range. 21 | ## If set min > max of illegal numbers, pftp will set full range too. 22 | data_listen_port_range = "65000-65100" # "min-max"(default : random) 23 | 24 | ## This configure set data connect mode between pftp and origin ftp server. 25 | ## If set passive/pasv, pftp always use passive mode for connect to origin. 26 | ## Set client(the default setup), use client's connected mode. 27 | transfer_mode = "pasv" # PASV / Passive / PORT / Active / client (default : client) 28 | 29 | ## Should we ignore the passive data channel IP sent by the origin FTP server ? (default: false) 30 | ignore_passive_ip = false 31 | 32 | ## Masquerade pftp's ip to setted IP(may be LB's IP). 33 | ## It might necessary if pftp server is at behind the LB. 34 | masquerade_ip = "127.0.0.1" 35 | 36 | [tls] 37 | ## Set SSL certification and secret key file's path 38 | ## cipher_suite set by IANA ciphersuites. if not set, or no available names, use hardware default ciphersuites 39 | cert = "./tls/server.crt" 40 | key = "./tls/server.key" 41 | #cipher_suite = "TLS_RSA_WITH_AES_256_GCM_SHA384:TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA:TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384" 42 | # ca_cert = "./tls/ca_cert.crt" 43 | 44 | ## Make sure TLS protocol range allowed in origin ftp server 45 | ## If only one protocol support, set min and max to same 46 | ## Can set TLSv1 TLSv1.1 TLSv1.2 47 | min_protocol = "TLSv1" 48 | max_protocol = "TLSv1" 49 | 50 | [webapiserver] 51 | # %s replace by username on running 52 | uri = "http://127.0.0.1:8080/getDomain?username=%s" 53 | -------------------------------------------------------------------------------- /example/webapi/webapi.go: -------------------------------------------------------------------------------- 1 | package webapi 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | 10 | "github.com/BurntSushi/toml" 11 | ) 12 | 13 | type config struct { 14 | Apiserver serverConfig `toml:"webapiserver"` 15 | } 16 | 17 | type serverConfig struct { 18 | URI string `toml:"uri"` 19 | } 20 | 21 | // Response from server will contain 3 elements with JSON type. 22 | // { 23 | // code : http response code 24 | // message : response message from server 25 | // data : destination url 26 | // } 27 | type Response struct { 28 | Code int `json:"code"` 29 | Message string `json:"message"` 30 | Data string `json:"data"` 31 | } 32 | 33 | // RequestToServer will return response data from webapi server 34 | // If response code doesn't got 2xx, return error. 35 | func RequestToServer(requestURI string, param string) (*Response, error) { 36 | resp, err := http.Get(fmt.Sprintf(requestURI, param)) 37 | if err != nil { 38 | return nil, err 39 | } 40 | defer resp.Body.Close() 41 | 42 | respBody, err := io.ReadAll(resp.Body) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | var decodedBody = new(Response) 48 | json.Unmarshal(respBody, &decodedBody) 49 | 50 | if decodedBody.Code != 200 { 51 | return nil, errors.New(decodedBody.Message) 52 | } 53 | 54 | return decodedBody, nil 55 | } 56 | 57 | // GetDomainFromWebAPI will return destination url by string. 58 | // Make request URL from config file and has request to server with username parameter. 59 | func GetDomainFromWebAPI(path string, param string) (*string, error) { 60 | var conf config 61 | _, err := toml.DecodeFile(path, &conf) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | domain, err := RequestToServer(conf.Apiserver.URI, param) 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | return &domain.Data, nil 72 | } 73 | -------------------------------------------------------------------------------- /example/webapi/webapi_test.go: -------------------------------------------------------------------------------- 1 | package webapi 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/pyama86/pftp/test" 8 | ) 9 | 10 | func Test_restapi_RequestToServer(t *testing.T) { 11 | type fields struct { 12 | serverURI string 13 | } 14 | 15 | testsrv := test.LaunchUnitTestRestServer(t) 16 | defer testsrv.Close() 17 | 18 | tests := []struct { 19 | name string 20 | fields fields 21 | want *Response 22 | wantErr bool 23 | }{ 24 | { 25 | name: "vsuser", 26 | fields: fields{ 27 | serverURI: testsrv.URL + "/getDomain?username=%s", 28 | }, 29 | want: &Response{ 30 | Code: 200, 31 | Message: "Username found", 32 | Data: "127.0.0.1:10021", 33 | }, 34 | wantErr: false, 35 | }, 36 | { 37 | name: "prouser", 38 | fields: fields{ 39 | serverURI: testsrv.URL + "/getDomain?username=%s", 40 | }, 41 | want: &Response{ 42 | Code: 200, 43 | Message: "Username found", 44 | Data: "127.0.0.1:20021", 45 | }, 46 | wantErr: false, 47 | }, 48 | { 49 | name: "hogemoge", 50 | fields: fields{ 51 | serverURI: testsrv.URL + "/getDomain?username=%s", 52 | }, 53 | want: &Response{ 54 | Code: 400, 55 | Message: "Username not found", 56 | Data: "", 57 | }, 58 | wantErr: true, 59 | }, 60 | } 61 | 62 | for _, tt := range tests { 63 | t.Run(tt.name, func(t *testing.T) { 64 | got, err := RequestToServer(tt.fields.serverURI, tt.name) 65 | if err != nil { 66 | if !tt.wantErr || (err.Error() != tt.want.Message) { 67 | t.Errorf("got error when request to server = %v", err) 68 | } 69 | } 70 | 71 | if !reflect.DeepEqual(got, tt.want) && !tt.wantErr { 72 | t.Errorf("restapi.RequestToServer() = %v, want %v", got, tt.want) 73 | } 74 | }) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/pyama86/pftp 2 | 3 | go 1.23.2 4 | 5 | require ( 6 | github.com/BurntSushi/toml v1.4.0 7 | github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4 8 | github.com/jlaffaye/ftp v0.2.0 9 | github.com/julienschmidt/httprouter v1.3.0 10 | github.com/lestrrat/go-server-starter v0.0.0-20180220115249-6ac0b358431b 11 | github.com/marcobeierer/ftps v0.0.0-20180713194618-60e520f5feed 12 | github.com/pires/go-proxyproto v0.8.0 13 | github.com/sirupsen/logrus v1.9.3 14 | github.com/tevino/abool v1.2.0 15 | golang.org/x/sync v0.10.0 16 | golang.org/x/sys v0.29.0 17 | ) 18 | 19 | require ( 20 | github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect 21 | github.com/hashicorp/errwrap v1.0.0 // indirect 22 | github.com/hashicorp/go-multierror v1.1.1 // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= 2 | github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 3 | github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4 h1:vdT7QwBhJJEVNFMBNhRSFDRCB6O16T28VhvqRgqFyn8= 4 | github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4/go.mod h1:SvXOG8ElV28oAiG9zv91SDe5+9PfIr7PPccpr8YyXNs= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A= 9 | github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg= 10 | github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= 11 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 12 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 13 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 14 | github.com/jlaffaye/ftp v0.2.0 h1:lXNvW7cBu7R/68bknOX3MrRIIqZ61zELs1P2RAiA3lg= 15 | github.com/jlaffaye/ftp v0.2.0/go.mod h1:is2Ds5qkhceAPy2xD6RLI6hmp/qysSoymZ+Z2uTnspI= 16 | github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= 17 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 18 | github.com/lestrrat/go-server-starter v0.0.0-20180220115249-6ac0b358431b h1:Ial71qYQufttbgLhmAAAgf4Y+p9KRKYklAoE9a8rXOA= 19 | github.com/lestrrat/go-server-starter v0.0.0-20180220115249-6ac0b358431b/go.mod h1:3T+o9dIpjId0dpv2Aa7+HivBIW9h9nra0VuN5ARP/ec= 20 | github.com/marcobeierer/ftps v0.0.0-20180713194618-60e520f5feed h1:1VAr2hlM3isutlZdn7L0CnKc2vVvxY+KNWJMzTMs3wQ= 21 | github.com/marcobeierer/ftps v0.0.0-20180713194618-60e520f5feed/go.mod h1:kFk0pASx+lFf7MyQg428JrEFYIYLpqQnFJjUaXWunic= 22 | github.com/pires/go-proxyproto v0.8.0 h1:5unRmEAPbHXHuLjDg01CxJWf91cw3lKHc/0xzKpXEe0= 23 | github.com/pires/go-proxyproto v0.8.0/go.mod h1:iknsfgnH8EkjrMeMyvfKByp9TiBZCKZM0jx2xmKqnVY= 24 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 25 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 26 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 27 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 28 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 29 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 30 | github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= 31 | github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 32 | github.com/tevino/abool v1.2.0 h1:heAkClL8H6w+mK5md9dzsuohKeXHUpY7Vw0ZCKW+huA= 33 | github.com/tevino/abool v1.2.0/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg= 34 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 35 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 36 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 37 | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= 38 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 39 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 40 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 41 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 42 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 43 | -------------------------------------------------------------------------------- /integration_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/md5" 5 | "crypto/tls" 6 | "errors" 7 | "flag" 8 | "fmt" 9 | "io" 10 | "os" 11 | "os/exec" 12 | "path" 13 | "path/filepath" 14 | "reflect" 15 | "runtime" 16 | "testing" 17 | 18 | "github.com/jlaffaye/ftp" 19 | "github.com/marcobeierer/ftps" 20 | "github.com/pyama86/pftp/test" 21 | "golang.org/x/sync/errgroup" 22 | ) 23 | 24 | var ( 25 | integration = flag.Bool("integration", false, "run integration tests") 26 | ) 27 | 28 | const testCount = 2 29 | 30 | type userInfo struct { 31 | ID string 32 | Pass string 33 | } 34 | 35 | type testSet struct { 36 | User userInfo 37 | Dir string 38 | } 39 | 40 | var testset = []testSet{ 41 | {userInfo{"prouser", "prouser"}, "misc/test/data/prouser"}, 42 | {userInfo{"vsuser", "vsuser"}, "misc/test/data/vsuser"}, 43 | } 44 | 45 | const dataPath = "misc/test/data" 46 | 47 | // goroutine leak test count 48 | const leaktestCount = 400 49 | 50 | func localConnect(port int, t *testing.T) *ftp.ServerConn { 51 | client, err := ftp.Dial(fmt.Sprintf("localhost:%d", port)) 52 | if err != nil { 53 | t.Fatalf("integration.localConnect() error = %v, wantErr %v", err, nil) 54 | } 55 | return client 56 | } 57 | 58 | func loggedin(port int, t *testing.T, user userInfo) *ftp.ServerConn { 59 | client := localConnect(port, t) 60 | 61 | err := client.Login(user.ID, user.Pass) 62 | if err != nil { 63 | t.Fatalf("integration.loggedin() error = %v, wantErr %v", err, nil) 64 | } 65 | return client 66 | } 67 | 68 | func TestMain(m *testing.M) { 69 | flag.Parse() 70 | 71 | srv, err := test.LaunchTestRestServer() 72 | if err != nil { 73 | fmt.Println("unable to run test webapi server") 74 | os.Exit(1) 75 | } 76 | defer srv.Close() 77 | 78 | result := m.Run() 79 | os.Exit(result) 80 | } 81 | 82 | func TestConnect(t *testing.T) { 83 | if !*integration { 84 | t.Skip() 85 | } 86 | client := localConnect(2121, t) 87 | defer client.Quit() 88 | } 89 | 90 | func TestLogin(t *testing.T) { 91 | if !*integration { 92 | t.Skip() 93 | } 94 | eg := errgroup.Group{} 95 | 96 | for i := 0; i < len(testset); i++ { 97 | index := i 98 | 99 | eg.Go(func() error { 100 | client := localConnect(2121, t) 101 | defer client.Quit() 102 | 103 | // If Login failed, Return Error 104 | if err := client.Login(testset[index].User.ID, testset[index].User.Pass); err != nil { 105 | return fmt.Errorf("integration.TestLogin() error = %v, wantErr %v", err, nil) 106 | } 107 | return nil 108 | }) 109 | } 110 | 111 | if err := eg.Wait(); err != nil { 112 | t.Fatal(err) 113 | } 114 | } 115 | 116 | func TestAuth(t *testing.T) { 117 | if !*integration { 118 | t.Skip() 119 | } 120 | eg := errgroup.Group{} 121 | 122 | for i := 0; i < len(testset); i++ { 123 | index := i 124 | 125 | eg.Go(func() error { 126 | client := new(ftps.FTPS) 127 | defer client.Quit() 128 | client.Debug = true 129 | client.TLSConfig.InsecureSkipVerify = true 130 | client.TLSConfig.MinVersion = tls.VersionTLS10 131 | client.TLSConfig.MaxVersion = tls.VersionTLS13 132 | 133 | if err := client.Connect("localhost", 2121); err != nil { 134 | return fmt.Errorf("integration.TestAuth() error = %v, want %v", err, "234 AUTH command ok") 135 | } 136 | 137 | err := client.Login(testset[index].User.ID, testset[index].User.Pass) 138 | // If Login success with vsftpd user(vsuser), Return Error 139 | if err == nil && testset[index].User.ID == "vsuser" { 140 | return fmt.Errorf("integration.TestAuth() error = %v, wantErr %v", err, errors.New("550 Permission denied")) 141 | } else if err != nil && testset[index].User.ID == "prouser" { 142 | return fmt.Errorf("integration.TestAuth() error = %v, want %s", err, "230 User prouser logged in") 143 | } 144 | 145 | return nil 146 | }) 147 | } 148 | 149 | if err := eg.Wait(); err != nil { 150 | t.Fatal(err) 151 | } 152 | } 153 | 154 | func removeDirFiles(t *testing.T, dir string) { 155 | for i := 0; i < len(testset); i++ { 156 | f := path.Join(testset[i].Dir, dir) 157 | filepath.Walk(f, 158 | func(fpath string, info os.FileInfo, err error) error { 159 | if err != nil { 160 | fmt.Println(err) 161 | } 162 | 163 | rel, err := filepath.Rel(f, fpath) 164 | if err != nil { 165 | fmt.Println(err) 166 | } 167 | if rel == `.` || rel == `..` { 168 | return nil 169 | } 170 | out, err := exec.Command("rm", "-f", fpath).CombinedOutput() 171 | if err != nil { 172 | t.Fatal(string(out)) 173 | } 174 | return err 175 | }) 176 | } 177 | } 178 | 179 | func fileExists(filename string) bool { 180 | _, err := os.Stat(filename) 181 | return err == nil 182 | } 183 | 184 | func makeRandomFiles(t *testing.T) { 185 | eg := errgroup.Group{} 186 | for i := 0; i < testCount; i++ { 187 | num := i 188 | 189 | eg.Go(func() error { 190 | f := fmt.Sprintf("%s/%d", dataPath, num) 191 | if !fileExists(f) { 192 | // make 500MB files 193 | out, err := exec.Command("sudo", "dd", "if=/dev/urandom", fmt.Sprintf("of=%s", f), "bs=1024", "count=500000").CombinedOutput() 194 | if err != nil { 195 | return errors.New(string(out)) 196 | } 197 | } 198 | return nil 199 | }) 200 | } 201 | 202 | if err := eg.Wait(); err != nil { 203 | t.Fatal(err) 204 | } 205 | } 206 | 207 | func TestUpload(t *testing.T) { 208 | if !*integration { 209 | t.Skip() 210 | } 211 | eg := errgroup.Group{} 212 | userCount := len(testset) 213 | 214 | makeRandomFiles(t) 215 | 216 | removeDirFiles(t, "stor") 217 | 218 | for u := 0; u < userCount; u++ { 219 | for i := 0; i < testCount; i++ { 220 | user := u 221 | num := i 222 | 223 | eg.Go(func() error { 224 | a := md5.New() 225 | b := md5.New() 226 | 227 | client := loggedin(2121, t, testset[user].User) 228 | defer client.Quit() 229 | 230 | f, err := os.Open(fmt.Sprintf("%s/%d", dataPath, num)) 231 | if err != nil { 232 | return err 233 | } 234 | defer f.Close() 235 | 236 | if err := os.MkdirAll(fmt.Sprintf("%s/stor", testset[user].Dir), 0777); err != nil { 237 | return err 238 | } 239 | 240 | if err = client.Stor(fmt.Sprintf("stor/%d", num), f); err != nil { 241 | return err 242 | } 243 | 244 | s, err := os.Open(fmt.Sprintf("%s/stor/%d", testset[user].Dir, num)) 245 | if err != nil { 246 | return err 247 | } 248 | defer s.Close() 249 | 250 | _, err = io.Copy(a, s) 251 | if err != nil { 252 | return err 253 | } 254 | 255 | // Set file pointer to front of origin file 256 | _, err = f.Seek(0, 0) 257 | if err != nil { 258 | return err 259 | } 260 | 261 | _, err = io.Copy(b, f) 262 | if err != nil { 263 | return err 264 | } 265 | 266 | if !reflect.DeepEqual(a.Sum(nil), b.Sum(nil)) { 267 | return fmt.Errorf("upload file check sum error: %d", num) 268 | } 269 | return nil 270 | }) 271 | } 272 | } 273 | 274 | if err := eg.Wait(); err != nil { 275 | t.Fatal(err) 276 | } 277 | } 278 | 279 | func TestDownload(t *testing.T) { 280 | if !*integration { 281 | t.Skip() 282 | } 283 | eg := errgroup.Group{} 284 | 285 | userCount := len(testset) 286 | 287 | for u := 0; u < userCount; u++ { 288 | for i := 0; i < testCount; i++ { 289 | user := u 290 | num := i 291 | 292 | eg.Go(func() error { 293 | a := md5.New() 294 | a2 := md5.New() 295 | b := md5.New() 296 | 297 | client := loggedin(2121, t, testset[user].User) 298 | defer client.Quit() 299 | 300 | r, err := client.Retr(fmt.Sprintf("stor/%d", num)) 301 | if err != nil { 302 | return err 303 | } 304 | defer r.Close() 305 | 306 | _, err = io.Copy(a, r) 307 | if err != nil { 308 | return err 309 | } 310 | // must close ftp reader after download complete 311 | r.Close() 312 | 313 | f, err := os.Open(fmt.Sprintf("%s/stor/%d", testset[user].Dir, num)) 314 | if err != nil { 315 | return err 316 | } 317 | defer f.Close() 318 | 319 | _, err = io.Copy(b, f) 320 | if err != nil { 321 | return err 322 | } 323 | 324 | if !reflect.DeepEqual(a.Sum(nil), b.Sum(nil)) { 325 | return fmt.Errorf("download file check sum error: %d", num) 326 | } 327 | 328 | // Download twice in same ftp connection for check loop lock in dataHandler problem 329 | r, err = client.Retr(fmt.Sprintf("stor/%d", num)) 330 | if err != nil { 331 | return fmt.Errorf("error when twice download: %s\n", err.Error()) 332 | } 333 | _, err = io.Copy(a2, r) 334 | if err != nil { 335 | return err 336 | } 337 | // must close ftp reader after download complete 338 | r.Close() 339 | 340 | if !reflect.DeepEqual(a2.Sum(nil), b.Sum(nil)) { 341 | return fmt.Errorf("2nd download file check sum error: %d", num) 342 | } 343 | 344 | return nil 345 | }) 346 | } 347 | } 348 | 349 | if err := eg.Wait(); err != nil { 350 | t.Fatal(err) 351 | } 352 | } 353 | 354 | func TestGoroutineLeak(t *testing.T) { 355 | // check goroutine leak when get a lot of connection 356 | if !*integration { 357 | t.Skip() 358 | } 359 | eg := errgroup.Group{} 360 | 361 | beforeLoadTest := runtime.NumGoroutine() 362 | 363 | for i := 0; i < leaktestCount; i++ { 364 | eg.Go(func() error { 365 | client, err := ftp.Dial("localhost:2121") 366 | if err != nil { 367 | return nil 368 | } 369 | defer client.Quit() 370 | 371 | return nil 372 | }) 373 | } 374 | 375 | eg.Wait() 376 | 377 | afterLoadTest := runtime.NumGoroutine() 378 | 379 | if beforeLoadTest < afterLoadTest { 380 | t.Fatal(fmt.Errorf("goroutine count increased! before test = %d, after test = %d", beforeLoadTest, afterLoadTest)) 381 | } 382 | } 383 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | logrus_stack "github.com/Gurpartap/logrus-stack" 7 | "github.com/pyama86/pftp/example/webapi" 8 | "github.com/pyama86/pftp/pftp" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | var confFile = "./config.toml" 13 | 14 | func init() { 15 | logrus.SetLevel(logrus.ErrorLevel) 16 | stackLevels := []logrus.Level{logrus.PanicLevel, logrus.FatalLevel} 17 | logrus.AddHook(logrus_stack.NewHook(stackLevels, stackLevels)) 18 | } 19 | 20 | func main() { 21 | ftpServer, err := pftp.NewFtpServer(confFile) 22 | if err != nil { 23 | logrus.Fatal(err) 24 | } 25 | 26 | ftpServer.Use("user", User) 27 | if err := ftpServer.Start(); err != nil { 28 | logrus.Fatal(err) 29 | } 30 | } 31 | 32 | // User function will setup Origin ftp server domain from ftp username 33 | // If failed get domain from server, the origin will set by local (localhost:21) 34 | func User(c *pftp.Context, param string) error { 35 | res, err := webapi.GetDomainFromWebAPI(confFile, param) 36 | if err != nil { 37 | logrus.Debug(fmt.Sprintf("cannot get origin host from webapi server:%v", err)) 38 | c.RemoteAddr = "" 39 | } else { 40 | c.RemoteAddr = *res 41 | } 42 | 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /misc/server: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | start_server() { 4 | make server & 5 | pid=sudo lsof -i:2121 | tail -1 | awk '{print $2}' 6 | while [ -z "$pid" ] 7 | do 8 | pid=`sudo lsof -i:2121 | tail -1 | awk '{print $2}'` 9 | sleep 1 10 | done 11 | echo $pid > pftp.pid 12 | } 13 | 14 | stop_server() { 15 | PID=$(cat pftp.pid) 16 | 17 | sudo kill -SIGINT $PID 18 | if [ $? -ne 0 ]; then 19 | echo "process not found" 20 | exit 1 21 | fi 22 | sleep 1 23 | sudo kill -0 $PID 2>/dev/null 24 | if [ $? -ne 1 ]; then 25 | echo "sent signal as SIGKILL" 26 | sudo kill -SIGKILL $PID 27 | fi 28 | } 29 | 30 | hup_server() { 31 | PID=$(cat pftp.pid) 32 | sudo kill -SIGHUP $PID 2>/dev/null 33 | [ -x /proc/$PID ] 34 | if [ $? -ne 1 ]; then 35 | exit 1 36 | fi 37 | exit 0 38 | } 39 | 40 | case "$1" in 41 | start) 42 | start_server 43 | ;; 44 | hup) 45 | hup_server 46 | ;; 47 | stop) 48 | stop_server 49 | ;; 50 | *) 51 | echo $"Usage: $0 {start|stop|hup}" 52 | exit 2 53 | esac 54 | -------------------------------------------------------------------------------- /pftp/client_handler.go: -------------------------------------------------------------------------------- 1 | package pftp 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "net" 8 | "strings" 9 | "sync" 10 | "sync/atomic" 11 | "time" 12 | 13 | "github.com/tevino/abool" 14 | "golang.org/x/sync/errgroup" 15 | ) 16 | 17 | type handleFunc struct { 18 | f func(*clientHandler) *result 19 | suspend bool 20 | } 21 | 22 | var handlers map[string]*handleFunc 23 | 24 | func init() { 25 | handlers = make(map[string]*handleFunc) 26 | handlers["PROXY"] = &handleFunc{(*clientHandler).handlePROXY, false} 27 | handlers["USER"] = &handleFunc{(*clientHandler).handleUSER, true} 28 | handlers["AUTH"] = &handleFunc{(*clientHandler).handleAUTH, true} 29 | handlers["PBSZ"] = &handleFunc{(*clientHandler).handlePBSZ, true} 30 | handlers["PROT"] = &handleFunc{(*clientHandler).handlePROT, true} 31 | handlers["PORT"] = &handleFunc{(*clientHandler).handleDATA, false} 32 | handlers["EPRT"] = &handleFunc{(*clientHandler).handleDATA, false} 33 | handlers["PASV"] = &handleFunc{(*clientHandler).handleDATA, false} 34 | handlers["EPSV"] = &handleFunc{(*clientHandler).handleDATA, false} 35 | 36 | // handle data transfer begin commands 37 | handlers["RETR"] = &handleFunc{(*clientHandler).handleTransfer, false} 38 | handlers["STOR"] = &handleFunc{(*clientHandler).handleTransfer, false} 39 | handlers["STOU"] = &handleFunc{(*clientHandler).handleTransfer, false} 40 | handlers["APPE"] = &handleFunc{(*clientHandler).handleTransfer, false} 41 | handlers["LIST"] = &handleFunc{(*clientHandler).handleTransfer, false} 42 | handlers["MLSD"] = &handleFunc{(*clientHandler).handleTransfer, false} 43 | handlers["NLST"] = &handleFunc{(*clientHandler).handleTransfer, false} 44 | } 45 | 46 | type clientHandler struct { 47 | id uint64 48 | conn net.Conn 49 | config *config 50 | tlsDatas *tlsDataSet 51 | controlInTLS *abool.AtomicBool 52 | transferInTLS *abool.AtomicBool 53 | middleware middleware 54 | writer *bufio.Writer 55 | reader *bufio.Reader 56 | line string 57 | command string 58 | param string 59 | proxy *proxyServer 60 | context *Context 61 | currentConnection *int32 62 | connCounts int32 63 | mutex *sync.Mutex 64 | log *logger 65 | srcIP string 66 | previousTLSCommands []string 67 | inDataTransfer *abool.AtomicBool 68 | } 69 | 70 | func newClientHandler(connection net.Conn, c *config, sharedTLSData *tlsData, m middleware, id uint64, currentConnection *int32) *clientHandler { 71 | p := &clientHandler{ 72 | id: id, 73 | conn: connection, 74 | config: c, 75 | controlInTLS: abool.New(), 76 | transferInTLS: abool.New(), 77 | middleware: m, 78 | writer: bufio.NewWriter(connection), 79 | reader: bufio.NewReader(connection), 80 | context: newContext(c, connection), 81 | currentConnection: currentConnection, 82 | mutex: &sync.Mutex{}, 83 | log: &logger{fromip: connection.RemoteAddr().String(), user: "-", id: id}, 84 | srcIP: connection.RemoteAddr().String(), 85 | inDataTransfer: abool.New(), 86 | } 87 | 88 | // increase current connection count 89 | p.connCounts = atomic.AddInt32(p.currentConnection, 1) 90 | p.log.info("FTP Client connected. clientIP: %s. current connection count: %d", p.conn.RemoteAddr(), p.connCounts) 91 | 92 | // is masquerade IP not setted, set local IP of client connection 93 | if len(p.config.MasqueradeIP) == 0 { 94 | p.config.MasqueradeIP = strings.Split(connection.LocalAddr().String(), ":")[0] 95 | } 96 | 97 | // make TLS configs by shared pftp server conf(for client) and client own conf(for origin) 98 | p.tlsDatas = &tlsDataSet{ 99 | forClient: sharedTLSData, 100 | forOrigin: buildTLSConfigForOrigin(c), 101 | } 102 | 103 | return p 104 | } 105 | 106 | // Close client connection and check return 107 | func (c *clientHandler) Close() error { 108 | if c.conn != nil { 109 | if err := c.conn.Close(); err != nil { 110 | return err 111 | } 112 | } 113 | 114 | return nil 115 | } 116 | 117 | func (c *clientHandler) setClientDeadLine(t int) { 118 | // do not time out during transfer data 119 | if c.inDataTransfer.IsSet() { 120 | c.conn.SetDeadline(time.Time{}) 121 | } else { 122 | c.conn.SetDeadline(time.Now().Add(time.Duration(t) * time.Second)) 123 | } 124 | } 125 | 126 | func (c *clientHandler) handleCommands() error { 127 | defer func() { 128 | // decrease current connection count 129 | c.log.info("FTP Client disconnect. clientIP: %s. current connection count: %d", c.conn.RemoteAddr(), atomic.AddInt32(c.currentConnection, -1)) 130 | 131 | // close each connection again 132 | connectionCloser(c, c.log) 133 | if c.proxy != nil { 134 | connectionCloser(c.proxy, c.log) 135 | } 136 | }() 137 | 138 | // Check max client. If exceeded, send 530 error to client and disconnect 139 | if c.connCounts > c.config.MaxConnections { 140 | err := fmt.Errorf("exceeded client connection limit") 141 | r := result{ 142 | code: 530, 143 | msg: "max client exceeded", 144 | err: err, 145 | log: c.log, 146 | } 147 | if err := r.Response(c); err != nil { 148 | c.log.err("cannot send response to client") 149 | } 150 | 151 | return err 152 | } 153 | 154 | eg := errgroup.Group{} 155 | 156 | err := c.connectProxy() 157 | if err != nil { 158 | return err 159 | } 160 | 161 | // run origin response read routine 162 | eg.Go(func() error { return c.getResponseFromOrigin() }) 163 | 164 | // run client command read routine 165 | eg.Go(func() error { return c.readClientCommands() }) 166 | 167 | // wait until all goroutine has done 168 | if err = eg.Wait(); err != nil && err == io.EOF { 169 | c.log.info("client disconnected by error") 170 | } else { 171 | c.log.info("client disconnected") 172 | err = nil 173 | } 174 | 175 | return err 176 | } 177 | 178 | func (c *clientHandler) getResponseFromOrigin() error { 179 | var err error 180 | 181 | // close origin connection when close goroutine 182 | defer func() { 183 | // send EOF to client connection. if fail, close immediately 184 | c.log.debug("send EOF to client") 185 | 186 | if err := sendEOF(c.conn); err != nil { 187 | c.log.debug("send EOF to client failed. close connection.") 188 | connectionCloser(c, c.log) 189 | } 190 | 191 | // close current proxy connection 192 | connectionCloser(c.proxy, c.log) 193 | }() 194 | 195 | // サーバからのレスポンスはSuspendしない限り自動で返却される 196 | for { 197 | err = c.proxy.responseProxy() 198 | if err != nil { 199 | if err == io.EOF { 200 | c.log.debug("EOF from origin connection") 201 | err = nil 202 | } else { 203 | if !strings.Contains(err.Error(), alreadyClosedMsg) { 204 | c.log.debug("error from origin connection: %s", err.Error()) 205 | } 206 | } 207 | 208 | break 209 | } 210 | 211 | // wait until switching origin server complate 212 | if c.proxy.stop { 213 | if !<-c.proxy.waitSwitching { 214 | err = fmt.Errorf("switch origin to %s is failed", c.context.RemoteAddr) 215 | c.log.err(err.Error()) 216 | 217 | break 218 | } 219 | } 220 | } 221 | 222 | return err 223 | } 224 | 225 | func (c *clientHandler) readClientCommands() error { 226 | lastError := error(nil) 227 | 228 | // close client connection when close goroutine 229 | defer func() { 230 | // send EOF to origin connection. if fail, close immediately 231 | c.log.debug("send EOF to origin") 232 | 233 | if err := sendEOF(c.proxy.GetConn()); err != nil { 234 | c.log.debug("send EOF to origin failed. close connection.") 235 | connectionCloser(c.proxy, c.log) 236 | } 237 | 238 | // close current client connection 239 | connectionCloser(c, c.log) 240 | }() 241 | 242 | for { 243 | if c.config.IdleTimeout > 0 { 244 | c.setClientDeadLine(c.config.IdleTimeout) 245 | } 246 | 247 | line, err := c.reader.ReadString('\n') 248 | if err != nil { 249 | lastError = err 250 | if err == io.EOF { 251 | c.log.debug("EOF from client connection") 252 | lastError = nil 253 | } else if c.command == "QUIT" { 254 | lastError = nil 255 | } else { 256 | switch err := err.(type) { 257 | case net.Error: 258 | nErr := net.Error(err) 259 | if nErr.Timeout() { 260 | c.conn.SetDeadline(time.Now().Add(time.Minute)) 261 | r := result{ 262 | code: 421, 263 | msg: "command timeout : closing control connection", 264 | err: err, 265 | log: c.log, 266 | } 267 | if err := r.Response(c); err != nil { 268 | lastError = fmt.Errorf("response to client error: %v", err) 269 | 270 | break 271 | } 272 | 273 | // if timeout, send EOF to client connection for graceful disconnect 274 | c.log.debug("send EOF to client") 275 | 276 | // if send EOF failed, close immediately 277 | if err := sendEOF(c.conn); err != nil { 278 | c.log.debug("send EOF to client failed. try to close connection.") 279 | connectionCloser(c, c.log) 280 | } 281 | 282 | continue 283 | } else { 284 | c.log.debug("error from client connection: %s", err.Error()) 285 | } 286 | 287 | default: 288 | c.log.debug("error from client connection: %s", err.Error()) 289 | } 290 | } 291 | 292 | break 293 | } else { 294 | commandResponse := c.handleCommand(line) 295 | if commandResponse != nil { 296 | if err = commandResponse.Response(c); err != nil { 297 | lastError = err 298 | break 299 | } 300 | } 301 | } 302 | } 303 | 304 | return lastError 305 | } 306 | 307 | func (c *clientHandler) writeLine(line string) error { 308 | c.mutex.Lock() 309 | defer c.mutex.Unlock() 310 | 311 | if _, err := c.writer.WriteString(line + "\r\n"); err != nil { 312 | return err 313 | } 314 | if err := c.writer.Flush(); err != nil { 315 | return err 316 | } 317 | 318 | c.log.debug("send to client: %s", line) 319 | return nil 320 | } 321 | 322 | func (c *clientHandler) writeMessage(code int, message string) error { 323 | line := fmt.Sprintf("%d %s", code, message) 324 | return c.writeLine(line) 325 | } 326 | 327 | func (c *clientHandler) handleCommand(line string) (r *result) { 328 | c.parseLine(line) 329 | defer func() { 330 | if r := recover(); r != nil { 331 | r = &result{ 332 | code: 500, 333 | msg: fmt.Sprintf("Internal error: %s", r), 334 | } 335 | } 336 | }() 337 | 338 | c.commandLog(line) 339 | 340 | if c.middleware[c.command] != nil { 341 | if err := c.middleware[c.command](c.context, c.param); err != nil { 342 | return &result{ 343 | code: 500, 344 | msg: fmt.Sprintf("Internal error: %s", err), 345 | } 346 | } 347 | } 348 | 349 | cmd := handlers[c.command] 350 | if cmd != nil { 351 | if cmd.suspend { 352 | c.proxy.suspend() 353 | defer c.proxy.unsuspend() 354 | } 355 | res := cmd.f(c) 356 | if res != nil { 357 | return res 358 | } 359 | } else { 360 | if err := c.proxy.sendToOrigin(line); err != nil { 361 | return &result{ 362 | code: 500, 363 | msg: fmt.Sprintf("Internal error: %s", err), 364 | } 365 | } 366 | } 367 | 368 | return nil 369 | } 370 | 371 | func (c *clientHandler) connectProxy() error { 372 | if c.proxy != nil { 373 | err := c.proxy.switchOrigin(c.srcIP, c.context.RemoteAddr, c.previousTLSCommands) 374 | if err != nil { 375 | return err 376 | } 377 | } else { 378 | p, err := newProxyServer( 379 | &proxyServerConfig{ 380 | clientReader: c.reader, 381 | clientWriter: c.writer, 382 | tlsDatas: c.tlsDatas, 383 | originAddr: c.context.RemoteAddr, 384 | mutex: c.mutex, 385 | log: c.log, 386 | config: c.config, 387 | inDataTransfer: c.inDataTransfer, 388 | }) 389 | if err != nil { 390 | return err 391 | } 392 | c.proxy = p 393 | } 394 | 395 | return nil 396 | } 397 | 398 | // Get command from command line 399 | func getCommand(line string) []string { 400 | return strings.SplitN(strings.Trim(line, "\r\n"), " ", 2) 401 | } 402 | 403 | func (c *clientHandler) parseLine(line string) { 404 | params := getCommand(line) 405 | c.line = line 406 | c.command = strings.ToUpper(params[0]) 407 | if len(params) > 1 { 408 | c.param = params[1] 409 | } 410 | } 411 | 412 | // Hide parameters from log 413 | func (c *clientHandler) commandLog(line string) { 414 | if strings.Compare(strings.ToUpper(getCommand(line)[0]), secureCommand) == 0 { 415 | c.log.info("read from client: %s ********\r\n", secureCommand) 416 | } else { 417 | c.log.info("read from client: %s", strings.TrimSuffix(line, "\r\n")) 418 | } 419 | } 420 | -------------------------------------------------------------------------------- /pftp/client_handler_test.go: -------------------------------------------------------------------------------- 1 | package pftp 2 | 3 | import ( 4 | "bufio" 5 | "crypto/tls" 6 | "errors" 7 | "net" 8 | "strings" 9 | "testing" 10 | "time" 11 | 12 | "github.com/pyama86/pftp/test" 13 | ) 14 | 15 | func Test_clientHandler_handleCommands(t *testing.T) { 16 | var server net.Listener 17 | serverready := make(chan struct{}) 18 | conn := make(chan net.Conn) 19 | done := make(chan struct{}) 20 | 21 | go test.LaunchTestServer(&server, conn, done, serverready, t) 22 | 23 | type fields struct { 24 | config *config 25 | } 26 | 27 | tests := []struct { 28 | name string 29 | fields fields 30 | command string 31 | hook func() 32 | wantErr bool 33 | }{ 34 | { 35 | name: "idle_timeout", 36 | fields: fields{ 37 | config: &config{ 38 | IdleTimeout: 1, 39 | RemoteAddr: "127.0.0.1:21", 40 | }, 41 | }, 42 | hook: func() { time.Sleep(2 * time.Second) }, 43 | wantErr: true, 44 | }, 45 | { 46 | name: "max_connection", 47 | fields: fields{ 48 | config: &config{ 49 | IdleTimeout: 1, 50 | MaxConnections: 0, 51 | RemoteAddr: "127.0.0.1:21", 52 | }, 53 | }, 54 | wantErr: true, 55 | }, 56 | } 57 | <-serverready 58 | var cn int32 59 | for _, tt := range tests { 60 | t.Run(tt.name, func(t *testing.T) { 61 | c, err := net.Dial("tcp", server.Addr().String()) 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | defer c.Close() 66 | clientHandler := newClientHandler( 67 | <-conn, 68 | tt.fields.config, 69 | nil, 70 | nil, 71 | 1, 72 | &cn, 73 | ) 74 | 75 | if tt.hook != nil { 76 | tt.hook() 77 | } 78 | 79 | err = clientHandler.handleCommands() 80 | if (err != nil) != tt.wantErr { 81 | t.Errorf("clientHandler.handleCommands() error = %v, wantErr %v", err, tt.wantErr) 82 | return 83 | } 84 | }) 85 | } 86 | server.Close() 87 | <-done 88 | 89 | } 90 | 91 | func Test_clientHandler_handleCommand(t *testing.T) { 92 | var server net.Listener 93 | conn := make(chan net.Conn) 94 | done := make(chan struct{}) 95 | serverready := make(chan struct{}) 96 | 97 | go test.LaunchTestServer(&server, conn, done, serverready, t) 98 | 99 | type fields struct { 100 | config *config 101 | } 102 | type args struct { 103 | line string 104 | } 105 | type want struct { 106 | result *result 107 | srcIP string 108 | } 109 | 110 | tests := []struct { 111 | name string 112 | fields fields 113 | args args 114 | want want 115 | }{ 116 | { 117 | name: "user_ok", 118 | fields: fields{ 119 | config: &config{ 120 | IdleTimeout: 3, 121 | RemoteAddr: "127.0.0.1:21", 122 | }, 123 | }, 124 | args: args{ 125 | line: "user pftp", 126 | }, 127 | }, 128 | { 129 | name: "proxy_invalid_proxyheader", 130 | fields: fields{ 131 | config: &config{ 132 | IdleTimeout: 5, 133 | RemoteAddr: "127.0.0.1:21", 134 | }, 135 | }, 136 | args: args{ 137 | line: "PROXY 192.168.10.1 100.100.100.100 53172 21\r\n", 138 | }, 139 | want: want{ 140 | result: &result{ 141 | code: 500, 142 | msg: "Proxy header parse error", 143 | err: errors.New("wrong proxy header parameters"), 144 | }, 145 | }, 146 | }, 147 | { 148 | name: "proxy_invalid_source_ip", 149 | fields: fields{ 150 | config: &config{ 151 | IdleTimeout: 5, 152 | RemoteAddr: "127.0.0.1:21", 153 | }, 154 | }, 155 | args: args{ 156 | line: "PROXY TCP4 192.168.10.256 100.100.100.100 53172 21\r\n", 157 | }, 158 | want: want{ 159 | result: &result{ 160 | code: 500, 161 | msg: "Proxy header parse error", 162 | err: errors.New("wrong source ip address"), 163 | }, 164 | }, 165 | }, 166 | { 167 | name: "proxy_ok", 168 | fields: fields{ 169 | config: &config{ 170 | IdleTimeout: 5, 171 | RemoteAddr: "127.0.0.1:21", 172 | }, 173 | }, 174 | args: args{ 175 | line: "PROXY TCP4 192.168.10.1 100.100.100.100 12345 21\r\n", 176 | }, 177 | want: want{ 178 | srcIP: "192.168.10.1:12345", 179 | }, 180 | }, 181 | } 182 | 183 | <-serverready 184 | 185 | var cn int32 186 | for _, tt := range tests { 187 | t.Run(tt.name, func(t *testing.T) { 188 | c, err := net.Dial("tcp", server.Addr().String()) 189 | if err != nil { 190 | t.Fatal(err) 191 | } 192 | defer c.Close() 193 | 194 | clientHandler := newClientHandler( 195 | <-conn, 196 | tt.fields.config, 197 | nil, 198 | nil, 199 | 1, 200 | &cn, 201 | ) 202 | 203 | got := clientHandler.handleCommand(tt.args.line) 204 | if (got != nil && tt.want.result == nil) || (tt.want.result != nil && (got.code != tt.want.result.code || got.msg != tt.want.result.msg || got.err.Error() != tt.want.result.err.Error())) { 205 | t.Errorf("clientHandler.handleCommand() = %v, want %v", got, tt.want.result) 206 | } else if tt.name == "proxy_ok" && clientHandler.srcIP != tt.want.srcIP { 207 | t.Errorf("clientHandler.sourceIP = %v, want %v", clientHandler.srcIP, tt.want.srcIP) 208 | } 209 | }) 210 | } 211 | 212 | server.Close() 213 | <-done 214 | } 215 | 216 | func Test_clientHandler_TLS_error_type_bug(t *testing.T) { 217 | var server net.Listener 218 | serverready := make(chan struct{}) 219 | conn := make(chan net.Conn) 220 | done := make(chan struct{}) 221 | handlerDone := make(chan error) 222 | 223 | go test.LaunchTestServer(&server, conn, done, serverready, t) 224 | 225 | type fields struct { 226 | config *config 227 | } 228 | 229 | tests := []struct { 230 | name string 231 | fields fields 232 | command string 233 | hook func() 234 | wantErr bool 235 | }{ 236 | { 237 | name: "tls_err_type_check", 238 | fields: fields{ 239 | config: &config{ 240 | IdleTimeout: 1, 241 | MaxConnections: 5, 242 | RemoteAddr: "127.0.0.1:21", 243 | WelcomeMsg: "TLS test server", 244 | TLS: &tlsPair{ 245 | Cert: "../tls/server.crt", 246 | Key: "../tls/server.key", 247 | }, 248 | }, 249 | }, 250 | hook: func() { time.Sleep(2 * time.Second) }, 251 | wantErr: false, 252 | }, 253 | } 254 | <-serverready 255 | 256 | var cn int32 257 | 258 | for _, tt := range tests { 259 | tlsData := buildTLSConfigForOrigin(tt.fields.config) 260 | 261 | t.Run(tt.name, func(t *testing.T) { 262 | serverTLSConfig, err := buildTLSConfigForClient(tt.fields.config.TLS) 263 | if err != nil { 264 | t.Fatal(err) 265 | } 266 | // run client handler 267 | go func() { 268 | clientHandler := newClientHandler( 269 | <-conn, 270 | tt.fields.config, 271 | serverTLSConfig, 272 | nil, 273 | 1, 274 | &cn, 275 | ) 276 | 277 | err := clientHandler.handleCommands() 278 | handlerDone <- err 279 | }() 280 | 281 | // connect to test server 282 | c, err := net.Dial("tcp", server.Addr().String()) 283 | if err != nil { 284 | t.Fatal(err) 285 | } 286 | defer c.Close() 287 | 288 | reader := bufio.NewReader(c) 289 | writer := bufio.NewWriter(c) 290 | 291 | // read welcome message 292 | _, err = reader.ReadString('\n') 293 | if err != nil { 294 | t.Errorf("clientHandler.TLS_error_type_bug() can not read welcome message from origin: %v", err) 295 | } 296 | 297 | // send AUTH command to server 298 | if _, err = writer.WriteString("AUTH TLS\r\n"); err != nil { 299 | t.Errorf("clientHandler.TLS_error_type_bug() can not write string to proxy: %v", err) 300 | } 301 | if err = writer.Flush(); err != nil { 302 | t.Errorf("clientHandler.TLS_error_type_bug() can not write string to proxy: %v", err) 303 | } 304 | _, err = reader.ReadString('\n') 305 | if err != nil { 306 | t.Errorf("clientHandler.TLS_error_type_bug() can not read response from origin: %v", err) 307 | } 308 | 309 | // make tls handshake for full tls connection 310 | tlsConn := tls.Client(c, tlsData.config) 311 | err = tlsConn.Handshake() 312 | if err != nil { 313 | t.Errorf("TLS Handshake Error: %v", err) 314 | } 315 | 316 | // comment out tls wrapping client connection 317 | // reader = bufio.NewReader(tlsConn) 318 | // writer = bufio.NewWriter(tlsConn) 319 | 320 | // send some command using by non wrapped conn 321 | if _, err = writer.WriteString("NOOP\r\n"); err != nil { 322 | t.Errorf("clientHandler.TLS_error_type_bug() can not write string to proxy: %v", err) 323 | } 324 | if err = writer.Flush(); err != nil { 325 | t.Errorf("clientHandler.TLS_error_type_bug() can not write string to proxy: %v", err) 326 | } 327 | 328 | // if err == nil and received message starts with 200(OK), it means communication success without TLS. so failed on test 329 | if res, err := reader.ReadString('\n'); err == nil && strings.HasPrefix(res, "200") { 330 | t.Errorf("clientHandler.TLS_error_type_bug() test failed! successfully read response from origin: %v, want err != nil", err) 331 | } 332 | 333 | // check connection normally finished 334 | serverErr := <-handlerDone 335 | if (serverErr != nil) != tt.wantErr { 336 | t.Errorf("clientHandler.TLS_error_type_bug() error = %v, wantErr %v\n", serverErr, tt.wantErr) 337 | } 338 | }) 339 | } 340 | server.Close() 341 | <-done 342 | } 343 | 344 | func Test_clientHandler_TLS_Session_Resumption(t *testing.T) { 345 | var server net.Listener 346 | serverready := make(chan struct{}) 347 | conn := make(chan net.Conn) 348 | done := make(chan struct{}) 349 | clientHandlerDone := make(chan error) 350 | 351 | go test.LaunchTestServer(&server, conn, done, serverready, t) 352 | 353 | type fields struct { 354 | config *config 355 | } 356 | 357 | tests := []struct { 358 | name string 359 | fields fields 360 | command string 361 | hook func() 362 | wantErr bool 363 | }{ 364 | { 365 | name: "err_type_check", 366 | fields: fields{ 367 | config: &config{ 368 | IdleTimeout: 1, 369 | MaxConnections: 5, 370 | RemoteAddr: "127.0.0.1:21", 371 | WelcomeMsg: "TLS test server", 372 | TLS: &tlsPair{ 373 | Cert: "../tls/server.crt", 374 | Key: "../tls/server.key", 375 | }, 376 | }, 377 | }, 378 | hook: func() { time.Sleep(2 * time.Second) }, 379 | wantErr: false, 380 | }, 381 | } 382 | <-serverready 383 | 384 | var cn int32 385 | 386 | for _, tt := range tests { 387 | tlsData := buildTLSConfigForOrigin(tt.fields.config) 388 | 389 | t.Run(tt.name, func(t *testing.T) { 390 | serverTLSConfig, err := buildTLSConfigForClient(tt.fields.config.TLS) 391 | if err != nil { 392 | t.Fatal(err) 393 | } 394 | // run 1st client handler 395 | go func() { 396 | clientHandler := newClientHandler( 397 | <-conn, 398 | tt.fields.config, 399 | serverTLSConfig, 400 | nil, 401 | 1, 402 | &cn, 403 | ) 404 | 405 | err := clientHandler.handleCommands() 406 | clientHandlerDone <- err 407 | }() 408 | 409 | // connect to test server 410 | c, err := net.Dial("tcp", server.Addr().String()) 411 | if err != nil { 412 | t.Fatal(err) 413 | } 414 | defer c.Close() 415 | 416 | reader := bufio.NewReader(c) 417 | writer := bufio.NewWriter(c) 418 | 419 | // read welcome message 420 | _, err = reader.ReadString('\n') 421 | if err != nil { 422 | t.Errorf("clientHandler.TLS_Session_Resumption() can not read welcome message from origin: %v", err) 423 | } 424 | 425 | // send AUTH command to server 426 | if _, err = writer.WriteString("AUTH TLS\r\n"); err != nil { 427 | t.Errorf("clientHandler.TLS_Session_Resumption() can not write string to proxy: %v", err) 428 | } 429 | if err = writer.Flush(); err != nil { 430 | t.Errorf("clientHandler.TLS_Session_Resumption() can not write string to proxy: %v", err) 431 | } 432 | _, err = reader.ReadString('\n') 433 | if err != nil { 434 | t.Errorf("clientHandler.TLS_Session_Resumption() can not read response from origin: %v", err) 435 | } 436 | 437 | // make tls handshake for full tls connection 438 | tlsConn := tls.Client(c, tlsData.config) 439 | err = tlsConn.Handshake() 440 | if err != nil { 441 | t.Errorf("TLS Handshake Error: %v", err) 442 | } 443 | 444 | // check 1st TLS connection is resumed. if already resumed(true) in first time, fail 445 | state := tlsConn.ConnectionState() 446 | if state.DidResume { 447 | t.Errorf("clientHandler.TLS_Session_Resumption() 1st TLS session resumption failed: got = %v, want = false", state.DidResume) 448 | } 449 | 450 | // close 1st connections 451 | tlsConn.Close() 452 | 453 | // check 1st connection normally finished 454 | serverErr := <-clientHandlerDone 455 | if (serverErr != nil) != tt.wantErr { 456 | t.Errorf("clientHandler.TLS_Session_Resumption() error = %v, wantErr %v\n", serverErr, tt.wantErr) 457 | } 458 | 459 | // 1st TLS connection is done 460 | // run 2nd client handler 461 | go func() { 462 | clientHandler := newClientHandler( 463 | <-conn, 464 | tt.fields.config, 465 | serverTLSConfig, 466 | nil, 467 | 1, 468 | &cn, 469 | ) 470 | 471 | err := clientHandler.handleCommands() 472 | clientHandlerDone <- err 473 | }() 474 | 475 | // connect to test server 476 | c, err = net.Dial("tcp", server.Addr().String()) 477 | if err != nil { 478 | t.Fatal(err) 479 | } 480 | defer c.Close() 481 | 482 | reader = bufio.NewReader(c) 483 | writer = bufio.NewWriter(c) 484 | 485 | // read welcome message 486 | _, err = reader.ReadString('\n') 487 | if err != nil { 488 | t.Errorf("clientHandler.TLS_Session_Resumption() can not read welcome message from origin: %v", err) 489 | } 490 | 491 | // send AUTH command to server 492 | if _, err = writer.WriteString("AUTH TLS\r\n"); err != nil { 493 | t.Errorf("clientHandler.TLS_Session_Resumption() can not write string to proxy: %v", err) 494 | } 495 | if err = writer.Flush(); err != nil { 496 | t.Errorf("clientHandler.TLS_Session_Resumption() can not write string to proxy: %v", err) 497 | } 498 | _, err = reader.ReadString('\n') 499 | if err != nil { 500 | t.Errorf("clientHandler.TLS_Session_Resumption() can not read response from origin: %v", err) 501 | } 502 | 503 | // make tls handshake for full tls connection 504 | tlsConn = tls.Client(c, tlsData.config) 505 | err = tlsConn.Handshake() 506 | if err != nil { 507 | t.Errorf("TLS Handshake Error: %v", err) 508 | } 509 | 510 | // check 2nd TLS connection is resumed. if not resumed(false), fail 511 | state = tlsConn.ConnectionState() 512 | if !state.DidResume { 513 | t.Errorf("clientHandler.TLS_Session_Resumption() 2nd TLS session resumption failed: got = %v, want = true", state.DidResume) 514 | } 515 | 516 | reader = bufio.NewReader(tlsConn) 517 | writer = bufio.NewWriter(tlsConn) 518 | 519 | // test some FTP commands under TLS session 520 | if _, err = writer.WriteString("FEAT\r\n"); err != nil { 521 | t.Errorf("clientHandler.TLS_Session_Resumption() can not write string to proxy: %v", err) 522 | } 523 | if err = writer.Flush(); err != nil { 524 | t.Errorf("clientHandler.TLS_Session_Resumption() can not write string to proxy: %v", err) 525 | } 526 | _, err = reader.ReadString('\n') 527 | if err != nil { 528 | t.Errorf("clientHandler.TLS_Session_Resumption() can not read response from origin: %v", err) 529 | } 530 | 531 | if _, err = writer.WriteString("QUIT\r\n"); err != nil { 532 | t.Errorf("clientHandler.TLS_Session_Resumption() can not write string to proxy: %v", err) 533 | } 534 | if err = writer.Flush(); err != nil { 535 | t.Errorf("clientHandler.TLS_Session_Resumption() can not write string to proxy: %v", err) 536 | } 537 | _, err = reader.ReadString('\n') 538 | if err != nil { 539 | t.Errorf("clientHandler.TLS_Session_Resumption() can not read response from origin: %v", err) 540 | } 541 | 542 | tlsConn.Close() 543 | 544 | // wait until 2nd client Handler end 545 | serverErr = <-clientHandlerDone 546 | 547 | if (serverErr != nil) != tt.wantErr { 548 | t.Errorf("clientHandler.TLS_Session_Resumption() error = %v, wantErr %v\n", serverErr, tt.wantErr) 549 | } 550 | }) 551 | } 552 | server.Close() 553 | <-done 554 | } 555 | -------------------------------------------------------------------------------- /pftp/config.go: -------------------------------------------------------------------------------- 1 | package pftp 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/sirupsen/logrus" 10 | 11 | "github.com/BurntSushi/toml" 12 | ) 13 | 14 | const ( 15 | // PortRangeLength is const parameter for port range configuration check 16 | // port range must set like 100-110 so it might split 2 strings by '-' 17 | PortRangeLength = 2 18 | ) 19 | 20 | type config struct { 21 | ListenAddr string `toml:"listen_addr"` 22 | RemoteAddr string `toml:"remote_addr"` 23 | IdleTimeout int `toml:"idle_timeout"` 24 | ProxyTimeout int `toml:"proxy_timeout"` 25 | TransferTimeout int `toml:"transfer_timeout"` 26 | MaxConnections int32 `toml:"max_connections"` 27 | ProxyProtocol bool `toml:"send_proxy_protocol"` 28 | WelcomeMsg string `toml:"welcome_message"` 29 | KeepaliveTime int `toml:"keepalive_time"` 30 | DataChanProxy bool `toml:"data_channel_proxy"` 31 | DataPortRange string `toml:"data_listen_port_range"` 32 | MasqueradeIP string `toml:"masquerade_ip"` 33 | TransferMode string `toml:"transfer_mode"` 34 | IgnorePassiveIP bool `toml:"ignore_passive_ip"` 35 | TLS *tlsPair `toml:"tls"` 36 | } 37 | 38 | // NewConfig creates a new config instance and applies the provided options. 39 | // Validates the configuration and returns an error if validation fails. 40 | func NewConfig(opts ...ConfigOption) (config, error) { 41 | cfg := config{} 42 | defaultConfig(&cfg) 43 | 44 | for _, o := range opts { 45 | o(&cfg) 46 | } 47 | 48 | if err := validateConfig(&cfg); err != nil { 49 | return config{}, err 50 | } 51 | 52 | return cfg, nil 53 | } 54 | 55 | func validateConfig(c *config) error { 56 | // validate Data listen port randg 57 | if err := dataPortRangeValidation(c.DataPortRange); err != nil { 58 | logrus.Debug(err) 59 | c.DataPortRange = "" 60 | } 61 | 62 | // validate Masquerade IP 63 | if (len(c.MasqueradeIP) > 0) && (net.ParseIP(c.MasqueradeIP)) == nil { 64 | return fmt.Errorf("configuration error: Masquerade IP is wrong") 65 | } 66 | 67 | // validate Transfer mode config 68 | c.TransferMode = strings.ToUpper(c.TransferMode) 69 | switch c.TransferMode { 70 | case "PORT", "ACTIVE": 71 | c.TransferMode = "PORT" 72 | case "PASV", "PASSIVE": 73 | c.TransferMode = "PASV" 74 | case "EPSV": 75 | c.TransferMode = "EPSV" 76 | case "CLIENT": 77 | break 78 | default: 79 | return fmt.Errorf("configuration error: Transfer mode config is wrong") 80 | } 81 | 82 | return nil 83 | } 84 | 85 | type tlsPair struct { 86 | Cert string `toml:"cert"` 87 | Key string `toml:"key"` 88 | CACert string `toml:"ca_cert"` 89 | CipherSuite string `toml:"cipher_suite"` 90 | MinProtocol string `toml:"min_protocol"` 91 | MaxProtocol string `toml:"max_protocol"` 92 | } 93 | 94 | // NewTLSConfig creates a new tlsPair instance and applies the provided options. 95 | // Returns the configured TLS settings. 96 | func NewTLSConfig(opts ...TLSConfigOption) tlsPair { 97 | tls := tlsPair{} 98 | 99 | for _, o := range opts { 100 | o(&tls) 101 | } 102 | 103 | return tls 104 | } 105 | 106 | func loadConfig(path string) (*config, error) { 107 | var c config 108 | defaultConfig(&c) 109 | 110 | _, err := toml.DecodeFile(path, &c) 111 | if err != nil { 112 | return nil, err 113 | } 114 | 115 | if err := validateConfig(&c); err != nil { 116 | return nil, err 117 | } 118 | 119 | return &c, nil 120 | } 121 | 122 | func defaultConfig(config *config) { 123 | config.ListenAddr = "127.0.0.1:2121" 124 | config.IdleTimeout = 900 125 | config.ProxyTimeout = 900 126 | config.TransferTimeout = 900 127 | config.KeepaliveTime = 900 128 | config.ProxyProtocol = false 129 | config.DataChanProxy = false 130 | config.DataPortRange = "" 131 | config.WelcomeMsg = "FTP proxy ready" 132 | config.TransferMode = "CLIENT" 133 | config.IgnorePassiveIP = false 134 | } 135 | 136 | func dataPortRangeValidation(r string) error { 137 | if len(r) == 0 { 138 | return nil 139 | } 140 | 141 | lastErr := fmt.Errorf("data port range config wrong. set default(random port)") 142 | portRange := strings.Split(r, "-") 143 | 144 | if len(portRange) != PortRangeLength { 145 | return lastErr 146 | } 147 | 148 | min, err := strconv.Atoi(strings.TrimSpace(portRange[0])) 149 | if err != nil { 150 | return lastErr 151 | } 152 | max, err := strconv.Atoi(strings.TrimSpace(portRange[1])) 153 | if err != nil { 154 | return lastErr 155 | } 156 | 157 | // check each configs 158 | if min <= 0 || min > 65535 || max <= 0 || max > 65535 || min > max { 159 | return lastErr 160 | } 161 | 162 | return nil 163 | } 164 | 165 | type ConfigOption func(c *config) 166 | 167 | // WithListenAddr sets the listening address for the server. 168 | func WithListenAddr(addr string) ConfigOption { 169 | return func(c *config) { 170 | c.ListenAddr = addr 171 | } 172 | } 173 | 174 | // WithRemoteAddr sets the remote address for the server. 175 | func WithRemoteAddr(addr string) ConfigOption { 176 | return func(c *config) { 177 | c.RemoteAddr = addr 178 | } 179 | } 180 | 181 | // WithIdleTimeout sets the idle timeout for the server in seconds. 182 | func WithIdleTimeout(timeout int) ConfigOption { 183 | return func(c *config) { 184 | c.IdleTimeout = timeout 185 | } 186 | } 187 | 188 | // WithProxyTimeout sets the timeout for the proxy connection in seconds. 189 | func WithProxyTimeout(timeout int) ConfigOption { 190 | return func(c *config) { 191 | c.ProxyTimeout = timeout 192 | } 193 | } 194 | 195 | // WithTransferTimeout sets the timeout for data transfers in seconds. 196 | func WithTransferTimeout(timeout int) ConfigOption { 197 | return func(c *config) { 198 | c.TransferTimeout = timeout 199 | } 200 | } 201 | 202 | // WithMaxConnections sets the maximum number of simultaneous connections. 203 | func WithMaxConnections(maxConn int32) ConfigOption { 204 | return func(c *config) { 205 | c.MaxConnections = maxConn 206 | } 207 | } 208 | 209 | // WithProxyProtocol enables or disables the proxy protocol. 210 | func WithProxyProtocol(proxyProtocol bool) ConfigOption { 211 | return func(c *config) { 212 | c.ProxyProtocol = proxyProtocol 213 | } 214 | } 215 | 216 | // WithWelcomeMessage sets the welcome message for the server. 217 | func WithWelcomeMessage(msg string) ConfigOption { 218 | return func(c *config) { 219 | c.WelcomeMsg = msg 220 | } 221 | } 222 | 223 | // WithKeepaliveTime sets the keep-alive time for the server in seconds. 224 | func WithKeepaliveTime(keepalive int) ConfigOption { 225 | return func(c *config) { 226 | c.KeepaliveTime = keepalive 227 | } 228 | } 229 | 230 | // WithDataChanProxy enables or disables data channel proxying. 231 | func WithDataChanProxy(dataChanProxy bool) ConfigOption { 232 | return func(c *config) { 233 | c.DataChanProxy = dataChanProxy 234 | } 235 | } 236 | 237 | // WithDataPortRange sets the range of data ports available for the server. 238 | func WithDataPortRange(dataPortRange string) ConfigOption { 239 | return func(c *config) { 240 | c.DataPortRange = dataPortRange 241 | } 242 | } 243 | 244 | // WithMasqueradeIP sets the IP address for masquerading connections. 245 | func WithMasqueradeIP(masqueradeIP string) ConfigOption { 246 | return func(c *config) { 247 | c.MasqueradeIP = masqueradeIP 248 | } 249 | } 250 | 251 | // WithTransferMode sets the transfer mode for data transfers. 252 | func WithTransferMode(transferMode string) ConfigOption { 253 | return func(c *config) { 254 | c.TransferMode = transferMode 255 | } 256 | } 257 | 258 | // WithIgnorePassiveIP enables or disables ignoring of the passive IP address. 259 | func WithIgnorePassiveIP(ignorePassiveIP bool) ConfigOption { 260 | return func(c *config) { 261 | c.IgnorePassiveIP = ignorePassiveIP 262 | } 263 | } 264 | 265 | // WithTLSConfig sets the TLS configuration for the server. 266 | func WithTLSConfig(tls *tlsPair) ConfigOption { 267 | return func(c *config) { 268 | c.TLS = tls 269 | } 270 | } 271 | 272 | type TLSConfigOption func(t *tlsPair) 273 | 274 | // WithCertificate sets the certificate file for TLS configuration. 275 | func WithCertificate(cert string) TLSConfigOption { 276 | return func(t *tlsPair) { 277 | t.Cert = cert 278 | } 279 | } 280 | 281 | // WithKey sets the key file for TLS configuration. 282 | func WithKey(key string) TLSConfigOption { 283 | return func(t *tlsPair) { 284 | t.Key = key 285 | } 286 | } 287 | 288 | // WithCACert sets the CA certificate file for TLS configuration. 289 | func WithCACert(ca string) TLSConfigOption { 290 | return func(t *tlsPair) { 291 | t.CACert = ca 292 | } 293 | } 294 | 295 | func WithCipherSuite(cs string) TLSConfigOption { 296 | return func(t *tlsPair) { 297 | t.CipherSuite = cs 298 | } 299 | } 300 | 301 | // WithMinProtocol sets the minimum protocol version for TLS configuration. 302 | func WithMinProtocol(minProtocol string) TLSConfigOption { 303 | return func(t *tlsPair) { 304 | t.MinProtocol = minProtocol 305 | } 306 | } 307 | 308 | // WithMaxProtocol sets the maximum protocol version for TLS configuration. 309 | func WithMaxProtocol(maxProtocol string) TLSConfigOption { 310 | return func(t *tlsPair) { 311 | t.MaxProtocol = maxProtocol 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /pftp/context.go: -------------------------------------------------------------------------------- 1 | package pftp 2 | 3 | import "net" 4 | 5 | // Context struct got remote server address 6 | type Context struct { 7 | RemoteAddr string 8 | ClientAddr string 9 | } 10 | 11 | func newContext(c *config, conn net.Conn) *Context { 12 | return &Context{ 13 | RemoteAddr: c.RemoteAddr, 14 | ClientAddr: conn.RemoteAddr().String(), 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /pftp/data_handler.go: -------------------------------------------------------------------------------- 1 | package pftp 2 | 3 | import ( 4 | "crypto/tls" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "math/rand" 9 | "net" 10 | "strconv" 11 | "strings" 12 | "sync" 13 | "time" 14 | 15 | "github.com/tevino/abool" 16 | "golang.org/x/sync/errgroup" 17 | ) 18 | 19 | const ( 20 | uploadStream = "upload" 21 | downloadStream = "download" 22 | abortStream = "abort" 23 | ) 24 | 25 | type dataHandler struct { 26 | clientConn connector 27 | originConn connector 28 | config *config 29 | log *logger 30 | tlsDataSet *tlsDataSet 31 | needTLSForTransfer *abool.AtomicBool 32 | inDataTransfer *abool.AtomicBool 33 | closed bool 34 | mutex *sync.Mutex 35 | } 36 | 37 | type connector struct { 38 | listener *net.TCPListener 39 | communicationConn net.Conn 40 | dataConn net.Conn 41 | originalRemoteIP string 42 | remoteIP string 43 | remotePort string 44 | localIP string 45 | localPort string 46 | needsListen bool 47 | isClient bool 48 | mode string 49 | } 50 | 51 | // Make listener for data connection 52 | func newDataHandler(config *config, log *logger, clientConn net.Conn, originConn net.Conn, mode string, tlsDataSet *tlsDataSet, transferOverTLS *abool.AtomicBool, inDataTransfer *abool.AtomicBool) (*dataHandler, error) { 53 | var err error 54 | 55 | d := &dataHandler{ 56 | originConn: connector{ 57 | listener: nil, 58 | communicationConn: originConn, 59 | dataConn: nil, 60 | needsListen: false, 61 | isClient: false, 62 | mode: config.TransferMode, 63 | }, 64 | clientConn: connector{ 65 | listener: nil, 66 | communicationConn: clientConn, 67 | dataConn: nil, 68 | needsListen: false, 69 | isClient: true, 70 | mode: mode, 71 | }, 72 | config: config, 73 | log: log, 74 | inDataTransfer: inDataTransfer, 75 | tlsDataSet: tlsDataSet, 76 | needTLSForTransfer: transferOverTLS, 77 | closed: false, 78 | mutex: &sync.Mutex{}, 79 | } 80 | 81 | if d.originConn.communicationConn != nil { 82 | d.originConn.originalRemoteIP, _, _ = net.SplitHostPort(originConn.RemoteAddr().String()) 83 | d.originConn.localIP, d.originConn.localPort, _ = net.SplitHostPort(originConn.LocalAddr().String()) 84 | } 85 | 86 | if d.clientConn.communicationConn != nil { 87 | d.clientConn.originalRemoteIP, _, _ = net.SplitHostPort(clientConn.RemoteAddr().String()) 88 | d.clientConn.localIP, d.clientConn.localPort, _ = net.SplitHostPort(clientConn.LocalAddr().String()) 89 | } 90 | 91 | // When connections are nil, will not set listener 92 | if clientConn == nil || originConn == nil { 93 | return d, nil 94 | } 95 | 96 | // init client connection 97 | if checkNeedListen(d.clientConn.mode, d.originConn.mode, true) { 98 | d.clientConn.listener, err = d.setNewListener() 99 | if err != nil { 100 | connectionCloser(d, d.log) 101 | 102 | return nil, err 103 | } 104 | d.clientConn.needsListen = true 105 | } 106 | 107 | // init origin connection 108 | if checkNeedListen(d.clientConn.mode, d.originConn.mode, false) { 109 | d.originConn.listener, err = d.setNewListener() 110 | if err != nil { 111 | connectionCloser(d, d.log) 112 | 113 | return nil, err 114 | } 115 | d.originConn.needsListen = true 116 | } 117 | 118 | return d, nil 119 | } 120 | 121 | // check needs to open listener 122 | func checkNeedListen(clientMode string, originMode string, isClient bool) bool { 123 | if isClient { 124 | if clientMode == "PASV" || clientMode == "EPSV" { 125 | return true 126 | } 127 | } else { 128 | if originMode == "PORT" || originMode == "EPRT" { 129 | return true 130 | } else if originMode == "CLIENT" { 131 | if clientMode == "PORT" || clientMode == "EPRT" { 132 | return true 133 | } 134 | } 135 | } 136 | 137 | return false 138 | } 139 | 140 | // get listen port 141 | func getListenPort(dataPortRange string) string { 142 | // random port select 143 | if len(dataPortRange) == 0 { 144 | return "" 145 | } 146 | 147 | portRange := strings.Split(dataPortRange, "-") 148 | min, _ := strconv.Atoi(strings.TrimSpace(portRange[0])) 149 | max, _ := strconv.Atoi(strings.TrimSpace(portRange[1])) 150 | 151 | // return min port number 152 | if min == max { 153 | return strconv.Itoa(min) 154 | } 155 | 156 | // random select in min - max range 157 | return strconv.Itoa(min + rand.Intn(max-min)) 158 | } 159 | 160 | // assign listen port create listener 161 | func (d *dataHandler) setNewListener() (*net.TCPListener, error) { 162 | var listener *net.TCPListener 163 | var lAddr *net.TCPAddr 164 | var err error 165 | 166 | // reallocate listener port when selected port is busy until LISTEN_TIMEOUT 167 | counter := 0 168 | for { 169 | counter++ 170 | 171 | lAddr, err = net.ResolveTCPAddr("tcp", ":"+getListenPort(d.config.DataPortRange)) 172 | if err != nil { 173 | d.log.err("cannot resolve TCPAddr") 174 | return nil, err 175 | } 176 | 177 | if listener, err = net.ListenTCP("tcp", lAddr); err != nil { 178 | if counter > connectionTimeout { 179 | d.log.err("cannot set listener") 180 | 181 | return nil, err 182 | } 183 | 184 | d.log.debug("cannot use choosen port. try to select another port after 1 second... (%d/%d)", counter, connectionTimeout) 185 | 186 | time.Sleep(time.Duration(1) * time.Second) 187 | continue 188 | 189 | } else { 190 | d.log.debug("data listen port selected: '%s'", lAddr.String()) 191 | break 192 | } 193 | } 194 | 195 | return listener, nil 196 | } 197 | 198 | // close all connection and listener 199 | func (d *dataHandler) Close() error { 200 | d.mutex.Lock() 201 | defer d.mutex.Unlock() 202 | 203 | // return nil when handler already closed 204 | if d.closed { 205 | return nil 206 | } 207 | 208 | lastErr := error(nil) 209 | 210 | // close net.Conn 211 | if d.clientConn.dataConn != nil { 212 | if err := d.clientConn.dataConn.Close(); err != nil { 213 | if !strings.Contains(err.Error(), alreadyClosedMsg) { 214 | lastErr = fmt.Errorf("client data connection close error: %s", err.Error()) 215 | } 216 | } 217 | d.clientConn.dataConn = nil 218 | } 219 | if d.originConn.dataConn != nil { 220 | if err := d.originConn.dataConn.Close(); err != nil { 221 | if !strings.Contains(err.Error(), alreadyClosedMsg) { 222 | lastErr = fmt.Errorf("origin data connection close error: %s", err.Error()) 223 | } 224 | } 225 | d.originConn.dataConn = nil 226 | } 227 | 228 | // close listener 229 | if d.clientConn.listener != nil { 230 | if err := d.clientConn.listener.Close(); err != nil { 231 | if !strings.Contains(err.Error(), alreadyClosedMsg) { 232 | lastErr = fmt.Errorf("client data listener close error: %s", err.Error()) 233 | } 234 | } 235 | d.clientConn.listener = nil 236 | } 237 | if d.originConn.listener != nil { 238 | if err := d.originConn.listener.Close(); err != nil { 239 | if !strings.Contains(err.Error(), alreadyClosedMsg) { 240 | lastErr = fmt.Errorf("origin data listener close error: %s", err.Error()) 241 | } 242 | } 243 | d.originConn.listener = nil 244 | } 245 | 246 | d.closed = true 247 | d.inDataTransfer.UnSet() 248 | 249 | d.log.debug("proxy data channel disconnected") 250 | 251 | return lastErr 252 | } 253 | 254 | // return current handler closed state 255 | func (d *dataHandler) isClosed() bool { 256 | d.mutex.Lock() 257 | defer d.mutex.Unlock() 258 | 259 | return d.closed 260 | } 261 | 262 | // return true when handler start transfer progress 263 | func (d *dataHandler) isStarted() bool { 264 | d.mutex.Lock() 265 | defer d.mutex.Unlock() 266 | 267 | return d.inDataTransfer.IsSet() 268 | } 269 | 270 | // Make listener for data connection 271 | func (d *dataHandler) StartDataTransfer(direction string) error { 272 | var err error 273 | 274 | defer connectionCloser(d, d.log) 275 | 276 | eg := errgroup.Group{} 277 | 278 | // make data connection (client first) 279 | clientConnected := make(chan error) 280 | eg.Go(func() error { 281 | if err := d.clientListenOrDial(clientConnected); err != nil { 282 | connectionCloser(d, d.log) 283 | return err 284 | } 285 | 286 | return nil 287 | }) 288 | eg.Go(func() error { 289 | if err := d.originListenOrDial(clientConnected); err != nil { 290 | connectionCloser(d, d.log) 291 | return err 292 | } 293 | 294 | return nil 295 | }) 296 | 297 | // wait until copy goroutine end 298 | if err := eg.Wait(); err != nil { 299 | if strings.Contains(err.Error(), "EOF") { 300 | d.log.debug("data connection aborted by EOF") 301 | } else { 302 | d.log.err("data connection creation failed: %s", err.Error()) 303 | } 304 | 305 | return err 306 | } 307 | 308 | d.log.debug("start %s data transfer", direction) 309 | 310 | // do not timeout communication connection during data transfer 311 | d.clientConn.communicationConn.SetDeadline(time.Time{}) 312 | d.originConn.communicationConn.SetDeadline(time.Time{}) 313 | 314 | if err := d.run(); err != nil { 315 | if !strings.Contains(err.Error(), alreadyClosedMsg) { 316 | d.log.err("got error on %s data transfer: %s", direction, err.Error()) 317 | } 318 | } else { 319 | d.log.debug("%s data transfer finished", direction) 320 | } 321 | 322 | // set timeout to each connection 323 | d.clientConn.communicationConn.SetDeadline(time.Now().Add(time.Duration(d.config.IdleTimeout) * time.Second)) 324 | d.originConn.communicationConn.SetDeadline(time.Now().Add(time.Duration(d.config.ProxyTimeout) * time.Second)) 325 | 326 | return err 327 | } 328 | 329 | // make client connection 330 | func (d *dataHandler) clientListenOrDial(clientConnected chan error) error { 331 | // if client connect needs listen, open listener 332 | if d.clientConn.needsListen { 333 | d.mutex.Lock() 334 | if d.closed { 335 | d.mutex.Unlock() 336 | return errors.New("abort: data handler already closed") 337 | } 338 | listener := d.clientConn.listener 339 | d.mutex.Unlock() 340 | 341 | // set listener timeout 342 | listener.SetDeadline(time.Now().Add(time.Duration(connectionTimeout) * time.Second)) 343 | 344 | conn, err := listener.AcceptTCP() 345 | clientConnected <- err 346 | if err != nil { 347 | return err 348 | } 349 | 350 | d.log.debug("client connected from %s", conn.RemoteAddr().String()) 351 | d.log.debug("close listener %s", listener.Addr().String()) 352 | 353 | // release listener for reuse 354 | if err := listener.Close(); err != nil { 355 | if !strings.Contains(err.Error(), alreadyClosedMsg) { 356 | d.log.err("cannot close client data listener: %s", err.Error()) 357 | } 358 | } 359 | 360 | // set linger 0 and tcp keepalive setting between client connection 361 | if d.config.KeepaliveTime > 0 { 362 | conn.SetKeepAlive(true) 363 | conn.SetKeepAlivePeriod(time.Duration(d.config.KeepaliveTime) * time.Second) 364 | conn.SetLinger(0) 365 | } 366 | 367 | d.clientConn.dataConn = conn 368 | } else { 369 | var conn net.Conn 370 | var err error 371 | 372 | // when connect to client(use active mode), dial to client use port 20 only 373 | lAddr, err := net.ResolveTCPAddr("tcp", net.JoinHostPort("", "20")) 374 | if err != nil { 375 | return fmt.Errorf("cannot resolve local address: %s", err.Error()) 376 | } 377 | // set port reuse and local address 378 | netDialer := net.Dialer{ 379 | Control: setReuseIPPort, 380 | LocalAddr: lAddr, 381 | Deadline: time.Now().Add(time.Duration(connectionTimeout) * time.Second), 382 | } 383 | 384 | conn, err = netDialer.Dial("tcp", net.JoinHostPort(d.clientConn.remoteIP, d.clientConn.remotePort)) 385 | clientConnected <- err 386 | if err != nil { 387 | return fmt.Errorf("cannot connect to client data address: %v, %s", conn, err.Error()) 388 | } 389 | 390 | // set linger 0 and tcp keepalive setting between client connection 391 | tcpConn := conn.(*net.TCPConn) 392 | tcpConn.SetKeepAlive(true) 393 | tcpConn.SetKeepAlivePeriod(time.Duration(d.config.KeepaliveTime) * time.Second) 394 | tcpConn.SetLinger(0) 395 | 396 | d.clientConn.dataConn = tcpConn 397 | } 398 | 399 | if d.needTLSForTransfer.IsSet() { 400 | if d.tlsDataSet.forClient.getTLSConfig() == nil { 401 | return errors.New("cannot get client TLS config for data transfer. abort data transfer") 402 | } 403 | 404 | d.mutex.Lock() 405 | if d.closed { 406 | d.mutex.Unlock() 407 | return errors.New("abort: data handler already closed") 408 | } 409 | dataConn := d.clientConn.dataConn 410 | d.mutex.Unlock() 411 | 412 | tlsConn := tls.Server(dataConn, d.tlsDataSet.forClient.getTLSConfig()) 413 | if err := tlsConn.Handshake(); err != nil { 414 | return fmt.Errorf("TLS client data connection handshake got error: %v", err) 415 | } 416 | d.log.debug("TLS data connection with client has set. TLS protocol version: %s and Cipher Suite: %s. (resumed?: %v)", getTLSProtocolName(tlsConn.ConnectionState().Version), tls.CipherSuiteName(tlsConn.ConnectionState().CipherSuite), tlsConn.ConnectionState().DidResume) 417 | 418 | d.clientConn.dataConn = tlsConn 419 | } 420 | 421 | // set transfer timeout to data connection 422 | d.mutex.Lock() 423 | if !d.closed { 424 | d.clientConn.dataConn.SetDeadline(time.Now().Add(time.Duration(d.config.TransferTimeout) * time.Second)) 425 | } 426 | d.mutex.Unlock() 427 | 428 | return nil 429 | } 430 | 431 | // make origin connection 432 | func (d *dataHandler) originListenOrDial(clientConnected chan error) error { 433 | // if client data connection got error, abort origin connection too 434 | if <-clientConnected != nil { 435 | return nil 436 | } 437 | 438 | // if origin connect needs listen, open listener 439 | if d.originConn.needsListen { 440 | d.mutex.Lock() 441 | if d.closed { 442 | d.mutex.Unlock() 443 | return errors.New("abort: data handler already closed") 444 | } 445 | listener := d.originConn.listener 446 | d.mutex.Unlock() 447 | 448 | // set listener timeout 449 | listener.SetDeadline(time.Now().Add(time.Duration(connectionTimeout) * time.Second)) 450 | 451 | conn, err := listener.AcceptTCP() 452 | if err != nil { 453 | return err 454 | } 455 | 456 | d.log.debug("origin connected from %s", conn.RemoteAddr().String()) 457 | d.log.debug("close listener %s", listener.Addr().String()) 458 | 459 | // release listener for reuse 460 | if err := listener.Close(); err != nil { 461 | if !strings.Contains(err.Error(), alreadyClosedMsg) { 462 | d.log.err("cannot close origin data listener: %s", err.Error()) 463 | } 464 | } 465 | 466 | // set linger 0 and tcp keepalive setting between origin connection 467 | if d.config.KeepaliveTime > 0 { 468 | conn.SetKeepAlive(true) 469 | conn.SetKeepAlivePeriod(time.Duration(d.config.KeepaliveTime) * time.Second) 470 | conn.SetLinger(0) 471 | } 472 | 473 | d.originConn.dataConn = conn 474 | 475 | } else { 476 | var conn net.Conn 477 | var err error 478 | 479 | conn, err = net.DialTimeout( 480 | "tcp", 481 | net.JoinHostPort(d.originConn.remoteIP, d.originConn.remotePort), 482 | time.Duration(connectionTimeout)*time.Second, 483 | ) 484 | if err != nil { 485 | return fmt.Errorf("cannot connect to origin data address: %v, %s", conn, err.Error()) 486 | } 487 | 488 | d.log.debug("connected to origin %s", conn.RemoteAddr().String()) 489 | 490 | // set linger 0 and tcp keepalive setting between origin connection 491 | tcpConn := conn.(*net.TCPConn) 492 | tcpConn.SetKeepAlive(true) 493 | tcpConn.SetKeepAlivePeriod(time.Duration(d.config.KeepaliveTime) * time.Second) 494 | tcpConn.SetLinger(0) 495 | 496 | d.originConn.dataConn = tcpConn 497 | } 498 | 499 | // set TLS session. 500 | if d.needTLSForTransfer.IsSet() { 501 | if d.tlsDataSet.forOrigin.getTLSConfig() == nil { 502 | return errors.New("cannot get origin TLS config for data transfer. abort data transfer") 503 | } 504 | 505 | d.mutex.Lock() 506 | if d.closed { 507 | d.mutex.Unlock() 508 | return errors.New("abort: data handler already closed") 509 | } 510 | dataConn := d.originConn.dataConn 511 | d.mutex.Unlock() 512 | 513 | tlsConn := tls.Client(dataConn, d.tlsDataSet.forOrigin.getTLSConfig()) 514 | if err := tlsConn.Handshake(); err != nil { 515 | return fmt.Errorf("TLS origin data connection handshake got error: %v", err) 516 | } 517 | d.log.debug("TLS data connection with origin has set. TLS protocol version: %s and Cipher Suite: %s. (resumed?: %v)", getTLSProtocolName(tlsConn.ConnectionState().Version), tls.CipherSuiteName(tlsConn.ConnectionState().CipherSuite), tlsConn.ConnectionState().DidResume) 518 | 519 | d.originConn.dataConn = tlsConn 520 | } 521 | 522 | // set transfer timeout to data connection 523 | d.mutex.Lock() 524 | if !d.closed { 525 | d.originConn.dataConn.SetDeadline(time.Now().Add(time.Duration(d.config.TransferTimeout) * time.Second)) 526 | } 527 | d.mutex.Unlock() 528 | 529 | return nil 530 | } 531 | 532 | // make full duplex connection between client and origin sockets 533 | func (d *dataHandler) run() error { 534 | eg := errgroup.Group{} 535 | 536 | // origin to client 537 | eg.Go(func() error { 538 | return d.copyPackets(d.clientConn.dataConn, d.originConn.dataConn, d.config.TransferTimeout) 539 | }) 540 | // client to origin 541 | eg.Go(func() error { 542 | return d.copyPackets(d.originConn.dataConn, d.clientConn.dataConn, d.config.TransferTimeout) 543 | }) 544 | 545 | // wait until copy goroutine end 546 | err := eg.Wait() 547 | 548 | return err 549 | } 550 | 551 | // send src packet to dst. 552 | // replace io.Copy function to manual coding because io.Copy 553 | // function can not increase src conn's deadline per each read. 554 | func (d *dataHandler) copyPackets(dst net.Conn, src net.Conn, timeout int) error { 555 | lastErr := error(nil) 556 | buff := make([]byte, bufferSize) 557 | 558 | for { 559 | // check about aborted from outside of handler 560 | if d.isClosed() { 561 | break 562 | } 563 | 564 | n, err := src.Read(buff) 565 | if n > 0 { 566 | // stop coping when failed to write dst socket 567 | if _, err := dst.Write(buff[:n]); err != nil { 568 | dst.Close() 569 | break 570 | } 571 | // increase data transfer timeout 572 | src.SetDeadline(time.Now().Add(time.Duration(timeout) * time.Second)) 573 | } 574 | if err != nil { 575 | if err == io.EOF { 576 | // got EOF from src, send EOF to dst 577 | lastErr = sendEOF(dst) 578 | } else { 579 | lastErr = err 580 | } 581 | 582 | break 583 | } 584 | } 585 | 586 | return lastErr 587 | } 588 | 589 | // parse port comand line (active data conn) 590 | func (d *dataHandler) parsePORTcommand(line string) error { 591 | // PORT command format : "PORT h1,h2,h3,h4,p1,p2\r\n" 592 | var err error 593 | 594 | d.clientConn.remoteIP, d.clientConn.remotePort, err = parseLineToAddr(strings.Split(strings.Trim(line, "\r\n"), " ")[1]) 595 | 596 | // if received ip is not public IP, ignore it 597 | if !isPublicIP(net.ParseIP(d.clientConn.remoteIP)) { 598 | d.clientConn.remoteIP = d.clientConn.originalRemoteIP 599 | } 600 | 601 | return err 602 | } 603 | 604 | // parse eprt comand line (active data conn) 605 | func (d *dataHandler) parseEPRTcommand(line string) error { 606 | // EPRT command format 607 | // - IPv4 : "EPRT |1|h1.h2.h3.h4|port|\r\n" 608 | // - IPv6 : "EPRT |2|h1::h2:h3:h4:h5|port|\r\n" 609 | var err error 610 | 611 | d.clientConn.remoteIP, d.clientConn.remotePort, err = parseEPRTtoAddr(strings.Split(strings.Trim(line, "\r\n"), " ")[1]) 612 | 613 | // if received ip is not public IP, ignore it 614 | if !isPublicIP(net.ParseIP(d.clientConn.remoteIP)) { 615 | d.clientConn.remoteIP = d.clientConn.originalRemoteIP 616 | } 617 | 618 | return err 619 | } 620 | 621 | // parse pasv comand line (passive data conn) 622 | func (d *dataHandler) parsePASVresponse(line string) error { 623 | // PASV response format : "227 Entering Passive Mode (h1,h2,h3,h4,p1,p2).\r\n" 624 | var err error 625 | 626 | startIndex := strings.Index(line, "(") 627 | endIndex := strings.LastIndex(line, ")") 628 | 629 | if startIndex == -1 || endIndex == -1 { 630 | return errors.New("invalid data address") 631 | } 632 | 633 | d.originConn.remoteIP, d.originConn.remotePort, err = parseLineToAddr(line[startIndex+1 : endIndex]) 634 | 635 | // if received ip is not public IP, ignore it 636 | if !isPublicIP(net.ParseIP(d.originConn.remoteIP)) || d.config.IgnorePassiveIP { 637 | d.originConn.remoteIP = d.originConn.originalRemoteIP 638 | } 639 | 640 | return err 641 | } 642 | 643 | // parse epsv comand line (passive data conn) 644 | func (d *dataHandler) parseEPSVresponse(line string) error { 645 | // EPSV response format : "229 Entering Extended Passive Mode (|||port|)\r\n" 646 | startIndex := strings.Index(line, "(") 647 | endIndex := strings.LastIndex(line, ")") 648 | 649 | if startIndex == -1 || endIndex == -1 { 650 | return errors.New("invalid data address") 651 | } 652 | 653 | // get port and verify it 654 | originPort := strings.Trim(line[startIndex+1:endIndex], "|") 655 | port, _ := strconv.Atoi(originPort) 656 | if port <= 0 || port > 65535 { 657 | return fmt.Errorf("invalid data address") 658 | } 659 | 660 | d.originConn.remotePort = originPort 661 | 662 | return nil 663 | } 664 | 665 | // parse IP and Port from line 666 | func parseLineToAddr(line string) (string, string, error) { 667 | addr := strings.Split(line, ",") 668 | 669 | if len(addr) != 6 { 670 | return "", "", fmt.Errorf("invalid data address") 671 | } 672 | 673 | // Get IP string from line 674 | ip := strings.Join(addr[0:4], ".") 675 | 676 | // get port number from line 677 | port1, _ := strconv.Atoi(addr[4]) 678 | port2, _ := strconv.Atoi(addr[5]) 679 | 680 | port := (port1 << 8) + port2 681 | 682 | // check IP and Port is valid 683 | if net.ParseIP(ip) == nil { 684 | return "", "", fmt.Errorf("invalid data address") 685 | } 686 | 687 | if port <= 0 || port > 65535 { 688 | return "", "", fmt.Errorf("invalid data address") 689 | } 690 | 691 | return ip, strconv.Itoa(port), nil 692 | } 693 | 694 | // parse EPRT command from client 695 | func parseEPRTtoAddr(line string) (string, string, error) { 696 | addr := strings.Split(line, "|") 697 | 698 | if len(addr) != 5 { 699 | return "", "", fmt.Errorf("invalid data address") 700 | } 701 | 702 | netProtocol := addr[1] 703 | IP := addr[2] 704 | 705 | // check port is valid 706 | port := addr[3] 707 | if integerPort, err := strconv.Atoi(port); err != nil { 708 | return "", "", fmt.Errorf("invalid data address") 709 | } else if integerPort <= 0 || integerPort > 65535 { 710 | return "", "", fmt.Errorf("invalid data address") 711 | } 712 | 713 | switch netProtocol { 714 | case "1", "2": 715 | // use protocol 1 means IPv4. 2 means IPv6 716 | // net.ParseIP for validate IP 717 | if net.ParseIP(IP) == nil { 718 | return "", "", fmt.Errorf("invalid data address") 719 | } 720 | default: 721 | // wrong network protocol 722 | return "", "", fmt.Errorf("unknown network protocol") 723 | } 724 | 725 | return IP, port, nil 726 | } 727 | 728 | // check IP is public 729 | // ** private IP range ** 730 | // Class Starting IPAddress Ending IP Address # Host counts 731 | // A 10.0.0.0 10.255.255.255 16,777,216 732 | // B 172.16.0.0 172.31.255.255 1,048,576 733 | // C 192.168.0.0 192.168.255.255 65,536 734 | // Link-local 169.254.0.0 169.254.255.255 65,536 735 | // Local 127.0.0.0 127.255.255.255 16777216 736 | func isPublicIP(IP net.IP) bool { 737 | if IP.IsLoopback() || IP.IsLinkLocalMulticast() || IP.IsLinkLocalUnicast() { 738 | return false 739 | } 740 | if ip4 := IP.To4(); ip4 != nil { 741 | switch { 742 | case ip4[0] == 10: 743 | return false 744 | case ip4[0] == 172 && ip4[1] >= 16 && ip4[1] <= 31: 745 | return false 746 | case ip4[0] == 192 && ip4[1] == 168: 747 | return false 748 | default: 749 | return true 750 | } 751 | } 752 | return false 753 | } 754 | -------------------------------------------------------------------------------- /pftp/data_handler_test.go: -------------------------------------------------------------------------------- 1 | package pftp 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/tevino/abool" 8 | ) 9 | 10 | func Test_dataHandler_parsePORTcommand(t *testing.T) { 11 | type fields struct { 12 | line string 13 | mode string 14 | config *config 15 | } 16 | 17 | type want struct { 18 | ip string 19 | port string 20 | err string 21 | } 22 | 23 | tests := []struct { 24 | name string 25 | fields fields 26 | want want 27 | wantErr bool 28 | }{ 29 | { 30 | name: "active_mode_invalid_ip", 31 | fields: fields{ 32 | line: "PORT 256,777,0,10,235,64\r\n", 33 | mode: "PORT", 34 | config: &config{}, 35 | }, 36 | want: want{ 37 | ip: "", 38 | port: "", 39 | err: "invalid data address", 40 | }, 41 | wantErr: true, 42 | }, 43 | { 44 | name: "active_mode_invalid_port", 45 | fields: fields{ 46 | line: "PORT 10,10,10,10,530,64\r\n", 47 | mode: "PORT", 48 | config: &config{}, 49 | }, 50 | want: want{ 51 | ip: "", 52 | port: "", 53 | err: "invalid data address", 54 | }, 55 | wantErr: true, 56 | }, 57 | { 58 | name: "active_mode_wrong_line", 59 | fields: fields{ 60 | line: "PORT (10,10,10,10,100,10(\r\n", 61 | mode: "PORT", 62 | config: &config{}, 63 | }, 64 | want: want{ 65 | ip: "", 66 | port: "", 67 | err: "invalid data address", 68 | }, 69 | wantErr: true, 70 | }, 71 | { 72 | name: "active_mode_parse_ok", 73 | fields: fields{ 74 | line: "PORT 1,1,1,1,100,10\r\n", 75 | mode: "PORT", 76 | config: &config{}, 77 | }, 78 | want: want{ 79 | ip: "1.1.1.1", 80 | port: "25610", 81 | err: "", 82 | }, 83 | wantErr: false, 84 | }, 85 | } 86 | 87 | transferInTLS := abool.New() 88 | inDataTransfer := abool.New() 89 | 90 | for _, tt := range tests { 91 | t.Run(tt.name, func(t *testing.T) { 92 | got := want{} 93 | d, _ := newDataHandler( 94 | tt.fields.config, 95 | nil, 96 | nil, 97 | nil, 98 | tt.fields.mode, 99 | nil, 100 | transferInTLS, 101 | inDataTransfer, 102 | ) 103 | err := d.parsePORTcommand(tt.fields.line) 104 | if (err != nil) != tt.wantErr { 105 | t.Errorf("dataHandler.parsePORTcommand() error = %v, wantErr %v", got.err, tt.wantErr) 106 | return 107 | } 108 | if err == nil { 109 | got.err = "" 110 | } else { 111 | got.err = err.Error() 112 | } 113 | 114 | got.ip = d.clientConn.remoteIP 115 | got.port = d.clientConn.remotePort 116 | if !reflect.DeepEqual(got, tt.want) { 117 | t.Errorf("dataHandler.parsePORTresponse() = %v, want %v", got, tt.want) 118 | } 119 | }) 120 | } 121 | } 122 | 123 | func Test_dataHandler_parseEPRTcommand(t *testing.T) { 124 | type fields struct { 125 | line string 126 | mode string 127 | config *config 128 | } 129 | 130 | type want struct { 131 | ip string 132 | port string 133 | err string 134 | } 135 | 136 | tests := []struct { 137 | name string 138 | fields fields 139 | want want 140 | wantErr bool 141 | }{ 142 | { 143 | name: "eprt_mode_invalid_ip", 144 | fields: fields{ 145 | line: "EPRT |1|256.777.0.10|25610|\r\n", 146 | mode: "EPRT", 147 | config: &config{}, 148 | }, 149 | want: want{ 150 | ip: "", 151 | port: "", 152 | err: "invalid data address", 153 | }, 154 | wantErr: true, 155 | }, 156 | { 157 | name: "eprt_mode_invalid_port", 158 | fields: fields{ 159 | line: "EPRT |1|10.10.10.10|73000|\r\n", 160 | mode: "EPRT", 161 | config: &config{}, 162 | }, 163 | want: want{ 164 | ip: "", 165 | port: "", 166 | err: "invalid data address", 167 | }, 168 | wantErr: true, 169 | }, 170 | { 171 | name: "eprt_mode_invalid_protocol", 172 | fields: fields{ 173 | line: "EPRT |3|10.10.10.10|25610|\r\n", 174 | mode: "EPRT", 175 | config: &config{}, 176 | }, 177 | want: want{ 178 | ip: "", 179 | port: "", 180 | err: "unknown network protocol", 181 | }, 182 | wantErr: true, 183 | }, 184 | { 185 | name: "eprt_mode_wrong_line", 186 | fields: fields{ 187 | line: "EPRT |1|10.10.10.10|25610||\r\n", 188 | mode: "EPRT", 189 | config: &config{}, 190 | }, 191 | want: want{ 192 | ip: "", 193 | port: "", 194 | err: "invalid data address", 195 | }, 196 | wantErr: true, 197 | }, 198 | { 199 | name: "eprt_mode_parse_ok", 200 | fields: fields{ 201 | line: "EPRT |1|1.1.1.1|25610|\r\n", 202 | mode: "EPRT", 203 | config: &config{}, 204 | }, 205 | want: want{ 206 | ip: "1.1.1.1", 207 | port: "25610", 208 | err: "", 209 | }, 210 | wantErr: false, 211 | }, 212 | } 213 | 214 | transferInTLS := abool.New() 215 | inDataTransfer := abool.New() 216 | 217 | for _, tt := range tests { 218 | t.Run(tt.name, func(t *testing.T) { 219 | got := want{} 220 | d, _ := newDataHandler( 221 | tt.fields.config, 222 | nil, 223 | nil, 224 | nil, 225 | tt.fields.mode, 226 | nil, 227 | transferInTLS, 228 | inDataTransfer, 229 | ) 230 | err := d.parseEPRTcommand(tt.fields.line) 231 | if (err != nil) != tt.wantErr { 232 | t.Errorf("dataHandler.parseEPRTcommand() error = %v, wantErr %v", got.err, tt.wantErr) 233 | return 234 | } 235 | if err == nil { 236 | got.err = "" 237 | } else { 238 | got.err = err.Error() 239 | } 240 | 241 | got.ip = d.clientConn.remoteIP 242 | got.port = d.clientConn.remotePort 243 | if !reflect.DeepEqual(got, tt.want) { 244 | t.Errorf("dataHandler.parseEPRTresponse() = %v, want %v", got, tt.want) 245 | } 246 | }) 247 | } 248 | } 249 | 250 | func Test_dataHandler_parsePASVresponse(t *testing.T) { 251 | type fields struct { 252 | line string 253 | mode string 254 | config *config 255 | } 256 | 257 | type want struct { 258 | ip string 259 | port string 260 | err string 261 | } 262 | 263 | tests := []struct { 264 | name string 265 | fields fields 266 | want want 267 | wantErr bool 268 | }{ 269 | { 270 | name: "passive_mode_invalid_ip", 271 | fields: fields{ 272 | line: "227 Entering Passive Mode (256,777,0,10,235,64).\r\n", 273 | mode: "PASV", 274 | config: &config{}, 275 | }, 276 | want: want{ 277 | ip: "", 278 | port: "", 279 | err: "invalid data address", 280 | }, 281 | wantErr: true, 282 | }, 283 | { 284 | name: "passive_mode_invalid_port", 285 | fields: fields{ 286 | line: "227 Entering Passive Mode (10,10,10,10,530,64).\r\n", 287 | mode: "PASV", 288 | config: &config{}, 289 | }, 290 | want: want{ 291 | ip: "", 292 | port: "", 293 | err: "invalid data address", 294 | }, 295 | wantErr: true, 296 | }, 297 | { 298 | name: "passive_mode_wrong_line", 299 | fields: fields{ 300 | line: "227 Entering Passive Mode 10,10,10,10,100,10\r\n", 301 | mode: "PASV", 302 | config: &config{}, 303 | }, 304 | want: want{ 305 | ip: "", 306 | port: "", 307 | err: "invalid data address", 308 | }, 309 | wantErr: true, 310 | }, 311 | { 312 | name: "passive_mode_parse_ok_public", 313 | fields: fields{ 314 | line: "227 Entering Passive Mode (20,30,40,50,100,10).\r\n", 315 | mode: "PASV", 316 | config: &config{}, 317 | }, 318 | want: want{ 319 | ip: "20.30.40.50", 320 | port: "25610", 321 | err: "", 322 | }, 323 | wantErr: false, 324 | }, 325 | { 326 | name: "passive_mode_parse_ok_private", 327 | fields: fields{ 328 | line: "227 Entering Passive Mode (10,30,40,50,100,10).\r\n", 329 | mode: "PASV", 330 | config: &config{}, 331 | }, 332 | want: want{ 333 | ip: "", 334 | port: "25610", 335 | err: "", 336 | }, 337 | wantErr: false, 338 | }, 339 | { 340 | name: "passive_mode_parse_ignore_public_passive_ip", 341 | fields: fields{ 342 | line: "227 Entering Passive Mode (20,30,40,50,100,10).\r\n", 343 | mode: "PASV", 344 | config: &config{ 345 | IgnorePassiveIP: true, 346 | }, 347 | }, 348 | want: want{ 349 | ip: "", 350 | port: "25610", 351 | err: "", 352 | }, 353 | wantErr: false, 354 | }, 355 | } 356 | 357 | transferInTLS := abool.New() 358 | inDataTransfer := abool.New() 359 | 360 | for _, tt := range tests { 361 | t.Run(tt.name, func(t *testing.T) { 362 | got := want{} 363 | d, _ := newDataHandler( 364 | tt.fields.config, 365 | nil, 366 | nil, 367 | nil, 368 | tt.fields.mode, 369 | nil, 370 | transferInTLS, 371 | inDataTransfer, 372 | ) 373 | err := d.parsePASVresponse(tt.fields.line) 374 | if (err != nil) != tt.wantErr { 375 | t.Errorf("dataHandler.parsePASVresponse() error = %v, wantErr %v", got.err, tt.wantErr) 376 | return 377 | } 378 | if err == nil { 379 | got.err = "" 380 | } else { 381 | got.err = err.Error() 382 | } 383 | 384 | got.ip = d.originConn.remoteIP 385 | got.port = d.originConn.remotePort 386 | if !reflect.DeepEqual(got, tt.want) { 387 | t.Errorf("dataHandler.parsePASVresponse() = %v, want %v", got, tt.want) 388 | } 389 | }) 390 | } 391 | } 392 | 393 | func Test_dataHandler_parseEPSV(t *testing.T) { 394 | type fields struct { 395 | line string 396 | mode string 397 | config *config 398 | } 399 | 400 | type want struct { 401 | ip string 402 | port string 403 | err string 404 | } 405 | 406 | tests := []struct { 407 | name string 408 | fields fields 409 | want want 410 | wantErr bool 411 | }{ 412 | { 413 | name: "epsv_mode_invalid_port", 414 | fields: fields{ 415 | line: "229 Entering Extended Passive Mode (|||70000|)\r\n", 416 | mode: "EPSV", 417 | config: &config{}, 418 | }, 419 | want: want{ 420 | ip: "", 421 | port: "", 422 | err: "invalid data address", 423 | }, 424 | wantErr: true, 425 | }, 426 | { 427 | name: "epsv_mode_wrong_line", 428 | fields: fields{ 429 | line: "229 Entering Extended Passive Mode (|||70000|\r\n", 430 | mode: "EPSV", 431 | config: &config{}, 432 | }, 433 | want: want{ 434 | ip: "", 435 | port: "", 436 | err: "invalid data address", 437 | }, 438 | wantErr: true, 439 | }, 440 | { 441 | name: "epsve_mode_parse_ok", 442 | fields: fields{ 443 | line: "229 Entering Extended Passive Mode (|||25610|)\r\n", 444 | mode: "EPSV", 445 | config: &config{}, 446 | }, 447 | want: want{ 448 | ip: "", 449 | port: "25610", 450 | err: "", 451 | }, 452 | wantErr: false, 453 | }, 454 | } 455 | 456 | transferInTLS := abool.New() 457 | inDataTransfer := abool.New() 458 | 459 | for _, tt := range tests { 460 | t.Run(tt.name, func(t *testing.T) { 461 | got := want{} 462 | d, _ := newDataHandler( 463 | tt.fields.config, 464 | nil, 465 | nil, 466 | nil, 467 | tt.fields.mode, 468 | nil, 469 | transferInTLS, 470 | inDataTransfer, 471 | ) 472 | err := d.parseEPSVresponse(tt.fields.line) 473 | if (err != nil) != tt.wantErr { 474 | t.Errorf("dataHandler.parseEPSVresponse() error = %v, wantErr %v", got.err, tt.wantErr) 475 | return 476 | } 477 | if err == nil { 478 | got.err = "" 479 | } else { 480 | got.err = err.Error() 481 | } 482 | 483 | got.ip = d.originConn.remoteIP 484 | got.port = d.originConn.remotePort 485 | if !reflect.DeepEqual(got, tt.want) { 486 | t.Errorf("dataHandler.parseEPSVresponse() = %v, want %v", got, tt.want) 487 | } 488 | }) 489 | } 490 | } 491 | -------------------------------------------------------------------------------- /pftp/handle_commands.go: -------------------------------------------------------------------------------- 1 | package pftp 2 | 3 | import ( 4 | "bufio" 5 | "crypto/tls" 6 | "errors" 7 | "fmt" 8 | "net" 9 | "strconv" 10 | "strings" 11 | ) 12 | 13 | func (c *clientHandler) handleUSER() *result { 14 | // make fail when try to login after logged in 15 | if c.proxy != nil { 16 | if c.proxy.isLoggedIn() { 17 | return &result{ 18 | code: 500, 19 | msg: "Already logged in", 20 | err: fmt.Errorf("already logged in"), 21 | log: c.log, 22 | } 23 | } 24 | } 25 | 26 | c.log.user = c.param 27 | 28 | if err := c.connectProxy(); err != nil { 29 | // user not found 30 | if err.Error() == "user id not found" { 31 | return &result{ 32 | code: 530, 33 | msg: err.Error(), 34 | err: err, 35 | log: c.log, 36 | } 37 | } 38 | 39 | return &result{ 40 | code: 530, 41 | msg: "I can't deal with you (proxy error for user)", 42 | err: err, 43 | log: c.log, 44 | } 45 | } 46 | 47 | // unsuspend proxy before send command to origin 48 | c.proxy.unsuspend() 49 | 50 | if err := c.proxy.sendToOrigin(c.line); err != nil { 51 | return &result{ 52 | code: 530, 53 | msg: "I can't deal with you (proxy error) for user", 54 | err: err, 55 | log: c.log, 56 | } 57 | } 58 | 59 | return nil 60 | } 61 | 62 | func (c *clientHandler) handleAUTH() *result { 63 | if c.tlsDatas.forClient.getTLSConfig() != nil { 64 | r := &result{ 65 | code: 234, 66 | msg: fmt.Sprintf("AUTH command ok. Expecting %s Negotiation.", c.param), 67 | } 68 | 69 | if err := r.Response(c); err != nil { 70 | return &result{ 71 | code: 550, 72 | msg: "Client Response Error", 73 | err: err, 74 | log: c.log, 75 | } 76 | } 77 | 78 | tlsConn := tls.Server(c.conn, c.tlsDatas.forClient.getTLSConfig()) 79 | err := tlsConn.Handshake() 80 | if err != nil { 81 | return &result{ 82 | code: 550, 83 | msg: "TLS Handshake Error", 84 | err: err, 85 | log: c.log, 86 | } 87 | } 88 | 89 | c.log.debug("TLS control connection finished with client. TLS protocol version: %s and Cipher Suite: %s", getTLSProtocolName(tlsConn.ConnectionState().Version), tls.CipherSuiteName(tlsConn.ConnectionState().CipherSuite)) 90 | 91 | c.conn = tlsConn 92 | c.reader = bufio.NewReader(c.conn) 93 | c.writer = bufio.NewWriter(c.conn) 94 | 95 | // if proxy server attached, change proxy handler's client reader & writer to TLS conn 96 | if c.proxy != nil { 97 | c.proxy.clientReader = c.reader 98 | c.proxy.clientWriter = c.writer 99 | } 100 | 101 | c.previousTLSCommands = append(c.previousTLSCommands, c.line) 102 | 103 | c.controlInTLS.Set() 104 | 105 | c.tlsDatas.serverName = tlsConn.ConnectionState().ServerName 106 | c.tlsDatas.version = tlsConn.ConnectionState().Version 107 | c.tlsDatas.cipherSuite = tlsConn.ConnectionState().CipherSuite 108 | 109 | // set specific client TLS informations to origin TLS config 110 | c.tlsDatas.forOrigin.setServerName(c.tlsDatas.serverName) 111 | c.tlsDatas.forOrigin.setSpecificTLSVersion(c.tlsDatas.version) 112 | c.tlsDatas.forOrigin.setCipherSUite(c.tlsDatas.cipherSuite) 113 | 114 | return nil 115 | } 116 | return &result{ 117 | code: 550, 118 | msg: "Cannot get a TLS config", 119 | } 120 | } 121 | 122 | // response PBSZ to client and store command line when connect by TLS & not loggined 123 | func (c *clientHandler) handlePBSZ() *result { 124 | if c.controlInTLS.IsSet() { 125 | if !c.proxy.isLoggedIn() { 126 | r := &result{ 127 | code: 200, 128 | msg: fmt.Sprintf("PBSZ %s successful", c.param), 129 | } 130 | 131 | if err := r.Response(c); err != nil { 132 | return &result{ 133 | code: 550, 134 | msg: "Client Response Error", 135 | err: err, 136 | log: c.log, 137 | } 138 | } 139 | 140 | c.previousTLSCommands = append(c.previousTLSCommands, c.line) 141 | } else { 142 | // unsuspend proxy before send command to origin 143 | c.proxy.unsuspend() 144 | 145 | if err := c.proxy.sendToOrigin(c.line); err != nil { 146 | return &result{ 147 | code: 530, 148 | msg: "I can't deal with you (proxy error for pbsz)", 149 | err: err, 150 | log: c.log, 151 | } 152 | } 153 | } 154 | 155 | return nil 156 | } 157 | return &result{ 158 | code: 503, 159 | msg: "Not using TLS connection", 160 | } 161 | } 162 | 163 | // response PROT to client and store command line when connect by TLS & not loggined 164 | func (c *clientHandler) handlePROT() *result { 165 | if c.controlInTLS.IsSet() { 166 | if !c.proxy.isLoggedIn() { 167 | var r *result 168 | if c.param == "C" { 169 | r = &result{ 170 | code: 200, 171 | msg: "Protection Set to Clear", 172 | } 173 | } else if c.param == "P" { 174 | r = &result{ 175 | code: 200, 176 | msg: "Protection Set to Private.", 177 | } 178 | } else { 179 | r = &result{ 180 | code: 534, 181 | msg: "Only C or P Level supported.", 182 | } 183 | } 184 | 185 | if err := r.Response(c); err != nil { 186 | return &result{ 187 | code: 550, 188 | msg: "Client Response Error", 189 | err: err, 190 | log: c.log, 191 | } 192 | } 193 | 194 | c.previousTLSCommands = append(c.previousTLSCommands, c.line) 195 | 196 | } else { 197 | // unsuspend proxy before send command to origin 198 | c.proxy.unsuspend() 199 | 200 | if err := c.proxy.sendToOrigin(c.line); err != nil { 201 | return &result{ 202 | code: 530, 203 | msg: "I can't deal with you (proxy error for prot)", 204 | err: err, 205 | log: c.log, 206 | } 207 | } 208 | } 209 | 210 | if c.param == "P" { 211 | c.transferInTLS.Set() 212 | } else { 213 | c.transferInTLS.UnSet() 214 | } 215 | 216 | return nil 217 | } 218 | return &result{ 219 | code: 503, 220 | msg: "Not using TLS connection", 221 | } 222 | } 223 | 224 | func (c *clientHandler) handleTransfer() *result { 225 | if !c.proxy.isLoggedIn() { 226 | return &result{ 227 | code: 530, 228 | msg: "Please login with USER and PASS", 229 | } 230 | } 231 | 232 | if !c.proxy.isDataHandlerAvailable() { 233 | return &result{ 234 | code: 425, 235 | msg: "Can't open data connection", 236 | } 237 | } 238 | 239 | if c.proxy.isDataTransferStarted() { 240 | return &result{ 241 | code: 450, 242 | msg: fmt.Sprintf("%s: data transfer in progress", c.command), 243 | } 244 | } 245 | 246 | // start data transfer by direction 247 | switch c.command { 248 | case "RETR", "LIST", "MLSD", "NLST": 249 | // set transfer direction to download 250 | go c.proxy.dataConnector.StartDataTransfer(downloadStream) 251 | case "STOR", "STOU", "APPE": 252 | // set transfer direction to upload 253 | go c.proxy.dataConnector.StartDataTransfer(uploadStream) 254 | } 255 | 256 | if err := c.proxy.sendToOrigin(c.line); err != nil { 257 | return &result{ 258 | code: 500, 259 | msg: fmt.Sprintf("Internal error: %s", err), 260 | } 261 | } 262 | return nil 263 | } 264 | 265 | func (c *clientHandler) handlePROXY() *result { 266 | params := strings.SplitN(strings.Trim(c.line, "\r\n"), " ", 6) 267 | if len(params) != 6 { 268 | return &result{ 269 | code: 500, 270 | msg: "Proxy header parse error", 271 | err: errors.New("wrong proxy header parameters"), 272 | } 273 | } 274 | 275 | if net.ParseIP(params[2]) == nil || net.ParseIP(params[3]) == nil { 276 | return &result{ 277 | code: 500, 278 | msg: "Proxy header parse error", 279 | err: errors.New("wrong source ip address"), 280 | } 281 | } 282 | 283 | c.srcIP = params[2] + ":" + params[4] 284 | 285 | return nil 286 | } 287 | 288 | // handle PORT, EPRT, PASV, EPSV commands when set data channel proxy is true 289 | func (c *clientHandler) handleDATA() *result { 290 | if !c.proxy.isLoggedIn() { 291 | return &result{ 292 | code: 530, 293 | msg: "Please login with USER and PASS", 294 | } 295 | } 296 | 297 | // if data channel proxy used 298 | if c.config.DataChanProxy { 299 | var toOriginMsg string 300 | 301 | // only one data connection available in same time. 302 | // Return 450 response code to client without create 303 | // & attach new data handler when data transfer in progress. 304 | if c.proxy.isDataTransferStarted() { 305 | return &result{ 306 | code: 450, 307 | msg: fmt.Sprintf("%s: data transfer in progress", c.command), 308 | } 309 | } 310 | 311 | // make new listener and store listener port 312 | dataHandler, err := newDataHandler( 313 | c.config, 314 | c.log, 315 | c.conn, 316 | c.proxy.GetConn(), 317 | c.command, 318 | c.tlsDatas, 319 | c.transferInTLS, 320 | c.inDataTransfer, 321 | ) 322 | if err != nil { 323 | return &result{ 324 | code: 421, 325 | msg: "cannot create data channel socket", 326 | err: err, 327 | log: c.log, 328 | } 329 | } 330 | 331 | c.proxy.SetDataHandler(dataHandler) 332 | 333 | switch c.command { 334 | case "PORT": 335 | if err := c.proxy.dataConnector.parsePORTcommand(c.line); err != nil { 336 | c.log.err(err.Error()) 337 | 338 | c.proxy.dataConnector.Close() 339 | 340 | return &result{ 341 | code: 501, 342 | msg: "cannot parse PORT command", 343 | err: err, 344 | log: c.log, 345 | } 346 | } 347 | case "EPRT": 348 | if err := c.proxy.dataConnector.parseEPRTcommand(c.line); err != nil { 349 | c.log.err(err.Error()) 350 | 351 | c.proxy.dataConnector.Close() 352 | 353 | if err.Error() == "unknown network protocol" { 354 | return &result{ 355 | code: 522, 356 | msg: err.Error(), 357 | err: err, 358 | log: c.log, 359 | } 360 | } 361 | 362 | return &result{ 363 | code: 501, 364 | msg: "cannot parse EPRT command", 365 | err: err, 366 | log: c.log, 367 | } 368 | } 369 | } 370 | 371 | if !c.proxy.isDataHandlerAvailable() { 372 | return &result{ 373 | code: 425, 374 | msg: "Can't open data connection", 375 | } 376 | } 377 | 378 | // if origin connect mode is PORT or CLIENT(with client use some kind of active mode) 379 | if c.proxy.dataConnector.originConn.needsListen { 380 | _, lPort, _ := net.SplitHostPort(c.proxy.dataConnector.originConn.listener.Addr().String()) 381 | listenPort, _ := strconv.Atoi(lPort) 382 | 383 | listenIP := strings.Split(c.proxy.GetConn().LocalAddr().String(), ":")[0] 384 | 385 | // prepare PORT command line to origin 386 | // only use PORT command because connect to server support IPv4 now 387 | toOriginMsg = fmt.Sprintf("PORT %s,%s,%s\r\n", 388 | strings.ReplaceAll(listenIP, ".", ","), 389 | strconv.Itoa(listenPort/256), 390 | strconv.Itoa(listenPort%256)) 391 | } else { 392 | if c.config.TransferMode == "CLIENT" { 393 | toOriginMsg = c.command + "\r\n" 394 | } else { 395 | toOriginMsg = c.config.TransferMode + "\r\n" 396 | } 397 | } 398 | 399 | // send command to origin 400 | if err := c.proxy.sendToOrigin(toOriginMsg); err != nil { 401 | c.log.err(err.Error()) 402 | 403 | c.proxy.dataConnector.Close() 404 | 405 | return &result{ 406 | code: 500, 407 | msg: fmt.Sprintf("Internal error: %s", err), 408 | err: err, 409 | log: c.log, 410 | } 411 | } 412 | } else { 413 | if err := c.proxy.sendToOrigin(c.line); err != nil { 414 | c.log.err(err.Error()) 415 | 416 | return &result{ 417 | code: 530, 418 | msg: "I can't deal with you (proxy error for handle data)", 419 | err: err, 420 | log: c.log, 421 | } 422 | } 423 | } 424 | 425 | return nil 426 | } 427 | -------------------------------------------------------------------------------- /pftp/handle_commands_test.go: -------------------------------------------------------------------------------- 1 | package pftp 2 | 3 | import ( 4 | "net" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/tevino/abool" 9 | ) 10 | 11 | func Test_clientHandler_handleAUTH(t *testing.T) { 12 | type fields struct { 13 | config *config 14 | } 15 | 16 | type res struct { 17 | code int 18 | msg string 19 | err string 20 | log *logger 21 | } 22 | 23 | tests := []struct { 24 | name string 25 | fields fields 26 | want *res 27 | }{ 28 | { 29 | name: "undefined", 30 | fields: fields{ 31 | config: &config{}, 32 | }, 33 | want: &res{ 34 | code: 550, 35 | msg: "Cannot get a TLS config", 36 | }, 37 | }, 38 | } 39 | for _, tt := range tests { 40 | t.Run(tt.name, func(t *testing.T) { 41 | c := &clientHandler{ 42 | config: tt.fields.config, 43 | tlsDatas: &tlsDataSet{ 44 | forOrigin: &tlsData{}, 45 | forClient: &tlsData{}, 46 | }, 47 | } 48 | r := c.handleAUTH() 49 | got := &res{ 50 | code: r.code, 51 | msg: r.msg, 52 | log: r.log, 53 | } 54 | if r.err == nil { 55 | got.err = "" 56 | } else { 57 | got.err = r.err.Error() 58 | } 59 | 60 | if !reflect.DeepEqual(got, tt.want) { 61 | t.Errorf("clientHandler.handleAUTH() = %v, want %v", got, tt.want) 62 | } 63 | }) 64 | } 65 | } 66 | 67 | func Test_clientHandler_handlePBSZ(t *testing.T) { 68 | type fields struct { 69 | config *config 70 | } 71 | 72 | type res struct { 73 | code int 74 | msg string 75 | err string 76 | log *logger 77 | } 78 | 79 | tests := []struct { 80 | name string 81 | fields fields 82 | want *res 83 | }{ 84 | { 85 | name: "none_tls", 86 | fields: fields{ 87 | config: &config{}, 88 | }, 89 | want: &res{ 90 | code: 503, 91 | msg: "Not using TLS connection", 92 | }, 93 | }, 94 | } 95 | for _, tt := range tests { 96 | t.Run(tt.name, func(t *testing.T) { 97 | c := &clientHandler{ 98 | config: tt.fields.config, 99 | controlInTLS: abool.New(), 100 | } 101 | r := c.handlePBSZ() 102 | got := &res{ 103 | code: r.code, 104 | msg: r.msg, 105 | log: r.log, 106 | } 107 | if r.err == nil { 108 | got.err = "" 109 | } else { 110 | got.err = r.err.Error() 111 | } 112 | if !reflect.DeepEqual(got, tt.want) { 113 | t.Errorf("clientHandler.handlePBSZ() = %v, want %v", got, tt.want) 114 | } 115 | }) 116 | } 117 | } 118 | 119 | func Test_clientHandler_handlePROT(t *testing.T) { 120 | type fields struct { 121 | config *config 122 | } 123 | 124 | type res struct { 125 | code int 126 | msg string 127 | err string 128 | log *logger 129 | } 130 | 131 | tests := []struct { 132 | name string 133 | fields fields 134 | want *res 135 | }{ 136 | { 137 | name: "none_tls", 138 | fields: fields{ 139 | config: &config{}, 140 | }, 141 | want: &res{ 142 | code: 503, 143 | msg: "Not using TLS connection", 144 | }, 145 | }, 146 | } 147 | for _, tt := range tests { 148 | t.Run(tt.name, func(t *testing.T) { 149 | c := &clientHandler{ 150 | config: tt.fields.config, 151 | controlInTLS: abool.New(), 152 | } 153 | r := c.handlePBSZ() 154 | got := &res{ 155 | code: r.code, 156 | msg: r.msg, 157 | log: r.log, 158 | } 159 | if r.err == nil { 160 | got.err = "" 161 | } else { 162 | got.err = r.err.Error() 163 | } 164 | if !reflect.DeepEqual(got, tt.want) { 165 | t.Errorf("clientHandler.handlePROT() = %v, want %v", got, tt.want) 166 | } 167 | }) 168 | } 169 | } 170 | 171 | func Test_clientHandler_handleUSER(t *testing.T) { 172 | c, err := net.Dial("tcp", "127.0.0.1:21") 173 | if err != nil { 174 | panic(err) 175 | } 176 | defer c.Close() 177 | 178 | type fields struct { 179 | conn net.Conn 180 | config *config 181 | context *Context 182 | line string 183 | } 184 | tests := []struct { 185 | name string 186 | fields fields 187 | want *result 188 | }{ 189 | { 190 | name: "ok", 191 | fields: fields{ 192 | conn: c, 193 | config: &config{}, 194 | context: &Context{ 195 | RemoteAddr: "127.0.0.1:21", 196 | }, 197 | line: "user pftp", 198 | }, 199 | }, 200 | { 201 | name: "not connect", 202 | fields: fields{ 203 | conn: c, 204 | config: &config{}, 205 | context: &Context{ 206 | RemoteAddr: "127.0.0.1:28080", 207 | }, 208 | line: "user pftp", 209 | }, 210 | want: &result{ 211 | code: 530, 212 | msg: "I can't deal with you (proxy error for user)", 213 | }, 214 | }, 215 | } 216 | var cn int32 217 | for _, tt := range tests { 218 | t.Run(tt.name, func(t *testing.T) { 219 | c := &clientHandler{ 220 | config: tt.fields.config, 221 | conn: tt.fields.conn, 222 | context: tt.fields.context, 223 | log: &logger{}, 224 | currentConnection: &cn, 225 | line: tt.fields.line, 226 | } 227 | got := c.handleUSER() 228 | if (got != nil && tt.want == nil) || (tt.want != nil && (got.code != tt.want.code || got.msg != tt.want.msg)) { 229 | t.Errorf("clientHandler.handleUSER() = %v, want %v", got, tt.want) 230 | } 231 | }) 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /pftp/logger.go: -------------------------------------------------------------------------------- 1 | package pftp 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/sirupsen/logrus" 7 | ) 8 | 9 | type logger struct { 10 | fromip string 11 | user string 12 | id uint64 13 | } 14 | 15 | func (l *logger) debug(format string, args ...interface{}) { 16 | format = fmt.Sprintf("[%d] user:%s addr:%s %s", l.id, l.user, l.fromip, format) 17 | logrus.Debugf(format, args...) 18 | } 19 | 20 | func (l *logger) info(format string, args ...interface{}) { 21 | format = fmt.Sprintf("[%d] user:%s addr:%s %s", l.id, l.user, l.fromip, format) 22 | logrus.Infof(format, args...) 23 | } 24 | 25 | func (l *logger) err(format string, args ...interface{}) { 26 | format = fmt.Sprintf("[%d] user:%s addr:%s %s", l.id, l.user, l.fromip, format) 27 | logrus.Errorf(format, args...) 28 | } 29 | -------------------------------------------------------------------------------- /pftp/proxy.go: -------------------------------------------------------------------------------- 1 | package pftp 2 | 3 | import ( 4 | "bufio" 5 | "crypto/tls" 6 | "errors" 7 | "fmt" 8 | "net" 9 | "strconv" 10 | "strings" 11 | "sync" 12 | "time" 13 | 14 | proxyproto "github.com/pires/go-proxyproto" 15 | "github.com/tevino/abool" 16 | ) 17 | 18 | const ( 19 | bufferSize = 4096 20 | dataTransferBufferSize = 4096 21 | connectionTimeout = 30 22 | secureCommand = "PASS" 23 | alreadyClosedMsg = "use of closed" 24 | ) 25 | 26 | type proxyServer struct { 27 | clientReader *bufio.Reader 28 | clientWriter *bufio.Writer 29 | origin net.Conn 30 | originReader *bufio.Reader 31 | originWriter *bufio.Writer 32 | tlsDatas *tlsDataSet 33 | passThrough bool 34 | mutex *sync.Mutex 35 | log *logger 36 | stopChan chan struct{} 37 | stopChanDone chan struct{} 38 | stop bool 39 | isLoggedin bool 40 | welcomeMsg string 41 | config *config 42 | dataConnector *dataHandler 43 | waitSwitching chan bool 44 | inDataTransfer *abool.AtomicBool 45 | isDataCommandResponse bool 46 | } 47 | 48 | type proxyServerConfig struct { 49 | clientReader *bufio.Reader 50 | clientWriter *bufio.Writer 51 | tlsDatas *tlsDataSet 52 | originAddr string 53 | mutex *sync.Mutex 54 | log *logger 55 | config *config 56 | inDataTransfer *abool.AtomicBool 57 | } 58 | 59 | func newProxyServer(conf *proxyServerConfig) (*proxyServer, error) { 60 | c, err := net.DialTimeout("tcp", 61 | conf.originAddr, 62 | time.Duration(connectionTimeout)*time.Second) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | // set linger 0 and tcp keepalive setting between origin connection 68 | tcpConn := c.(*net.TCPConn) 69 | tcpConn.SetKeepAlive(true) 70 | tcpConn.SetKeepAlivePeriod(time.Duration(conf.config.KeepaliveTime) * time.Second) 71 | tcpConn.SetLinger(0) 72 | 73 | p := &proxyServer{ 74 | clientReader: conf.clientReader, 75 | clientWriter: conf.clientWriter, 76 | originWriter: bufio.NewWriter(c), 77 | originReader: bufio.NewReader(c), 78 | origin: tcpConn, 79 | tlsDatas: conf.tlsDatas, 80 | passThrough: true, 81 | mutex: conf.mutex, 82 | log: conf.log, 83 | stopChan: make(chan struct{}), 84 | stopChanDone: make(chan struct{}), 85 | welcomeMsg: "220 " + conf.config.WelcomeMsg + "\r\n", 86 | isLoggedin: false, 87 | config: conf.config, 88 | waitSwitching: make(chan bool), 89 | inDataTransfer: conf.inDataTransfer, 90 | } 91 | 92 | p.log.debug("new proxy from=%s to=%s", c.LocalAddr(), c.RemoteAddr()) 93 | 94 | return p, err 95 | } 96 | 97 | // check command line validation 98 | func (s *proxyServer) commandLineCheck(line string) (string, error) { 99 | // if first byte of command line is not alphabet, delete it until start with alphabet for avoid errors 100 | // FTP commands always start with alphabet. 101 | // ex) "\xff\xf4\xffABOR\r\n" -> "ABOR\r\n" 102 | for { 103 | // if line is empty, abort check 104 | if len(line) == 0 { 105 | return "", fmt.Errorf("aborted : wrong command line") 106 | } 107 | b := line[0] 108 | if (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') { 109 | break 110 | } 111 | line = line[1:] 112 | } 113 | 114 | // command line must contain CRLF only once in the end 115 | if !strings.HasSuffix(line, "\r\n") || strings.Count(line, "\r") != 1 || strings.Count(line, "\n") != 1 { 116 | s.log.debug("wrong command line. make line end by CRLF") 117 | 118 | // delete CR & LF characters from line (only allow to end of line "\r\n") 119 | line = strings.Replace(line, "\n", "", -1) 120 | line = strings.Replace(line, "\r", "", -1) 121 | 122 | // add write CRLF to end of line 123 | line += "\r\n" 124 | } 125 | 126 | return line, nil 127 | } 128 | 129 | func (s *proxyServer) sendToOrigin(line string) error { 130 | var err error 131 | 132 | // check command line and fix 133 | line, err = s.commandLineCheck(line) 134 | if err != nil { 135 | return err 136 | } 137 | 138 | s.commandLog(line) 139 | 140 | if _, err := s.originWriter.WriteString(line); err != nil { 141 | s.log.err("send to origin error: %s", err.Error()) 142 | return err 143 | } 144 | if err := s.originWriter.Flush(); err != nil { 145 | return err 146 | } 147 | 148 | return nil 149 | } 150 | 151 | func (s *proxyServer) sendToClient(line string) error { 152 | s.mutex.Lock() 153 | defer s.mutex.Unlock() 154 | 155 | if _, err := s.clientWriter.WriteString(line + "\r\n"); err != nil { 156 | return err 157 | } 158 | if err := s.clientWriter.Flush(); err != nil { 159 | return err 160 | } 161 | 162 | s.log.debug("send to client: %s", line) 163 | return nil 164 | } 165 | 166 | func (s *proxyServer) responseProxy() error { 167 | return s.startProxy() 168 | } 169 | 170 | func (s *proxyServer) suspend() { 171 | s.log.debug("suspend proxy") 172 | s.passThrough = false 173 | } 174 | 175 | func (s *proxyServer) unsuspend() { 176 | s.log.debug("unsuspend proxy") 177 | s.passThrough = true 178 | } 179 | 180 | // Close origin connection and check return 181 | func (s *proxyServer) Close() error { 182 | if s.origin != nil { 183 | if err := s.origin.Close(); err != nil { 184 | return err 185 | } 186 | } 187 | 188 | if s.dataConnector != nil { 189 | s.DestroyDataHandler() 190 | } 191 | 192 | return nil 193 | } 194 | 195 | func (s *proxyServer) GetConn() net.Conn { 196 | return s.origin 197 | } 198 | 199 | // basically, this function never called during data transfer 200 | // in progress, so block by chan is not necessary. 201 | func (s *proxyServer) SetDataHandler(handler *dataHandler) { 202 | // cleanup previous data connector. 203 | if s.dataConnector != nil { 204 | s.DestroyDataHandler() 205 | } 206 | 207 | s.dataConnector = handler 208 | } 209 | 210 | // Destroy data handler 211 | func (s *proxyServer) DestroyDataHandler() { 212 | if s.dataConnector != nil { 213 | connectionCloser(s.dataConnector, s.log) 214 | } 215 | } 216 | 217 | // return true when data handler is available now 218 | func (s *proxyServer) isDataHandlerAvailable() bool { 219 | if s.dataConnector == nil { 220 | return false 221 | } 222 | 223 | return !s.dataConnector.isClosed() 224 | } 225 | 226 | // return true when data transfer in progress 227 | func (s *proxyServer) isDataTransferStarted() bool { 228 | if s.dataConnector == nil { 229 | return false 230 | } 231 | 232 | return s.dataConnector.isStarted() 233 | } 234 | 235 | // return switch origin & user logged in state 236 | func (s *proxyServer) isLoggedIn() bool { 237 | return s.isLoggedin 238 | } 239 | 240 | func (s *proxyServer) sendProxyHeader(clientAddr string, originAddr string) error { 241 | sourceAddr, sourcePort, err := net.SplitHostPort(clientAddr) 242 | if err != nil { 243 | return err 244 | } 245 | 246 | sourcePortInt, err := strconv.Atoi(sourcePort) 247 | if err != nil { 248 | return err 249 | } 250 | 251 | destinationAddr, destinationPort, err := net.SplitHostPort(originAddr) 252 | if err != nil { 253 | return err 254 | } 255 | 256 | destinationPortInt, err := strconv.Atoi(destinationPort) 257 | if err != nil { 258 | return err 259 | } 260 | 261 | // proxyProtocolHeader's DestinationAddress must be IP! not domain name 262 | hostIP, err := net.LookupIP(destinationAddr) 263 | if err != nil { 264 | return err 265 | } 266 | 267 | transportProtocol := proxyproto.TCPv4 268 | if strings.Count(sourceAddr, ":") > 0 { 269 | transportProtocol = proxyproto.TCPv6 270 | } 271 | 272 | proxyProtocolHeader := proxyproto.Header{ 273 | Version: byte(1), 274 | Command: proxyproto.PROXY, 275 | TransportProtocol: transportProtocol, 276 | SourceAddr: &net.TCPAddr{IP: net.ParseIP(sourceAddr), Port: sourcePortInt}, 277 | DestinationAddr: &net.TCPAddr{IP: net.ParseIP(hostIP[0].String()), Port: destinationPortInt}, 278 | } 279 | 280 | _, err = proxyProtocolHeader.WriteTo(s.origin) 281 | return err 282 | } 283 | 284 | // send command before login to origin 285 | func (s *proxyServer) sendTLSCommand(previousTLSCommands []string) error { 286 | lastError := error(nil) 287 | 288 | for _, cmd := range previousTLSCommands { 289 | s.commandLog(cmd) 290 | if _, err := s.originWriter.WriteString(cmd); err != nil { 291 | return fmt.Errorf("failed to send AUTH command to origin") 292 | } 293 | if err := s.originWriter.Flush(); err != nil { 294 | return err 295 | } 296 | 297 | for { 298 | // Read response from new origin server 299 | str, err := s.originReader.ReadString('\n') 300 | if err != nil { 301 | return fmt.Errorf("failed to make TLS connection") 302 | } 303 | 304 | s.log.debug("response from origin: %s", strings.TrimSuffix(str, "\r\n")) 305 | 306 | if strings.Compare(strings.ToUpper(getCommand(cmd)[0]), "AUTH") == 0 { 307 | code := getCode(str)[0] 308 | if code != "234" { 309 | // when got 500 PROXY not understood, ignore it 310 | // this ignore setting for complex origins. 311 | // if some origins needs proxy protocol and some else is not, 312 | // pftp cannot support both in same time. So, pftp ignore the 313 | // 500 PROXY not understood then client can connect any servers. 314 | if s.config.ProxyProtocol && strings.Contains(str, "500 PROXY") { 315 | continue 316 | } else { 317 | lastError = fmt.Errorf("%s origin server has not support TLS connection", code) 318 | 319 | break 320 | } 321 | } else { 322 | // SSL/TLS wrapping on connection 323 | tlsConn := tls.Client(s.origin, s.tlsDatas.forOrigin.getTLSConfig()) 324 | err = tlsConn.Handshake() 325 | if err != nil { 326 | return fmt.Errorf("TLS handshake with origin has failed %s", err) 327 | } 328 | 329 | s.log.debug("TLS control connection finished with origin. TLS protocol version: %s and Cipher Suite: %s", getTLSProtocolName(tlsConn.ConnectionState().Version), tls.CipherSuiteName(tlsConn.ConnectionState().CipherSuite)) 330 | 331 | s.origin = tlsConn 332 | s.originReader = bufio.NewReader(s.origin) 333 | s.originWriter = bufio.NewWriter(s.origin) 334 | 335 | break 336 | } 337 | } else { 338 | break 339 | } 340 | } 341 | } 342 | 343 | return lastError 344 | } 345 | 346 | func (s *proxyServer) switchOrigin(clientAddr string, originAddr string, previousTLSCommands []string) error { 347 | // return error when user not found 348 | if len(originAddr) == 0 { 349 | return fmt.Errorf("user id not found") 350 | } 351 | 352 | // if client switched before, return error 353 | if s.isLoggedin { 354 | return fmt.Errorf("origin already switched") 355 | } 356 | 357 | s.log.info("switch origin to: %s", originAddr) 358 | var err error 359 | 360 | if s.passThrough { 361 | s.suspend() 362 | defer s.unsuspend() 363 | } 364 | 365 | // disconnect old origin and close response listener 366 | s.stopChan <- struct{}{} 367 | <-s.stopChanDone 368 | 369 | lastError := error(nil) 370 | switchResult := false 371 | 372 | defer func() { 373 | s.stop = false 374 | 375 | // send switching complate signal 376 | s.waitSwitching <- switchResult 377 | }() 378 | 379 | // change connection and reset reader and writer buffer 380 | s.origin, err = net.DialTimeout("tcp", originAddr, time.Duration(connectionTimeout)*time.Second) 381 | if err != nil { 382 | return err 383 | } 384 | s.originReader = bufio.NewReader(s.origin) 385 | s.originWriter = bufio.NewWriter(s.origin) 386 | 387 | // Send proxy protocol v1 header when set proxy protocol true 388 | if s.config.ProxyProtocol { 389 | s.log.debug("send proxy protocol to origin") 390 | if err := s.sendProxyHeader(clientAddr, originAddr); err != nil { 391 | return err 392 | } 393 | } 394 | 395 | // Read welcome message from ftp connection 396 | res, err := s.originReader.ReadString('\n') 397 | if err != nil { 398 | return errors.New("cannot connect to new origin server") 399 | } 400 | 401 | s.log.debug("response from new origin: %s", strings.TrimSuffix(res, "\r\n")) 402 | 403 | // set linger 0 and tcp keepalive setting between switched origin connection 404 | tcpConn := s.origin.(*net.TCPConn) 405 | tcpConn.SetKeepAlive(true) 406 | tcpConn.SetKeepAlivePeriod(time.Duration(s.config.KeepaliveTime) * time.Second) 407 | tcpConn.SetLinger(0) 408 | 409 | s.origin = tcpConn 410 | 411 | // If client connect with TLS connection, make TLS connection to origin ftp server too. 412 | if err := s.sendTLSCommand(previousTLSCommands); err != nil { 413 | return err 414 | } 415 | 416 | // set switch process complate 417 | switchResult = true 418 | 419 | return lastError 420 | } 421 | 422 | func (s *proxyServer) startProxy() error { 423 | // return if proxy still unsuspended or s.stop is true 424 | if s.stop || !s.passThrough { 425 | return nil 426 | } 427 | 428 | read := make(chan string) 429 | done := make(chan struct{}) 430 | send := make(chan struct{}) 431 | errchan := make(chan error) 432 | lastError := error(nil) 433 | 434 | go func() { 435 | for { 436 | s.isDataCommandResponse = false 437 | buff, err := s.originReader.ReadString('\n') 438 | if err != nil { 439 | if !s.stop { 440 | safeSetChanel(errchan, err) 441 | } 442 | break 443 | } else { 444 | if s.config.ProxyTimeout > 0 { 445 | // do not time out during transfer data 446 | if s.inDataTransfer.IsSet() { 447 | s.origin.SetDeadline(time.Time{}) 448 | } else { 449 | s.origin.SetDeadline(time.Now().Add(time.Duration(s.config.ProxyTimeout) * time.Second)) 450 | } 451 | } 452 | 453 | s.log.debug("response from origin: %s", strings.TrimSuffix(buff, "\r\n")) 454 | 455 | // response user setted welcome message 456 | if strings.Compare(getCode(buff)[0], "220") == 0 && !s.isLoggedin { 457 | buff = s.welcomeMsg 458 | } 459 | 460 | // check login and switch origin success 461 | if strings.Compare(getCode(buff)[0], "230") == 0 { 462 | s.isLoggedin = true 463 | } 464 | 465 | // when got 500 PROXY not understood, ignore it 466 | // this ignore setting for complex origins. 467 | // if some origins needs proxy protocol and some else is not, 468 | // pftp cannot support both in same time. So, pftp ignore the 469 | // 500 PROXY not understood then client can connect any servers. 470 | if s.config.ProxyProtocol && strings.Contains(buff, "500 PROXY") { 471 | continue 472 | } 473 | 474 | // is data channel proxy used 475 | if s.config.DataChanProxy && s.isLoggedin { 476 | if strings.HasPrefix(buff, "227 ") { 477 | s.isDataCommandResponse = true 478 | s.dataConnector.parsePASVresponse(buff) 479 | } 480 | if strings.HasPrefix(buff, "229 ") { 481 | s.isDataCommandResponse = true 482 | s.dataConnector.parseEPSVresponse(buff) 483 | } 484 | if strings.HasPrefix(buff, "200 PORT command successful") { 485 | s.isDataCommandResponse = true 486 | } 487 | 488 | // when got 150 from origin, it means data transfer has started 489 | // set transfer in progress flag to 1 490 | if strings.HasPrefix(buff, "150 ") { 491 | s.inDataTransfer.Set() 492 | } 493 | 494 | // when got 226 from origin, it means data transfer finished 495 | // set data transfer in p rogress flag to 0 for accept next data transfers 496 | if strings.HasPrefix(buff, "226 ") { 497 | s.inDataTransfer.UnSet() 498 | } 499 | 500 | if s.isDataCommandResponse { 501 | if s.isDataHandlerAvailable() { 502 | switch s.dataConnector.clientConn.mode { 503 | case "PORT", "EPRT": 504 | buff = fmt.Sprintf("200 %s command successful\r\n", s.dataConnector.clientConn.mode) 505 | case "PASV": 506 | // prepare PASV response line to client 507 | _, lPort, _ := net.SplitHostPort(s.dataConnector.clientConn.listener.Addr().String()) 508 | listenPort, _ := strconv.Atoi(lPort) 509 | buff = fmt.Sprintf("227 Entering Passive Mode (%s,%s,%s).\r\n", 510 | strings.ReplaceAll(s.config.MasqueradeIP, ".", ","), 511 | strconv.Itoa(listenPort/256), 512 | strconv.Itoa(listenPort%256)) 513 | case "EPSV": 514 | // prepare EPSV response line to client 515 | _, listenPort, _ := net.SplitHostPort(s.dataConnector.clientConn.listener.Addr().String()) 516 | buff = fmt.Sprintf("229 Entering Extended Passive Mode (|||%s|).\r\n", listenPort) 517 | } 518 | } else { 519 | buff = "425 Can't open data connection\r\n" 520 | } 521 | } 522 | } 523 | 524 | // handling multi-line response 525 | if len(buff) >= 4 && buff[3] == '-' { 526 | params := getCode(buff) 527 | multiLine := buff 528 | 529 | for { 530 | res, err := s.originReader.ReadString('\n') 531 | if err != nil { 532 | safeSetChanel(errchan, err) 533 | done <- struct{}{} 534 | return 535 | } 536 | 537 | // store multi-line response 538 | multiLine += res 539 | 540 | // check multi-line end 541 | if getCode(res)[0] == params[0] && res[3] == ' ' { 542 | buff = multiLine 543 | break 544 | } 545 | } 546 | } 547 | 548 | if s.passThrough { 549 | read <- buff 550 | <-send 551 | } 552 | } 553 | } 554 | done <- struct{}{} 555 | }() 556 | 557 | loop: 558 | for { 559 | select { 560 | case b := <-read: 561 | if err := s.sendToClient(strings.TrimRight(b, "\r\n")); err != nil { 562 | if !strings.Contains(err.Error(), alreadyClosedMsg) { 563 | s.log.err("error on write response to client: %s, err: %s", strings.TrimSuffix(b, "\r\n"), err.Error()) 564 | } 565 | } 566 | send <- struct{}{} 567 | case err := <-errchan: 568 | lastError = err 569 | connectionCloser(s, s.log) 570 | 571 | break loop 572 | case <-s.stopChan: 573 | s.stop = true 574 | 575 | // close read goroutine 576 | connectionCloser(s, s.log) 577 | 578 | s.stopChanDone <- struct{}{} 579 | break loop 580 | } 581 | } 582 | <-done 583 | 584 | return lastError 585 | } 586 | 587 | // Hide parameters from log 588 | func (s *proxyServer) commandLog(line string) { 589 | if strings.Compare(strings.ToUpper(getCommand(line)[0]), secureCommand) == 0 { 590 | s.log.debug("send to origin: %s ********", secureCommand) 591 | } else { 592 | s.log.debug("send to origin: %s", strings.TrimSuffix(line, "\r\n")) 593 | } 594 | } 595 | 596 | // split response line 597 | func getCode(line string) []string { 598 | if len(line) >= 4 { 599 | return strings.SplitN(strings.Trim(line, "\r\n"), string(line[3]), 2) 600 | } 601 | 602 | return []string{line} 603 | } 604 | -------------------------------------------------------------------------------- /pftp/result.go: -------------------------------------------------------------------------------- 1 | package pftp 2 | 3 | type result struct { 4 | code int 5 | msg string 6 | err error 7 | log *logger 8 | } 9 | 10 | func (r *result) Response(handler *clientHandler) error { 11 | if r.log != nil && r.err != nil { 12 | r.log.err("command error response: %s", r.err) 13 | } 14 | 15 | if r.code != 0 { 16 | return handler.writeMessage(r.code, r.msg) 17 | } 18 | return nil 19 | } 20 | -------------------------------------------------------------------------------- /pftp/server.go: -------------------------------------------------------------------------------- 1 | package pftp 2 | 3 | import ( 4 | "net" 5 | "os" 6 | "os/signal" 7 | "runtime" 8 | "strings" 9 | "syscall" 10 | "time" 11 | 12 | "github.com/lestrrat/go-server-starter/listener" 13 | "github.com/sirupsen/logrus" 14 | "golang.org/x/sync/errgroup" 15 | ) 16 | 17 | type middlewareFunc func(*Context, string) error 18 | type middleware map[string]middlewareFunc 19 | 20 | // FtpServer struct type 21 | type FtpServer struct { 22 | listener net.Listener 23 | clientCounter uint64 24 | config *config 25 | serverTLSData *tlsData 26 | middleware middleware 27 | shutdown bool 28 | } 29 | 30 | // NewFtpServer load config and create new ftp server struct 31 | func NewFtpServer(confFile string) (*FtpServer, error) { 32 | c, err := loadConfig(confFile) 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | return NewFtpServerFromConfig(c) 38 | } 39 | 40 | // NewFtpServerFromConfig creates new ftp server from the given config 41 | func NewFtpServerFromConfig(c *config) (*FtpServer, error) { 42 | m := middleware{} 43 | server := &FtpServer{ 44 | config: c, 45 | middleware: m, 46 | } 47 | 48 | // build and set TLS configuration 49 | if server.config.TLS != nil { 50 | logrus.Info("build server TLS configurations...") 51 | serverTLSData, err := buildTLSConfigForClient(server.config.TLS) 52 | if err != nil { 53 | return nil, err 54 | } 55 | server.serverTLSData = serverTLSData 56 | logrus.Infof("TLS certificate successfully loaded") 57 | } 58 | 59 | return server, nil 60 | } 61 | 62 | // Use set middleware function 63 | func (server *FtpServer) Use(command string, m middlewareFunc) { 64 | server.middleware[strings.ToUpper(command)] = m 65 | } 66 | 67 | func (server *FtpServer) listen() (err error) { 68 | if os.Getenv("SERVER_STARTER_PORT") != "" { 69 | listeners, err := listener.ListenAll() 70 | if listeners == nil || err != nil { 71 | return err 72 | } 73 | server.listener = listeners[0] 74 | } else { 75 | l, err := net.Listen("tcp", server.config.ListenAddr) 76 | if err != nil { 77 | return err 78 | } 79 | server.listener = l 80 | } 81 | 82 | logrus.Info("Listening address ", server.listener.Addr()) 83 | 84 | return err 85 | } 86 | 87 | func (server *FtpServer) serve() error { 88 | currentConnection := int32(0) 89 | eg := errgroup.Group{} 90 | 91 | for { 92 | netConn, err := server.listener.Accept() 93 | if err != nil { 94 | // if use server starter, break for while all childs end 95 | if os.Getenv("SERVER_STARTER_PORT") != "" { 96 | logrus.Info("Close listener") 97 | break 98 | } 99 | 100 | if server.listener != nil { 101 | return err 102 | } 103 | } 104 | 105 | // set linger 0 and tcp keepalive setting between client connection 106 | conn := netConn.(*net.TCPConn) 107 | conn.SetKeepAlive(true) 108 | conn.SetKeepAlivePeriod(time.Duration(server.config.KeepaliveTime) * time.Second) 109 | conn.SetLinger(0) 110 | 111 | if server.config.IdleTimeout > 0 { 112 | conn.SetDeadline(time.Now().Add(time.Duration(server.config.IdleTimeout) * time.Second)) 113 | } 114 | 115 | server.clientCounter++ 116 | 117 | c := newClientHandler(conn, server.config, server.serverTLSData, server.middleware, server.clientCounter, ¤tConnection) 118 | eg.Go(func() error { 119 | err := c.handleCommands() 120 | logrus.Info("handle command end runtime goroutine count: ", runtime.NumGoroutine()) 121 | if err != nil { 122 | logrus.Error(err.Error()) 123 | } 124 | return err 125 | }) 126 | } 127 | 128 | return eg.Wait() 129 | } 130 | 131 | // Start start pFTP server 132 | func (server *FtpServer) Start() error { 133 | var lastError error 134 | done := make(chan struct{}) 135 | 136 | if err := server.listen(); err != nil { 137 | return err 138 | } 139 | 140 | logrus.Info("Starting...") 141 | 142 | go func() { 143 | if err := server.serve(); err != nil { 144 | if !server.shutdown { 145 | lastError = err 146 | } 147 | } 148 | done <- struct{}{} 149 | }() 150 | 151 | ch := make(chan os.Signal, 1) 152 | signal.Notify(ch, syscall.SIGHUP, syscall.SIGTERM) 153 | L: 154 | for { 155 | switch <-ch { 156 | case syscall.SIGHUP, syscall.SIGTERM: 157 | if err := server.stop(); err != nil { 158 | lastError = err 159 | } 160 | break L 161 | } 162 | } 163 | 164 | <-done 165 | return lastError 166 | } 167 | 168 | func (server *FtpServer) stop() error { 169 | server.shutdown = true 170 | if server.listener != nil { 171 | if err := server.listener.Close(); err != nil { 172 | return err 173 | } 174 | } 175 | return nil 176 | } 177 | -------------------------------------------------------------------------------- /pftp/socket_control.go: -------------------------------------------------------------------------------- 1 | package pftp 2 | 3 | import ( 4 | "net" 5 | "strings" 6 | "syscall" 7 | 8 | "golang.org/x/sys/unix" 9 | ) 10 | 11 | type closer interface { 12 | Close() error 13 | } 14 | 15 | // send EOF to write 16 | func sendEOF(conn net.Conn) error { 17 | // anonymous interface. Could explicitly use TCP instead. 18 | if v, ok := conn.(interface{ CloseWrite() error }); ok { 19 | if err := v.CloseWrite(); err != nil { 20 | if !strings.Contains(err.Error(), alreadyClosedMsg) { 21 | return err 22 | } 23 | } 24 | } 25 | 26 | return nil 27 | } 28 | 29 | // close connection 30 | func connectionCloser(c closer, log *logger) { 31 | if err := c.Close(); err != nil { 32 | if !strings.Contains(err.Error(), alreadyClosedMsg) { 33 | // log is nil when unit test 34 | if log != nil { 35 | log.err(err.Error()) 36 | } 37 | } 38 | } 39 | } 40 | 41 | // set reuse IP & Port for sharing port 20 (just set active mode) 42 | func setReuseIPPort(network, address string, c syscall.RawConn) error { 43 | var err error 44 | c.Control(func(fd uintptr) { 45 | err = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1) 46 | if err != nil { 47 | return 48 | } 49 | 50 | err = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1) 51 | if err != nil { 52 | return 53 | } 54 | }) 55 | return err 56 | } 57 | -------------------------------------------------------------------------------- /pftp/tls.go: -------------------------------------------------------------------------------- 1 | package pftp 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "fmt" 7 | "os" 8 | "strings" 9 | "sync" 10 | 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | // TLS version codes 15 | const ( 16 | defaultTLSVer = "TLSv1.2" 17 | ) 18 | 19 | // get TLS protocol from string version name 20 | func getTLSProtocol(protocol string) uint16 { 21 | switch protocol { 22 | case "TLSv1": 23 | return tls.VersionTLS10 24 | case "TLSv1.1": 25 | return tls.VersionTLS11 26 | case "TLSv1.2": 27 | return tls.VersionTLS12 28 | case "TLSv1.3": 29 | return tls.VersionTLS13 30 | default: 31 | logrus.Debugf("%s is an unsupported TLS protocol version. use default: %s", protocol, defaultTLSVer) 32 | return tls.VersionTLS12 // the default TLS protocol is TLSv1.2 33 | } 34 | } 35 | 36 | // get TLS protocol name from uint16 id 37 | func getTLSProtocolName(version uint16) string { 38 | switch version { 39 | case tls.VersionTLS10: 40 | return "TLSv1" 41 | case tls.VersionTLS11: 42 | return "TLSv1.1" 43 | case tls.VersionTLS12: 44 | return "TLSv1.2" 45 | case tls.VersionTLS13: 46 | return "TLSv1.3" 47 | default: 48 | return "unsupported TLS version" 49 | } 50 | } 51 | 52 | type tlsData struct { 53 | rootCA *x509.CertPool 54 | cert *tls.Certificate 55 | config *tls.Config 56 | mutex sync.Mutex 57 | } 58 | 59 | // tls configset for client and origin 60 | type tlsDataSet struct { 61 | forClient *tlsData 62 | forOrigin *tlsData 63 | version uint16 64 | cipherSuite uint16 65 | serverName string 66 | } 67 | 68 | // build origin side tls config 69 | // it is working TLS client 70 | func buildTLSConfigForOrigin(c *config) *tlsData { 71 | tc := &tls.Config{ 72 | InsecureSkipVerify: true, 73 | ClientSessionCache: tls.NewLRUClientSessionCache(10), 74 | SessionTicketsDisabled: false, 75 | } 76 | 77 | if c != nil && c.TLS != nil { 78 | tc.MinVersion = getTLSProtocol(c.TLS.MinProtocol) 79 | tc.MaxVersion = getTLSProtocol(c.TLS.MaxProtocol) 80 | } 81 | 82 | return &tlsData{ 83 | config: tc, 84 | rootCA: nil, 85 | cert: nil, 86 | } 87 | } 88 | 89 | // build client side tls config (pftp works like server) 90 | // it is working TLS server 91 | func buildTLSConfigForClient(TLS *tlsPair) (*tlsData, error) { 92 | var t *tlsData 93 | 94 | caCertFile := TLS.CACert 95 | 96 | if len(caCertFile) == 0 { 97 | caCertFile = TLS.Cert 98 | } 99 | caCertPEM, err := os.ReadFile(caCertFile) 100 | if err != nil { 101 | return nil, err 102 | } 103 | 104 | caCert := x509.NewCertPool() 105 | ok := caCert.AppendCertsFromPEM(caCertPEM) 106 | if !ok { 107 | return nil, fmt.Errorf("failed to parse CA cert") 108 | } 109 | 110 | cert, err := tls.LoadX509KeyPair(TLS.Cert, TLS.Key) 111 | if err != nil { 112 | return nil, fmt.Errorf("TLS configuration error: %s", err.Error()) 113 | } 114 | 115 | t = &tlsData{ 116 | config: nil, 117 | rootCA: caCert, 118 | cert: &cert, 119 | } 120 | 121 | t.config = &tls.Config{ 122 | NextProtos: []string{"ftp"}, 123 | Certificates: []tls.Certificate{cert}, 124 | MinVersion: getTLSProtocol(TLS.MinProtocol), 125 | MaxVersion: getTLSProtocol(TLS.MaxProtocol), 126 | CipherSuites: getCiphers(TLS.CipherSuite), 127 | PreferServerCipherSuites: true, 128 | VerifyConnection: t.verifyTLSConnection, 129 | } 130 | 131 | return t, nil 132 | } 133 | 134 | // verify TLS connection using Peer certificates 135 | func (t *tlsData) verifyTLSConnection(cs tls.ConnectionState) error { 136 | t.mutex.Lock() 137 | defer t.mutex.Unlock() 138 | opts := x509.VerifyOptions{ 139 | Roots: t.rootCA, 140 | DNSName: cs.ServerName, 141 | Intermediates: x509.NewCertPool(), 142 | } 143 | 144 | if len(cs.PeerCertificates) > 0 { 145 | if len(cs.PeerCertificates) >= 2 { 146 | for _, cert := range cs.PeerCertificates[1:] { 147 | opts.Intermediates.AddCert(cert) 148 | } 149 | } 150 | 151 | _, err := cs.PeerCertificates[0].Verify(opts) 152 | if err != nil { 153 | return fmt.Errorf("variation error: %v", err) 154 | } 155 | } 156 | 157 | return nil 158 | } 159 | 160 | // get tls config 161 | func (t *tlsData) getTLSConfig() *tls.Config { 162 | t.mutex.Lock() 163 | defer t.mutex.Unlock() 164 | return t.config 165 | } 166 | 167 | // set specific tls version to tls.Config 168 | func (t *tlsData) setSpecificTLSVersion(tlsVersion uint16) { 169 | t.mutex.Lock() 170 | defer t.mutex.Unlock() 171 | t.config.MinVersion = tlsVersion 172 | t.config.MaxVersion = tlsVersion 173 | } 174 | 175 | // set server name to tls.Config 176 | func (t *tlsData) setServerName(name string) { 177 | t.mutex.Lock() 178 | defer t.mutex.Unlock() 179 | t.config.ServerName = name 180 | } 181 | 182 | // set specific cipher suite name to tls.Config 183 | func (t *tlsData) setCipherSUite(cipherSuite uint16) { 184 | t.mutex.Lock() 185 | defer t.mutex.Unlock() 186 | t.config.CipherSuites = []uint16{cipherSuite} 187 | } 188 | 189 | // get available Ciphersuites from config 190 | func getCiphers(ciphers string) []uint16 { 191 | cipherNames := strings.Split(ciphers, ":") 192 | 193 | var result []uint16 194 | 195 | for _, cipherName := range removeDuplicates(cipherNames) { 196 | for _, c := range tls.CipherSuites() { 197 | if c.Name == strings.TrimSpace(cipherName) { 198 | result = append(result, c.ID) 199 | } 200 | } 201 | } 202 | 203 | return result 204 | } 205 | 206 | // remove duplicate ciphersuites from config 207 | func removeDuplicates(params []string) []string { 208 | if len(params) == 0 { 209 | return params 210 | } 211 | 212 | var result []string 213 | exist := make(map[string]bool) 214 | 215 | for _, param := range params { 216 | if _, ok := exist[param]; !ok { 217 | result = append(result, param) 218 | } 219 | exist[param] = true 220 | } 221 | 222 | return result 223 | } 224 | -------------------------------------------------------------------------------- /pftp/utils.go: -------------------------------------------------------------------------------- 1 | package pftp 2 | 3 | func safeSetChanel(c chan error, err error) { 4 | var ok bool 5 | select { 6 | case _, ok = <-c: 7 | default: 8 | ok = true 9 | } 10 | 11 | if ok { 12 | c <- err 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/rest_server.go: -------------------------------------------------------------------------------- 1 | // This is test server for get domain name 2 | 3 | package test 4 | 5 | import ( 6 | "encoding/json" 7 | "fmt" 8 | "net/http" 9 | "net/http/httptest" 10 | "testing" 11 | 12 | "github.com/julienschmidt/httprouter" 13 | ) 14 | 15 | type response struct { 16 | Code int `json:"code"` 17 | Message string `json:"message"` 18 | Data string `json:"data"` 19 | } 20 | 21 | type resource interface { 22 | URI() string 23 | Get(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) response 24 | Post(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) response 25 | Put(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) response 26 | Delete(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) response 27 | } 28 | 29 | type ( 30 | postNotSupported struct{} 31 | putNotSupported struct{} 32 | deleteNotSupported struct{} 33 | ) 34 | 35 | func (postNotSupported) Post(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) response { 36 | return response{405, "", ""} 37 | } 38 | 39 | func (putNotSupported) Put(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) response { 40 | return response{405, "", ""} 41 | } 42 | 43 | func (deleteNotSupported) Delete(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) response { 44 | return response{405, "", ""} 45 | } 46 | 47 | func abort(rw http.ResponseWriter, statusCode int) { 48 | rw.WriteHeader(statusCode) 49 | } 50 | 51 | func httpResponse(rw http.ResponseWriter, req *http.Request, res response) { 52 | content, err := json.Marshal(res) 53 | 54 | if err != nil { 55 | abort(rw, 500) 56 | } 57 | 58 | rw.WriteHeader(res.Code) 59 | rw.Write(content) 60 | } 61 | 62 | func addResource(router *httprouter.Router, resource resource) { 63 | router.GET(resource.URI(), func(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) { 64 | res := resource.Get(rw, r, ps) 65 | httpResponse(rw, r, res) 66 | }) 67 | router.POST(resource.URI(), func(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) { 68 | res := resource.Post(rw, r, ps) 69 | httpResponse(rw, r, res) 70 | }) 71 | router.PUT(resource.URI(), func(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) { 72 | res := resource.Put(rw, r, ps) 73 | httpResponse(rw, r, res) 74 | }) 75 | router.DELETE(resource.URI(), func(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) { 76 | res := resource.Delete(rw, r, ps) 77 | httpResponse(rw, r, res) 78 | }) 79 | } 80 | 81 | // Testdomain data array 82 | var domains = []string{ 83 | "127.0.0.1:10021", // for vsuser 84 | "127.0.0.1:20021", // for prouser 85 | } 86 | 87 | type getUserDomain struct { 88 | postNotSupported 89 | putNotSupported 90 | deleteNotSupported 91 | } 92 | 93 | func (getUserDomain) URI() string { 94 | return "/getDomain" 95 | } 96 | 97 | func (getUserDomain) Get(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) response { 98 | user := r.FormValue("username") 99 | 100 | if user == "vsuser" { 101 | return response{200, "Username found", domains[0]} 102 | } 103 | 104 | if user == "prouser" { 105 | return response{200, "Username found", domains[1]} 106 | } 107 | 108 | return response{400, "Username not found", ""} 109 | } 110 | 111 | // LaunchTestRestServer Launch test server 112 | func LaunchTestRestServer() (*http.Server, error) { 113 | router := httprouter.New() 114 | addResource(router, new(getUserDomain)) 115 | 116 | srv := &http.Server{Addr: "127.0.0.1:8080", Handler: router} 117 | 118 | go func() { 119 | if err := srv.ListenAndServe(); err != nil { 120 | fmt.Println("unable run test webapi server") 121 | } 122 | }() 123 | 124 | return srv, nil 125 | } 126 | 127 | // LaunchUnitTestRestServer Launch test server for unit test 128 | func LaunchUnitTestRestServer(t *testing.T) *httptest.Server { 129 | router := httprouter.New() 130 | addResource(router, new(getUserDomain)) 131 | 132 | srv := httptest.NewServer(router) 133 | 134 | return srv 135 | } 136 | -------------------------------------------------------------------------------- /test/utils.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "crypto/rsa" 7 | "crypto/tls" 8 | "crypto/x509" 9 | "crypto/x509/pkix" 10 | "encoding/pem" 11 | "math/big" 12 | "net" 13 | "strings" 14 | "testing" 15 | "time" 16 | ) 17 | 18 | // GetCertificate method is quoted 19 | // https://github.com/fclairamb/ftpserver 20 | func GetCertificate() (*tls.Certificate, error) { 21 | priv, err := rsa.GenerateKey(rand.Reader, 2048) 22 | 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | now := time.Now().UTC() 28 | 29 | template := &x509.Certificate{ 30 | SerialNumber: big.NewInt(1337), 31 | Subject: pkix.Name{ 32 | CommonName: "localhost", 33 | Organization: []string{"FTPServer"}, 34 | }, 35 | DNSNames: []string{"localhost"}, 36 | SignatureAlgorithm: x509.SHA256WithRSA, 37 | PublicKeyAlgorithm: x509.RSA, 38 | NotBefore: now.Add(-time.Hour), 39 | NotAfter: now.Add(time.Hour * 24 * 7), 40 | SubjectKeyId: []byte{1, 2, 3, 4, 5}, 41 | BasicConstraintsValid: true, 42 | IsCA: false, 43 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, 44 | KeyUsage: x509.KeyUsageDigitalSignature, 45 | } 46 | derBytes, err := x509.CreateCertificate(rand.Reader, template, template, &priv.PublicKey, priv) 47 | 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | var certPem, keyPem bytes.Buffer 53 | if err := pem.Encode(&certPem, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil { 54 | return nil, err 55 | } 56 | if err := pem.Encode(&keyPem, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}); err != nil { 57 | return nil, err 58 | } 59 | c, err := tls.X509KeyPair(certPem.Bytes(), keyPem.Bytes()) 60 | return &c, err 61 | } 62 | 63 | func launchTestServer(t *testing.T) net.Listener { 64 | s, err := net.Listen("tcp", "127.0.0.1:0") 65 | if err != nil { 66 | t.Fatal(err) 67 | } 68 | return s 69 | } 70 | 71 | // LaunchTestServer Launch test server 72 | func LaunchTestServer(server *net.Listener, conn chan net.Conn, done chan struct{}, serverready chan struct{}, t *testing.T) { 73 | *server = launchTestServer(t) 74 | defer (*server).Close() 75 | 76 | serverready <- struct{}{} 77 | for { 78 | c, err := (*server).Accept() 79 | if err != nil { 80 | if !strings.Contains(err.Error(), "use of closed network connection") { 81 | t.Fatal(err) 82 | } 83 | break 84 | } 85 | 86 | conn <- c 87 | } 88 | done <- struct{}{} 89 | } 90 | -------------------------------------------------------------------------------- /tls/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDBjCCAe4CCQD+h4TwR2Tg6DANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJB 3 | VTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0 4 | cyBQdHkgTHRkMB4XDTE4MDcwMTEyMDQyN1oXDTI4MDYyODEyMDQyN1owRTELMAkG 5 | A1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0 6 | IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB 7 | ALB9623xSC/C+ia1gRnQ6GiujwZjOwSe1G6O3C/z8qYWvAPU2YkfwjFXA9av8HLD 8 | IRMegrr7MA4DHJvvaSmuWt0qNkkA9dZsh+5NLF+8JtlrbD4JBldwFeExZRTmvzj+ 9 | ojSjoa9Kzxwtaz7L7YoEvQQ0Tc/bQezCUxGtt1wAN5I3JXi9uP7IoquR81AJMsGl 10 | gDM9yxe6SX9C1uuLLKiCeygwfAhe6sa7oXT7M2fu+xJsQQ/5gfxJYjISYwng/YKU 11 | awFHngLwju6u9NlmH/cLeY34RXbeMLIQk5idGRIgRGMxSj1R8+5FqYNOJFvmFnr6 12 | BkyhVzPKnpP2skg5Ur0G02UCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAUb6EqqiQ 13 | qPlCyKbagBYBsxAoy13Mn9rlv4v7OqbhsqEQsa3JN5ziP7h4buBxiqJf4kPRUmux 14 | Nlq8B+t7TB9FKAJauRQ1OCiTWfEZEFDQ3GcO7FdyA8jrwQn0clNLerYjheDAmb7G 15 | wHl9tNrpciJpGefXGRBCHMPtkn0MYQqxWo46cBFyKbh6GSG4GtuLBMcIY0K7VOps 16 | lHTF7GxIG0RrpeQcE1h8RqZkK8qlE9x/RGAWSmMrjAcLM/oN0IqzwcCUzPajdKpz 17 | Hgl9vsKGgFDTaeOIf1oXoz9d78KWbMGzzj6h36dam3Yt8p3M930n8fzSyVpCdmOq 18 | 3rK1eUb6xqOGMA== 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /tls/server.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICijCCAXICAQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUx 3 | ITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcN 4 | AQEBBQADggEPADCCAQoCggEBALB9623xSC/C+ia1gRnQ6GiujwZjOwSe1G6O3C/z 5 | 8qYWvAPU2YkfwjFXA9av8HLDIRMegrr7MA4DHJvvaSmuWt0qNkkA9dZsh+5NLF+8 6 | JtlrbD4JBldwFeExZRTmvzj+ojSjoa9Kzxwtaz7L7YoEvQQ0Tc/bQezCUxGtt1wA 7 | N5I3JXi9uP7IoquR81AJMsGlgDM9yxe6SX9C1uuLLKiCeygwfAhe6sa7oXT7M2fu 8 | +xJsQQ/5gfxJYjISYwng/YKUawFHngLwju6u9NlmH/cLeY34RXbeMLIQk5idGRIg 9 | RGMxSj1R8+5FqYNOJFvmFnr6BkyhVzPKnpP2skg5Ur0G02UCAwEAAaAAMA0GCSqG 10 | SIb3DQEBCwUAA4IBAQCaIgT6lZnURxVDvFJ/3d46WOcZ3aaKEOuvNAE8mFUv+cdI 11 | N+E1DFePakRciLGCRIHtwJ8b18zVwYIrSBMSOsiw3CRgV/MkixMK9haW3eoWcWAI 12 | J+Xp93jx7HXAorlNv0Jcy8ytZa+IvRLo4LZQaMkuPaUYt6pcSJus61KyLEIc9r9t 13 | s3Sofx+kUh7V+aK+6JrXzUeo9KQ35tbZyKPXYWk2zZaYch5aNdAqawF0g383Mq58 14 | 9gIU2mpJVAzk2GP5OL3SnrhDgQNzhgIjmZArCRp1Lk1V0OxM4YVA0BDWRgpONj0B 15 | H1agzRizEFGpOvmAUlXu07h+0yhoYcVxmkQH+DuS 16 | -----END CERTIFICATE REQUEST----- 17 | -------------------------------------------------------------------------------- /tls/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAsH3rbfFIL8L6JrWBGdDoaK6PBmM7BJ7Ubo7cL/Pypha8A9TZ 3 | iR/CMVcD1q/wcsMhEx6CuvswDgMcm+9pKa5a3So2SQD11myH7k0sX7wm2WtsPgkG 4 | V3AV4TFlFOa/OP6iNKOhr0rPHC1rPsvtigS9BDRNz9tB7MJTEa23XAA3kjcleL24 5 | /siiq5HzUAkywaWAMz3LF7pJf0LW64ssqIJ7KDB8CF7qxruhdPszZ+77EmxBD/mB 6 | /EliMhJjCeD9gpRrAUeeAvCO7q702WYf9wt5jfhFdt4wshCTmJ0ZEiBEYzFKPVHz 7 | 7kWpg04kW+YWevoGTKFXM8qek/aySDlSvQbTZQIDAQABAoIBAFvvzCMCTEMtAxgE 8 | 9pJXI6ncPH7sVAMNJcXNv19vaVghgaFRUFxA4ezH0iUUk2GEygbmntz+GkNCVbXZ 9 | GePvgj+xTIPs7yLaeFindNUN8wLRFZqfIuGLbqpXC7u+k6AB7McU8hWZF5grBLG6 10 | kyuRWlEFWf9LzOSjOZh88IYJSulr2SUJM3cixm+9VLlND5Ej4OO24E7vmQ81xLtG 11 | Lb3gCRl0K9vHAzV4ro6MyhPO0lyHr1sEViEJ8YsUiGfxq4WkrrwPCeXJ4Hi9q4gK 12 | eWauG+M/wfvsItIVYdx8VWth5qiTZzKNauyNQ7b5gSJmLZBqB4Z3s3JOb9W//6xZ 13 | 7zVjP8UCgYEA2SiT6xjDU/FdKPYh+7vSDtBVcO1YFhyBslBz4SOp/KguL/Y/xIwS 14 | uNhyyHjwgedZVICrpAe5VZX3Yb+g9hmaLV9U+bem04PXXByOti2dfJbhSi/mzEsg 15 | Cx2cyO5pDHGJd67SExJPK+27d2wfVx7rK33O4xsPtb23PJX5eHk/FC8CgYEA0A9E 16 | N/kvjRz3cIyBo0X1nzye6MNnZbvdC9okJdKBJC5tkIMmwM/1+R854OgxhB0iJbuG 17 | 0aeQn07JBmaOrg10Iz8V/6zbI3U8AL9gQTPRcocqf8KXBxIqJX92JjGQQTOL8Z3n 18 | pH6WQc1qHxdFG/rTIKmeevNOAQizZROQT17TKKsCgYAX65tXkzO45GZho7McmpTJ 19 | 4vL2bH5+eQdkT+5jx1zrIs9roxFlIhTR3a3PHVtIw+YYI9BEUkF9BvboAobFdQ6B 20 | nvWqSCuNMwGe+NpTgPTPC4One11N9ZyC/PEPRQu3Pi5pS42CYkrsSNSUlAljvFkl 21 | QkduxsVVPJ+Zgd+oAJsNKwKBgQDMRODOL4ke+zx4NIqmmW4AK408q3QdbqckFsR8 22 | mcKOmkKZqhnnHYW6U40Iog7TTaMT9pvzxJb5wWkeLpyQh8bpP+vCPJxdoKELftjq 23 | ywwsbEYubwbGO3BDpnOCJhEh4pDX5Bbj6iBtdFZnNfp08PpNzBo7ThrcawVuDBoX 24 | wtp/xwKBgAckyimrX66b96voAKZnFnMS4M84/+oM0Y3PiMdE44RXAUOf1izr8L+s 25 | 8N61GE/ovdUg+ygXTRG0Ee822qsNd6kTCGerbfVyUynMj9CHw6YK6DRBqzC6kt04 26 | 83aBqqbAWXQozSDymevPXkCVYwWoLXtAanRVQnUGeI/37ELjrIjc 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /version: -------------------------------------------------------------------------------- 1 | 0.5.5 2 | --------------------------------------------------------------------------------