├── .github ├── ISSUE_TEMPLATE.md └── workflows │ └── build.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── LICENSE ├── README.md ├── authorization.go ├── azure-pipelines.yml ├── cache └── cache.go ├── cmd ├── license.go ├── main.go ├── migrate.go ├── reload.go ├── root.go ├── run.go ├── shutdown.go └── version.go ├── commons └── close.go ├── config.go ├── data └── license.go ├── docs ├── docs.go ├── swagger.json └── swagger.yaml ├── entry └── entry.go ├── environments ├── download.go ├── environmentloader.go ├── environmentloader_linux.go ├── environmentloader_windows.go ├── envs │ ├── environment.go │ └── environmentfactory.go └── impl │ ├── docker │ ├── docker.go │ └── factory.go │ ├── lxd │ ├── factory.go │ └── lxd.go │ ├── standard │ ├── standard.go │ └── standardfactory.go │ └── tty │ ├── tty.go │ └── ttyfactory.go ├── errors.go ├── go.mod ├── go.sum ├── httphandlers └── oauth2.go ├── httpmodels.go ├── messages ├── filedesc.go └── message.go ├── oauth2 ├── shared.go └── ssh.go ├── programs ├── loader.go ├── operations │ ├── impl │ │ ├── command │ │ │ ├── command.go │ │ │ └── factory.go │ │ ├── download │ │ │ ├── download.go │ │ │ └── factory.go │ │ ├── forgedl │ │ │ ├── factory.go │ │ │ └── forgedl.go │ │ ├── mkdir │ │ │ ├── factory.go │ │ │ └── mkdir.go │ │ ├── mojangdl │ │ │ ├── factory.go │ │ │ └── mojangdl.go │ │ ├── move │ │ │ ├── factory.go │ │ │ └── move.go │ │ ├── spongeforgedl │ │ │ ├── factory.go │ │ │ └── spongeforgedl.go │ │ └── writefile │ │ │ ├── factory.go │ │ │ └── writefile.go │ ├── operationprocess.go │ ├── operationprocess_linux.go │ ├── operationprocess_windows.go │ └── ops │ │ └── operation.go └── program.go ├── routing ├── root.go ├── server │ ├── server.go │ └── websocket.go └── swagger │ ├── loader_docs.go │ └── loader_nodocs.go ├── sftp ├── requestprefix.go └── server.go ├── shutdown └── shutdown.go ├── utils └── websockettracker.go └── version.go /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Please use the [community forums](https://community.pufferpanel.com) for issues, questions, or help with installation or configuration of PufferPanel. 2 | This issue tracker is only for confirmed bug reports. Please consider opening a thread on the community forums before opening an issue here. 3 | Please delete these 3 lines before submitting your issue. 4 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | build: 9 | name: Build 10 | runs-on: ${{ matrix.os }} 11 | 12 | strategy: 13 | matrix: 14 | os: [ubuntu-18.04] 15 | goos: [ linux, windows ] 16 | arch: [amd64, arm] 17 | go: [ '1.12' ] 18 | exclude: 19 | - goos: windows 20 | arch: arm 21 | 22 | steps: 23 | 24 | - name: Set up Go 25 | uses: actions/setup-go@v1 26 | with: 27 | go-version: ${{ matrix.go }} 28 | id: go 29 | 30 | - name: Check out code into the Go module directory 31 | uses: actions/checkout@v1 32 | 33 | - name: Generate swagger docs 34 | run: | 35 | go get -u github.com/swaggo/swag/cmd/swag 36 | export PATH=$PATH:$(go env GOPATH)/bin 37 | go build github.com/pufferpanel/pufferd/v2 38 | swag init --parseDependency -g routing/root.go 39 | 40 | - name: Build 41 | run: | 42 | go build -ldflags "-X github.com/pufferpanel/pufferd/version.Hash=${{ github.sha }}" -v github.com/pufferpanel/pufferd/v2 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | .idea/ 26 | *.iml 27 | 28 | .DS_Store 29 | 30 | vendor/ 31 | config.json 32 | !resources/config.json 33 | logs/ 34 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at report@pufferpanel.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ### 2 | # Builder container 3 | ### 4 | FROM golang:alpine AS builder 5 | 6 | ARG tags=none 7 | 8 | RUN go version && \ 9 | apk add --update --no-cache gcc musl-dev git curl make gcc g++ python && \ 10 | mkdir /pufferpanel && \ 11 | wget https://github.com/swaggo/swag/releases/download/v1.6.3/swag_1.6.3_Linux_x86_64.tar.gz && \ 12 | tar -zxf swag*.tar.gz && \ 13 | mv swag /go/bin/ 14 | 15 | WORKDIR /build/apufferi 16 | RUN git clone https://github.com/pufferpanel/apufferi /build/apufferi 17 | 18 | WORKDIR /build/pufferd 19 | COPY . . 20 | RUN echo replace github.com/pufferpanel/apufferi/v4 =\> ../apufferi >> go.mod && \ 21 | go get -u github.com/pufferpanel/pufferd/v2/cmd 22 | 23 | RUN go build -v -tags $tags -o /pufferpanel/pufferd github.com/pufferpanel/pufferd/v2/cmd 24 | 25 | ### 26 | # Generate final image 27 | ### 28 | 29 | FROM alpine 30 | COPY --from=builder /pufferpanel /pufferpanel 31 | 32 | EXPOSE 5656 5657 33 | VOLUME /var/lib/pufferd 34 | 35 | ENV PUFFERD_CONSOLE_BUFFER=50 \ 36 | PUFFERD_CONSOLE_FORWARD=false \ 37 | PUFFERD_LISTEN_WEB=0.0.0.0:5656 \ 38 | PUFFERD_LISTEN_WEBCERT=/var/lib/pufferd/web/https.pem \ 39 | PUFFERD_LISTEN_WEBKEY=/var/lib/pufferd/web/https.key \ 40 | PUFFERD_LISTEN_SFTP=0.0.0.0:5657 \ 41 | PUFFERD_LISTEN_SFTPKEY=/var/lib/pufferd/sftp.key \ 42 | PUFFERD_AUTH_PUBLICKEY=/var/lib/pufferd/panel.pem \ 43 | PUFFERD_AUTH_URL=http://pufferpanel:8080 \ 44 | PUFFERD_AUTH_CLIENTID=unknown \ 45 | PUFFERD_AUTH_CLIENTSECRET=unknown \ 46 | PUFFERD_DATA_CACHE=/var/lib/pufferd/cache \ 47 | PUFFERD_DATA_SERVERS=/var/lib/pufferd/servers \ 48 | PUFFERD_DATA_MODULES=/var/lib/pufferd/modules \ 49 | PUFFERD_DATA_LOGS=/var/lib/pufferd/logs \ 50 | PUFFERD_DATA_CRASHLIMIT=3 51 | 52 | WORKDIR /var/lib/pufferd 53 | 54 | ENTRYPOINT ["/pufferpanel/pufferd"] 55 | CMD ["--logging=DEVEL", "run"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pufferd 2 | The PufferPanel daemon 3 | 4 | Travis Status - [![Travis Build Status](https://travis-ci.org/PufferPanel/pufferd.svg?branch=master)](https://travis-ci.org/PufferPanel/pufferd) 5 | 6 | Linux Build Status - [![Linux Build Status](https://ci.pufferpanel.com/app/rest/builds/buildType:core/statusIcon.svg)](https://ci.pufferpanel.com/viewType.html?buildTypeId=core) 7 | 8 | Windows Build Status - [![Windows Build Status](https://ci.pufferpanel.com/app/rest/builds/buildType:core-windows/statusIcon.svg)](https://ci.pufferpanel.com/viewType.html?buildTypeId=pufferd_core_windows) 9 | 10 | [Documentation](https://pufferpanel.com) 11 | 12 | [API Documentation](https://speca.io/PufferPanel/pufferd) 13 | 14 | # Coming soon 15 | Eventually we'll fill the readme in, but not now. 16 | 17 | Repositories graciously hosted by [packagecloud](https://packagecloud.io) 18 | 19 | [![packagecloud](https://packagecloud.io/images/packagecloud-badge.png)](https://packagecloud.io) 20 | -------------------------------------------------------------------------------- /authorization.go: -------------------------------------------------------------------------------- 1 | package pufferd 2 | 3 | import ( 4 | "bytes" 5 | "crypto/ecdsa" 6 | "crypto/x509" 7 | "encoding/pem" 8 | "github.com/pufferpanel/apufferi/v4" 9 | "github.com/spf13/viper" 10 | "golang.org/x/crypto/ssh" 11 | "io" 12 | "os" 13 | "sync" 14 | ) 15 | 16 | type SFTPAuthorization interface { 17 | Validate(username, password string) (perms *ssh.Permissions, err error) 18 | } 19 | 20 | var publicKey *ecdsa.PublicKey 21 | 22 | var atLocker = &sync.RWMutex{} 23 | 24 | func SetPublicKey(key *ecdsa.PublicKey) { 25 | atLocker.Lock() 26 | defer atLocker.Unlock() 27 | publicKey = key 28 | } 29 | 30 | func GetPublicKey() *ecdsa.PublicKey { 31 | atLocker.RLock() 32 | defer atLocker.RUnlock() 33 | return publicKey 34 | } 35 | 36 | func LoadPublicKey() (*ecdsa.PublicKey, error) { 37 | publicKey := GetPublicKey() 38 | if publicKey != nil { 39 | return publicKey, nil 40 | } 41 | 42 | f, err := os.OpenFile(viper.GetString("auth.publicKey"), os.O_RDONLY, 660) 43 | defer apufferi.Close(f) 44 | 45 | var buf bytes.Buffer 46 | 47 | _, _ = io.Copy(&buf, f) 48 | 49 | block, _ := pem.Decode(buf.Bytes()) 50 | if block == nil { 51 | return nil, ErrKeyNotPEM 52 | } 53 | pub, err := x509.ParsePKIXPublicKey(block.Bytes) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | publicKey, ok := pub.(*ecdsa.PublicKey) 59 | if !ok { 60 | return nil, ErrKeyNotECDSA 61 | } 62 | 63 | SetPublicKey(publicKey) 64 | return publicKey, nil 65 | } 66 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # Go 2 | # Build your Go project. 3 | # Add steps that test, save build artifacts, deploy, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/go 5 | 6 | trigger: 7 | - master 8 | 9 | name: $(majorVersion)$(Rev:.r) 10 | 11 | pool: 12 | vmImage: 'Ubuntu-16.04' 13 | 14 | variables: 15 | version: $(Build.BuildNumber) 16 | packageVersion: v2 17 | majorVersion: 2.0.0 18 | 19 | steps: 20 | - script: | 21 | mkdir /build 22 | go version 23 | displayName: 'Set up the Go workspace' 24 | 25 | - script: | 26 | export GOOS=windows 27 | export GOARCH=amd64 28 | go build -o /build/pufferd.exe -ldflags "-X github.com/pufferpanel/pufferd/version.Hash=$(Build.SourceVersion) -X github.com/pufferpanel/pufferd.Version=$(version)" -v github.com/pufferpanel/pufferd/$(packageVersion)/cmd 29 | displayName: 'Windows Build' 30 | 31 | - script: | 32 | export GOOS=linux 33 | export GOARCH=amd64 34 | go build -o /build/pufferd -ldflags "-X github.com/pufferpanel/pufferd/version.Hash=$(Build.SourceVersion) -X github.com/pufferpanel/pufferd.Version=$(version)" -v github.com/pufferpanel/pufferd/$(packageVersion)/cmd 35 | displayName: 'Linux Build' 36 | 37 | - script: | 38 | export GOOS=linux 39 | export GOARCH=arm 40 | export GOARM=7 41 | go build -o /build/pufferd-arm -ldflags "-X github.com/pufferpanel/pufferd/version.Hash=$(Build.SourceVersion) -X github.com/pufferpanel/pufferd.Version=$(version)" -v github.com/pufferpanel/pufferd/$(packageVersion)/cmd 42 | displayName: 'Linux Build - ARM' 43 | 44 | - task: UseRubyVersion@0 45 | inputs: 46 | versionSpec: '>= 2.4' 47 | addToPath: true 48 | 49 | - script: | 50 | gem install --no-doc rake 51 | gem install --no-doc fpm && fpm -v 52 | gem install --no-doc package_cloud && package_cloud version 53 | displayName: 'Install gem deps' 54 | 55 | - script: | 56 | git clone https://github.com/PufferPanel/systemd 57 | git clone https://github.com/PufferPanel/templates 58 | displayName: 'Get supporting files' 59 | 60 | - script: | 61 | mkdir /build/templates 62 | cd templates 63 | cp */*.json /build/templates 64 | displayName: 'Build templates' 65 | 66 | - script: | 67 | fpm -s dir -t deb \ 68 | --name pufferd -v $(version) \ 69 | --maintainer dev@pufferpanel.com \ 70 | --deb-user pufferd --deb-group pufferd \ 71 | --deb-systemd servicefiles/systemd/pufferd \ 72 | --before-install scripts/preinst.sh \ 73 | --after-upgrade scripts/postupgrade.sh \ 74 | --before-remove scripts/prerm.sh \ 75 | --deb-after-purge scripts/purge.sh \ 76 | /build/pufferd=/usr/sbin/pufferd \ 77 | /build/templates=/var/lib/pufferd/ 78 | 79 | mkdir /build/xenial 80 | mv *.deb /build/xenial 81 | displayName: 'Create packages' 82 | workingDirectory: systemd/pufferd/xenial 83 | 84 | - script: | 85 | fpm -s deb -t rpm \ 86 | --name pufferd \ 87 | --maintainer dev@pufferpanel.com \ 88 | --rpm-user pufferd --rpm-group pufferd \ 89 | --before-install scripts/preinst.sh \ 90 | --after-upgrade scripts/postupgrade.sh \ 91 | --before-remove scripts/prerm.sh \ 92 | /build/xenial/pufferd_*_amd64.deb 93 | 94 | mkdir /build/rpm 95 | mv *.rpm /build/rpm 96 | displayName: 'Create packages' 97 | workingDirectory: systemd/pufferd/rpm7 98 | 99 | - script: | 100 | fpm -s dir -t deb \ 101 | --name pufferd -v $(version) \ 102 | --maintainer dev@pufferpanel.com \ 103 | --deb-user pufferd --deb-group pufferd \ 104 | --deb-init servicefiles/initd/pufferd \ 105 | --before-install scripts/preinst.sh \ 106 | --before-remove scripts/prerm.sh \ 107 | --deb-after-purge scripts/purge.sh \ 108 | /build/pufferd=/usr/sbin/pufferd \ 109 | /build/templates=/var/lib/pufferd/ 110 | 111 | mkdir /build/trusty 112 | mv *.deb /build/trusty 113 | displayName: 'Create packages' 114 | workingDirectory: systemd/pufferd/trusty 115 | 116 | - script: | 117 | fpm -s dir -t deb \ 118 | --name pufferd -v $(version) \ 119 | --maintainer dev@pufferpanel.com \ 120 | --deb-user pufferpanel --deb-group pufferd \ 121 | --deb-systemd servicefiles/systemd/pufferd \ 122 | --before-install scripts/preinst.sh \ 123 | --after-upgrade scripts/postupgrade.sh \ 124 | --before-remove scripts/prerm.sh \ 125 | --deb-after-purge scripts/purge.sh \ 126 | -a armhf \ 127 | /build/pufferd-arm=/usr/sbin/pufferd \ 128 | /build/templates=/var/lib/pufferd/ 129 | 130 | mkdir /build/buster-arm 131 | mv *.deb /build/buster-arm 132 | displayName: 'Create packages - buster' 133 | workingDirectory: systemd/pufferd/xenial 134 | 135 | - task: PublishPipelineArtifact@0 136 | inputs: 137 | artifactName: 'pufferd' 138 | targetPath: '/build/.' 139 | -------------------------------------------------------------------------------- /cache/cache.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cache 18 | 19 | import ( 20 | "github.com/pufferpanel/apufferi/v4" 21 | "github.com/spf13/viper" 22 | ) 23 | 24 | func CreateCache() *apufferi.MemoryCache { 25 | capacity := viper.GetInt("console.buffer") 26 | if capacity <= 0 { 27 | capacity = 50 28 | } 29 | return &apufferi.MemoryCache{ 30 | Buffer: make([]apufferi.Message, 0), 31 | Capacity: capacity, 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /cmd/license.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "fmt" 21 | "github.com/pufferpanel/pufferd/v2/data" 22 | "github.com/spf13/cobra" 23 | ) 24 | 25 | var licenseCmd = &cobra.Command{ 26 | Use: "license", 27 | Short: "Prints the license", 28 | Run: func(cmd *cobra.Command, args []string) { 29 | fmt.Println(data.LICENSE) 30 | }, 31 | } 32 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/pufferpanel/apufferi/v4/logging" 4 | 5 | func main() { 6 | defer logging.Close() 7 | 8 | Execute() 9 | } 10 | -------------------------------------------------------------------------------- /cmd/migrate.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/pufferpanel/apufferi/v4" 7 | "github.com/pufferpanel/pufferd/v2" 8 | "github.com/pufferpanel/pufferd/v2/programs" 9 | "github.com/spf13/cobra" 10 | "io" 11 | "io/ioutil" 12 | "os" 13 | "path" 14 | "strings" 15 | ) 16 | 17 | var migrateCmd = &cobra.Command{ 18 | Use: "migrate", 19 | Short: "This will migrate v1 servers to v2", 20 | Run: migrate, 21 | } 22 | 23 | func migrate(cmd *cobra.Command, args [] string) { 24 | _ = pufferd.LoadConfig() 25 | 26 | programFiles, err := ioutil.ReadDir(programs.ServerFolder) 27 | if err != nil { 28 | fmt.Printf("Error reading from server data folder: %s\n", err) 29 | return 30 | } 31 | 32 | backupDir := path.Join(programs.ServerFolder, "backup") 33 | 34 | err = os.MkdirAll(backupDir, 0755) 35 | if err != nil { 36 | fmt.Printf("Error creating backup directory %s: %s\n", backupDir, err) 37 | return 38 | } 39 | 40 | for _, element := range programFiles { 41 | if element.IsDir() || !strings.HasSuffix(element.Name(), ".json") { 42 | continue 43 | } 44 | 45 | var fullPath = path.Join(programs.ServerFolder, element.Name()) 46 | 47 | err = backupFile(fullPath, backupDir) 48 | if err != nil { 49 | fmt.Printf("Error backing up file: %s\n", err) 50 | continue 51 | } 52 | 53 | err = migrateFile(fullPath) 54 | if err != nil { 55 | fmt.Printf("Error migrating file: %s\n", err) 56 | continue 57 | } 58 | } 59 | } 60 | 61 | func migrateFile(name string) (err error) { 62 | fmt.Printf("Attempting to migrate %s\n", name) 63 | 64 | program, err := loadFile(name) 65 | if err != nil { 66 | return 67 | } 68 | 69 | //copy base items over that map directly 70 | replacement := apufferi.Server{ 71 | Variables: make(map[string]apufferi.Variable), 72 | Display: program.ProgramData.Display, 73 | Environment: apufferi.Type{ 74 | Type: program.ProgramData.EnvironmentData["type"].(string), 75 | }, 76 | Installation: make([]interface{}, 0), 77 | Uninstallation: make([]interface{}, 0), 78 | Type: program.ProgramData.Type, 79 | Identifier: program.ProgramData.Identifier, 80 | Execution: apufferi.Execution{ 81 | Arguments: program.ProgramData.RunData.Arguments, 82 | ProgramName: program.ProgramData.RunData.Program, 83 | StopCommand: program.ProgramData.RunData.Stop, 84 | //Disabled: !program.ProgramData.RunData.Enabled, 85 | Disabled: false, 86 | AutoStart: program.ProgramData.RunData.AutoStart, 87 | AutoRestartFromCrash: program.ProgramData.RunData.AutoRestartFromCrash, 88 | AutoRestartFromGraceful: program.ProgramData.RunData.AutoRestartFromGraceful, 89 | PreExecution: make([]interface{}, 0), 90 | PostExecution: make([]interface{}, 0), 91 | StopCode: program.ProgramData.RunData.StopCode, 92 | EnvironmentVariables: program.ProgramData.RunData.EnvironmentVariables, 93 | }, 94 | } 95 | 96 | //copy data 97 | for k, v := range program.ProgramData.Data { 98 | replacement.Variables[k] = apufferi.Variable{ 99 | Description: v.Description, 100 | Display: v.Display, 101 | Internal: v.Internal, 102 | Required: v.Required, 103 | Value: v.Value, 104 | UserEditable: v.UserEditable, 105 | Type: "text", 106 | Options: nil, 107 | } 108 | } 109 | 110 | //copy installation and uninstall sections by removing type from map 111 | for _, v := range program.ProgramData.InstallData.Operations { 112 | c := make(map[string]interface{}) 113 | for i, o := range v { 114 | if i == "type" { 115 | continue 116 | } 117 | c[i] = o 118 | } 119 | 120 | replacement.Installation = append(replacement.Installation, apufferi.MetadataType{ 121 | Type: v["type"].(string), 122 | Metadata: c, 123 | }) 124 | } 125 | 126 | for _, v := range program.ProgramData.UninstallData.Operations { 127 | c := make(map[string]interface{}) 128 | for i, o := range v { 129 | if i == "type" { 130 | continue 131 | } 132 | c[i] = o 133 | } 134 | 135 | replacement.Uninstallation = append(replacement.Uninstallation, apufferi.MetadataType{ 136 | Type: v["type"].(string), 137 | Metadata: c, 138 | }) 139 | } 140 | 141 | for _, v := range program.ProgramData.RunData.Pre { 142 | c := make(map[string]interface{}) 143 | for i, o := range v { 144 | if i == "type" { 145 | continue 146 | } 147 | c[i] = o 148 | } 149 | 150 | replacement.Execution.PreExecution = append(replacement.Execution.PreExecution, apufferi.MetadataType{ 151 | Type: v["type"].(string), 152 | Metadata: c, 153 | }) 154 | } 155 | 156 | for _, v := range program.ProgramData.RunData.Post { 157 | c := make(map[string]interface{}) 158 | for i, o := range v { 159 | if i == "type" { 160 | continue 161 | } 162 | c[i] = o 163 | } 164 | 165 | replacement.Execution.PostExecution = append(replacement.Execution.PostExecution, apufferi.MetadataType{ 166 | Type: v["type"].(string), 167 | Metadata: c, 168 | }) 169 | } 170 | 171 | target, err := os.OpenFile(name, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) 172 | encoder := json.NewEncoder(target) 173 | encoder.SetIndent("", " ") 174 | err = encoder.Encode(replacement) 175 | return 176 | } 177 | 178 | func backupFile(name string, targetDir string) error { 179 | fmt.Printf("Attempting to backup %s to %s\n", name, targetDir) 180 | source, err := os.Open(name) 181 | if err != nil { 182 | return err 183 | } 184 | defer apufferi.Close(source) 185 | 186 | filename := path.Base(name) 187 | 188 | target, err := os.OpenFile(path.Join(targetDir, filename), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) 189 | if err != nil { 190 | return err 191 | } 192 | defer apufferi.Close(target) 193 | 194 | _, err = io.Copy(target, source) 195 | return err 196 | } 197 | 198 | func loadFile(name string) (program ServerJson, err error) { 199 | file, err := os.Open(name) 200 | if err != nil { 201 | return 202 | } 203 | defer apufferi.Close(file) 204 | 205 | err = json.NewDecoder(file).Decode(&program) 206 | return 207 | } 208 | 209 | //copy of the old v1 objects 210 | type ServerJson struct { 211 | ProgramData ProgramData `json:"pufferd"` 212 | } 213 | 214 | type ProgramData struct { 215 | Data map[string]DataObject `json:"data,omitempty"` 216 | Display string `json:"display,omitempty"` 217 | EnvironmentData map[string]interface{} `json:"environment,omitempty"` 218 | InstallData InstallSection `json:"install,omitempty"` 219 | UninstallData InstallSection `json:"uninstall,omitempty"` 220 | Type string `json:"type,omitempty"` 221 | Identifier string `json:"id,omitempty"` 222 | RunData RunObject `json:"run,omitempty"` 223 | Template string `json:"template,omitempty"` 224 | } 225 | 226 | type DataObject struct { 227 | Description string `json:"desc,omitempty"` 228 | Display string `json:"display,omitempty"` 229 | Internal bool `json:"internal,omitempty"` 230 | Required bool `json:"required,omitempty"` 231 | Value interface{} `json:"value,omitempty"` 232 | UserEditable bool `json:"userEdit,omitempty"` 233 | } 234 | 235 | type RunObject struct { 236 | Arguments []string `json:"arguments,omitempty"` 237 | Program string `json:"program,omitempty"` 238 | Stop string `json:"stop,omitempty"` 239 | Enabled bool `json:"enabled,omitempty"` 240 | AutoStart bool `json:"autostart,omitempty"` 241 | AutoRestartFromCrash bool `json:"autorecover,omitempty"` 242 | AutoRestartFromGraceful bool `json:"autorestart,omitempty"` 243 | Pre []map[string]interface{} `json:"pre,omitempty"` 244 | Post []map[string]interface{} `json:"post,omitempty"` 245 | StopCode int `json:"stopCode,omitempty"` 246 | EnvironmentVariables map[string]string `json:"environmentVars,omitempty"` 247 | } 248 | 249 | type InstallSection struct { 250 | Operations []map[string]interface{} `json:"commands,,omitempty"` 251 | } 252 | -------------------------------------------------------------------------------- /cmd/reload.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "errors" 21 | "github.com/pufferpanel/apufferi/v4/logging" 22 | "github.com/spf13/cobra" 23 | "os" 24 | "syscall" 25 | ) 26 | 27 | var reloadCmd = &cobra.Command{ 28 | Use: "reload", 29 | Short: "Reloads pufferd", 30 | Run: func(cmd *cobra.Command, args []string) { 31 | err := runReload() 32 | if err != nil { 33 | logging.Exception("error running reload", err) 34 | } 35 | }, 36 | } 37 | 38 | var reloadPid int 39 | 40 | func init() { 41 | reloadCmd.Flags().IntVar(&reloadPid, "pid", 0, "process id of daemon") 42 | reloadCmd.MarkPersistentFlagRequired("pid") 43 | } 44 | 45 | func runReload() error { 46 | proc, err := os.FindProcess(reloadPid) 47 | if err != nil || proc == nil { 48 | if err == nil && proc == nil { 49 | err = errors.New("no process found") 50 | } 51 | return err 52 | } 53 | return proc.Signal(syscall.Signal(1)) 54 | } 55 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Padduck, LLC 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package main 15 | 16 | import ( 17 | "fmt" 18 | "github.com/pufferpanel/apufferi/v4/logging" 19 | "github.com/pufferpanel/pufferd/v2" 20 | "github.com/spf13/cobra" 21 | "os" 22 | ) 23 | 24 | var rootCmd = &cobra.Command{ 25 | Use: "pufferd", 26 | Short: "pufferpanel daemon", 27 | } 28 | 29 | var configPath = "config.json" 30 | var loggingLevel = "INFO" 31 | 32 | func init() { 33 | cobra.OnInitialize(load) 34 | 35 | rootCmd.AddCommand( 36 | licenseCmd, 37 | shutdownCmd, 38 | RunCmd, 39 | reloadCmd, 40 | migrateCmd, 41 | versionCmd) 42 | 43 | rootCmd.PersistentFlags().StringVar(&configPath, "config", configPath, "Path to the config to use") 44 | rootCmd.PersistentFlags().StringVar(&loggingLevel, "logging", loggingLevel, "Logging level to print to stdout") 45 | rootCmd.SetVersionTemplate(pufferd.Display) 46 | } 47 | 48 | func Execute() { 49 | rootCmd.SetVersionTemplate(pufferd.Display) 50 | 51 | if err := rootCmd.Execute(); err != nil { 52 | fmt.Println(err) 53 | os.Exit(1) 54 | } 55 | } 56 | 57 | func load() { 58 | level := logging.GetLevel(loggingLevel) 59 | if level == nil { 60 | level = logging.INFO 61 | } 62 | 63 | logging.SetLevel(os.Stdout, level) 64 | } 65 | -------------------------------------------------------------------------------- /cmd/run.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "fmt" 21 | "github.com/pufferpanel/apufferi/v4/logging" 22 | "github.com/pufferpanel/pufferd/v2" 23 | "github.com/pufferpanel/pufferd/v2/entry" 24 | "github.com/spf13/cobra" 25 | "github.com/spf13/viper" 26 | ) 27 | 28 | var RunCmd = &cobra.Command{ 29 | Use: "run", 30 | Short: "Runs the daemon", 31 | Run: runRun, 32 | } 33 | 34 | func runRun(cmd *cobra.Command, args []string) { 35 | pufferd.SetDefaults() 36 | _ = pufferd.LoadConfig() 37 | 38 | var logPath = viper.GetString("data.logs") 39 | _ = logging.WithLogDirectory(logPath, logging.DEBUG, nil) 40 | 41 | err := <-entry.Start() 42 | if err != nil { 43 | fmt.Printf("Error running: %s", err.Error()) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /cmd/shutdown.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "errors" 21 | "github.com/pufferpanel/apufferi/v4/logging" 22 | "github.com/spf13/cobra" 23 | "os" 24 | "syscall" 25 | "time" 26 | ) 27 | 28 | var shutdownCmd = &cobra.Command{ 29 | Use: "shutdown", 30 | Short: "Shuts down pufferd", 31 | Run: func(cmd *cobra.Command, args []string) { 32 | err := runShutdown() 33 | if err != nil { 34 | logging.Exception("error running shutdown", err) 35 | } 36 | }, 37 | } 38 | 39 | var shutdownPid int 40 | 41 | func init() { 42 | shutdownCmd.Flags().IntVar(&shutdownPid, "pid", 0, "process id of daemon") 43 | shutdownCmd.MarkFlagRequired("pid") 44 | } 45 | 46 | func runShutdown() error { 47 | proc, err := os.FindProcess(shutdownPid) 48 | if err != nil || proc == nil { 49 | if err == nil && proc == nil { 50 | err = errors.New("no process found") 51 | } 52 | return err 53 | } 54 | err = proc.Signal(syscall.Signal(15)) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | wait := make(chan error) 60 | 61 | waitForProcess(proc, wait) 62 | 63 | err = <-wait 64 | 65 | if err != nil { 66 | return err 67 | } 68 | 69 | err = proc.Release() 70 | if err != nil { 71 | return err 72 | } 73 | 74 | return nil 75 | } 76 | 77 | func waitForProcess(process *os.Process, c chan error) { 78 | var err error 79 | timer := time.NewTicker(100 * time.Millisecond) 80 | go func() { 81 | for range timer.C { 82 | err = process.Signal(syscall.Signal(0)) 83 | if err != nil { 84 | if err.Error() == "os: process already finished" { 85 | c <- nil 86 | } else { 87 | c <- err 88 | } 89 | 90 | timer.Stop() 91 | } else { 92 | } 93 | } 94 | }() 95 | } 96 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Padduck, LLC 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package main 15 | 16 | import ( 17 | "fmt" 18 | "github.com/pufferpanel/pufferd/v2" 19 | "github.com/spf13/cobra" 20 | ) 21 | 22 | var versionCmd = &cobra.Command{ 23 | Use: "version", 24 | Short: "Print the version number of pufferd", 25 | Run: executeVersion, 26 | } 27 | 28 | func executeVersion(cmd *cobra.Command, args []string) { 29 | fmt.Println(pufferd.Display) 30 | } -------------------------------------------------------------------------------- /commons/close.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package commons 18 | 19 | import ( 20 | "github.com/pufferpanel/apufferi/v4" 21 | "net/http" 22 | ) 23 | 24 | func CloseResponse(response *http.Response) { 25 | if response != nil { 26 | apufferi.Close(response.Body) 27 | } 28 | } -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package pufferd 2 | 3 | import ( 4 | "github.com/spf13/viper" 5 | "strings" 6 | ) 7 | 8 | func init() { 9 | //env configuration 10 | viper.SetEnvPrefix("PUFFERPANEL") 11 | viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) 12 | viper.AutomaticEnv() 13 | } 14 | 15 | func SetDefaults() { 16 | //defaults we can set at this point in time 17 | viper.SetDefault("console.buffer", 50) 18 | viper.SetDefault("console.forward", false) 19 | 20 | viper.SetDefault("listen.web", "0.0.0.0:5656") 21 | //viper.SetDefault("listen.socket", "unix:/var/run/pufferd.sock") 22 | viper.SetDefault("listen.webCert", "https.pem") 23 | viper.SetDefault("listen.webKey", "https.key") 24 | viper.SetDefault("listen.sftp", "0.0.0.0:5657") 25 | viper.SetDefault("listen.sftpKey", "sftp.key") 26 | 27 | viper.SetDefault("auth.publicKey", "panel.pem") 28 | 29 | viper.SetDefault("auth.url", "http://localhost:8080") 30 | 31 | viper.SetDefault("auth.clientId", "") 32 | viper.SetDefault("auth.clientSecret", "") 33 | 34 | viper.SetDefault("data.cache", "cache") 35 | viper.SetDefault("data.servers", "servers") 36 | viper.SetDefault("data.modules", "modules") 37 | viper.SetDefault("data.logs", "logs") 38 | viper.SetDefault("data.crashLimit", 3) 39 | viper.SetDefault("data.maxWSDownloadSize", int64(1024*1024*20)) //1024 bytes (1KB) * 1024 (1MB) * 50 (50MB)) 40 | } 41 | 42 | func LoadConfig() error { 43 | if err := viper.ReadInConfig(); err != nil { 44 | if _, ok := err.(viper.ConfigFileNotFoundError); ok { 45 | //this is just a missing config, since ENV is supported, ignore 46 | } else { 47 | return err 48 | } 49 | } 50 | return nil 51 | } 52 | -------------------------------------------------------------------------------- /data/license.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package data 18 | 19 | const LICENSE = `Copyright 2016 Padduck, LLC 20 | 21 | Licensed under the Apache License, Version 2.0 (the "License"); 22 | you may not use this file except in compliance with the License. 23 | You may obtain a copy of the License at 24 | 25 | http://www.apache.org/licenses/LICENSE-2.0 26 | 27 | Unless required by applicable law or agreed to in writing, software 28 | distributed under the License is distributed on an "AS IS" BASIS, 29 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 30 | See the License for the specific language governing permissions and 31 | limitations under the License.` 32 | -------------------------------------------------------------------------------- /entry/entry.go: -------------------------------------------------------------------------------- 1 | package entry 2 | 3 | import ( 4 | "fmt" 5 | "github.com/braintree/manners" 6 | "github.com/pufferpanel/apufferi/v4/logging" 7 | "github.com/pufferpanel/pufferd/v2" 8 | "github.com/pufferpanel/pufferd/v2/environments" 9 | "github.com/pufferpanel/pufferd/v2/programs" 10 | "github.com/pufferpanel/pufferd/v2/routing" 11 | "github.com/pufferpanel/pufferd/v2/sftp" 12 | "github.com/pufferpanel/pufferd/v2/shutdown" 13 | "github.com/spf13/viper" 14 | "os" 15 | "os/signal" 16 | "runtime/debug" 17 | "syscall" 18 | ) 19 | 20 | var runService = true 21 | 22 | func Start() chan error { 23 | errChan := make(chan error) 24 | go entry(errChan) 25 | return errChan 26 | } 27 | 28 | func entry(errChan chan error) { 29 | logging.Info(pufferd.Display) 30 | 31 | environments.LoadModules() 32 | programs.Initialize() 33 | 34 | var err error 35 | 36 | if _, err = os.Stat(programs.ServerFolder); os.IsNotExist(err) { 37 | logging.Info("No server directory found, creating") 38 | err = os.MkdirAll(programs.ServerFolder, 0755) 39 | if err != nil && !os.IsExist(err) { 40 | errChan <- err 41 | return 42 | } 43 | } 44 | 45 | programs.LoadFromFolder() 46 | 47 | programs.InitService() 48 | 49 | for _, element := range programs.GetAll() { 50 | if element.IsEnabled() { 51 | element.GetEnvironment().DisplayToConsole(true, "Daemon has been started\n") 52 | if element.IsAutoStart() { 53 | logging.Info("Queued server %s", element.Id()) 54 | element.GetEnvironment().DisplayToConsole(true, "Server has been queued to start\n") 55 | programs.StartViaService(element) 56 | } 57 | } 58 | } 59 | 60 | createHook() 61 | 62 | for runService && err == nil { 63 | err = runServices() 64 | } 65 | 66 | shutdown.Shutdown() 67 | 68 | errChan <- err 69 | return 70 | } 71 | 72 | func runServices() error { 73 | defer recoverPanic() 74 | 75 | router := routing.ConfigureWeb() 76 | 77 | useHttps := false 78 | 79 | httpsPem := viper.GetString("listen.webCert") 80 | httpsKey := viper.GetString("listen.webKey") 81 | 82 | if _, err := os.Stat(httpsPem); os.IsNotExist(err) { 83 | logging.Warn("No HTTPS.PEM found in data folder, will use http instead") 84 | } else if _, err := os.Stat(httpsKey); os.IsNotExist(err) { 85 | logging.Warn("No HTTPS.KEY found in data folder, will use http instead") 86 | } else { 87 | useHttps = true 88 | } 89 | 90 | sftp.Run() 91 | 92 | web := viper.GetString("listen.web") 93 | 94 | logging.Info("Starting web access on %s", web) 95 | var err error 96 | if useHttps { 97 | err = manners.ListenAndServeTLS(web, httpsPem, httpsKey, router) 98 | } else { 99 | err = manners.ListenAndServe(web, router) 100 | } 101 | 102 | return err 103 | } 104 | 105 | func createHook() { 106 | c := make(chan os.Signal, 1) 107 | signal.Notify(c, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGPIPE) 108 | go func() { 109 | defer func() { 110 | if err := recover(); err != nil { 111 | logging.Error("%+v\n%s", err, debug.Stack()) 112 | } 113 | }() 114 | 115 | var sig os.Signal 116 | 117 | for sig != syscall.SIGTERM { 118 | sig = <-c 119 | switch sig { 120 | case syscall.SIGHUP: 121 | //manners.Close() 122 | //sftp.Stop() 123 | _ = pufferd.LoadConfig() 124 | case syscall.SIGPIPE: 125 | //ignore SIGPIPEs for now, we're somehow getting them and it's causing issues 126 | } 127 | } 128 | 129 | runService = false 130 | shutdown.CompleteShutdown() 131 | }() 132 | } 133 | 134 | func recoverPanic() { 135 | if rec := recover(); rec != nil { 136 | err := rec.(error) 137 | fmt.Printf("CRITICAL: %s", err.Error()) 138 | logging.Critical("Unhandled error: %s", err.Error()) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /environments/download.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package environments 18 | 19 | import ( 20 | "crypto/sha1" 21 | "fmt" 22 | "github.com/pufferpanel/apufferi/v4" 23 | "github.com/pufferpanel/apufferi/v4/logging" 24 | "github.com/pufferpanel/pufferd/v2/commons" 25 | "github.com/pufferpanel/pufferd/v2/environments/envs" 26 | "github.com/spf13/viper" 27 | "io" 28 | "log" 29 | "net/http" 30 | "os" 31 | "path" 32 | "path/filepath" 33 | "strings" 34 | ) 35 | 36 | func DownloadFile(url, fileName string, env envs.Environment) error { 37 | target, err := os.Create(path.Join(env.GetRootDirectory(), fileName)) 38 | defer apufferi.Close(target) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | client := &http.Client{} 44 | 45 | logging.Debug("Downloading: %s", url) 46 | env.DisplayToConsole(true, "Downloading: "+url+"\n") 47 | 48 | response, err := client.Get(url) 49 | defer commons.CloseResponse(response) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | _, err = io.Copy(target, response.Body) 55 | return err 56 | } 57 | 58 | func DownloadFileToCache(url, fileName string) error { 59 | parent := filepath.Dir(fileName) 60 | err := os.MkdirAll(parent, 0755) 61 | if err != nil && !os.IsExist(err) { 62 | return err 63 | } 64 | 65 | target, err := os.Create(fileName) 66 | defer apufferi.Close(target) 67 | if err != nil { 68 | return err 69 | } 70 | 71 | client := &http.Client{} 72 | 73 | logging.Debug("Downloading: " + url) 74 | 75 | response, err := client.Get(url) 76 | defer commons.CloseResponse(response) 77 | if err != nil { 78 | return err 79 | } 80 | 81 | _, err = io.Copy(target, response.Body) 82 | return err 83 | } 84 | 85 | func DownloadViaMaven(downloadUrl string, env envs.Environment) (string, error) { 86 | localPath := path.Join(viper.GetString("data.cache"), strings.TrimPrefix(strings.TrimPrefix(downloadUrl, "http://"), "https://")) 87 | 88 | if os.PathSeparator != '/' { 89 | localPath = strings.Replace(localPath, "/", string(os.PathSeparator), -1) 90 | } 91 | 92 | sha1Url := downloadUrl + ".sha1" 93 | 94 | useCache := true 95 | f, err := os.Open(localPath) 96 | defer apufferi.Close(f) 97 | //cache was readable, so validate 98 | if err == nil { 99 | h := sha1.New() 100 | if _, err := io.Copy(h, f); err != nil { 101 | log.Fatal(err) 102 | } 103 | apufferi.Close(f) 104 | 105 | actualHash := fmt.Sprintf("%x", h.Sum(nil)) 106 | 107 | client := &http.Client{} 108 | logging.Devel("Downloading hash from %s", sha1Url) 109 | response, err := client.Get(sha1Url) 110 | defer commons.CloseResponse(response) 111 | if err != nil { 112 | useCache = false 113 | } else { 114 | data := make([]byte, 40) 115 | _, err := response.Body.Read(data) 116 | expectedHash := string(data) 117 | 118 | if err != nil { 119 | useCache = false 120 | } else if expectedHash != actualHash { 121 | logging.Warn("Cache expected %s but was actually %s", expectedHash, actualHash) 122 | useCache = false 123 | } 124 | } 125 | } else if !os.IsNotExist(err) { 126 | logging.Warn("Cached file is not readable, will download (%s)", localPath) 127 | } else { 128 | useCache = false 129 | } 130 | 131 | //if we can't use cache, redownload it to the cache 132 | if !useCache { 133 | logging.Info("Downloading new version and caching to %s", localPath) 134 | if env != nil { 135 | env.DisplayToConsole(true, "Downloading:"+downloadUrl) 136 | } 137 | err = DownloadFileToCache(downloadUrl, localPath) 138 | } 139 | if err == nil { 140 | return localPath, err 141 | } else { 142 | return "", err 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /environments/environmentloader.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package environments 18 | 19 | import ( 20 | "fmt" 21 | "github.com/pkg/errors" 22 | "github.com/pufferpanel/apufferi/v4" 23 | "github.com/pufferpanel/pufferd/v2/cache" 24 | "github.com/pufferpanel/pufferd/v2/environments/envs" 25 | "github.com/pufferpanel/pufferd/v2/environments/impl/docker" 26 | "github.com/pufferpanel/pufferd/v2/environments/impl/standard" 27 | "github.com/pufferpanel/pufferd/v2/utils" 28 | "sync" 29 | ) 30 | 31 | var mapping map[string]envs.EnvironmentFactory 32 | 33 | func LoadModules() { 34 | mapping = make(map[string]envs.EnvironmentFactory) 35 | 36 | mapping["standard"] = standard.EnvironmentFactory{} 37 | mapping["docker"] = docker.EnvironmentFactory{} 38 | 39 | loadAdditionalModules(mapping) 40 | } 41 | 42 | func Create(environmentType, folder, id string, environmentSection interface{}) (envs.Environment, error) { 43 | factory := mapping[environmentType] 44 | 45 | if factory == nil { 46 | return nil, errors.New(fmt.Sprintf("undefined environment: %s", environmentType)) 47 | } 48 | 49 | item := factory.Create(id) 50 | err := apufferi.UnmarshalTo(environmentSection, item) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | serverRoot := apufferi.JoinPath(folder, id) 56 | envCache := cache.CreateCache() 57 | wsManager := utils.CreateWSManager() 58 | 59 | e := item.GetBase() 60 | if e.RootDirectory == "" { 61 | e.RootDirectory = serverRoot 62 | } 63 | e.WSManager = wsManager 64 | e.ConsoleBuffer = envCache 65 | e.Wait = &sync.WaitGroup{} 66 | 67 | return item, nil 68 | } 69 | 70 | func GetSupportedEnvironments() []string { 71 | result := make([]string, len(mapping)) 72 | i := 0 73 | for _, v := range mapping { 74 | result[i] = v.Key() 75 | i++ 76 | } 77 | 78 | return result 79 | } 80 | -------------------------------------------------------------------------------- /environments/environmentloader_linux.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package environments 18 | 19 | import ( 20 | "github.com/pufferpanel/pufferd/v2/environments/envs" 21 | "github.com/pufferpanel/pufferd/v2/environments/impl/tty" 22 | ) 23 | 24 | func loadAdditionalModules(mapping map[string]envs.EnvironmentFactory) { 25 | mapping["tty"] = tty.EnvironmentFactory{} 26 | //mapping["lxd"] = lxd.EnvironmentFactory{} 27 | } 28 | -------------------------------------------------------------------------------- /environments/environmentloader_windows.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package environments 18 | 19 | import "github.com/pufferpanel/pufferd/v2/environments/envs" 20 | 21 | func loadAdditionalModules(mapping map[string]envs.EnvironmentFactory) { 22 | } 23 | -------------------------------------------------------------------------------- /environments/envs/environment.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package envs 18 | 19 | import ( 20 | "fmt" 21 | "github.com/gorilla/websocket" 22 | "github.com/pufferpanel/apufferi/v4" 23 | "github.com/pufferpanel/pufferd/v2" 24 | "github.com/pufferpanel/pufferd/v2/utils" 25 | "github.com/spf13/viper" 26 | "io" 27 | "os" 28 | "sync" 29 | ) 30 | 31 | type Environment interface { 32 | //Executes a command within the environment. 33 | Execute(cmd string, args []string, env map[string]string, callback func(graceful bool)) (stdOut []byte, err error) 34 | 35 | //Executes a command within the environment and immediately return 36 | ExecuteAsync(cmd string, args []string, env map[string]string, callback func(graceful bool)) (err error) 37 | 38 | //Sends a string to the StdIn of the main program process 39 | ExecuteInMainProcess(cmd string) (err error) 40 | 41 | //Kills the main process, but leaves the environment running. 42 | Kill() (err error) 43 | 44 | //Creates the environment setting needed to run programs. 45 | Create() (err error) 46 | 47 | //Deletes the environment. 48 | Delete() (err error) 49 | 50 | Update() (err error) 51 | 52 | IsRunning() (isRunning bool, err error) 53 | 54 | WaitForMainProcess() (err error) 55 | 56 | WaitForMainProcessFor(timeout int) (err error) 57 | 58 | GetRootDirectory() string 59 | 60 | GetConsole() (console []string, epoch int64) 61 | 62 | GetConsoleFrom(time int64) (console []string, epoch int64) 63 | 64 | AddListener(ws *websocket.Conn) 65 | 66 | GetStats() (*pufferd.ServerStats, error) 67 | 68 | DisplayToConsole(prefix bool, msg string, data ...interface{}) 69 | 70 | SendCode(code int) error 71 | 72 | GetBase() *BaseEnvironment 73 | } 74 | 75 | type BaseEnvironment struct { 76 | Environment 77 | Type string 78 | RootDirectory string `json:"root"` 79 | ConsoleBuffer apufferi.Cache `json:"-"` 80 | WSManager utils.WebSocketManager `json:"-"` 81 | Wait *sync.WaitGroup `json:"-"` 82 | ExecutionFunction ExecutionFunction `json:"-"` 83 | WaitFunction func() (err error) `json:"-"` 84 | } 85 | 86 | type ExecutionFunction func(cmd string, args []string, env map[string]string, callback func(graceful bool)) (err error) 87 | 88 | func (e *BaseEnvironment) Execute(cmd string, args []string, env map[string]string, callback func(graceful bool)) (stdOut []byte, err error) { 89 | stdOut = make([]byte, 0) 90 | err = e.ExecuteAsync(cmd, args, env, callback) 91 | if err != nil { 92 | return 93 | } 94 | err = e.WaitForMainProcess() 95 | return 96 | } 97 | 98 | func (e *BaseEnvironment) WaitForMainProcess() (err error) { 99 | return e.WaitFunction() 100 | } 101 | 102 | func (e *BaseEnvironment) ExecuteAsync(cmd string, args []string, env map[string]string, callback func(graceful bool)) (err error) { 103 | return e.ExecutionFunction(cmd, args, env, callback) 104 | } 105 | 106 | func (e *BaseEnvironment) GetRootDirectory() string { 107 | return e.RootDirectory 108 | } 109 | 110 | func (e *BaseEnvironment) GetConsole() (console []string, epoch int64) { 111 | console, epoch = e.ConsoleBuffer.Read() 112 | return 113 | } 114 | 115 | func (e *BaseEnvironment) GetConsoleFrom(time int64) (console []string, epoch int64) { 116 | console, epoch = e.ConsoleBuffer.ReadFrom(time) 117 | return 118 | } 119 | 120 | func (e *BaseEnvironment) AddListener(ws *websocket.Conn) { 121 | e.WSManager.Register(ws) 122 | } 123 | 124 | func (e *BaseEnvironment) DisplayToConsole(daemon bool, msg string, data ...interface{}) { 125 | format := msg 126 | if daemon { 127 | format = "[DAEMON] " + msg 128 | } 129 | if len(data) == 0 { 130 | _, _ = fmt.Fprint(e.ConsoleBuffer, format) 131 | _, _ = fmt.Fprint(e.WSManager, format) 132 | } else { 133 | _, _ = fmt.Fprintf(e.ConsoleBuffer, format, data...) 134 | _, _ = fmt.Fprintf(e.WSManager, format, data...) 135 | } 136 | } 137 | 138 | func (e *BaseEnvironment) Update() error { 139 | return nil 140 | } 141 | 142 | func (e *BaseEnvironment) Delete() (err error) { 143 | err = os.RemoveAll(e.RootDirectory) 144 | return 145 | } 146 | 147 | func (e *BaseEnvironment) CreateWrapper() io.Writer { 148 | if viper.GetBool("console.forward") { 149 | return io.MultiWriter(os.Stdout, e.ConsoleBuffer, e.WSManager) 150 | } 151 | return io.MultiWriter(e.ConsoleBuffer, e.WSManager) 152 | } 153 | 154 | func (e *BaseEnvironment) GetBase() *BaseEnvironment { 155 | return e 156 | } 157 | -------------------------------------------------------------------------------- /environments/envs/environmentfactory.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package envs 18 | 19 | type EnvironmentFactory interface { 20 | Create(id string) Environment 21 | 22 | Key() string 23 | } 24 | -------------------------------------------------------------------------------- /environments/impl/docker/docker.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package docker 18 | 19 | import ( 20 | "context" 21 | "encoding/json" 22 | "fmt" 23 | "github.com/docker/docker/api/types" 24 | "github.com/docker/docker/api/types/container" 25 | "github.com/docker/docker/api/types/filters" 26 | "github.com/docker/docker/api/types/network" 27 | "github.com/docker/docker/api/types/strslice" 28 | "github.com/docker/docker/client" 29 | "github.com/docker/go-connections/nat" 30 | "github.com/pufferpanel/apufferi/v4" 31 | "github.com/pufferpanel/apufferi/v4/logging" 32 | "github.com/pufferpanel/pufferd/v2" 33 | "github.com/pufferpanel/pufferd/v2/environments/envs" 34 | "io" 35 | "io/ioutil" 36 | "os" 37 | "runtime" 38 | "syscall" 39 | "time" 40 | ) 41 | 42 | type docker struct { 43 | *envs.BaseEnvironment 44 | ContainerId string `json:"-"` 45 | ImageName string `json:"image"` 46 | Binds map[string]string `json:"bindings,omitempty"` 47 | NetworkMode string `json:"networkMode,omitempty"` 48 | Network string `json:"networkName,omitempty"` 49 | Ports []string `json:"portBindings,omitempty"` 50 | 51 | connection types.HijackedResponse 52 | cli *client.Client 53 | downloadingImage bool 54 | } 55 | 56 | func (d *docker) dockerExecuteAsync(cmd string, args []string, env map[string]string, callback func(graceful bool)) error { 57 | running, err := d.IsRunning() 58 | if err != nil { 59 | return err 60 | } 61 | if running { 62 | return pufferd.ErrContainerRunning 63 | } 64 | 65 | d.Wait.Wait() 66 | 67 | if d.downloadingImage { 68 | return pufferd.ErrImageDownloading 69 | } 70 | 71 | dockerClient, err := d.getClient() 72 | ctx := context.Background() 73 | 74 | //TODO: This logic may not work anymore, it's complicated to use an existing container with install/uninstall 75 | exists, err := d.doesContainerExist(dockerClient, ctx) 76 | 77 | if err != nil { 78 | return err 79 | } 80 | 81 | //container does not exist 82 | if !exists { 83 | err = d.createContainer(dockerClient, ctx, cmd, args, env, d.RootDirectory) 84 | if err != nil { 85 | return err 86 | } 87 | } 88 | 89 | config := types.ContainerAttachOptions{ 90 | Stdin: true, 91 | Stdout: true, 92 | Stderr: true, 93 | Stream: true, 94 | } 95 | 96 | d.connection, err = dockerClient.ContainerAttach(ctx, d.ContainerId, config) 97 | if err != nil { 98 | return err 99 | } 100 | 101 | d.Wait.Add(1) 102 | 103 | go func() { 104 | defer d.connection.Close() 105 | wrapper := d.CreateWrapper() 106 | _, _ = io.Copy(wrapper, d.connection.Reader) 107 | c, _ := d.getClient() 108 | err = c.ContainerStop(context.Background(), d.ContainerId, nil) 109 | d.Wait.Done() 110 | if err != nil { 111 | logging.Exception("Error stopping container "+d.ContainerId, err) 112 | } 113 | if callback != nil { 114 | callback(err == nil) 115 | } 116 | }() 117 | 118 | startOpts := types.ContainerStartOptions{ 119 | } 120 | 121 | err = dockerClient.ContainerStart(ctx, d.ContainerId, startOpts) 122 | if err != nil { 123 | return err 124 | } 125 | return err 126 | } 127 | 128 | func (d *docker) ExecuteInMainProcess(cmd string) (err error) { 129 | running, err := d.IsRunning() 130 | if err != nil { 131 | return 132 | } 133 | if !running { 134 | err = pufferd.ErrServerOffline 135 | return 136 | } 137 | 138 | _, _ = d.connection.Conn.Write([]byte(cmd + "\n")) 139 | return 140 | } 141 | 142 | func (d *docker) Kill() (err error) { 143 | running, err := d.IsRunning() 144 | if err != nil { 145 | return err 146 | } 147 | 148 | if !running { 149 | return 150 | } 151 | 152 | dockerClient, err := d.getClient() 153 | if err != nil { 154 | return err 155 | } 156 | err = dockerClient.ContainerKill(context.Background(), d.ContainerId, "SIGKILL") 157 | return 158 | } 159 | 160 | func (d *docker) Create() error { 161 | err := os.Mkdir(d.RootDirectory, 0755) 162 | if err != nil { 163 | return err 164 | } 165 | 166 | /*go func() { 167 | cli, err := d.getClient() 168 | if err != nil { 169 | return 170 | } 171 | err = d.pullImage(cli, context.Background(), false) 172 | }()*/ 173 | 174 | return err 175 | } 176 | 177 | func (d *docker) IsRunning() (bool, error) { 178 | dockerClient, err := d.getClient() 179 | if err != nil { 180 | return false, err 181 | } 182 | 183 | ctx := context.Background() 184 | 185 | exists, err := d.doesContainerExist(dockerClient, ctx) 186 | if !exists { 187 | return false, err 188 | } 189 | 190 | stats, err := dockerClient.ContainerInspect(ctx, d.ContainerId) 191 | if err != nil { 192 | return false, err 193 | } 194 | return stats.State.Running, nil 195 | } 196 | 197 | func (d *docker) GetStats() (*pufferd.ServerStats, error) { 198 | running, err := d.IsRunning() 199 | if err != nil { 200 | return nil, err 201 | } 202 | 203 | if !running { 204 | return nil, pufferd.ErrServerOffline 205 | } 206 | 207 | dockerClient, err := d.getClient() 208 | 209 | if err != nil { 210 | return nil, err 211 | } 212 | 213 | ctx := context.Background() 214 | res, err := dockerClient.ContainerStats(ctx, d.ContainerId, false) 215 | defer func() { 216 | if res.Body != nil { 217 | apufferi.Close(res.Body) 218 | } 219 | }() 220 | if err != nil { 221 | return nil, err 222 | } 223 | 224 | data := &types.StatsJSON{} 225 | err = json.NewDecoder(res.Body).Decode(&data) 226 | if err != nil { 227 | return nil, err 228 | } 229 | 230 | return &pufferd.ServerStats{ 231 | Memory: calculateMemoryPercent(data), 232 | Cpu: calculateCPUPercent(data), 233 | }, nil 234 | } 235 | 236 | func (d *docker) WaitForMainProcess() error { 237 | return d.WaitForMainProcessFor(0) 238 | } 239 | 240 | func (d *docker) WaitForMainProcessFor(timeout int) (err error) { 241 | running, err := d.IsRunning() 242 | if err != nil { 243 | return 244 | } 245 | if running { 246 | if timeout > 0 { 247 | var timer = time.AfterFunc(time.Duration(timeout)*time.Millisecond, func() { 248 | err = d.Kill() 249 | }) 250 | d.Wait.Wait() 251 | timer.Stop() 252 | } else { 253 | d.Wait.Wait() 254 | } 255 | } 256 | return 257 | } 258 | 259 | func (d *docker) getClient() (*client.Client, error) { 260 | var err error = nil 261 | if d.cli == nil { 262 | d.cli, err = client.NewClientWithOpts(client.FromEnv) 263 | ctx := context.Background() 264 | d.cli.NegotiateAPIVersion(ctx) 265 | } 266 | return d.cli, err 267 | } 268 | 269 | func (d *docker) doesContainerExist(client *client.Client, ctx context.Context) (bool, error) { 270 | opts := types.ContainerListOptions{ 271 | Filters: filters.NewArgs(), 272 | } 273 | 274 | opts.All = true 275 | opts.Filters.Add("name", d.ContainerId) 276 | 277 | existingContainers, err := client.ContainerList(ctx, opts) 278 | 279 | logging.Debug("Does container (%s) exist?: %t", d.ContainerId, len(existingContainers) > 0) 280 | 281 | if len(existingContainers) == 0 { 282 | return false, err 283 | } else { 284 | return true, err 285 | } 286 | } 287 | 288 | func (d *docker) pullImage(client *client.Client, ctx context.Context, force bool) error { 289 | exists := false 290 | 291 | opts := types.ImageListOptions{ 292 | All: true, 293 | Filters: filters.NewArgs(), 294 | } 295 | opts.Filters.Add("reference", d.ImageName) 296 | images, err := client.ImageList(ctx, opts) 297 | 298 | if err != nil { 299 | return err 300 | } 301 | 302 | if len(images) >= 1 { 303 | exists = true 304 | } 305 | 306 | logging.Debug("Does image %v exist? %v", d.ImageName, exists) 307 | 308 | if exists && !force { 309 | return nil 310 | } 311 | 312 | op := types.ImagePullOptions{} 313 | 314 | logging.Debug("Downloading image %v", d.ImageName) 315 | d.DisplayToConsole(true, "Downloading image for container, please wait\n") 316 | 317 | d.downloadingImage = true 318 | 319 | r, err := client.ImagePull(ctx, d.ImageName, op) 320 | defer apufferi.Close(r) 321 | if err != nil { 322 | return err 323 | } 324 | _, err = io.Copy(ioutil.Discard, r) 325 | 326 | d.downloadingImage = false 327 | logging.Debug("Downloaded image %v", d.ImageName) 328 | d.DisplayToConsole(true, "Downloaded image for container\n") 329 | return err 330 | } 331 | 332 | func (d *docker) createContainer(client *client.Client, ctx context.Context, cmd string, args []string, env map[string]string, root string) error { 333 | logging.Debug("Creating container") 334 | containerRoot := "/var/lib/pufferd/server/" 335 | err := d.pullImage(client, ctx, false) 336 | 337 | if err != nil { 338 | return err 339 | } 340 | 341 | cmdSlice := strslice.StrSlice{} 342 | 343 | cmdSlice = append(cmdSlice, cmd) 344 | 345 | for _, v := range args { 346 | cmdSlice = append(cmdSlice, v) 347 | } 348 | 349 | //newEnv := os.Environ() 350 | newEnv := []string{"HOME=" + containerRoot} 351 | 352 | for k, v := range env { 353 | newEnv = append(newEnv, fmt.Sprintf("%s=%s", k, v)) 354 | } 355 | 356 | config := &container.Config{ 357 | AttachStderr: true, 358 | AttachStdin: true, 359 | AttachStdout: true, 360 | Tty: true, 361 | OpenStdin: true, 362 | NetworkDisabled: false, 363 | Cmd: cmdSlice, 364 | Image: d.ImageName, 365 | WorkingDir: root, 366 | Env: newEnv, 367 | } 368 | 369 | if runtime.GOOS == "linux" { 370 | config.User = fmt.Sprintf("%d:%d", os.Getuid(), os.Getgid()) 371 | } 372 | 373 | hostConfig := &container.HostConfig{ 374 | AutoRemove: true, 375 | NetworkMode: container.NetworkMode(d.NetworkMode), 376 | Resources: container.Resources{}, 377 | Binds: []string{root + ":" + containerRoot}, 378 | PortBindings: nat.PortMap{}, 379 | } 380 | 381 | config.WorkingDir = containerRoot 382 | 383 | for k, v := range d.Binds { 384 | hostConfig.Binds = append(hostConfig.Binds, k+":"+v) 385 | } 386 | 387 | networkConfig := &network.NetworkingConfig{} 388 | 389 | _, bindings, err := nat.ParsePortSpecs(d.Ports) 390 | if err != nil { 391 | return err 392 | } 393 | hostConfig.PortBindings = bindings 394 | 395 | _, err = client.ContainerCreate(ctx, config, hostConfig, networkConfig, d.ContainerId) 396 | return err 397 | } 398 | 399 | func (d *docker) SendCode(code int) error { 400 | running, err := d.IsRunning() 401 | 402 | if err != nil || !running { 403 | return err 404 | } 405 | 406 | dockerClient, err := d.getClient() 407 | 408 | if err != nil { 409 | return err 410 | } 411 | 412 | ctx := context.Background() 413 | return dockerClient.ContainerKill(ctx, d.ContainerId, syscall.Signal(code).String()) 414 | } 415 | 416 | func calculateCPUPercent(v *types.StatsJSON) float64 { 417 | // Max number of 100ns intervals between the previous time read and now 418 | possIntervals := uint64(v.Read.Sub(v.PreRead).Nanoseconds()) // Start with number of ns intervals 419 | possIntervals /= 100 // Convert to number of 100ns intervals 420 | //possIntervals *= uint64(v.NumProcs) // Multiple by the number of processors 421 | 422 | // Intervals used 423 | intervalsUsed := v.CPUStats.CPUUsage.TotalUsage - v.PreCPUStats.CPUUsage.TotalUsage 424 | 425 | // Percentage avoiding divide-by-zero 426 | if possIntervals > 0 { 427 | return float64(intervalsUsed) / float64(possIntervals) 428 | } 429 | return 0.00 430 | } 431 | 432 | func calculateMemoryPercent(v *types.StatsJSON) float64 { 433 | return float64(v.MemoryStats.Usage) / (1024 * 1024) //convert from bytes to MB 434 | } 435 | -------------------------------------------------------------------------------- /environments/impl/docker/factory.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package docker 18 | 19 | import ( 20 | "github.com/pufferpanel/pufferd/v2/environments/envs" 21 | ) 22 | 23 | type EnvironmentFactory struct { 24 | envs.EnvironmentFactory 25 | } 26 | 27 | func (ef EnvironmentFactory) Create(id string) envs.Environment { 28 | d := &docker{ 29 | BaseEnvironment: &envs.BaseEnvironment{Type: "docker"}, 30 | ContainerId: id, 31 | ImageName: "pufferpanel/generic", 32 | NetworkMode: "host", 33 | Ports: make([]string, 0), 34 | Binds: make(map[string]string), 35 | } 36 | 37 | d.ExecutionFunction = d.dockerExecuteAsync 38 | return d 39 | } 40 | 41 | func (ef EnvironmentFactory) Key() string { 42 | return "docker" 43 | } 44 | -------------------------------------------------------------------------------- /environments/impl/lxd/factory.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package lxd 18 | 19 | import ( 20 | "github.com/pufferpanel/apufferi/v4" 21 | "github.com/pufferpanel/pufferd/v2/environments/envs" 22 | "github.com/pufferpanel/pufferd/v2/utils" 23 | "sync" 24 | ) 25 | 26 | type EnvironmentFactory struct { 27 | envs.EnvironmentFactory 28 | } 29 | 30 | func (ef EnvironmentFactory) Create(folder, id string, environmentSection map[string]interface{}, rootDirectory string, cache apufferi.Cache, wsManager utils.WebSocketManager) envs.Environment { 31 | d := &lxc{BaseEnvironment: &envs.BaseEnvironment{Type: "lxc"}, TypeWithMetadata: apufferi.TypeWithMetadata{Type: "docker"}, ContainerId: id} 32 | d.BaseEnvironment.ExecutionFunction = d.executeAsync 33 | d.BaseEnvironment.WaitFunction = d.WaitForMainProcess 34 | d.wait = &sync.WaitGroup{} 35 | 36 | d.ImageName = apufferi.GetStringOrDefault(environmentSection, "image", "alpine/3.9") 37 | 38 | d.ContainerId = "pufferd-" + id 39 | d.RootDirectory = rootDirectory 40 | d.ConsoleBuffer = cache 41 | d.WSManager = wsManager 42 | return d 43 | } 44 | 45 | func (ef EnvironmentFactory) Key() string { 46 | return "lxd" 47 | } 48 | -------------------------------------------------------------------------------- /environments/impl/lxd/lxd.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package lxd 18 | 19 | import ( 20 | "github.com/docker/docker/pkg/ioutils" 21 | client "github.com/lxc/lxd/client" 22 | "github.com/lxc/lxd/shared/api" 23 | "github.com/pufferpanel/apufferi/v4" 24 | "github.com/pufferpanel/apufferi/v4/logging" 25 | "github.com/pufferpanel/pufferd/v2" 26 | "github.com/pufferpanel/pufferd/v2/environments/envs" 27 | "io" 28 | "os" 29 | "sync" 30 | "time" 31 | ) 32 | 33 | type lxc struct { 34 | *envs.BaseEnvironment 35 | apufferi.TypeWithMetadata 36 | ContainerId string `json:"-"` 37 | ImageName string `json:"image"` 38 | 39 | connection client.InstanceServer 40 | wait *sync.WaitGroup 41 | stdin io.Writer 42 | stdinReader io.ReadCloser 43 | stdout io.Reader 44 | stdoutWriter io.WriteCloser 45 | } 46 | 47 | func (l *lxc) executeAsync(cmd string, args []string, env map[string]string, callback func(graceful bool)) error { 48 | running, err := l.IsRunning() 49 | if err != nil { 50 | return err 51 | } 52 | if running { 53 | return pufferd.ErrContainerRunning 54 | } 55 | 56 | //send an exec 57 | req := api.ContainerExecPost{ 58 | Command: append([]string{cmd}, args...), 59 | WaitForWS: true, 60 | Interactive: true, 61 | } 62 | 63 | l.stdinReader, l.stdin = io.Pipe() 64 | l.stdout, l.stdoutWriter = io.Pipe() 65 | 66 | out := l.CreateWrapper() 67 | l.stdoutWriter = ioutils.NopWriteCloser(io.MultiWriter(out, l.stdoutWriter)) 68 | 69 | finished := make(chan bool) 70 | 71 | execArgs := &client.ContainerExecArgs{ 72 | Stdin: l.stdinReader, 73 | Stdout: l.stdoutWriter, 74 | Stderr: l.stdoutWriter, 75 | DataDone: finished, 76 | } 77 | 78 | l.wait.Add(1) 79 | 80 | _, _, err = l.connection.GetContainer(l.ContainerId) 81 | if err != nil { 82 | logging.Devel("Container does not exist (%s)", err.Error()) 83 | //container no exist, make it 84 | req := api.ContainersPost{ 85 | Name: l.ContainerId, 86 | Source: api.ContainerSource{ 87 | Type: "image", 88 | Alias: l.ImageName, 89 | Server: "https://images.linuxcontainers.org", 90 | Protocol: "simplestreams", 91 | }, 92 | ContainerPut: api.ContainerPut{ 93 | Ephemeral: true, 94 | }, 95 | } 96 | 97 | // Get LXD to create the container (background operation) 98 | op, err := l.connection.CreateContainer(req) 99 | if err != nil { 100 | return err 101 | } 102 | 103 | // Wait for the operation to complete 104 | err = op.Wait() 105 | if err != nil { 106 | return err 107 | } 108 | } 109 | 110 | // Get LXD to start the container (background operation) 111 | reqState := api.ContainerStatePut{ 112 | Action: "start", 113 | Timeout: -1, 114 | } 115 | 116 | op, err := l.connection.UpdateContainerState(l.ContainerId, reqState, "") 117 | if err != nil { 118 | return err 119 | } 120 | 121 | // Wait for the operation to complete 122 | err = op.Wait() 123 | if err != nil { 124 | return err 125 | } 126 | 127 | op, err = l.connection.ExecContainer(l.ContainerId, req, execArgs) 128 | if err != nil { 129 | return err 130 | } 131 | 132 | go func() { 133 | defer l.wait.Done() 134 | <-finished 135 | var internalError error 136 | defer func() { 137 | if internalError != nil { 138 | logging.Exception("Error stopping container "+l.ContainerId, internalError) 139 | } 140 | if callback != nil { 141 | callback(internalError == nil) 142 | } 143 | }() 144 | 145 | /*internalError = op.Wait() 146 | if internalError != nil { 147 | return 148 | }*/ 149 | 150 | op2, _ := l.connection.UpdateContainerState(l.ContainerId, api.ContainerStatePut{ 151 | Action: "stop", 152 | Timeout: -1, 153 | Force: true, 154 | }, "") 155 | 156 | if op2 != nil { 157 | internalError = op2.Wait() 158 | } 159 | }() 160 | 161 | return nil 162 | } 163 | 164 | func (l *lxc) ExecuteInMainProcess(cmd string) (err error) { 165 | running, err := l.IsRunning() 166 | if err != nil { 167 | return 168 | } 169 | if !running { 170 | err = pufferd.ErrServerOffline 171 | return 172 | } 173 | 174 | _, _ = l.stdin.Write([]byte(cmd + "\n")) 175 | return 176 | } 177 | 178 | func (l *lxc) IsRunning() (bool, error) { 179 | logging.Debug("Checking if %s is running", l.ContainerId) 180 | c, err := l.getConnection() 181 | if err != nil { 182 | return false, err 183 | } 184 | 185 | state, _, err := c.GetContainerState(l.ContainerId) 186 | //since errors can mean it doesn't exist, and they don't expose a way to tell, we have to ignore all errors 187 | if err != nil { 188 | return false, nil 189 | } 190 | 191 | if state.StatusCode == api.Running { 192 | return true, nil 193 | } 194 | 195 | return false, nil 196 | } 197 | 198 | func (l *lxc) Create() error { 199 | err := os.Mkdir(l.RootDirectory, 0755) 200 | if err != nil { 201 | return err 202 | } 203 | 204 | return err 205 | } 206 | 207 | func (l *lxc) GetStats() (*pufferd.ServerStats, error) { 208 | return &pufferd.ServerStats{ 209 | Cpu: 0, 210 | Memory: 0, 211 | }, nil 212 | } 213 | 214 | func (l *lxc) WaitForMainProcess() error { 215 | return l.WaitForMainProcessFor(0) 216 | } 217 | 218 | func (l *lxc) WaitForMainProcessFor(timeout int) (err error) { 219 | running, err := l.IsRunning() 220 | if err != nil { 221 | return 222 | } 223 | if running { 224 | if timeout > 0 { 225 | var timer = time.AfterFunc(time.Duration(timeout)*time.Millisecond, func() { 226 | err = l.Kill() 227 | }) 228 | l.wait.Wait() 229 | timer.Stop() 230 | } else { 231 | l.wait.Wait() 232 | } 233 | } 234 | return 235 | } 236 | 237 | func (l *lxc) Kill() error { 238 | op2, err := l.connection.UpdateContainerState(l.ContainerId, api.ContainerStatePut{ 239 | Action: "stop", 240 | Timeout: -1, 241 | Force: true, 242 | }, "") 243 | 244 | if err != nil { 245 | return err 246 | } 247 | 248 | return op2.Wait() 249 | } 250 | 251 | func (l *lxc) getConnection() (client.InstanceServer, error) { 252 | var err error 253 | if l.connection == nil { 254 | l.connection, err = client.ConnectLXDUnix("", nil) 255 | } 256 | return l.connection, err 257 | } 258 | -------------------------------------------------------------------------------- /environments/impl/standard/standard.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package standard 18 | 19 | import ( 20 | "github.com/pufferpanel/pufferd/v2" 21 | "github.com/pufferpanel/pufferd/v2/environments/envs" 22 | "github.com/spf13/cast" 23 | "io" 24 | "os" 25 | "os/exec" 26 | "runtime" 27 | "syscall" 28 | "time" 29 | 30 | "fmt" 31 | "github.com/pufferpanel/apufferi/v4/logging" 32 | "github.com/shirou/gopsutil/process" 33 | "strings" 34 | ) 35 | 36 | type standard struct { 37 | *envs.BaseEnvironment 38 | mainProcess *exec.Cmd 39 | stdInWriter io.Writer 40 | } 41 | 42 | func (s *standard) standardExecuteAsync(cmd string, args []string, env map[string]string, callback func(graceful bool)) (err error) { 43 | running, err := s.IsRunning() 44 | if err != nil { 45 | return 46 | } 47 | if running { 48 | err = pufferd.ErrProcessRunning 49 | return 50 | } 51 | s.Wait.Wait() 52 | s.Wait.Add(1) 53 | s.mainProcess = exec.Command(cmd, args...) 54 | s.mainProcess.Dir = s.RootDirectory 55 | s.mainProcess.Env = append(os.Environ(), "HOME="+s.RootDirectory) 56 | for k, v := range env { 57 | s.mainProcess.Env = append(s.mainProcess.Env, fmt.Sprintf("%s=%s", k, v)) 58 | } 59 | wrapper := s.CreateWrapper() 60 | s.mainProcess.Stdout = wrapper 61 | s.mainProcess.Stderr = wrapper 62 | pipe, err := s.mainProcess.StdinPipe() 63 | if err != nil { 64 | return 65 | } 66 | s.stdInWriter = pipe 67 | logging.Debug("Starting process: %s %s", s.mainProcess.Path, strings.Join(s.mainProcess.Args[1:], " ")) 68 | err = s.mainProcess.Start() 69 | if err != nil && err.Error() != "exit status 1" { 70 | return 71 | } else { 72 | logging.Debug("Process started (%d)", s.mainProcess.Process.Pid) 73 | } 74 | 75 | go s.handleClose(callback) 76 | return 77 | } 78 | 79 | func (s *standard) ExecuteInMainProcess(cmd string) (err error) { 80 | running, err := s.IsRunning() 81 | if err != nil { 82 | return err 83 | } 84 | if !running { 85 | err = pufferd.ErrServerOffline 86 | return 87 | } 88 | stdIn := s.stdInWriter 89 | _, err = io.WriteString(stdIn, cmd+"\n") 90 | return 91 | } 92 | 93 | func (s *standard) Kill() (err error) { 94 | running, err := s.IsRunning() 95 | if err != nil { 96 | return err 97 | } 98 | if !running { 99 | return 100 | } 101 | return s.mainProcess.Process.Kill() 102 | } 103 | 104 | func (s *standard) IsRunning() (isRunning bool, err error) { 105 | isRunning = s.mainProcess != nil && s.mainProcess.Process != nil 106 | if isRunning { 107 | pr, pErr := os.FindProcess(s.mainProcess.Process.Pid) 108 | if pr == nil || pErr != nil { 109 | isRunning = false 110 | } else if runtime.GOOS != "windows" && pr.Signal(syscall.Signal(0)) != nil { 111 | isRunning = false 112 | } 113 | } 114 | return 115 | } 116 | 117 | func (s *standard) GetStats() (*pufferd.ServerStats, error) { 118 | running, err := s.IsRunning() 119 | if err != nil { 120 | return nil, err 121 | } 122 | if !running { 123 | return nil, pufferd.ErrServerOffline 124 | } 125 | pr, err := process.NewProcess(int32(s.mainProcess.Process.Pid)) 126 | if err != nil { 127 | return nil, err 128 | } 129 | 130 | memMap, _ := pr.MemoryInfo() 131 | cpu, _ := pr.Percent(time.Second * 1) 132 | 133 | return &pufferd.ServerStats{ 134 | Cpu: cpu, 135 | Memory: cast.ToFloat64(memMap.RSS), 136 | }, nil 137 | } 138 | 139 | func (s *standard) Create() error { 140 | return os.Mkdir(s.RootDirectory, 0755) 141 | } 142 | 143 | func (s *standard) WaitForMainProcess() error { 144 | return s.WaitForMainProcessFor(0) 145 | } 146 | 147 | func (s *standard) WaitForMainProcessFor(timeout int) (err error) { 148 | running, err := s.IsRunning() 149 | if err != nil { 150 | return 151 | } 152 | if running { 153 | if timeout > 0 { 154 | var timer = time.AfterFunc(time.Duration(timeout)*time.Millisecond, func() { 155 | err = s.Kill() 156 | }) 157 | s.Wait.Wait() 158 | timer.Stop() 159 | } else { 160 | s.Wait.Wait() 161 | } 162 | } 163 | return 164 | } 165 | 166 | func (s *standard) SendCode(code int) error { 167 | running, err := s.IsRunning() 168 | 169 | if err != nil || !running { 170 | return err 171 | } 172 | 173 | return s.mainProcess.Process.Signal(syscall.Signal(code)) 174 | } 175 | 176 | func (s *standard) handleClose(callback func(graceful bool)) { 177 | err := s.mainProcess.Wait() 178 | s.Wait.Done() 179 | 180 | var graceful bool 181 | if s.mainProcess == nil || s.mainProcess.ProcessState == nil || err != nil { 182 | graceful = false 183 | callback(false) 184 | } else { 185 | graceful = s.mainProcess.ProcessState.Success() 186 | } 187 | 188 | if s.mainProcess != nil && s.mainProcess.Process != nil { 189 | s.mainProcess.Process.Release() 190 | } 191 | 192 | s.mainProcess = nil 193 | s.stdInWriter = nil 194 | 195 | if callback != nil { 196 | callback(graceful) 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /environments/impl/standard/standardfactory.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package standard 18 | 19 | import ( 20 | "github.com/pufferpanel/pufferd/v2/environments/envs" 21 | ) 22 | 23 | type EnvironmentFactory struct { 24 | envs.EnvironmentFactory 25 | } 26 | 27 | func (ef EnvironmentFactory) Create(id string) envs.Environment { 28 | s := &standard{ 29 | BaseEnvironment: &envs.BaseEnvironment{Type: "standard"}, 30 | } 31 | s.BaseEnvironment.ExecutionFunction = s.standardExecuteAsync 32 | return s 33 | } 34 | 35 | func (ef EnvironmentFactory) Key() string { 36 | return "standard" 37 | } 38 | -------------------------------------------------------------------------------- /environments/impl/tty/tty.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | /* 4 | Copyright 2016 Padduck, LLC 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | package tty 20 | 21 | import ( 22 | "fmt" 23 | "github.com/creack/pty" 24 | "github.com/pufferpanel/apufferi/v4/logging" 25 | "github.com/pufferpanel/pufferd/v2" 26 | "github.com/pufferpanel/pufferd/v2/environments/envs" 27 | "github.com/shirou/gopsutil/process" 28 | "github.com/spf13/cast" 29 | "io" 30 | "os" 31 | "os/exec" 32 | "strings" 33 | "syscall" 34 | "time" 35 | ) 36 | 37 | type tty struct { 38 | *envs.BaseEnvironment 39 | mainProcess *exec.Cmd 40 | stdInWriter io.Writer 41 | } 42 | 43 | func (t *tty) ttyExecuteAsync(cmd string, args []string, env map[string]string, callback func(graceful bool)) (err error) { 44 | running, err := t.IsRunning() 45 | if err != nil { 46 | return 47 | } 48 | if running { 49 | err = pufferd.ErrProcessRunning 50 | return 51 | } 52 | t.Wait.Wait() 53 | 54 | pr := exec.Command(cmd, args...) 55 | pr.Dir = t.RootDirectory 56 | pr.Env = append(os.Environ(), "HOME="+t.RootDirectory) 57 | for k, v := range env { 58 | pr.Env = append(pr.Env, fmt.Sprintf("%s=%s", k, v)) 59 | } 60 | 61 | wrapper := t.CreateWrapper() 62 | t.Wait.Add(1) 63 | pr.SysProcAttr = &syscall.SysProcAttr{Setctty: true, Setsid: true} 64 | t.mainProcess = pr 65 | logging.Debug("Starting process: %s %s", t.mainProcess.Path, strings.Join(t.mainProcess.Args[1:], " ")) 66 | tty, err := pty.Start(pr) 67 | if err != nil { 68 | return 69 | } 70 | 71 | t.stdInWriter = tty 72 | 73 | go func(proxy io.Writer) { 74 | _, _ = io.Copy(proxy, tty) 75 | }(wrapper) 76 | 77 | go t.handleClose(callback) 78 | return 79 | } 80 | 81 | func (t *tty) ExecuteInMainProcess(cmd string) (err error) { 82 | running, err := t.IsRunning() 83 | if err != nil { 84 | return err 85 | } 86 | if !running { 87 | err = pufferd.ErrServerOffline 88 | return 89 | } 90 | stdIn := t.stdInWriter 91 | _, err = io.WriteString(stdIn, cmd+"\n") 92 | return 93 | } 94 | 95 | func (t *tty) Kill() (err error) { 96 | running, err := t.IsRunning() 97 | if err != nil { 98 | return 99 | } 100 | if !running { 101 | return 102 | } 103 | return t.mainProcess.Process.Kill() 104 | } 105 | 106 | func (t *tty) IsRunning() (isRunning bool, err error) { 107 | isRunning = t.mainProcess != nil && t.mainProcess.Process != nil 108 | if isRunning { 109 | pr, pErr := os.FindProcess(t.mainProcess.Process.Pid) 110 | if pr == nil || pErr != nil { 111 | isRunning = false 112 | } else if pr.Signal(syscall.Signal(0)) != nil { 113 | isRunning = false 114 | } 115 | } 116 | return 117 | } 118 | 119 | func (t *tty) GetStats() (*pufferd.ServerStats, error) { 120 | running, err := t.IsRunning() 121 | if err != nil { 122 | return nil, err 123 | } 124 | if !running { 125 | return nil, pufferd.ErrServerOffline 126 | } 127 | pr, err := process.NewProcess(int32(t.mainProcess.Process.Pid)) 128 | if err != nil { 129 | return nil, err 130 | } 131 | 132 | memMap, _ := pr.MemoryInfo() 133 | cpu, _ := pr.Percent(time.Second * 1) 134 | 135 | return &pufferd.ServerStats{ 136 | Cpu: cpu, 137 | Memory: cast.ToFloat64(memMap.RSS), 138 | }, nil 139 | } 140 | 141 | func (t *tty) Create() error { 142 | return os.Mkdir(t.RootDirectory, 0755) 143 | } 144 | 145 | func (t *tty) WaitForMainProcess() error { 146 | return t.WaitForMainProcessFor(0) 147 | } 148 | 149 | func (t *tty) WaitForMainProcessFor(timeout int) (err error) { 150 | running, err := t.IsRunning() 151 | if err != nil { 152 | return 153 | } 154 | if running { 155 | if timeout > 0 { 156 | var timer = time.AfterFunc(time.Duration(timeout)*time.Millisecond, func() { 157 | err = t.Kill() 158 | }) 159 | t.Wait.Wait() 160 | timer.Stop() 161 | } else { 162 | t.Wait.Wait() 163 | } 164 | } 165 | return 166 | } 167 | 168 | func (t *tty) SendCode(code int) error { 169 | running, err := t.IsRunning() 170 | 171 | if err != nil || !running { 172 | return err 173 | } 174 | 175 | return t.mainProcess.Process.Signal(syscall.Signal(code)) 176 | } 177 | 178 | func (t *tty) handleClose(callback func(graceful bool)) { 179 | err := t.mainProcess.Wait() 180 | 181 | var success bool 182 | if t.mainProcess == nil || t.mainProcess.ProcessState == nil || err != nil { 183 | success = false 184 | } else { 185 | success = t.mainProcess.ProcessState.Success() 186 | } 187 | 188 | if t.mainProcess != nil && t.mainProcess.Process != nil { 189 | _ = t.mainProcess.Process.Release() 190 | } 191 | t.mainProcess = nil 192 | t.Wait.Done() 193 | 194 | if callback != nil { 195 | callback(success) 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /environments/impl/tty/ttyfactory.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | /* 4 | Copyright 2019 Padduck, LLC 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | package tty 20 | 21 | import ( 22 | "github.com/pufferpanel/pufferd/v2/environments/envs" 23 | ) 24 | 25 | type EnvironmentFactory struct { 26 | envs.EnvironmentFactory 27 | } 28 | 29 | func (ef EnvironmentFactory) Create(id string) envs.Environment { 30 | t := &tty{ 31 | BaseEnvironment: &envs.BaseEnvironment{Type: "tty"}, 32 | } 33 | t.BaseEnvironment.ExecutionFunction = t.ttyExecuteAsync 34 | return t 35 | } 36 | 37 | func (ef EnvironmentFactory) Key() string { 38 | return "tty" 39 | } 40 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package pufferd 18 | 19 | import ( 20 | "github.com/pufferpanel/apufferi/v4" 21 | "github.com/pufferpanel/apufferi/v4/scope" 22 | ) 23 | 24 | var ErrServerOffline = apufferi.CreateError("server offline", "ErrServerOffline") 25 | var ErrIllegalFileAccess = apufferi.CreateError("invalid file access", "ErrIllegalFileAccess") 26 | var ErrServerDisabled = apufferi.CreateError("server is disabled", "ErrServerDisabled") 27 | var ErrContainerRunning = apufferi.CreateError("container already running", "ErrContainerRunning") 28 | var ErrImageDownloading = apufferi.CreateError("image downloading", "ErrImageDownloading") 29 | var ErrProcessRunning = apufferi.CreateError("process already running", "ErrProcessRunning") 30 | var ErrMissingFactory = apufferi.CreateError("missing factory", "ErrMissingFactory") 31 | var ErrServerAlreadyExists = apufferi.CreateError("server already exists", "ErrServerAlreadyExists") 32 | var ErrInvalidUnixTime = apufferi.CreateError("time provided is not a valid UNIX time", "ErrInvalidUnixTime") 33 | var ErrKeyNotPEM = apufferi.CreateError("key is not in PEM format", "ErrKeyNotPEM") 34 | var ErrCannotValidateToken = apufferi.CreateError("could not validate access token", "ErrCannotValidateToken") 35 | var ErrMissingAccessToken = apufferi.CreateError("access token not provided", "ErrMissingAccessToken") 36 | var ErrNotBearerToken = apufferi.CreateError("access token must be a Bearer token", "ErrNotBearerToken") 37 | var ErrKeyNotECDSA = apufferi.CreateError("key is not ECDSA key", "ErrKeyNotECDSA") 38 | var ErrMissingScope = apufferi.CreateError("missing scope", "ErrMissingScope") 39 | 40 | func CreateErrMissingScope(scope scope.Scope) *apufferi.Error { 41 | return apufferi.CreateError(ErrMissingScope.Message, ErrMissingScope.Code).Metadata(map[string]interface{}{"scope": scope}) 42 | } 43 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/pufferpanel/pufferd/v2 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect 7 | github.com/Microsoft/go-winio v0.4.14 // indirect 8 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc 9 | github.com/braintree/manners v0.0.0-20150503212558-0b5e6b2c2843 10 | github.com/cavaliercoder/grab v2.0.0+incompatible 11 | github.com/containerd/containerd v1.2.9 // indirect 12 | github.com/creack/pty v1.1.7 13 | github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c // indirect 14 | github.com/docker/docker v0.0.0-20190905191220-3b23f9033967 15 | github.com/docker/go-connections v0.4.0 16 | github.com/docker/go-units v0.4.0 // indirect 17 | github.com/flosch/pongo2 v0.0.0-20190707114632-bbf5a6c351f4 // indirect 18 | github.com/gin-gonic/gin v1.4.0 19 | github.com/go-ole/go-ole v1.2.4 // indirect 20 | github.com/gogo/protobuf v1.3.0 // indirect 21 | github.com/gorilla/websocket v1.4.1 22 | github.com/itsjamie/gin-cors v0.0.0-20160420130702-97b4a9da7933 23 | github.com/juju/clock v0.0.0-20190205081909-9c5c9712527c // indirect 24 | github.com/juju/retry v0.0.0-20180821225755-9058e192b216 // indirect 25 | github.com/juju/utils v0.0.0-20180820210520-bf9cc5bdd62d // indirect 26 | github.com/juju/version v0.0.0-20180108022336-b64dbd566305 // indirect 27 | github.com/juju/webbrowser v0.0.0-20180907093207-efb9432b2bcb // indirect 28 | github.com/julienschmidt/httprouter v1.3.0 // indirect 29 | github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect 30 | github.com/lxc/lxd v0.0.0-20191002163551-3a5f63bc4959 31 | github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c // indirect 32 | github.com/opencontainers/go-digest v1.0.0-rc1 // indirect 33 | github.com/opencontainers/image-spec v1.0.1 // indirect 34 | github.com/pkg/errors v0.8.1 35 | github.com/pkg/sftp v1.10.1 36 | github.com/pufferpanel/apufferi/v4 v4.0.3 37 | github.com/rogpeppe/fastuuid v1.2.0 // indirect 38 | github.com/satori/go.uuid v1.2.0 39 | github.com/shirou/gopsutil v0.0.0-20190901111213-e4ec7b275ada 40 | github.com/spf13/cast v1.3.0 41 | github.com/spf13/cobra v0.0.5 42 | github.com/spf13/viper v1.3.2 43 | github.com/swaggo/gin-swagger v1.2.0 44 | github.com/swaggo/swag v1.5.1 45 | golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472 46 | golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 // indirect 47 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect 48 | google.golang.org/genproto v0.0.0-20190905072037-92dd089d5514 // indirect 49 | google.golang.org/grpc v1.23.0 // indirect 50 | gopkg.in/errgo.v1 v1.0.1 // indirect 51 | gopkg.in/httprequest.v1 v1.2.0 // indirect 52 | gopkg.in/macaroon-bakery.v2 v2.1.0 // indirect 53 | gopkg.in/macaroon.v2 v2.1.0 // indirect 54 | gopkg.in/robfig/cron.v2 v2.0.0-20150107220207-be2e0b0deed5 // indirect 55 | ) 56 | -------------------------------------------------------------------------------- /httphandlers/oauth2.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package httphandlers 18 | 19 | import ( 20 | "github.com/gin-gonic/gin" 21 | "github.com/pufferpanel/apufferi/v4" 22 | "github.com/pufferpanel/apufferi/v4/response" 23 | "github.com/pufferpanel/apufferi/v4/scope" 24 | "github.com/pufferpanel/pufferd/v2" 25 | "github.com/pufferpanel/pufferd/v2/programs" 26 | "net/http" 27 | "strings" 28 | ) 29 | 30 | func OAuth2Handler(requiredScope scope.Scope, requireServer bool) gin.HandlerFunc { 31 | return func(c *gin.Context) { 32 | failure := true 33 | defer func() { 34 | if failure && !c.IsAborted() { 35 | c.Abort() 36 | } 37 | }() 38 | 39 | authHeader := c.Request.Header.Get("Authorization") 40 | var authToken string 41 | if authHeader == "" { 42 | authToken = c.Query("accessToken") 43 | if authToken == "" { 44 | response.HandleError(c, pufferd.ErrMissingAccessToken, http.StatusBadRequest) 45 | return 46 | } 47 | } else { 48 | authArr := strings.SplitN(authHeader, " ", 2) 49 | if len(authArr) < 2 || authArr[0] != "Bearer" { 50 | response.HandleError(c, pufferd.ErrNotBearerToken, http.StatusBadRequest) 51 | return 52 | } 53 | authToken = authArr[1] 54 | } 55 | 56 | var err error 57 | key := pufferd.GetPublicKey() 58 | if key == nil { 59 | key, err = pufferd.LoadPublicKey() 60 | if response.HandleError(c, err, http.StatusInternalServerError) { 61 | return 62 | } 63 | } 64 | 65 | token, err := apufferi.ParseToken(key, authToken) 66 | if response.HandleError(c, err, http.StatusForbidden) { 67 | return 68 | } 69 | 70 | serverId := c.Param("id") 71 | scopes := make([]scope.Scope, 0) 72 | if token.Claims.PanelClaims.Scopes[serverId] != nil { 73 | scopes = append(scopes, token.Claims.PanelClaims.Scopes[serverId]...) 74 | } 75 | if token.Claims.PanelClaims.Scopes[""] != nil { 76 | scopes = append(scopes, token.Claims.PanelClaims.Scopes[""]...) 77 | } 78 | 79 | if !apufferi.ContainsScope(scopes, requiredScope) { 80 | response.HandleError(c, pufferd.CreateErrMissingScope(requiredScope), http.StatusForbidden) 81 | return 82 | } 83 | 84 | if requireServer { 85 | program, _ := programs.Get(serverId) 86 | if program == nil { 87 | c.AbortWithStatus(http.StatusNotFound) 88 | return 89 | } 90 | 91 | c.Set("server", program) 92 | } 93 | 94 | c.Set("scopes", scopes) 95 | 96 | failure = false 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /httpmodels.go: -------------------------------------------------------------------------------- 1 | package pufferd 2 | 3 | import "github.com/pufferpanel/apufferi/v4" 4 | 5 | type ServerIdResponse struct { 6 | Id string `json:"id"` 7 | } 8 | 9 | type ServerStats struct { 10 | Cpu float64 `json:"cpu"` 11 | Memory float64 `json:"memory"` 12 | } 13 | 14 | type ServerLogs struct { 15 | Epoch int64 `json:"epoch"` 16 | Logs string `json:"logs"` 17 | } 18 | 19 | type ServerRunning struct { 20 | Running bool `json:"running"` 21 | } 22 | 23 | type ServerData struct { 24 | Variables map[string]apufferi.Variable `json:"data"` 25 | } 26 | 27 | type ServerDataAdmin struct { 28 | *apufferi.Server 29 | } 30 | 31 | type PufferdRunning struct { 32 | Message string `json:"message"` 33 | } 34 | -------------------------------------------------------------------------------- /messages/filedesc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Padduck, LLC 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package messages 15 | 16 | type FileDesc struct { 17 | Name string `json:"name"` 18 | Modified int64 `json:"modifyTime,omitempty"` 19 | Size int64 `json:"size,omitempty"` 20 | File bool `json:"isFile"` 21 | Extension string `json:"extension,omitempty"` 22 | } 23 | -------------------------------------------------------------------------------- /messages/message.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package messages 18 | 19 | import ( 20 | "github.com/gorilla/websocket" 21 | ) 22 | 23 | type Message interface { 24 | Key() string 25 | } 26 | 27 | type Transmission struct { 28 | Message Message `json:"data"` 29 | Type string `json:"type"` 30 | } 31 | 32 | func Write(c *websocket.Conn, msg Message) error { 33 | return c.WriteJSON(Transmission{Type: msg.Key(), Message: msg}) 34 | } 35 | 36 | type StatMessage struct { 37 | Memory float64 `json:"memory"` 38 | Cpu float64 `json:"cpu"` 39 | } 40 | 41 | type ConsoleMessage struct { 42 | Logs []string `json:"logs"` 43 | } 44 | 45 | type PingMessage struct { 46 | } 47 | 48 | type PongMessage struct { 49 | } 50 | 51 | type FileListMessage struct { 52 | CurrentPath string `json:"path"` 53 | Error string `json:"error,omitempty"` 54 | Url string `json:"url,omitempty"` 55 | FileList []FileDesc `json:"files,omitempty"` 56 | Contents []byte `json:"contents,omitempty"` 57 | Filename string `json:"name,omitempty"` 58 | } 59 | 60 | func (m StatMessage) Key() string { 61 | return "stat" 62 | } 63 | 64 | func (m ConsoleMessage) Key() string { 65 | return "console" 66 | } 67 | 68 | func (m PingMessage) Key() string { 69 | return "ping" 70 | } 71 | 72 | func (m PongMessage) Key() string { 73 | return "pong" 74 | } 75 | 76 | func (m FileListMessage) Key() string { 77 | return "file" 78 | } 79 | -------------------------------------------------------------------------------- /oauth2/shared.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package oauth2 18 | 19 | import ( 20 | "bytes" 21 | "encoding/json" 22 | "errors" 23 | "github.com/pufferpanel/apufferi/v4/logging" 24 | "github.com/pufferpanel/pufferd/v2/commons" 25 | "github.com/spf13/viper" 26 | "net/http" 27 | "net/url" 28 | "strconv" 29 | "sync" 30 | "time" 31 | ) 32 | 33 | var atLocker = &sync.RWMutex{} 34 | var daemonToken string 35 | var lastRefresh time.Time 36 | var expiresIn time.Duration 37 | var client = &http.Client{} 38 | 39 | func RefreshToken() bool { 40 | atLocker.Lock() 41 | defer atLocker.Unlock() 42 | 43 | //if we just refreshed in the last minute, don't refresh the token 44 | if lastRefresh.Add(1 * time.Minute).After(time.Now()) { 45 | return false 46 | } 47 | 48 | clientId := viper.GetString("auth.clientId") 49 | if clientId == "" { 50 | logging.Exception("error talking to auth server", errors.New("client id not specified")) 51 | return false 52 | } 53 | 54 | clientSecret := viper.GetString("auth.clientSecret") 55 | if clientSecret == "" { 56 | logging.Exception("error talking to auth server", errors.New("client secret not specified")) 57 | return false 58 | } 59 | 60 | data := url.Values{} 61 | data.Set("grant_type", "client_credentials") 62 | data.Set("client_id", clientId) 63 | data.Set("client_secret", clientSecret) 64 | encodedData := data.Encode() 65 | 66 | request := createRequest(encodedData) 67 | 68 | request.Header.Add("Authorization", "Bearer "+daemonToken) 69 | request.Header.Add("Content-Type", "application/x-www-form-urlencoded") 70 | request.Header.Add("Content-Length", strconv.Itoa(len(encodedData))) 71 | response, err := client.Do(request) 72 | defer commons.CloseResponse(response) 73 | if err != nil { 74 | logging.Exception("error talking to auth server", err) 75 | return false 76 | } 77 | 78 | var responseData requestResponse 79 | err = json.NewDecoder(response.Body).Decode(&responseData) 80 | 81 | if responseData.Error != "" { 82 | return false 83 | } 84 | 85 | daemonToken = responseData.AccessToken 86 | lastRefresh = time.Now() 87 | expiresIn = responseData.ExpiresIn 88 | 89 | return true 90 | } 91 | 92 | func RefreshIfStale() { 93 | //we know the token only lasts about an hour, 94 | //so we'll check to see if we know the cache is old 95 | atLocker.RLock() 96 | oldCache := lastRefresh.Add(expiresIn).Before(time.Now()) 97 | atLocker.RUnlock() 98 | if oldCache { 99 | RefreshToken() 100 | } 101 | } 102 | 103 | func createRequest(encodedData string) (request *http.Request) { 104 | authUrl := viper.GetString("auth.url") 105 | 106 | logging.Debug("Using standard http connection") 107 | request, _ = http.NewRequest("POST", authUrl+"/oauth2/token", bytes.NewBufferString(encodedData)) 108 | return 109 | } 110 | 111 | type requestResponse struct { 112 | AccessToken string `json:"access_token"` 113 | ExpiresIn time.Duration `json:"expires_in"` 114 | Error string `json:"error"` 115 | } 116 | -------------------------------------------------------------------------------- /oauth2/ssh.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package oauth2 18 | 19 | import ( 20 | "encoding/json" 21 | "errors" 22 | "github.com/pufferpanel/apufferi/v4/logging" 23 | "github.com/pufferpanel/pufferd/v2/commons" 24 | "golang.org/x/crypto/ssh" 25 | "io/ioutil" 26 | "net/url" 27 | "strconv" 28 | "strings" 29 | ) 30 | 31 | type WebSSHAuthorization struct { 32 | } 33 | 34 | func (ws *WebSSHAuthorization) Validate(username string, password string) (*ssh.Permissions, error) { 35 | return validateSSH(username, password, true) 36 | } 37 | 38 | func validateSSH(username string, password string, recurse bool) (*ssh.Permissions, error) { 39 | data := url.Values{} 40 | data.Set("grant_type", "password") 41 | data.Set("username", username) 42 | data.Set("password", password) 43 | data.Set("scope", "sftp") 44 | encodedData := data.Encode() 45 | 46 | request := createRequest(encodedData) 47 | 48 | RefreshIfStale() 49 | 50 | atLocker.RLock() 51 | request.Header.Add("Authorization", "Bearer "+daemonToken) 52 | atLocker.RUnlock() 53 | request.Header.Add("Content-Type", "application/x-www-form-urlencoded") 54 | request.Header.Add("Content-Length", strconv.Itoa(len(encodedData))) 55 | 56 | response, err := client.Do(request) 57 | defer commons.CloseResponse(response) 58 | if err != nil { 59 | logging.Exception("error talking to auth server", err) 60 | return nil, errors.New("invalid response from authorization server") 61 | } 62 | 63 | //we should only get a 200, if we get any others, we have a problem 64 | if response.StatusCode != 200 { 65 | if response.StatusCode == 401 { 66 | if recurse && RefreshToken() { 67 | commons.CloseResponse(response) 68 | return validateSSH(username, password, false) 69 | } 70 | } 71 | 72 | msg, _ := ioutil.ReadAll(response.Body) 73 | 74 | logging.Error("Error talking to auth server: [%d] [%s]", response.StatusCode, msg) 75 | return nil, errors.New("invalid response from authorization server") 76 | } 77 | 78 | var respArr map[string]interface{} 79 | err = json.NewDecoder(response.Body).Decode(&respArr) 80 | if err != nil { 81 | return nil, err 82 | } 83 | if respArr["error"] != nil { 84 | return nil, errors.New("incorrect username or password") 85 | } 86 | sshPerms := &ssh.Permissions{} 87 | scopes := strings.Split(respArr["scope"].(string), " ") 88 | if len(scopes) != 2 { 89 | return nil, errors.New("invalid response from authorization server") 90 | } 91 | for _, v := range scopes { 92 | if v != "sftp" { 93 | sshPerms.Extensions = make(map[string]string) 94 | sshPerms.Extensions["server_id"] = v 95 | return sshPerms, nil 96 | } 97 | } 98 | return nil, errors.New("incorrect username or password") 99 | } 100 | -------------------------------------------------------------------------------- /programs/loader.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | distributed under the License is distributed on an "AS IS" BASIS, 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package programs 18 | 19 | import ( 20 | "encoding/json" 21 | "errors" 22 | "fmt" 23 | "github.com/pufferpanel/pufferd/v2" 24 | "github.com/spf13/viper" 25 | "io/ioutil" 26 | "os" 27 | "path/filepath" 28 | "strings" 29 | 30 | "github.com/pufferpanel/apufferi/v4" 31 | "github.com/pufferpanel/apufferi/v4/logging" 32 | "github.com/pufferpanel/pufferd/v2/environments" 33 | "github.com/pufferpanel/pufferd/v2/programs/operations" 34 | ) 35 | 36 | var ( 37 | allPrograms = make([]*Program, 0) 38 | ServerFolder string 39 | ) 40 | 41 | func init() { 42 | ServerFolder = viper.GetString("data.servers") 43 | } 44 | 45 | func Initialize() { 46 | operations.LoadOperations() 47 | } 48 | 49 | func LoadFromFolder() { 50 | err := os.Mkdir(ServerFolder, 0755) 51 | if err != nil && !os.IsExist(err) { 52 | logging.Critical("Error creating server data folder: %s", err) 53 | } 54 | programFiles, err := ioutil.ReadDir(ServerFolder) 55 | if err != nil { 56 | logging.Critical("Error reading from server data folder: %s", err) 57 | } 58 | var program *Program 59 | for _, element := range programFiles { 60 | if element.IsDir() { 61 | continue 62 | } 63 | logging.Info("Attempting to load " + element.Name()) 64 | id := strings.TrimSuffix(element.Name(), filepath.Ext(element.Name())) 65 | program, err = Load(id) 66 | if err != nil { 67 | logging.Exception(fmt.Sprintf("Error loading server details from json (%s)", element.Name()), err) 68 | continue 69 | } 70 | logging.Info("Loaded server %s", program.Id()) 71 | allPrograms = append(allPrograms, program) 72 | } 73 | } 74 | 75 | func Get(id string) (program *Program, err error) { 76 | program = GetFromCache(id) 77 | if program == nil { 78 | program, err = Load(id) 79 | } 80 | return 81 | } 82 | 83 | func GetAll() []*Program { 84 | return allPrograms 85 | } 86 | 87 | func Load(id string) (program *Program, err error) { 88 | var data []byte 89 | data, err = ioutil.ReadFile(apufferi.JoinPath(ServerFolder, id+".json")) 90 | if len(data) == 0 || err != nil { 91 | return 92 | } 93 | program, err = LoadFromData(id, data) 94 | return 95 | } 96 | 97 | func LoadFromData(id string, source []byte) (*Program, error) { 98 | data := CreateProgram() 99 | err := json.Unmarshal(source, &data) 100 | if err != nil { 101 | return nil, err 102 | } 103 | 104 | data.Identifier = id 105 | 106 | var typeMap apufferi.Type 107 | err = apufferi.UnmarshalTo(data.Server.Environment, &typeMap) 108 | if err != nil { 109 | return nil, err 110 | } 111 | 112 | environmentType := typeMap.Type 113 | data.Environment, err = environments.Create(environmentType, ServerFolder, id, data.Server.Environment) 114 | if err != nil { 115 | return nil, err 116 | } 117 | return data, nil 118 | } 119 | 120 | func Create(program *Program) error { 121 | if GetFromCache(program.Id()) != nil { 122 | return pufferd.ErrServerAlreadyExists 123 | } 124 | 125 | var err error 126 | 127 | defer func() { 128 | if err != nil { 129 | //revert since we have an error 130 | _ = os.Remove(apufferi.JoinPath(ServerFolder, program.Id()+".json")) 131 | if program.Environment != nil { 132 | _ = program.Environment.Delete() 133 | } 134 | } 135 | }() 136 | 137 | f, err := os.Create(apufferi.JoinPath(ServerFolder, program.Id()+".json")) 138 | defer apufferi.Close(f) 139 | if err != nil { 140 | logging.Exception("error writing server", err) 141 | return err 142 | } 143 | 144 | encoder := json.NewEncoder(f) 145 | encoder.SetEscapeHTML(false) 146 | encoder.SetIndent("", " ") 147 | err = encoder.Encode(program) 148 | 149 | if err != nil { 150 | logging.Exception("error writing server", err) 151 | return err 152 | } 153 | 154 | var typeMap apufferi.Type 155 | err = apufferi.UnmarshalTo(program.Server.Environment, &typeMap) 156 | if err != nil { 157 | return err 158 | } 159 | 160 | program.Environment, err = environments.Create(typeMap.Type, ServerFolder, program.Id(), program.Server.Environment) 161 | 162 | err = program.Create() 163 | if err != nil { 164 | return err 165 | } 166 | 167 | allPrograms = append(allPrograms, program) 168 | return nil 169 | } 170 | 171 | func Delete(id string) (err error) { 172 | var index int 173 | var program *Program 174 | for i, element := range allPrograms { 175 | if element.Id() == id { 176 | program = element 177 | index = i 178 | break 179 | } 180 | } 181 | if program == nil { 182 | return 183 | } 184 | running, err := program.IsRunning() 185 | 186 | if err != nil { 187 | return 188 | } 189 | 190 | if running { 191 | err = program.Stop() 192 | if err != nil { 193 | return 194 | } 195 | } 196 | 197 | err = program.Destroy() 198 | if err != nil { 199 | return 200 | } 201 | err = os.Remove(apufferi.JoinPath(ServerFolder, program.Id()+".json")) 202 | if err != nil { 203 | logging.Exception("error removing server", err) 204 | } 205 | allPrograms = append(allPrograms[:index], allPrograms[index+1:]...) 206 | return 207 | } 208 | 209 | func GetFromCache(id string) *Program { 210 | for _, element := range allPrograms { 211 | if element != nil && element.Id() == id { 212 | return element 213 | } 214 | } 215 | return nil 216 | } 217 | 218 | func Save(id string) (err error) { 219 | program := GetFromCache(id) 220 | if program == nil { 221 | err = errors.New("no server with given id") 222 | return 223 | } 224 | err = program.Save(apufferi.JoinPath(ServerFolder, id+".json")) 225 | return 226 | } 227 | 228 | func Reload(id string) (err error) { 229 | program := GetFromCache(id) 230 | if program == nil { 231 | err = errors.New("server does not exist") 232 | return 233 | } 234 | logging.Info("Reloading server %s", program.Id()) 235 | newVersion, err := Load(id) 236 | if err != nil { 237 | logging.Exception("error reloading server", err) 238 | return 239 | } 240 | 241 | program.CopyFrom(newVersion) 242 | return 243 | } 244 | -------------------------------------------------------------------------------- /programs/operations/impl/command/command.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package command 18 | 19 | import ( 20 | "github.com/pufferpanel/pufferd/v2/environments/envs" 21 | "strings" 22 | 23 | "fmt" 24 | "github.com/pufferpanel/apufferi/v4/logging" 25 | ) 26 | 27 | type Command struct { 28 | Commands []string 29 | Env map[string]string 30 | } 31 | 32 | func (c Command) Run(env envs.Environment) error { 33 | for _, cmd := range c.Commands { 34 | logging.Debug("Executing command: %s", cmd) 35 | env.DisplayToConsole(true, fmt.Sprintf("Executing: %s\n", cmd)) 36 | parts := strings.Split(cmd, " ") 37 | cmd := parts[0] 38 | args := parts[1:] 39 | _, err := env.Execute(cmd, args, c.Env, nil) 40 | if err != nil { 41 | return err 42 | } 43 | } 44 | 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /programs/operations/impl/command/factory.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package command 18 | 19 | import ( 20 | "github.com/pufferpanel/pufferd/v2/programs/operations/ops" 21 | "github.com/spf13/cast" 22 | ) 23 | 24 | type OperationFactory struct { 25 | ops.OperationFactory 26 | } 27 | 28 | func (of OperationFactory) Create(op ops.CreateOperation) ops.Operation { 29 | cmds := cast.ToStringSlice(op.OperationArgs["commands"]) 30 | return Command{Commands: cmds, Env: op.EnvironmentVariables} 31 | } 32 | 33 | func (of OperationFactory) Key() string { 34 | return "command" 35 | } 36 | 37 | var Factory OperationFactory -------------------------------------------------------------------------------- /programs/operations/impl/download/download.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package download 18 | 19 | import ( 20 | "github.com/cavaliercoder/grab" 21 | "github.com/pufferpanel/apufferi/v4/logging" 22 | "github.com/pufferpanel/pufferd/v2/environments/envs" 23 | ) 24 | 25 | type Download struct { 26 | Files []string 27 | } 28 | 29 | func (d Download) Run(env envs.Environment) error { 30 | for _, file := range d.Files { 31 | logging.Debug("Download file from %s to %s", file, env.GetRootDirectory()) 32 | env.DisplayToConsole(true, "Downloading file %s\n", file) 33 | _, err := grab.Get(env.GetRootDirectory(), file) 34 | if err != nil { 35 | return err 36 | } 37 | } 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /programs/operations/impl/download/factory.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package download 18 | 19 | import ( 20 | "github.com/pufferpanel/pufferd/v2/programs/operations/ops" 21 | "github.com/spf13/cast" 22 | ) 23 | 24 | type OperationFactory struct { 25 | ops.OperationFactory 26 | } 27 | 28 | func (of OperationFactory) Create(op ops.CreateOperation) ops.Operation { 29 | files := cast.ToStringSlice(op.OperationArgs["files"]) 30 | return &Download{Files: files} 31 | } 32 | 33 | func (of OperationFactory) Key() string { 34 | return "download" 35 | } 36 | 37 | var Factory OperationFactory 38 | -------------------------------------------------------------------------------- /programs/operations/impl/forgedl/factory.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package forgedl 18 | 19 | import "github.com/pufferpanel/pufferd/v2/programs/operations/ops" 20 | 21 | type OperationFactory struct { 22 | ops.OperationFactory 23 | } 24 | 25 | func (of OperationFactory) Key() string { 26 | return "forgedl" 27 | } 28 | func (of OperationFactory) Create(op ops.CreateOperation) ops.Operation { 29 | version := op.OperationArgs["version"].(string) 30 | filename := op.OperationArgs["target"].(string) 31 | 32 | return ForgeDl{Version: version, Filename: filename} 33 | } 34 | 35 | var Factory OperationFactory 36 | -------------------------------------------------------------------------------- /programs/operations/impl/forgedl/forgedl.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package forgedl 18 | 19 | import ( 20 | "github.com/pufferpanel/apufferi/v4" 21 | "github.com/pufferpanel/pufferd/v2/environments" 22 | "github.com/pufferpanel/pufferd/v2/environments/envs" 23 | "path" 24 | "strings" 25 | ) 26 | 27 | const InstallerUrl = "https://files.minecraftforge.net/maven/net/minecraftforge/forge/${version}/forge-${version}-installer.jar" 28 | 29 | type ForgeDl struct { 30 | Version string 31 | Filename string 32 | } 33 | 34 | 35 | func (op ForgeDl) Run(env envs.Environment) error { 36 | jarDownload := strings.Replace(InstallerUrl, "${version}", op.Version, -1) 37 | 38 | localFile, err := environments.DownloadViaMaven(jarDownload, env) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | //copy from the cache 44 | return apufferi.CopyFile(localFile, path.Join(env.GetRootDirectory(), op.Filename)) 45 | } 46 | 47 | -------------------------------------------------------------------------------- /programs/operations/impl/mkdir/factory.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package mkdir 18 | 19 | import "github.com/pufferpanel/pufferd/v2/programs/operations/ops" 20 | 21 | type OperationFactory struct { 22 | ops.OperationFactory 23 | } 24 | 25 | func (of OperationFactory) Create(op ops.CreateOperation) ops.Operation { 26 | target := op.OperationArgs["target"].(string) 27 | return &Mkdir{TargetFile: target} 28 | } 29 | 30 | func (of OperationFactory) Key() string { 31 | return "mkdir" 32 | } 33 | 34 | var Factory OperationFactory 35 | -------------------------------------------------------------------------------- /programs/operations/impl/mkdir/mkdir.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package mkdir 18 | 19 | import ( 20 | "github.com/pufferpanel/pufferd/v2/environments/envs" 21 | "os" 22 | 23 | "github.com/pufferpanel/apufferi/v4" 24 | "github.com/pufferpanel/apufferi/v4/logging" 25 | ) 26 | 27 | type Mkdir struct { 28 | TargetFile string 29 | } 30 | 31 | func (m *Mkdir) Run(env envs.Environment) error { 32 | logging.Debug("Making directory: %s\n", m.TargetFile) 33 | env.DisplayToConsole(true, "Creating directory: %s\n", m.TargetFile) 34 | target := apufferi.JoinPath(env.GetRootDirectory(), m.TargetFile) 35 | return os.MkdirAll(target, 0755) 36 | } 37 | -------------------------------------------------------------------------------- /programs/operations/impl/mojangdl/factory.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package mojangdl 18 | 19 | import "github.com/pufferpanel/pufferd/v2/programs/operations/ops" 20 | 21 | type OperationFactory struct { 22 | ops.OperationFactory 23 | } 24 | 25 | func (of OperationFactory) Create(op ops.CreateOperation) ops.Operation { 26 | version := op.OperationArgs["version"].(string) 27 | target := op.OperationArgs["target"].(string) 28 | 29 | return MojangDl{Version: version, Target: target} 30 | } 31 | 32 | func (of OperationFactory) Key() string { 33 | return "mojangdl" 34 | } 35 | 36 | var Factory OperationFactory 37 | -------------------------------------------------------------------------------- /programs/operations/impl/mojangdl/mojangdl.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package mojangdl 18 | 19 | import ( 20 | "encoding/json" 21 | "errors" 22 | "fmt" 23 | "github.com/pufferpanel/apufferi/v4/logging" 24 | "github.com/pufferpanel/pufferd/v2/commons" 25 | "github.com/pufferpanel/pufferd/v2/environments" 26 | "github.com/pufferpanel/pufferd/v2/environments/envs" 27 | "net/http" 28 | ) 29 | 30 | const VersionJsonUrl = "https://launchermeta.mojang.com/mc/game/version_manifest.json" 31 | 32 | var client = &http.Client{} 33 | 34 | type MojangDl struct { 35 | Version string 36 | Target string 37 | } 38 | 39 | func (op MojangDl) Run(env envs.Environment) error { 40 | response, err := client.Get(VersionJsonUrl) 41 | if err != nil { 42 | return err 43 | } 44 | 45 | var data LauncherJson 46 | err = json.NewDecoder(response.Body).Decode(&data) 47 | if err != nil { 48 | return err 49 | } 50 | err = response.Body.Close() 51 | if err != nil { 52 | return err 53 | } 54 | 55 | var targetVersion string 56 | switch op.Version { 57 | case "release": 58 | targetVersion = data.Latest.Release 59 | case "latest": 60 | targetVersion = data.Latest.Release 61 | case "snapshot": 62 | targetVersion = data.Latest.Snapshot 63 | default: 64 | targetVersion = op.Version 65 | } 66 | 67 | for _, version := range data.Versions { 68 | if version.Id == targetVersion { 69 | logging.Debug("Version %s json located, downloading from %s", version.Id, version.Url) 70 | env.DisplayToConsole(true, fmt.Sprintf("Version %s json located, downloading from %s\n", version.Id, version.Url)) 71 | //now, get the version json for this one... 72 | return downloadServerFromJson(version.Url, op.Target, env) 73 | } 74 | } 75 | 76 | env.DisplayToConsole(true, "Could not locate version " + targetVersion + "\n") 77 | 78 | return errors.New("Version not located: " + op.Version) 79 | } 80 | 81 | func downloadServerFromJson(url, target string, env envs.Environment) error { 82 | response, err := client.Get(url) 83 | defer commons.CloseResponse(response) 84 | if err != nil { 85 | return err 86 | } 87 | 88 | var data VersionJson 89 | err = json.NewDecoder(response.Body).Decode(&data) 90 | if err != nil { 91 | return err 92 | } 93 | err = response.Body.Close() 94 | if err != nil { 95 | return err 96 | } 97 | 98 | serverBlock := data.Downloads["server"] 99 | 100 | logging.Debug("Version jar located, downloading from %s", serverBlock.Url) 101 | env.DisplayToConsole(true, fmt.Sprintf("Version jar located, downloading from %s\n", serverBlock.Url)) 102 | 103 | return environments.DownloadFile(serverBlock.Url, target, env) 104 | } 105 | 106 | type LauncherJson struct { 107 | Versions []LauncherVersion `json:"versions"` 108 | Latest Latest `json:"latest"` 109 | } 110 | 111 | type Latest struct { 112 | Release string `json:"release"` 113 | Snapshot string `json:"snapshot"` 114 | } 115 | 116 | type LauncherVersion struct { 117 | Id string `json:"id"` 118 | Url string `json:"url"` 119 | Type string `json:"type"` 120 | } 121 | 122 | type VersionJson struct { 123 | Downloads map[string]DownloadType `json:"downloads"` 124 | } 125 | 126 | type DownloadType struct { 127 | Sha1 string `json:"sha1"` 128 | Size uint64 `json:"size"` 129 | Url string `json:"url"` 130 | } 131 | -------------------------------------------------------------------------------- /programs/operations/impl/move/factory.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package move 18 | 19 | import "github.com/pufferpanel/pufferd/v2/programs/operations/ops" 20 | 21 | type OperationFactory struct { 22 | ops.OperationFactory 23 | } 24 | 25 | func (of OperationFactory) Create(op ops.CreateOperation) ops.Operation { 26 | source := op.OperationArgs["source"].(string) 27 | target := op.OperationArgs["target"].(string) 28 | return Move{SourceFile: source, TargetFile: target} 29 | } 30 | 31 | func (of OperationFactory) Key() string { 32 | return "move" 33 | } 34 | 35 | var Factory OperationFactory 36 | -------------------------------------------------------------------------------- /programs/operations/impl/move/move.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package move 18 | 19 | import ( 20 | "github.com/pufferpanel/pufferd/v2/environments/envs" 21 | "os" 22 | "path/filepath" 23 | 24 | "github.com/pufferpanel/apufferi/v4" 25 | "github.com/pufferpanel/apufferi/v4/logging" 26 | ) 27 | 28 | type Move struct { 29 | SourceFile string 30 | TargetFile string 31 | } 32 | 33 | func (m Move) Run(env envs.Environment) error { 34 | source := apufferi.JoinPath(env.GetRootDirectory(), m.SourceFile) 35 | target := apufferi.JoinPath(env.GetRootDirectory(), m.TargetFile) 36 | result, valid := validateMove(source, target) 37 | if !valid { 38 | return nil 39 | } 40 | 41 | for k, v := range result { 42 | logging.Debug("Moving file from %s to %s", source, target) 43 | env.DisplayToConsole(true, "Moving file from %s to %s\n", m.SourceFile, m.TargetFile) 44 | err := os.Rename(k, v) 45 | if err != nil { 46 | return err 47 | } 48 | } 49 | return nil 50 | } 51 | 52 | func validateMove(source string, target string) (result map[string]string, valid bool) { 53 | result = make(map[string]string) 54 | sourceFiles, _ := filepath.Glob(source) 55 | info, err := os.Stat(target) 56 | 57 | if err != nil { 58 | if os.IsNotExist(err) && len(sourceFiles) > 1 { 59 | logging.Error("Target folder does not exist") 60 | valid = false 61 | return 62 | } else if !os.IsNotExist(err) { 63 | valid = false 64 | logging.Exception("error reading target file", err) 65 | return 66 | } 67 | } else if info.IsDir() && len(sourceFiles) > 1 { 68 | logging.Error("Cannot move multiple files to single file target") 69 | valid = false 70 | return 71 | } 72 | 73 | if info != nil && info.IsDir() { 74 | for _, v := range sourceFiles { 75 | _, fileName := filepath.Split(v) 76 | result[v] = filepath.Join(target, fileName) 77 | } 78 | } else { 79 | for _, v := range sourceFiles { 80 | result[v] = target 81 | } 82 | } 83 | valid = true 84 | return 85 | } 86 | -------------------------------------------------------------------------------- /programs/operations/impl/spongeforgedl/factory.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package spongeforgedl 18 | 19 | import "github.com/pufferpanel/pufferd/v2/programs/operations/ops" 20 | 21 | type OperationFactory struct { 22 | ops.OperationFactory 23 | } 24 | 25 | func (of OperationFactory) Key() string { 26 | return "spongeforgedl" 27 | } 28 | func (of OperationFactory) Create(op ops.CreateOperation) ops.Operation { 29 | releaseType, ok := op.OperationArgs["releaseType"].(string) 30 | if !ok { 31 | releaseType = "recommended" 32 | } 33 | 34 | return SpongeForgeDl{ReleaseType: releaseType} 35 | } 36 | 37 | var Factory OperationFactory 38 | -------------------------------------------------------------------------------- /programs/operations/impl/spongeforgedl/spongeforgedl.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package spongeforgedl 18 | 19 | import ( 20 | "encoding/json" 21 | "errors" 22 | "github.com/pufferpanel/apufferi/v4" 23 | "github.com/pufferpanel/pufferd/v2/commons" 24 | "github.com/pufferpanel/pufferd/v2/environments" 25 | "github.com/pufferpanel/pufferd/v2/environments/envs" 26 | "github.com/pufferpanel/pufferd/v2/programs/operations/impl/forgedl" 27 | "github.com/pufferpanel/pufferd/v2/programs/operations/ops" 28 | "net/http" 29 | "os" 30 | "path" 31 | "strings" 32 | ) 33 | 34 | const DownloadApiUrl = "https://dl-api.spongepowered.org/v1/org.spongepowered/spongeforge/downloads?type=stable&limit=1" 35 | const RecommendedApiUrl = "https://dl-api.spongepowered.org/v1/org.spongepowered/spongeforge/downloads/recommended" 36 | 37 | var client = &http.Client{} 38 | type SpongeForgeDl struct { 39 | ReleaseType string 40 | } 41 | 42 | type download struct { 43 | Dependencies dependencies `json:"dependencies"` 44 | Artifacts map[string]artifact `json:"artifacts"` 45 | } 46 | 47 | type dependencies struct { 48 | Forge string `json:"forge"` 49 | Minecraft string `json:"minecraft"` 50 | } 51 | 52 | type artifact struct { 53 | Url string `json:"url"` 54 | } 55 | 56 | func (op SpongeForgeDl) Run(env envs.Environment) error { 57 | var versionData download 58 | 59 | if op.ReleaseType == "latest" { 60 | response, err := client.Get(DownloadApiUrl) 61 | defer commons.CloseResponse(response) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | var all []download 67 | err = json.NewDecoder(response.Body).Decode(&all) 68 | if err != nil { 69 | return err 70 | } 71 | err = response.Body.Close() 72 | if err != nil { 73 | return err 74 | } 75 | 76 | versionData = all[0] 77 | } else { 78 | response, err := client.Get(RecommendedApiUrl) 79 | defer commons.CloseResponse(response) 80 | 81 | if err != nil { 82 | return err 83 | } 84 | 85 | err = json.NewDecoder(response.Body).Decode(&versionData) 86 | if err != nil { 87 | return err 88 | } 89 | err = response.Body.Close() 90 | if err != nil { 91 | return err 92 | } 93 | } 94 | 95 | if versionData.Artifacts == nil || len(versionData.Artifacts) == 0 { 96 | return errors.New("no artifacts found to download") 97 | } 98 | 99 | //convert to a forge operation and have built-in process run this 100 | mapping := make(map[string]interface{}) 101 | var version = versionData.Dependencies.Forge 102 | if !strings.HasPrefix(version, versionData.Dependencies.Minecraft) { 103 | version += versionData.Dependencies.Minecraft + "-" + version 104 | } 105 | 106 | mapping["version"] = version 107 | mapping["target"] = "forge-installer.jar" 108 | forgeDlOp := forgedl.Factory.Create(ops.CreateOperation{OperationArgs: mapping}) 109 | 110 | err := forgeDlOp.Run(env) 111 | if err != nil { 112 | return err 113 | } 114 | 115 | err = os.Mkdir(path.Join(env.GetRootDirectory(), "mods"), 0755) 116 | if err != nil && !os.IsExist(err) { 117 | return err 118 | } 119 | 120 | file, err := environments.DownloadViaMaven(versionData.Artifacts[""].Url, env) 121 | if err != nil { 122 | return err 123 | } 124 | 125 | //going to stick the spongeforge rename in, to assist with those modpacks 126 | err = apufferi.CopyFile(file, path.Join("mods", "_aspongeforge.jar")) 127 | if err != nil { 128 | return err 129 | } 130 | 131 | return nil 132 | } 133 | -------------------------------------------------------------------------------- /programs/operations/impl/writefile/factory.go: -------------------------------------------------------------------------------- 1 | package writefile 2 | 3 | import "github.com/pufferpanel/pufferd/v2/programs/operations/ops" 4 | 5 | type OperationFactory struct { 6 | ops.OperationFactory 7 | } 8 | 9 | func (of OperationFactory) Create(op ops.CreateOperation) ops.Operation { 10 | text := op.OperationArgs["text"].(string) 11 | target := op.OperationArgs["target"].(string) 12 | return WriteFile{TargetFile: target, Text: text} 13 | } 14 | 15 | func (of OperationFactory) Key() string { 16 | return "writefile" 17 | } 18 | 19 | var Factory OperationFactory 20 | -------------------------------------------------------------------------------- /programs/operations/impl/writefile/writefile.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package writefile 18 | 19 | import ( 20 | "github.com/pufferpanel/pufferd/v2/environments/envs" 21 | "io/ioutil" 22 | 23 | "github.com/pufferpanel/apufferi/v4" 24 | "github.com/pufferpanel/apufferi/v4/logging" 25 | ) 26 | 27 | type WriteFile struct { 28 | TargetFile string 29 | Text string 30 | } 31 | 32 | func (c WriteFile) Run(env envs.Environment) error { 33 | logging.Debug("Writing data to file: %s", c.TargetFile) 34 | env.DisplayToConsole(true, "Writing some data to file: %s\n ", c.TargetFile) 35 | target := apufferi.JoinPath(env.GetRootDirectory(), c.TargetFile) 36 | return ioutil.WriteFile(target, []byte(c.Text), 0644) 37 | } 38 | -------------------------------------------------------------------------------- /programs/operations/operationprocess.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package operations 18 | 19 | import ( 20 | "github.com/pufferpanel/apufferi/v4" 21 | "github.com/pufferpanel/pufferd/v2" 22 | "github.com/pufferpanel/pufferd/v2/environments/envs" 23 | "github.com/pufferpanel/pufferd/v2/programs/operations/impl/command" 24 | "github.com/pufferpanel/pufferd/v2/programs/operations/impl/download" 25 | "github.com/pufferpanel/pufferd/v2/programs/operations/impl/forgedl" 26 | "github.com/pufferpanel/pufferd/v2/programs/operations/impl/mkdir" 27 | "github.com/pufferpanel/pufferd/v2/programs/operations/impl/mojangdl" 28 | "github.com/pufferpanel/pufferd/v2/programs/operations/impl/move" 29 | "github.com/pufferpanel/pufferd/v2/programs/operations/impl/spongeforgedl" 30 | "github.com/pufferpanel/pufferd/v2/programs/operations/impl/writefile" 31 | "github.com/pufferpanel/pufferd/v2/programs/operations/ops" 32 | "github.com/spf13/cast" 33 | ) 34 | 35 | var commandMapping map[string]ops.OperationFactory 36 | 37 | func LoadOperations() { 38 | commandMapping = make(map[string]ops.OperationFactory) 39 | 40 | loadCoreModules() 41 | 42 | loadOpModules() 43 | } 44 | 45 | func GenerateProcess(directions []interface{}, environment envs.Environment, dataMapping map[string]interface{}, env map[string]string) (OperationProcess, error) { 46 | dataMap := make(map[string]interface{}) 47 | for k, v := range dataMapping { 48 | dataMap[k] = v 49 | } 50 | 51 | dataMap["rootDir"] = environment.GetRootDirectory() 52 | operationList := make([]ops.Operation, 0) 53 | for _, mapping := range directions { 54 | 55 | var typeMap apufferi.MetadataType 56 | err := apufferi.UnmarshalTo(mapping, &typeMap) 57 | if err != nil { 58 | return OperationProcess{}, err 59 | } 60 | 61 | factory := commandMapping[typeMap.Type] 62 | if factory == nil { 63 | return OperationProcess{}, pufferd.ErrMissingFactory 64 | } 65 | 66 | mapCopy := make(map[string]interface{}, 0) 67 | 68 | //replace tokens 69 | for k, v := range typeMap.Metadata { 70 | switch r := v.(type) { 71 | case string: 72 | { 73 | mapCopy[k] = apufferi.ReplaceTokens(r, dataMap) 74 | } 75 | case []string: 76 | { 77 | mapCopy[k] = apufferi.ReplaceTokensInArr(r, dataMap) 78 | } 79 | case map[string]string: 80 | { 81 | mapCopy[k] = apufferi.ReplaceTokensInMap(r, dataMap) 82 | } 83 | case []interface{}: 84 | { 85 | //if we can convert this to a string list, we can work with it 86 | temp := cast.ToStringSlice(r) 87 | if len(temp) == len(r) { 88 | mapCopy[k] = apufferi.ReplaceTokensInArr(temp, dataMap) 89 | } else { 90 | mapCopy[k] = v 91 | } 92 | } 93 | default: 94 | mapCopy[k] = v 95 | } 96 | } 97 | 98 | envMap := apufferi.ReplaceTokensInMap(env, dataMap) 99 | 100 | opCreate := ops.CreateOperation{ 101 | OperationArgs: mapCopy, 102 | EnvironmentVariables: envMap, 103 | DataMap: dataMap, 104 | } 105 | 106 | op := factory.Create(opCreate) 107 | 108 | operationList = append(operationList, op) 109 | } 110 | return OperationProcess{processInstructions: operationList}, nil 111 | } 112 | 113 | type OperationProcess struct { 114 | processInstructions []ops.Operation 115 | } 116 | 117 | func (p *OperationProcess) Run(env envs.Environment) (err error) { 118 | for p.HasNext() { 119 | err = p.RunNext(env) 120 | if err != nil { 121 | break 122 | } 123 | } 124 | return 125 | } 126 | 127 | func (p *OperationProcess) RunNext(env envs.Environment) error { 128 | var op ops.Operation 129 | op, p.processInstructions = p.processInstructions[0], p.processInstructions[1:] 130 | err := op.Run(env) 131 | return err 132 | } 133 | 134 | func (p *OperationProcess) HasNext() bool { 135 | return len(p.processInstructions) != 0 && p.processInstructions[0] != nil 136 | } 137 | 138 | func loadCoreModules() { 139 | commandFactory := command.Factory 140 | commandMapping[commandFactory.Key()] = commandFactory 141 | 142 | downloadFactory := download.Factory 143 | commandMapping[downloadFactory.Key()] = downloadFactory 144 | 145 | mkdirFactory := mkdir.Factory 146 | commandMapping[mkdirFactory.Key()] = mkdirFactory 147 | 148 | moveFactory := move.Factory 149 | commandMapping[moveFactory.Key()] = moveFactory 150 | 151 | writeFileFactory := writefile.Factory 152 | commandMapping[writeFileFactory.Key()] = writeFileFactory 153 | 154 | mojangFactory := mojangdl.Factory 155 | commandMapping[mojangFactory.Key()] = mojangFactory 156 | 157 | spongeforgeDlFactory := spongeforgedl.Factory 158 | commandMapping[spongeforgeDlFactory.Key()] = spongeforgeDlFactory 159 | 160 | forgeDlFactory := forgedl.Factory 161 | commandMapping[forgeDlFactory.Key()] = forgeDlFactory 162 | } 163 | -------------------------------------------------------------------------------- /programs/operations/operationprocess_linux.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package operations 18 | 19 | import ( 20 | "github.com/spf13/viper" 21 | "io/ioutil" 22 | "os" 23 | "path" 24 | "plugin" 25 | "reflect" 26 | 27 | "github.com/pufferpanel/apufferi/v4/logging" 28 | "github.com/pufferpanel/pufferd/v2/programs/operations/ops" 29 | ) 30 | 31 | func loadOpModules() { 32 | var directory = path.Join(viper.GetString("data.modules"), "operations") 33 | 34 | files, err := ioutil.ReadDir(directory) 35 | if err != nil && os.IsNotExist(err) { 36 | return 37 | } else if err != nil { 38 | logging.Exception("error reading module directory", err) 39 | } 40 | 41 | for _, file := range files { 42 | logging.Info("Loading operation module: %s", file.Name()) 43 | p, e := plugin.Open(path.Join(directory, file.Name())) 44 | if e != nil { 45 | logging.Exception("error opening module", err) 46 | continue 47 | } 48 | 49 | factory, e := p.Lookup("Factory") 50 | if e != nil { 51 | logging.Exception("error locating factory", err) 52 | continue 53 | } 54 | 55 | fty, ok := factory.(ops.OperationFactory) 56 | if !ok { 57 | logging.Error("expected OperationFactory, but found %s", reflect.TypeOf(factory).Name()) 58 | continue 59 | } 60 | 61 | commandMapping[fty.Key()] = fty 62 | 63 | logging.Info("Loaded operation module: %s", fty.Key()) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /programs/operations/operationprocess_windows.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package operations 18 | 19 | func loadOpModules() { 20 | } 21 | -------------------------------------------------------------------------------- /programs/operations/ops/operation.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package ops 18 | 19 | import ( 20 | "github.com/pufferpanel/pufferd/v2/environments/envs" 21 | ) 22 | 23 | type Operation interface { 24 | Run(env envs.Environment) error 25 | } 26 | 27 | type OperationFactory interface { 28 | Create(CreateOperation) Operation 29 | 30 | Key() string 31 | } 32 | 33 | type CreateOperation struct { 34 | OperationArgs map[string]interface{} 35 | EnvironmentVariables map[string]string 36 | DataMap map[string]interface{} 37 | } 38 | -------------------------------------------------------------------------------- /programs/program.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package programs 18 | 19 | import ( 20 | "container/list" 21 | "encoding/json" 22 | "github.com/pufferpanel/apufferi/v4" 23 | "github.com/pufferpanel/apufferi/v4/logging" 24 | "github.com/pufferpanel/pufferd/v2" 25 | "github.com/pufferpanel/pufferd/v2/environments/envs" 26 | "github.com/pufferpanel/pufferd/v2/messages" 27 | "github.com/pufferpanel/pufferd/v2/programs/operations" 28 | "github.com/spf13/viper" 29 | "io" 30 | "io/ioutil" 31 | "os" 32 | "path/filepath" 33 | "sync" 34 | "time" 35 | ) 36 | 37 | type Program struct { 38 | apufferi.Server 39 | 40 | CrashCounter int 41 | Environment envs.Environment 42 | } 43 | 44 | var queue *list.List 45 | var lock = sync.Mutex{} 46 | var ticker *time.Ticker 47 | var running = false 48 | 49 | func InitService() { 50 | queue = list.New() 51 | ticker = time.NewTicker(1 * time.Second) 52 | running = true 53 | go processQueue() 54 | } 55 | 56 | func StartViaService(p *Program) { 57 | lock.Lock() 58 | defer func() { 59 | lock.Unlock() 60 | }() 61 | 62 | if running { 63 | queue.PushBack(p) 64 | } 65 | } 66 | 67 | func ShutdownService() { 68 | lock.Lock() 69 | defer func() { 70 | lock.Unlock() 71 | }() 72 | 73 | running = false 74 | ticker.Stop() 75 | } 76 | 77 | func processQueue() { 78 | for range ticker.C { 79 | lock.Lock() 80 | next := queue.Front() 81 | if next != nil { 82 | queue.Remove(next) 83 | } 84 | lock.Unlock() 85 | if next == nil { 86 | continue 87 | } 88 | program := next.Value.(*Program) 89 | if run, _ := program.IsRunning(); !run { 90 | err := program.Start() 91 | if err != nil { 92 | logging.Exception("Error starting server "+program.Id(), err) 93 | } 94 | } 95 | } 96 | } 97 | 98 | type FileData struct { 99 | Contents io.ReadCloser 100 | ContentLength int64 101 | FileList []messages.FileDesc 102 | Name string 103 | } 104 | 105 | func (p *Program) DataToMap() map[string]interface{} { 106 | var result = make(map[string]interface{}, len(p.Variables)) 107 | 108 | for k, v := range p.Variables { 109 | result[k] = v.Value 110 | } 111 | 112 | return result 113 | } 114 | 115 | func CreateProgram() *Program { 116 | return &Program{ 117 | Server: apufferi.Server{ 118 | Execution: apufferi.Execution{ 119 | Disabled: false, 120 | AutoStart: false, 121 | AutoRestartFromCrash: false, 122 | AutoRestartFromGraceful: false, 123 | PreExecution: make([]interface{}, 0), 124 | PostExecution: make([]interface{}, 0), 125 | EnvironmentVariables: make(map[string]string, 0), 126 | }, 127 | Type: "standard", 128 | Variables: make(map[string]apufferi.Variable, 0), 129 | Display: "Unknown server", 130 | Installation: make([]interface{}, 0), 131 | Uninstallation: make([]interface{}, 0), 132 | }, 133 | } 134 | } 135 | 136 | //Starts the program. 137 | //This includes starting the environment if it is not running. 138 | func (p *Program) Start() (err error) { 139 | if !p.IsEnabled() { 140 | logging.Error("Server %s is not enabled, cannot start", p.Id()) 141 | return pufferd.ErrServerDisabled 142 | } 143 | if running, err := p.IsRunning(); running || err != nil { 144 | return err 145 | } 146 | 147 | logging.Debug("Starting server %s", p.Id()) 148 | p.Environment.DisplayToConsole(true, "Starting server\n") 149 | data := make(map[string]interface{}) 150 | for k, v := range p.Variables { 151 | data[k] = v.Value 152 | } 153 | 154 | process, err := operations.GenerateProcess(p.Execution.PreExecution, p.Environment, p.DataToMap(), p.Execution.EnvironmentVariables) 155 | if err != nil { 156 | logging.Exception("Error generating pre-execution steps", err) 157 | p.Environment.DisplayToConsole(true, "Error running pre execute\n") 158 | p.Environment.DisplayToConsole(true, "%s\n", err.Error()) 159 | return 160 | } 161 | 162 | err = process.Run(p.Environment) 163 | if err != nil { 164 | logging.Exception("Error running pre-execution steps", err) 165 | p.Environment.DisplayToConsole(true, "Error running pre execute\n") 166 | p.Environment.DisplayToConsole(true, "%s\n", err.Error()) 167 | return 168 | } 169 | 170 | //HACK: add rootDir stuff 171 | data["rootDir"] = p.Environment.GetRootDirectory() 172 | 173 | err = p.Environment.ExecuteAsync(p.Execution.ProgramName, apufferi.ReplaceTokensInArr(p.Execution.Arguments, data), apufferi.ReplaceTokensInMap(p.Execution.EnvironmentVariables, data), p.afterExit) 174 | if err != nil { 175 | logging.Exception("error starting server "+p.Id(), err) 176 | p.Environment.DisplayToConsole(true, " Failed to start server\n") 177 | p.Environment.DisplayToConsole(true, "%s\n", err.Error()) 178 | } 179 | 180 | return 181 | } 182 | 183 | //Stops the program. 184 | //This will also stop the environment it is ran in. 185 | func (p *Program) Stop() (err error) { 186 | if running, err := p.IsRunning(); !running || err != nil { 187 | return err 188 | } 189 | 190 | logging.Debug("Stopping server %s", p.Id()) 191 | if p.Execution.StopCode != 0 { 192 | err = p.Environment.SendCode(p.Execution.StopCode) 193 | } else { 194 | err = p.Environment.ExecuteInMainProcess(p.Execution.StopCommand) 195 | } 196 | if err != nil { 197 | logging.Exception("Error stopping server", err) 198 | p.Environment.DisplayToConsole(true, "Failed to stop server\n") 199 | p.Environment.DisplayToConsole(true, "%s\n", err.Error()) 200 | } else { 201 | p.Environment.DisplayToConsole(true, "Server was told to stop\n") 202 | } 203 | return 204 | } 205 | 206 | //Kills the program. 207 | //This will also stop the environment it is ran in. 208 | func (p *Program) Kill() (err error) { 209 | logging.Debug("Killing server %s", p.Id()) 210 | err = p.Environment.Kill() 211 | if err != nil { 212 | logging.Exception("Error killing server", err) 213 | p.Environment.DisplayToConsole(true, "Failed to kill server\n") 214 | p.Environment.DisplayToConsole(true, "%s\n", err.Error()) 215 | } else { 216 | p.Environment.DisplayToConsole(true, "Server killed\n") 217 | } 218 | return 219 | } 220 | 221 | //Creates any files needed for the program. 222 | //This includes creating the environment. 223 | func (p *Program) Create() (err error) { 224 | logging.Debug("Creating server %s", p.Id()) 225 | p.Environment.DisplayToConsole(true, "Allocating server\n") 226 | err = p.Environment.Create() 227 | if err != nil { 228 | logging.Exception("Error creating server", err) 229 | p.Environment.DisplayToConsole(true, "Failed to create server\n") 230 | p.Environment.DisplayToConsole(true, "%s\n", err.Error()) 231 | } else { 232 | p.Environment.DisplayToConsole(true, "Server allocated\n") 233 | p.Environment.DisplayToConsole(true, "Ready to be installed\n") 234 | } 235 | 236 | return 237 | } 238 | 239 | //Destroys the server. 240 | //This will delete the server, environment, and any files related to it. 241 | func (p *Program) Destroy() (err error) { 242 | logging.Debug("Destroying server %s", p.Id()) 243 | process, err := operations.GenerateProcess(p.Uninstallation, p.Environment, p.DataToMap(), p.Execution.EnvironmentVariables) 244 | if err != nil { 245 | logging.Exception("Error uninstalling server", err) 246 | p.Environment.DisplayToConsole(true, "Failed to uninstall server\n") 247 | p.Environment.DisplayToConsole(true, "%s\n", err.Error()) 248 | return 249 | } 250 | 251 | err = process.Run(p.Environment) 252 | if err != nil { 253 | logging.Exception("Error uninstalling server", err) 254 | p.Environment.DisplayToConsole(true, "Failed to uninstall server\n") 255 | p.Environment.DisplayToConsole(true, "%s\n", err.Error()) 256 | return 257 | } 258 | 259 | err = p.Environment.Delete() 260 | if err != nil { 261 | logging.Exception("Error uninstalling server", err) 262 | p.Environment.DisplayToConsole(true, "Failed to uninstall server\n") 263 | p.Environment.DisplayToConsole(true, "%s\n", err.Error()) 264 | } 265 | return 266 | } 267 | 268 | func (p *Program) Install() (err error) { 269 | if !p.IsEnabled() { 270 | logging.Error("Server %s is not enabled, cannot install", p.Id()) 271 | return pufferd.ErrServerDisabled 272 | } 273 | 274 | logging.Debug("Installing server %s", p.Id()) 275 | running, err := p.IsRunning() 276 | if err != nil { 277 | logging.Exception("error checking server status", err) 278 | p.Environment.DisplayToConsole(true, "Error on checking to see if server is running\n") 279 | return 280 | } 281 | 282 | if running { 283 | err = p.Stop() 284 | } 285 | 286 | if err != nil { 287 | logging.Exception("Error stopping server", err) 288 | p.Environment.DisplayToConsole(true, "Failed to stop server\n") 289 | p.Environment.DisplayToConsole(true, "%s\n", err.Error()) 290 | return 291 | } 292 | 293 | p.Environment.DisplayToConsole(true, "Installing server\n") 294 | 295 | err = os.MkdirAll(p.Environment.GetRootDirectory(), 0755) 296 | if err != nil && !os.IsExist(err) { 297 | logging.Exception("Error creating server directory", err) 298 | p.Environment.DisplayToConsole(true, "Failed to create server directory\n") 299 | p.Environment.DisplayToConsole(true, "%s\n", err.Error()) 300 | return 301 | } 302 | 303 | if len(p.Installation) > 0 { 304 | process, err := operations.GenerateProcess(p.Installation, p.GetEnvironment(), p.DataToMap(), p.Execution.EnvironmentVariables) 305 | if err != nil { 306 | logging.Exception("Error installing server", err) 307 | p.Environment.DisplayToConsole(true, "Failed to install server\n") 308 | p.Environment.DisplayToConsole(true, "%s\n", err.Error()) 309 | return err 310 | } 311 | 312 | err = process.Run(p.Environment) 313 | if err != nil { 314 | logging.Exception("Error installing server", err) 315 | p.Environment.DisplayToConsole(true, "Failed to install server\n") 316 | p.Environment.DisplayToConsole(true, "%s\n", err.Error()) 317 | return err 318 | } 319 | } 320 | 321 | p.Environment.DisplayToConsole(true, "Server installed\n") 322 | return 323 | } 324 | 325 | //Determines if the server is running. 326 | func (p *Program) IsRunning() (isRunning bool, err error) { 327 | isRunning, err = p.Environment.IsRunning() 328 | return 329 | } 330 | 331 | //Sends a command to the process 332 | //If the program supports input, this will send the arguments to that. 333 | func (p *Program) Execute(command string) (err error) { 334 | err = p.Environment.ExecuteInMainProcess(command) 335 | return 336 | } 337 | 338 | func (p *Program) SetEnabled(isEnabled bool) (err error) { 339 | p.Execution.Disabled = !isEnabled 340 | return 341 | } 342 | 343 | func (p *Program) IsEnabled() (isEnabled bool) { 344 | return !p.Execution.Disabled 345 | } 346 | 347 | func (p *Program) SetEnvironment(environment envs.Environment) (err error) { 348 | p.Environment = environment 349 | return 350 | } 351 | 352 | func (p *Program) Id() string { 353 | return p.Identifier 354 | } 355 | 356 | func (p *Program) GetEnvironment() envs.Environment { 357 | return p.Environment 358 | } 359 | 360 | func (p *Program) SetAutoStart(isAutoStart bool) (err error) { 361 | p.Execution.AutoStart = isAutoStart 362 | return 363 | } 364 | 365 | func (p *Program) IsAutoStart() (isAutoStart bool) { 366 | isAutoStart = p.Execution.AutoStart 367 | return 368 | } 369 | 370 | func (p *Program) Save(file string) (err error) { 371 | logging.Debug("Saving server %s", p.Id()) 372 | 373 | data, err := json.MarshalIndent(p, "", " ") 374 | if err != nil { 375 | return 376 | } 377 | 378 | err = ioutil.WriteFile(file, data, 0664) 379 | return 380 | } 381 | 382 | func (p *Program) Edit(data map[string]apufferi.Variable, overrideUser bool) (err error) { 383 | for k, v := range data { 384 | var elem apufferi.Variable 385 | 386 | if _, ok := p.Variables[k]; ok { 387 | elem = p.Variables[k] 388 | } else { 389 | //copy from provided 390 | elem = v 391 | } 392 | if !elem.UserEditable && !overrideUser { 393 | continue 394 | } 395 | 396 | elem.Value = v.Value 397 | 398 | p.Variables[k] = elem 399 | } 400 | err = Save(p.Id()) 401 | return 402 | } 403 | 404 | func (p *Program) GetData() map[string]apufferi.Variable { 405 | return p.Variables 406 | } 407 | 408 | func (p *Program) GetNetwork() string { 409 | data := p.GetData() 410 | ip := "0.0.0.0" 411 | port := "0" 412 | 413 | if ipData, ok := data["ip"]; ok { 414 | if _, ok = ipData.Value.(string); ok { 415 | ip = ipData.Value.(string) 416 | } 417 | } 418 | 419 | if portData, ok := data["port"]; ok { 420 | if _, ok = portData.Value.(string); ok { 421 | port = portData.Value.(string) 422 | } 423 | } 424 | 425 | return ip + ":" + port 426 | } 427 | 428 | func (p *Program) CopyFrom(s *Program) { 429 | p.Variables = s.Variables 430 | p.Execution = s.Execution 431 | p.Display = s.Display 432 | p.Environment = s.Environment 433 | p.Installation = s.Installation 434 | p.Uninstallation = s.Uninstallation 435 | p.Type = s.Type 436 | } 437 | 438 | func (p *Program) afterExit(graceful bool) { 439 | if graceful { 440 | p.CrashCounter = 0 441 | } 442 | 443 | mapping := p.DataToMap() 444 | mapping["success"] = graceful 445 | 446 | processes, err := operations.GenerateProcess(p.Execution.PostExecution, p.Environment, mapping, p.Execution.EnvironmentVariables) 447 | if err != nil { 448 | logging.Exception("Error running post processing for server " + p.Id(), err) 449 | p.Environment.DisplayToConsole(true, "Failed to run post-execution steps\n") 450 | p.Environment.DisplayToConsole(true, "%s\n", err.Error()) 451 | return 452 | } 453 | p.Environment.DisplayToConsole(true, "Running post-execution steps\n") 454 | logging.Debug("Running post execution steps: %s", p.Id()) 455 | 456 | err = processes.Run(p.Environment) 457 | if err != nil { 458 | logging.Exception("Error running post processing for server", err) 459 | p.Environment.DisplayToConsole(true, "Failed to run post-execution steps\n") 460 | p.Environment.DisplayToConsole(true, "%s\n", err.Error()) 461 | return 462 | } 463 | 464 | if !p.Execution.AutoRestartFromCrash && !p.Execution.AutoRestartFromGraceful { 465 | return 466 | } 467 | 468 | if graceful && p.Execution.AutoRestartFromGraceful { 469 | StartViaService(p) 470 | } else if !graceful && p.Execution.AutoRestartFromCrash && p.CrashCounter < viper.GetInt("data.crashLimit") { 471 | p.CrashCounter++ 472 | StartViaService(p) 473 | } 474 | } 475 | 476 | func (p *Program) GetItem(name string) (*FileData, error) { 477 | targetFile := apufferi.JoinPath(p.GetEnvironment().GetRootDirectory(), name) 478 | if !apufferi.EnsureAccess(targetFile, p.GetEnvironment().GetRootDirectory()) { 479 | return nil, pufferd.ErrIllegalFileAccess 480 | } 481 | 482 | info, err := os.Stat(targetFile) 483 | 484 | if err != nil { 485 | return nil, err 486 | } 487 | 488 | if info.IsDir() { 489 | files, _ := ioutil.ReadDir(targetFile) 490 | var fileNames []messages.FileDesc 491 | offset := 0 492 | if name == "" || name == "." || name == "/" { 493 | fileNames = make([]messages.FileDesc, len(files)) 494 | } else { 495 | fileNames = make([]messages.FileDesc, len(files)+1) 496 | fileNames[0] = messages.FileDesc{ 497 | Name: "..", 498 | File: false, 499 | } 500 | offset = 1 501 | } 502 | 503 | //validate any symlinks are valid 504 | files = apufferi.RemoveInvalidSymlinks(files, targetFile, p.GetEnvironment().GetRootDirectory()) 505 | 506 | for i, file := range files { 507 | newFile := messages.FileDesc{ 508 | Name: file.Name(), 509 | File: !file.IsDir(), 510 | } 511 | 512 | if newFile.File { 513 | newFile.Size = file.Size() 514 | newFile.Modified = file.ModTime().Unix() 515 | newFile.Extension = filepath.Ext(file.Name()) 516 | } 517 | 518 | fileNames[i+offset] = newFile 519 | } 520 | 521 | return &FileData{FileList: fileNames}, nil 522 | } else { 523 | file, err := os.Open(targetFile) 524 | if err != nil { 525 | return nil, err 526 | } 527 | return &FileData{Contents: file, ContentLength: info.Size(), Name: info.Name()}, nil 528 | } 529 | } 530 | 531 | func (p *Program) CreateFolder(name string) error { 532 | folder := apufferi.JoinPath(p.GetEnvironment().GetRootDirectory(), name) 533 | 534 | if !apufferi.EnsureAccess(folder, p.GetEnvironment().GetRootDirectory()) { 535 | return pufferd.ErrIllegalFileAccess 536 | } 537 | return os.Mkdir(folder, 0755) 538 | } 539 | 540 | func (p *Program) OpenFile(name string) (io.WriteCloser, error) { 541 | targetFile := apufferi.JoinPath(p.GetEnvironment().GetRootDirectory(), name) 542 | 543 | if !apufferi.EnsureAccess(targetFile, p.GetEnvironment().GetRootDirectory()) { 544 | return nil, pufferd.ErrIllegalFileAccess 545 | } 546 | 547 | file, err := os.Create(targetFile) 548 | return file, err 549 | } 550 | 551 | func (p *Program) DeleteItem(name string) error { 552 | targetFile := apufferi.JoinPath(p.GetEnvironment().GetRootDirectory(), name) 553 | 554 | if !apufferi.EnsureAccess(targetFile, p.GetEnvironment().GetRootDirectory()) { 555 | return pufferd.ErrIllegalFileAccess 556 | } 557 | 558 | return os.RemoveAll(targetFile) 559 | } 560 | -------------------------------------------------------------------------------- /routing/root.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package routing 18 | 19 | import ( 20 | "github.com/gin-gonic/gin" 21 | "github.com/pufferpanel/apufferi/v4/logging" 22 | "github.com/pufferpanel/apufferi/v4/middleware" 23 | "github.com/pufferpanel/apufferi/v4/response" 24 | "github.com/pufferpanel/pufferd/v2" 25 | _ "github.com/pufferpanel/pufferd/v2/docs" 26 | "github.com/pufferpanel/pufferd/v2/routing/server" 27 | "github.com/pufferpanel/pufferd/v2/routing/swagger" 28 | "net/http" 29 | "strings" 30 | ) 31 | 32 | // @title Pufferd API 33 | // @version 2.0 34 | // @description PufferPanel daemon service 35 | // @contact.name PufferPanel 36 | // @contact.url https://pufferpanel.com 37 | // @license.name Apache 2.0 38 | // @license.url http://www.apache.org/licenses/LICENSE-2.0.html 39 | func ConfigureWeb() *gin.Engine { 40 | r := gin.New() 41 | { 42 | r.Use(gin.Recovery()) 43 | r.Use(gin.LoggerWithWriter(logging.AsWriter(logging.INFO))) 44 | r.Use(func(c *gin.Context) { 45 | if c.GetHeader("Connection") == "Upgrade" { 46 | return 47 | } 48 | if strings.HasPrefix(c.Request.URL.Path, "/swagger/") { 49 | return 50 | } 51 | middleware.ResponseAndRecover(c) 52 | }) 53 | RegisterRoutes(r) 54 | server.RegisterRoutes(r.Group("/")) 55 | swagger.Load(r) 56 | } 57 | 58 | return r 59 | } 60 | 61 | func RegisterRoutes(e *gin.Engine) { 62 | e.GET("", getStatusGET) 63 | e.HEAD("", getStatusHEAD) 64 | e.Handle("OPTIONS", "", response.CreateOptions("GET", "HEAD")) 65 | } 66 | 67 | // Root godoc 68 | // @Summary Is daemon up 69 | // @Description Easy way to tell if the daemon is running is by using this endpoint 70 | // @Accept json 71 | // @Produce json 72 | // @Success 200 {object} pufferd.PufferdRunning "Service running" 73 | // @Router / [get] 74 | func getStatusGET(c *gin.Context) { 75 | c.JSON(http.StatusOK, &pufferd.PufferdRunning{Message: "pufferd is running"}) 76 | } 77 | 78 | // Root godoc 79 | // @Summary Is daemon up 80 | // @Description Easy way to tell if the daemon is running is by using this endpoint 81 | // @Accept json 82 | // @Produce json 83 | // @Success 204 {object} response.Empty "Service running" 84 | // @Router / [head] 85 | func getStatusHEAD(c *gin.Context) { 86 | c.Status(http.StatusNoContent) 87 | } 88 | -------------------------------------------------------------------------------- /routing/server/websocket.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "github.com/gorilla/websocket" 7 | "github.com/pufferpanel/apufferi/v4" 8 | "github.com/pufferpanel/apufferi/v4/logging" 9 | "github.com/pufferpanel/apufferi/v4/scope" 10 | "github.com/pufferpanel/pufferd/v2/messages" 11 | "github.com/pufferpanel/pufferd/v2/programs" 12 | "github.com/spf13/viper" 13 | "io" 14 | path2 "path" 15 | "reflect" 16 | "strings" 17 | ) 18 | 19 | func listenOnSocket(conn *websocket.Conn, server *programs.Program, scopes []scope.Scope) { 20 | defer func() { 21 | if err := recover(); err != nil { 22 | logging.Error("Error with websocket connection for server %s: %s", server.Id(), err) 23 | } 24 | }() 25 | 26 | for { 27 | msgType, data, err := conn.ReadMessage() 28 | if err != nil { 29 | logging.Exception("error on reading from websocket", err) 30 | return 31 | } 32 | if msgType != websocket.TextMessage { 33 | continue 34 | } 35 | mapping := make(map[string]interface{}) 36 | 37 | err = json.Unmarshal(data, &mapping) 38 | if err != nil { 39 | logging.Exception("error on decoding websocket message", err) 40 | continue 41 | } 42 | 43 | messageType := mapping["type"] 44 | if message, ok := messageType.(string); ok { 45 | switch strings.ToLower(message) { 46 | case "stat": 47 | { 48 | if apufferi.ContainsScope(scopes, scope.ServersStat) { 49 | results, err := server.GetEnvironment().GetStats() 50 | msg := messages.StatMessage{} 51 | if err != nil { 52 | msg.Cpu = 0 53 | msg.Memory = 0 54 | } else { 55 | msg.Cpu = results.Cpu 56 | msg.Memory = results.Memory 57 | } 58 | _ = messages.Write(conn, msg) 59 | } 60 | } 61 | case "start": 62 | { 63 | if apufferi.ContainsScope(scopes, scope.ServersStart) { 64 | _ = server.Start() 65 | } 66 | break 67 | } 68 | case "stop": 69 | { 70 | if apufferi.ContainsScope(scopes, scope.ServersStop) { 71 | _ = server.Stop() 72 | } 73 | } 74 | case "install": 75 | { 76 | if apufferi.ContainsScope(scopes, scope.ServersInstall) { 77 | _ = server.Install() 78 | } 79 | } 80 | case "kill": 81 | { 82 | if apufferi.ContainsScope(scopes, scope.ServersStop) { 83 | _ = server.Kill() 84 | } 85 | } 86 | case "reload": 87 | { 88 | if apufferi.ContainsScope(scopes, scope.ServersEditAdmin) { 89 | _ = programs.Reload(server.Id()) 90 | } 91 | } 92 | case "ping": 93 | { 94 | _ = messages.Write(conn, messages.PongMessage{}) 95 | } 96 | case "console": 97 | { 98 | cmd, ok := mapping["command"].(string) 99 | if ok { 100 | if run, _ := server.IsRunning(); run { 101 | _ = server.GetEnvironment().ExecuteInMainProcess(cmd) 102 | } 103 | } 104 | } 105 | case "file": 106 | { 107 | if !apufferi.ContainsScope(scopes, scope.ServersFilesGet) { 108 | break 109 | } 110 | 111 | action, ok := mapping["action"].(string) 112 | if !ok { 113 | break 114 | } 115 | path, ok := mapping["path"].(string) 116 | if !ok { 117 | break 118 | } 119 | 120 | switch strings.ToLower(action) { 121 | case "get": 122 | { 123 | editMode, ok := mapping["edit"].(bool) 124 | handleGetFile(conn, server, path, ok && editMode) 125 | } 126 | break 127 | case "delete": 128 | { 129 | if !apufferi.ContainsScope(scopes, scope.ServersFilesPut) { 130 | break 131 | } 132 | 133 | err := server.DeleteItem(path) 134 | if err != nil { 135 | _ = messages.Write(conn, messages.FileListMessage{Error: err.Error()}) 136 | } else { 137 | //now get the root 138 | handleGetFile(conn, server, path2.Dir(path), false) 139 | } 140 | } 141 | break 142 | case "create": 143 | { 144 | if !apufferi.ContainsScope(scopes, scope.ServersFilesPut) { 145 | break 146 | } 147 | 148 | err := server.CreateFolder(path) 149 | 150 | if err != nil { 151 | _ = messages.Write(conn, messages.FileListMessage{Error: err.Error()}) 152 | } else { 153 | handleGetFile(conn, server, path, false) 154 | } 155 | } 156 | break 157 | } 158 | } 159 | default: 160 | _ = conn.WriteJSON(map[string]string{"error": "unknown command"}) 161 | } 162 | } else { 163 | logging.Error("message type is not a string, but was %s", reflect.TypeOf(messageType)) 164 | } 165 | } 166 | } 167 | 168 | func handleGetFile(conn *websocket.Conn, server *programs.Program, path string, editMode bool) { 169 | data, err := server.GetItem(path) 170 | if err != nil { 171 | _ = messages.Write(conn, messages.FileListMessage{Error: err.Error()}) 172 | return 173 | } 174 | 175 | defer apufferi.Close(data.Contents) 176 | 177 | if data.FileList != nil { 178 | _ = messages.Write(conn, messages.FileListMessage{FileList: data.FileList, CurrentPath: path}) 179 | } else if data.Contents != nil { 180 | //if the file is small enough, we'll send it over the websocket 181 | if editMode && data.ContentLength < viper.GetInt64("data.maxWSDownloadSize") { 182 | var buf bytes.Buffer 183 | _, _ = io.Copy(&buf, data.Contents) 184 | _ = messages.Write(conn, messages.FileListMessage{Contents: buf.Bytes(), Filename: data.Name}) 185 | } else { 186 | _ = messages.Write(conn, messages.FileListMessage{Url: path, Filename: data.Name}) 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /routing/swagger/loader_docs.go: -------------------------------------------------------------------------------- 1 | // +build !nodocs 2 | 3 | package swagger 4 | 5 | import ( 6 | "github.com/gin-gonic/gin" 7 | _ "github.com/pufferpanel/pufferd/v2/docs" 8 | ginSwagger "github.com/swaggo/gin-swagger" 9 | "github.com/swaggo/gin-swagger/swaggerFiles" 10 | ) 11 | 12 | func Load(e *gin.Engine) { 13 | e.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) 14 | } 15 | -------------------------------------------------------------------------------- /routing/swagger/loader_nodocs.go: -------------------------------------------------------------------------------- 1 | // +build nodocs 2 | 3 | package swagger 4 | 5 | import ( 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | func Load(e *gin.Engine) { 10 | } 11 | -------------------------------------------------------------------------------- /sftp/requestprefix.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package sftp 18 | 19 | import ( 20 | "errors" 21 | "fmt" 22 | "io" 23 | "os" 24 | "path/filepath" 25 | "strings" 26 | 27 | "github.com/pkg/sftp" 28 | utils "github.com/pufferpanel/apufferi/v4" 29 | "github.com/pufferpanel/apufferi/v4/logging" 30 | ) 31 | 32 | type requestPrefix struct { 33 | prefix string 34 | } 35 | 36 | func CreateRequestPrefix(prefix string) sftp.Handlers { 37 | h := requestPrefix{prefix: prefix} 38 | 39 | return sftp.Handlers{FileCmd: h, FileGet: h, FileList: h, FilePut: h} 40 | } 41 | 42 | func (rp requestPrefix) Fileread(request *sftp.Request) (io.ReaderAt, error) { 43 | logging.Devel("-----------------") 44 | logging.Devel("read request: %s", request.Filepath) 45 | logging.Devel("Flags: %v", request.Flags) 46 | logging.Devel("Attributes: %v", request.Attrs) 47 | logging.Devel("Target: %v", request.Target) 48 | logging.Devel("-----------------") 49 | file, err := rp.getFile(request.Filepath, os.O_RDONLY, 0644) 50 | if err != nil { 51 | logging.Devel("pp-sftp internal error: %s", err) 52 | } 53 | return file, err 54 | } 55 | 56 | func (rp requestPrefix) Filewrite(request *sftp.Request) (io.WriterAt, error) { 57 | logging.Devel("-----------------") 58 | logging.Devel("write request: %s", request.Filepath) 59 | logging.Devel("Flags: %v", request.Flags) 60 | logging.Devel("Attributes: %v", request.Attrs) 61 | logging.Devel("Target: %v", request.Target) 62 | logging.Devel("-----------------") 63 | file, err := rp.getFile(request.Filepath, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644) 64 | return file, err 65 | } 66 | 67 | func (rp requestPrefix) Filecmd(request *sftp.Request) error { 68 | logging.Devel("-----------------") 69 | logging.Devel("cmd request [%s]: %s", request.Method, request.Filepath) 70 | logging.Devel("Flags: %v", request.Flags) 71 | logging.Devel("Attributes: %v", request.Attrs) 72 | logging.Devel("Target: %v", request.Target) 73 | logging.Devel("-----------------") 74 | sourceName, err := rp.validate(request.Filepath) 75 | if err != nil { 76 | logging.Devel("pp-sftp internal error: %s", err) 77 | return rp.maskError(err) 78 | } 79 | var targetName string 80 | if request.Target != "" { 81 | targetName, err = rp.validate(request.Target) 82 | if err != nil { 83 | logging.Devel("pp-sftp internal error: %s", err) 84 | return rp.maskError(err) 85 | } 86 | } 87 | switch request.Method { 88 | case "SetStat", "Setstat": 89 | { 90 | return nil 91 | } 92 | case "Rename": 93 | { 94 | return os.Rename(sourceName, targetName) 95 | } 96 | case "Rmdir": 97 | { 98 | return os.RemoveAll(sourceName) 99 | } 100 | case "Mkdir": 101 | { 102 | return os.Mkdir(sourceName, 0755) 103 | } 104 | case "Symlink": 105 | { 106 | return nil 107 | } 108 | case "Remove": 109 | { 110 | return os.Remove(sourceName) 111 | } 112 | default: 113 | return errors.New(fmt.Sprintf("Unknown request method: %v", request.Method)) 114 | } 115 | } 116 | 117 | func (rp requestPrefix) Filelist(request *sftp.Request) (sftp.ListerAt, error) { 118 | logging.Devel("-----------------") 119 | logging.Devel("list request [%s]: %s", request.Method, request.Filepath) 120 | logging.Devel("Flags: %v", request.Flags) 121 | logging.Devel("Attributes: %v", request.Attrs) 122 | logging.Devel("Target: %v", request.Target) 123 | logging.Devel("-----------------") 124 | sourceName, err := rp.validate(request.Filepath) 125 | if err != nil { 126 | logging.Devel("pp-sftp internal error: %s", err) 127 | return nil, rp.maskError(err) 128 | } 129 | switch request.Method { 130 | case "List": 131 | { 132 | file, err := os.Open(sourceName) 133 | if err != nil { 134 | return nil, rp.maskError(err) 135 | } 136 | files, err := file.Readdir(0) 137 | if err != nil { 138 | return nil, err 139 | } 140 | err = file.Close() 141 | if err != nil { 142 | return nil, rp.maskError(err) 143 | } 144 | 145 | //validate any symlinks are valid 146 | files = utils.RemoveInvalidSymlinks(files, sourceName, rp.prefix) 147 | 148 | return listerat(files), nil 149 | } 150 | case "Stat": 151 | { 152 | file, err := os.Open(sourceName) 153 | if err != nil { 154 | return nil, rp.maskError(err) 155 | } 156 | fi, err := file.Stat() 157 | if err != nil { 158 | return nil, rp.maskError(err) 159 | } 160 | err = file.Close() 161 | if err != nil { 162 | return nil, rp.maskError(err) 163 | } 164 | return listerat([]os.FileInfo{fi}), nil 165 | } 166 | case "Readlink": 167 | { 168 | target, err := os.Readlink(sourceName) 169 | if err != nil { 170 | return nil, rp.maskError(err) 171 | } 172 | 173 | //determine if target is just a local link, or a full link 174 | //let's just assume linux really at this point 175 | if !strings.HasPrefix(target, string(os.PathSeparator)) { 176 | target = rp.prefix + string(os.PathSeparator) + target 177 | } 178 | 179 | if !utils.EnsureAccess(target, rp.prefix) { 180 | return nil, rp.maskError(errors.New("access denied")) 181 | } 182 | 183 | file, err := os.Open(target) 184 | if err != nil { 185 | return nil, rp.maskError(err) 186 | } 187 | fi, err := file.Stat() 188 | if err != nil { 189 | return nil, rp.maskError(err) 190 | } 191 | err = file.Close() 192 | if err != nil { 193 | return nil, rp.maskError(err) 194 | } 195 | return listerat([]os.FileInfo{fi}), nil 196 | } 197 | default: 198 | return nil, errors.New(fmt.Sprintf("Unknown request method: %s", request.Method)) 199 | } 200 | } 201 | 202 | func (rp requestPrefix) getFile(path string, flags int, mode os.FileMode) (*os.File, error) { 203 | logging.Devel("Requesting path: %s", path) 204 | filePath, err := rp.validate(path) 205 | if err != nil { 206 | logging.Devel("pp-sftp internal error: %s", err) 207 | return nil, rp.maskError(err) 208 | } 209 | 210 | folderPath := filepath.Dir(filePath) 211 | 212 | var file *os.File 213 | 214 | if flags&os.O_CREATE != 0 { 215 | _, err := os.Stat(filePath) 216 | if os.IsNotExist(err) { 217 | err = nil 218 | err = os.MkdirAll(folderPath, 0755) 219 | if err != nil { 220 | logging.Devel("pp-sftp internal error: %s", err) 221 | return nil, rp.maskError(err) 222 | } 223 | file, err = os.Create(filePath) 224 | } else if err == nil { 225 | file, err = os.OpenFile(filePath, flags, mode) 226 | } 227 | } else { 228 | file, err = os.OpenFile(filePath, flags, mode) 229 | } 230 | if err != nil { 231 | logging.Devel("pp-sftp internal error: %s", err) 232 | return nil, rp.maskError(err) 233 | } 234 | 235 | if file == nil { 236 | logging.Devel("no file loaded at this stage") 237 | } 238 | return file, err 239 | } 240 | 241 | func (rp requestPrefix) validate(path string) (string, error) { 242 | ok, path := rp.tryPrefix(path) 243 | if !ok { 244 | return "", errors.New("access denied") 245 | } 246 | return path, nil 247 | } 248 | 249 | func (rp requestPrefix) tryPrefix(path string) (bool, string) { 250 | newPath := filepath.Clean(filepath.Join(rp.prefix, path)) 251 | if utils.EnsureAccess(newPath, rp.prefix) { 252 | return true, newPath 253 | } else { 254 | return false, "" 255 | } 256 | } 257 | 258 | func (rp requestPrefix) stripPrefix(path string) string { 259 | prefix, err := filepath.Abs(rp.prefix) 260 | if err != nil { 261 | prefix = rp.prefix 262 | } 263 | newStr := strings.TrimPrefix(path, prefix) 264 | if len(newStr) == 0 { 265 | newStr = "/" 266 | } 267 | return newStr 268 | } 269 | 270 | func (rp requestPrefix) maskError(err error) error { 271 | return errors.New(rp.stripPrefix(err.Error())) 272 | } 273 | 274 | type listerat []os.FileInfo 275 | 276 | // Modeled after strings.Reader's ReadAt() implementation 277 | func (f listerat) ListAt(ls []os.FileInfo, offset int64) (int, error) { 278 | var n int 279 | if offset >= int64(len(f)) { 280 | return 0, io.EOF 281 | } 282 | n = copy(ls, f[offset:]) 283 | if n < len(ls) { 284 | return n, io.EOF 285 | } 286 | return n, nil 287 | } 288 | -------------------------------------------------------------------------------- /sftp/server.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package sftp 18 | 19 | import ( 20 | "crypto/rand" 21 | "crypto/rsa" 22 | "crypto/x509" 23 | "encoding/pem" 24 | "github.com/pkg/sftp" 25 | "github.com/pufferpanel/apufferi/v4" 26 | "github.com/pufferpanel/apufferi/v4/logging" 27 | "github.com/pufferpanel/pufferd/v2" 28 | "github.com/pufferpanel/pufferd/v2/oauth2" 29 | "github.com/pufferpanel/pufferd/v2/programs" 30 | "github.com/spf13/viper" 31 | "golang.org/x/crypto/ssh" 32 | "io/ioutil" 33 | "net" 34 | "os" 35 | "path/filepath" 36 | ) 37 | 38 | var sftpServer net.Listener 39 | 40 | var auth pufferd.SFTPAuthorization 41 | 42 | func Run() { 43 | err := runServer() 44 | if err != nil { 45 | logging.Exception("Error starting SFTP server", err) 46 | } 47 | } 48 | 49 | func SetAuthorization(service pufferd.SFTPAuthorization) { 50 | auth = service 51 | } 52 | 53 | func Stop() { 54 | _ = sftpServer.Close() 55 | } 56 | 57 | func runServer() error { 58 | if auth == nil { 59 | auth = &oauth2.WebSSHAuthorization{} 60 | } 61 | 62 | config := &ssh.ServerConfig{ 63 | PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) { 64 | return auth.Validate(c.User(), string(pass)) 65 | }, 66 | } 67 | 68 | serverKeyFile := viper.GetString("listen.sftpKey") 69 | 70 | _, e := os.Stat(serverKeyFile) 71 | 72 | if e != nil && os.IsNotExist(e) { 73 | logging.Debug("Generating new key") 74 | var key *rsa.PrivateKey 75 | key, e = rsa.GenerateKey(rand.Reader, 2048) 76 | if e != nil { 77 | return e 78 | } 79 | 80 | data := x509.MarshalPKCS1PrivateKey(key) 81 | block := pem.Block{ 82 | Type: "RSA PRIVATE KEY", 83 | Headers: nil, 84 | Bytes: data, 85 | } 86 | e = ioutil.WriteFile(serverKeyFile, pem.EncodeToMemory(&block), 0700) 87 | if e != nil { 88 | return e 89 | } 90 | } else if e != nil { 91 | return e 92 | } 93 | 94 | logging.Debug("Loading existing key") 95 | var data []byte 96 | data, e = ioutil.ReadFile(serverKeyFile) 97 | if e != nil { 98 | return e 99 | } 100 | 101 | hkey, e := ssh.ParsePrivateKey(data) 102 | 103 | if e != nil { 104 | return e 105 | } 106 | 107 | config.AddHostKey(hkey) 108 | 109 | bind := viper.GetString("listen.sftp") 110 | 111 | sftpServer, e = net.Listen("tcp", bind) 112 | if e != nil { 113 | return e 114 | } 115 | logging.Info("Started SFTP Server on %s", bind) 116 | 117 | go func() { 118 | for { 119 | conn, _ := sftpServer.Accept() 120 | if conn != nil { 121 | go HandleConn(conn, config) 122 | } 123 | } 124 | }() 125 | 126 | return nil 127 | } 128 | 129 | func HandleConn(conn net.Conn, config *ssh.ServerConfig) { 130 | defer apufferi.Close(conn) 131 | logging.Debug("SFTP connection from %s", conn.RemoteAddr().String()) 132 | e := handleConn(conn, config) 133 | if e != nil { 134 | if e.Error() != "EOF" { 135 | logging.Exception("sftpd connection error", e) 136 | } 137 | } 138 | } 139 | func handleConn(conn net.Conn, config *ssh.ServerConfig) error { 140 | sc, chans, reqs, e := ssh.NewServerConn(conn, config) 141 | defer apufferi.Close(sc) 142 | if e != nil { 143 | return e 144 | } 145 | 146 | // The incoming Request channel must be serviced. 147 | go PrintDiscardRequests(reqs) 148 | 149 | // Service the incoming Channel channel. 150 | for newChannel := range chans { 151 | // Channels have a type, depending on the application level 152 | // protocol intended. In the case of an SFTP session, this is "subsystem" 153 | // with a payload string of "sftp" 154 | if newChannel.ChannelType() != "session" { 155 | err := newChannel.Reject(ssh.UnknownChannelType, "unknown channel type") 156 | if err != nil { 157 | return err 158 | } 159 | continue 160 | } 161 | channel, requests, err := newChannel.Accept() 162 | if err != nil { 163 | return err 164 | } 165 | 166 | // Sessions have out-of-band requests such as "shell", 167 | // "pty-req" and "env". Here we handle only the 168 | // "subsystem" request. 169 | go func(in <-chan *ssh.Request) { 170 | for req := range in { 171 | ok := false 172 | switch req.Type { 173 | case "subsystem": 174 | if string(req.Payload[4:]) == "sftp" { 175 | ok = true 176 | } 177 | } 178 | _ = req.Reply(ok, nil) 179 | } 180 | }(requests) 181 | 182 | fs := CreateRequestPrefix(filepath.Join(programs.ServerFolder, sc.Permissions.Extensions["server_id"])) 183 | 184 | server := sftp.NewRequestServer(channel, fs) 185 | 186 | if err := server.Serve(); err != nil { 187 | return err 188 | } 189 | } 190 | return nil 191 | } 192 | 193 | func PrintDiscardRequests(in <-chan *ssh.Request) { 194 | for req := range in { 195 | if req.WantReply { 196 | _ = req.Reply(false, nil) 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /shutdown/shutdown.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package shutdown 18 | 19 | import ( 20 | "fmt" 21 | "github.com/braintree/manners" 22 | "github.com/pufferpanel/apufferi/v4/logging" 23 | "github.com/pufferpanel/pufferd/v2/programs" 24 | "os" 25 | "runtime/debug" 26 | "sync" 27 | ) 28 | 29 | func CompleteShutdown() { 30 | logging.Warn("Interrupt received, stopping servers") 31 | wg := Shutdown() 32 | wg.Wait() 33 | logging.Info("All servers stopped") 34 | os.Exit(0) 35 | } 36 | 37 | func Shutdown() *sync.WaitGroup { 38 | defer func() { 39 | if err := recover(); err != nil { 40 | logging.Error("%+v\n%s", err, debug.Stack()) 41 | } 42 | }() 43 | wg := sync.WaitGroup{} 44 | programs.ShutdownService() 45 | manners.Close() 46 | prgs := programs.GetAll() 47 | wg.Add(len(prgs)) 48 | for _, element := range prgs { 49 | go func(e *programs.Program) { 50 | defer wg.Done() 51 | defer func() { 52 | if err := recover(); err != nil { 53 | logging.Error("%+v\n%s", err, debug.Stack()) 54 | } 55 | }() 56 | logging.Warn("Stopping program %s", e.Id()) 57 | running, err := e.IsRunning() 58 | if err != nil { 59 | logging.Exception(fmt.Sprintf("Error stopping server %s", e.Id()), err) 60 | return 61 | } 62 | if !running { 63 | return 64 | } 65 | err = e.Stop() 66 | if err != nil { 67 | logging.Exception(fmt.Sprintf("Error stopping server %s", e.Id()), err) 68 | return 69 | } 70 | err = e.GetEnvironment().WaitForMainProcess() 71 | if err != nil { 72 | logging.Exception(fmt.Sprintf("Error stopping server %s", e.Id()), err) 73 | return 74 | } 75 | logging.Warn("Stopped program %s", e.Id()) 76 | }(element) 77 | } 78 | return &wg 79 | } 80 | -------------------------------------------------------------------------------- /utils/websockettracker.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package utils 18 | 19 | import ( 20 | "encoding/json" 21 | "github.com/pufferpanel/apufferi/v4/logging" 22 | "github.com/pufferpanel/pufferd/v2/messages" 23 | "sync" 24 | 25 | "github.com/gorilla/websocket" 26 | ) 27 | 28 | type WebSocketManager interface { 29 | Register(ws *websocket.Conn) 30 | 31 | Write(msg []byte) (n int, e error) 32 | } 33 | 34 | type wsManager struct { 35 | sockets []websocket.Conn 36 | locker sync.Mutex 37 | } 38 | 39 | func CreateWSManager() WebSocketManager { 40 | return &wsManager{sockets: make([]websocket.Conn, 0), locker: sync.Mutex{}} 41 | } 42 | 43 | func (ws *wsManager) Register(conn *websocket.Conn) { 44 | ws.sockets = append(ws.sockets, *conn) 45 | } 46 | 47 | func (ws *wsManager) Write(source []byte) (n int, e error) { 48 | ws.locker.Lock() 49 | logs := make([]string, 1) 50 | logs[0] = string(source) 51 | packet := messages.ConsoleMessage{Logs: logs} 52 | data, _ := json.Marshal(&messages.Transmission{Message: packet, Type: packet.Key()}) 53 | 54 | for i := 0; i < len(ws.sockets); i++ { 55 | socket := ws.sockets[i] 56 | err := socket.WriteMessage(websocket.TextMessage, data) 57 | if err != nil { 58 | logging.Debug("websocket encountered error, dropping (%s)", err.Error()) 59 | if i+1 == len(ws.sockets) { 60 | ws.sockets = ws.sockets[:i] 61 | } else { 62 | ws.sockets = append(ws.sockets[:i], ws.sockets[i+1:]...) 63 | } 64 | i-- 65 | } 66 | } 67 | ws.locker.Unlock() 68 | 69 | n = len(source) 70 | return 71 | } 72 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Padduck, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package pufferd 18 | 19 | import "fmt" 20 | 21 | var ( 22 | Version = "nightly" 23 | Hash = "unknown" 24 | Display string 25 | ) 26 | 27 | func init() { 28 | Display = fmt.Sprintf("pufferd %s (%s)", Version, Hash) 29 | } 30 | --------------------------------------------------------------------------------