├── .fsw.yml ├── .ghs.yml ├── .github └── workflows │ └── release.yml ├── .gitignore ├── .goreleaser.yml ├── LICENSE ├── Procfile ├── README.md ├── assets.go ├── assets ├── bootstrap-3.3.5 │ ├── css │ │ ├── bootstrap-theme.css │ │ ├── bootstrap-theme.css.map │ │ ├── bootstrap-theme.min.css │ │ ├── bootstrap.css │ │ ├── bootstrap.css.map │ │ └── bootstrap.min.css │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ └── js │ │ ├── bootstrap.js │ │ ├── bootstrap.min.js │ │ └── npm.js ├── css │ ├── dropzone.css │ ├── github-markdown.css │ ├── scrollUp-image.css │ └── style.css ├── favicon.png ├── font-awesome-4.6.3 │ ├── HELP-US-OUT.txt │ ├── css │ │ ├── font-awesome.css │ │ └── font-awesome.min.css │ └── fonts │ │ ├── FontAwesome.otf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ └── fontawesome-webfont.woff2 ├── imgs │ ├── top.png │ └── wx.png ├── index.html ├── ipa-install.html ├── js │ ├── clipboard-1.5.12.min.js │ ├── dropzone.js │ ├── index.js │ ├── jquery-3.1.0.min.js │ ├── jquery.qrcode.js │ ├── jquery.scrollUp.min.js │ ├── moment.min.js │ ├── qrcode.js │ ├── showdown-1.6.4.min.js │ ├── ua-parser.min.js │ ├── underscore-min.js │ └── vue-1.0.min.js ├── themes │ ├── black.css │ ├── cyan.css │ └── green.css └── video-player.html ├── build.sh ├── docker ├── Dockerfile ├── Dockerfile.alpine ├── Dockerfile.armhf ├── push_images └── push_manifest ├── go.mod ├── go.sum ├── httpstaticserver.go ├── ipa.go ├── main.go ├── oauth2-proxy.go ├── openid-login.go ├── res.go ├── scripts ├── README.md └── proxy.py ├── testdata ├── README.md ├── config.yml ├── deep1 │ └── deep2 │ │ └── deep3 │ │ └── .gitkeep ├── deletable │ ├── .ghs.yml │ ├── block.file │ ├── other.file │ └── visual.file ├── filetypes │ ├── code.go │ ├── gohttpserver.gif │ ├── image.jpeg │ ├── image.jpg │ ├── image.pdf │ ├── image.png │ ├── image.tiff │ ├── page.html │ ├── script.js │ └── style.css ├── test.zip ├── uploadable │ ├── .ghs.yml │ └── sub-upload │ │ └── .gitkeep └── 中文路径 │ └── .gitkeep ├── utils.go ├── utils_test.go ├── zip.go └── zip_test.go /.fsw.yml: -------------------------------------------------------------------------------- 1 | desc: Auto generated by fswatch [gohttp-vue] 2 | triggers: 3 | - name: "" 4 | pattens: 5 | - '!.git/' 6 | - '**/*.go' 7 | - '**/*.tmpl.html' 8 | env: 9 | DEBUG: "1" 10 | cmd: go build && ./gohttpserver --upload --root testdata 11 | shell: true 12 | delay: 100ms 13 | signal: KILL 14 | watch_paths: 15 | - . 16 | watch_depth: 5 17 | -------------------------------------------------------------------------------- /.ghs.yml: -------------------------------------------------------------------------------- 1 | upload: true 2 | delete: false 3 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | on: 3 | push: 4 | # run only against tags 5 | tags: 6 | - '*' 7 | permissions: 8 | contents: write 9 | # packages: write 10 | # issues: write 11 | 12 | jobs: 13 | goreleaser: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v2 18 | with: 19 | fetch-depth: 0 20 | - name: Fetch all tags 21 | run: git fetch --force --tags 22 | - name: Set up Go 23 | uses: actions/setup-go@v2 24 | with: 25 | go-version: 1.18 26 | - name: Run GoReleaser 27 | uses: goreleaser/goreleaser-action@v2 28 | with: 29 | # either 'goreleaser' (default) or 'goreleaser-pro' 30 | distribution: goreleaser 31 | version: latest 32 | args: release 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.TOKEN }} 35 | -------------------------------------------------------------------------------- /.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 | 26 | 27 | gohttpserver 28 | bindata_assetfs.go 29 | assets_vfsdata.go 30 | *.un~ 31 | *.swp 32 | 33 | dist/ 34 | .DS_Store 35 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | before: 3 | hooks: 4 | - go mod tidy 5 | builds: 6 | - env: 7 | - CGO_ENABLED=0 8 | main: . 9 | ldflags: -s -w -X main.VERSION={{.Version}} -X main.BUILDTIME={{.Date}} -X main.GITCOMMIT={{.Commit}} 10 | goos: 11 | - linux 12 | - darwin 13 | - windows 14 | goarch: 15 | - amd64 16 | - arm64 17 | - "386" 18 | goarm: 19 | - "6" 20 | 21 | archives: 22 | - format: tar.gz 23 | # use zip for windows archives 24 | format_overrides: 25 | - goos: windows 26 | format: zip 27 | 28 | # brews: 29 | # - tap: 30 | # owner: codeskyblue 31 | # name: homebrew-tap 32 | # folder: Formula 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 shengxiang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gohttpserver --addr :$PORT --root testdata --upload --theme green 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gohttpserver 2 | [![Docker Automated build](https://img.shields.io/docker/automated/codeskyblue/gohttpserver)](https://hub.docker.com/repository/docker/codeskyblue/gohttpserver) 3 | 4 | **I've recently ported a new one using Python. Have a look at it here: https://github.com/codeskyblue/servefs** 5 | 6 | - Goal: Make the best HTTP File Server. 7 | - Features: Human-friendly UI, file uploading support, direct QR-code generation for Apple & Android install package. 8 | 9 | - 目标: 做最好的HTTP文件服务器 10 | - 功能: 人性化的UI体验,文件的上传支持,安卓和苹果安装包的二维码直接生成。 11 | 12 | **Binaries** can be downloaded from [this repo releases](https://github.com/codeskyblue/gohttpserver/releases/) 13 | 14 | ## Requirements 15 | Tested with go-1.16 16 | 17 | ## Screenshots 18 | ![screen](testdata/filetypes/gohttpserver.gif) 19 | 20 | ## Features 21 | 1. [x] Support QRCode code generate 22 | 1. [x] Breadcrumb path quick change 23 | 1. [x] All assets package to Standalone binary 24 | 1. [x] Different file type different icon 25 | 1. [x] Support show or hide hidden files 26 | 1. [x] Upload support (auth by token or session) 27 | 1. [x] README.md preview 28 | 1. [x] HTTP Basic Auth 29 | 1. [x] Partial reload pages when directory change 30 | 1. [x] When only one dir under dir, path will combine two together 31 | 1. [x] Directory zip download 32 | 1. [x] Apple ipa auto generate .plist file, qrcode can be recognized by iphone (Require https) 33 | 1. [x] Plist proxy 34 | 1. [ ] Download count statistics 35 | 1. [x] CORS enabled 36 | 1. [ ] Offline download 37 | 1. [ ] Code file preview 38 | 1. [ ] Edit file support 39 | 1. [x] Global file search 40 | 1. [x] Hidden work `download` and `qrcode` in small screen 41 | 1. [x] Theme select support 42 | 1. [x] OK to working behide Nginx 43 | 1. [x] \.ghs.yml support (like \.htaccess) 44 | 1. [ ] Calculate md5sum and sha 45 | 1. [ ] Folder upload 46 | 1. [ ] Support sort by size or modified time 47 | 1. [x] Add version info into index page 48 | 1. [ ] Add api `/-/info/some.(apk|ipa)` to get detail info 49 | 1. [x] Add api `/-/apk/info/some.apk` to get android package info 50 | 1. [x] Auto tag version 51 | 1. [x] Custom title support 52 | 1. [x] Support setting from conf file 53 | 1. [x] Quick copy download link 54 | 1. [x] Show folder size 55 | 1. [x] Create folder 56 | 1. [x] Skip delete confirm when alt pressed 57 | 1. [x] Support unzip zip file when upload(with form: unzip=true) 58 | 59 | ## Installation 60 | ```bash 61 | $ go install github.com/codeskyblue/gohttpserver@latest 62 | ``` 63 | 64 | Or download binaries from [github releases](https://github.com/codeskyblue/gohttpserver/releases) 65 | 66 | If you are using Mac, simply run command 67 | 68 | ```bash 69 | $ brew install codeskyblue/tap/gohttpserver 70 | ``` 71 | 72 | ## Usage 73 | Listen on port 8000 of all interfaces, and enable file uploading. 74 | 75 | ``` 76 | $ gohttpserver -r ./ --port 8000 --upload 77 | ``` 78 | 79 | Use command `gohttpserver --help` to see more usage. 80 | 81 | ## Docker Usage 82 | share current directory 83 | 84 | ```bash 85 | $ docker run -it --rm -p 8000:8000 -v $PWD:/app/public --name gohttpserver codeskyblue/gohttpserver 86 | ``` 87 | 88 | Share current directory with http basic auth 89 | 90 | ```bash 91 | $ docker run -it --rm -p 8000:8000 -v $PWD:/app/public --name gohttpserver \ 92 | codeskyblue/gohttpserver \ 93 | --auth-type http --auth-http username1:password1 --auth-http username2:password2 94 | ``` 95 | 96 | Share current directory with openid auth. (Works only in netease company.) 97 | 98 | ```bash 99 | $ docker run -it --rm -p 8000:8000 -v $PWD:/app/public --name gohttpserver \ 100 | codeskyblue/gohttpserver \ 101 | --auth-type openid 102 | ``` 103 | 104 | To build image yourself, please change the PWD to the root of this repo. 105 | 106 | ```bash 107 | $ cd gohttpserver/ 108 | $ docker build -t codeskyblue/gohttpserver -f docker/Dockerfile . 109 | ``` 110 | 111 | ## Authentication options 112 | - Enable basic http authentication 113 | 114 | ```sh 115 | $ gohttpserver --auth-type http --auth-http username1:password1 --auth-http username2:password2 116 | ``` 117 | 118 | - Use openid auth 119 | 120 | ```sh 121 | $ gohttpserver --auth-type openid --auth-openid https://login.example-hostname.com/openid/ 122 | ``` 123 | 124 | - Use oauth2-proxy with 125 | 126 | ```sh 127 | $ gohttpserver --auth-type oauth2-proxy 128 | ``` 129 | You can configure to let a http reverse proxy handling authentication. 130 | When using oauth2-proxy, the backend will use identification info from request headers `X-Auth-Request-Email` as userId and `X-Auth-Request-Fullname` as user's display name. 131 | Please config your oauth2 reverse proxy yourself. 132 | More about [oauth2-proxy](https://github.com/oauth2-proxy/oauth2-proxy). 133 | 134 | All required headers list as following. 135 | 136 | |header|value| 137 | |---|---| 138 | |X-Auth-Request-Email| userId | 139 | |X-Auth-Request-Fullname| user's display name(urlencoded) | 140 | |X-Auth-Request-User| user's nickname (mostly email prefix) | 141 | 142 | - Enable upload 143 | 144 | ```sh 145 | $ gohttpserver --upload 146 | ``` 147 | 148 | - Enable delete and Create folder 149 | 150 | ```sh 151 | $ gohttpserver --delete 152 | ``` 153 | 154 | ## Advanced usage 155 | Add access rule by creating a `.ghs.yml` file under a sub-directory. An example: 156 | 157 | ```yaml 158 | --- 159 | upload: false 160 | delete: false 161 | users: 162 | - email: "codeskyblue@codeskyblue.com" 163 | delete: true 164 | upload: true 165 | token: 4567gf8asydhf293r23r 166 | ``` 167 | 168 | In this case, if openid auth is enabled and user "codeskyblue@codeskyblue.com" has logged in, he/she can delete/upload files under the directory where the `.ghs.yml` file exits. 169 | 170 | `token` is used for upload. see [upload with curl](#upload-with-curl) 171 | 172 | For example, in the following directory hierarchy, users can delete/uploade files in directory `foo`, but he/she cannot do this in directory `bar`. 173 | 174 | ``` 175 | root - 176 | |-- foo 177 | | |-- .ghs.yml 178 | | `-- world.txt 179 | `-- bar 180 | `-- hello.txt 181 | ``` 182 | 183 | User can specify config file name with `--conf`, see [example config.yml](testdata/config.yml). 184 | 185 | To specify which files is hidden and which file is visible, add the following lines to `.ghs.yml` 186 | 187 | ```yaml 188 | accessTables: 189 | - regex: block.file 190 | allow: false 191 | - regex: visual.file 192 | allow: true 193 | ``` 194 | 195 | ### ipa plist proxy 196 | This is used for server on which https is enabled. default use 197 | 198 | ```bash 199 | $ gohttpserver --plistproxy=https://someproxyhost.com/ 200 | ``` 201 | 202 | Test if proxy works: 203 | 204 | ```sh 205 | $ http POST https://someproxyhost.com/plist < app.plist 206 | { 207 | "key": "18f99211" 208 | } 209 | $ http GET https://someproxyhost.com/plist/18f99211 210 | # show the app.plist content 211 | ``` 212 | 213 | If your ghs running behide nginx server and have https configed. plistproxy will be disabled automaticly. 214 | 215 | ### Upload with CURL 216 | For example, upload a file named `foo.txt` to directory `somedir` 217 | 218 | ```sh 219 | $ curl -F file=@foo.txt localhost:8000/somedir 220 | {"destination":"somedir/foo.txt","success":true} 221 | # upload with token 222 | $ curl -F file=@foo.txt -F token=12312jlkjafs localhost:8000/somedir 223 | {"destination":"somedir/foo.txt","success":true} 224 | 225 | # upload and change filename 226 | $ curl -F file=@foo.txt -F filename=hi.txt localhost:8000/somedir 227 | {"destination":"somedir/hi.txt","success":true} 228 | ``` 229 | 230 | Upload zip file and unzip it (zip file will be delete when finished unzip) 231 | 232 | ``` 233 | $ curl -F file=@pkg.zip -F unzip=true localhost:8000/somedir 234 | {"success": true} 235 | ``` 236 | 237 | Note: `\/:*<>|` are not allowed in filenames. 238 | 239 | ### Deploy with nginx 240 | Recommended configuration, assume your gohttpserver listening on `127.0.0.1:8200` 241 | 242 | ``` 243 | server { 244 | listen 80; 245 | server_name your-domain-name.com; 246 | 247 | location / { 248 | proxy_pass http://127.0.0.1:8200; # here need to change 249 | proxy_redirect off; 250 | proxy_set_header Host $host; 251 | proxy_set_header X-Real-IP $remote_addr; 252 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 253 | proxy_set_header X-Forwarded-Proto $scheme; 254 | 255 | client_max_body_size 0; # disable upload limit 256 | } 257 | } 258 | ``` 259 | 260 | gohttpserver should started with `--xheaders` argument when behide nginx. 261 | 262 | Refs: 263 | 264 | gohttpserver also support `--prefix` flag which will help to when meet `/` is occupied by other service. relative issue 265 | 266 | Usage example: 267 | 268 | ```bash 269 | # for gohttpserver 270 | $ gohttpserver --prefix /foo --addr :8200 --xheaders 271 | ``` 272 | 273 | **Nginx settigns** 274 | 275 | ``` 276 | server { 277 | listen 80; 278 | server_name your-domain-name.com; 279 | 280 | location /foo { 281 | proxy_pass http://127.0.0.1:8200; # here need to change 282 | proxy_redirect off; 283 | proxy_set_header Host $host; 284 | proxy_set_header X-Real-IP $remote_addr; 285 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 286 | proxy_set_header X-Forwarded-Proto $scheme; 287 | 288 | client_max_body_size 0; # disable upload limit 289 | } 290 | } 291 | ``` 292 | 293 | ## FAQ 294 | - [How to generate self signed certificate with openssl](http://stackoverflow.com/questions/10175812/how-to-create-a-self-signed-certificate-with-openssl) 295 | 296 | ### How the query is formated 297 | The search query follows common format rules just like Google. Keywords are seperated with space(s), keywords with prefix `-` will be excluded in search results. 298 | 299 | 1. `hello world` means must contains `hello` and `world` 300 | 1. `hello -world` means must contains `hello` but not contains `world` 301 | 302 | ## Developer Guide 303 | Depdencies are managed by [govendor](https://github.com/kardianos/govendor) 304 | 305 | 1. Build develop version. **assets** directory must exists 306 | 307 | ```sh 308 | $ go build 309 | $ ./gohttpserver 310 | ``` 311 | 2. Build single binary release 312 | 313 | ```sh 314 | $ go build 315 | ``` 316 | 317 | Theme are defined in [assets/themes](assets/themes) directory. Now only two themes are available, "black" and "green". 318 | 319 | 320 | ## Reference Web sites 321 | 322 | * Core lib Vue 323 | * Icon from 324 | * Code Highlight 325 | * Markdown Parser 326 | * Markdown CSS 327 | * Upload support 328 | * ScrollUp 329 | * Clipboard 330 | * Underscore 331 | 332 | **Go Libraries** 333 | 334 | * [vfsgen](https://github.com/shurcooL/vfsgen) Not using now 335 | * [go-bindata-assetfs](https://github.com/elazarl/go-bindata-assetfs) Not using now 336 | * 337 | 338 | ## History 339 | The old version is hosted at 340 | 341 | ## LICENSE 342 | This project is licensed under [MIT](LICENSE). 343 | -------------------------------------------------------------------------------- /assets.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "embed" 5 | "net/http" 6 | ) 7 | 8 | //go:embed assets 9 | var assetsFS embed.FS 10 | 11 | // Assets contains project assets. 12 | var Assets = http.FS(assetsFS) 13 | -------------------------------------------------------------------------------- /assets/bootstrap-3.3.5/css/bootstrap-theme.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.5 (http://getbootstrap.com) 3 | * Copyright 2011-2015 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */.btn-danger,.btn-default,.btn-info,.btn-primary,.btn-success,.btn-warning{text-shadow:0 -1px 0 rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075)}.btn-danger.active,.btn-danger:active,.btn-default.active,.btn-default:active,.btn-info.active,.btn-info:active,.btn-primary.active,.btn-primary:active,.btn-success.active,.btn-success:active,.btn-warning.active,.btn-warning:active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-danger.disabled,.btn-danger[disabled],.btn-default.disabled,.btn-default[disabled],.btn-info.disabled,.btn-info[disabled],.btn-primary.disabled,.btn-primary[disabled],.btn-success.disabled,.btn-success[disabled],.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-danger,fieldset[disabled] .btn-default,fieldset[disabled] .btn-info,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-success,fieldset[disabled] .btn-warning{-webkit-box-shadow:none;box-shadow:none}.btn-danger .badge,.btn-default .badge,.btn-info .badge,.btn-primary .badge,.btn-success .badge,.btn-warning .badge{text-shadow:none}.btn.active,.btn:active{background-image:none}.btn-default{text-shadow:0 1px 0 #fff;background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-o-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e0e0e0));background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#dbdbdb;border-color:#ccc}.btn-default:focus,.btn-default:hover{background-color:#e0e0e0;background-position:0 -15px}.btn-default.active,.btn-default:active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-default.disabled,.btn-default.disabled.active,.btn-default.disabled.focus,.btn-default.disabled:active,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled],.btn-default[disabled].active,.btn-default[disabled].focus,.btn-default[disabled]:active,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default,fieldset[disabled] .btn-default.active,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:active,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#e0e0e0;background-image:none}.btn-primary{background-image:-webkit-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-o-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#265a88));background-image:linear-gradient(to bottom,#337ab7 0,#265a88 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#245580}.btn-primary:focus,.btn-primary:hover{background-color:#265a88;background-position:0 -15px}.btn-primary.active,.btn-primary:active{background-color:#265a88;border-color:#245580}.btn-primary.disabled,.btn-primary.disabled.active,.btn-primary.disabled.focus,.btn-primary.disabled:active,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled],.btn-primary[disabled].active,.btn-primary[disabled].focus,.btn-primary[disabled]:active,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-primary.active,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:active,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#265a88;background-image:none}.btn-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#419641));background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#3e8f3e}.btn-success:focus,.btn-success:hover{background-color:#419641;background-position:0 -15px}.btn-success.active,.btn-success:active{background-color:#419641;border-color:#3e8f3e}.btn-success.disabled,.btn-success.disabled.active,.btn-success.disabled.focus,.btn-success.disabled:active,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled],.btn-success[disabled].active,.btn-success[disabled].focus,.btn-success[disabled]:active,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success,fieldset[disabled] .btn-success.active,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:active,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#419641;background-image:none}.btn-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#2aabd2));background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#28a4c9}.btn-info:focus,.btn-info:hover{background-color:#2aabd2;background-position:0 -15px}.btn-info.active,.btn-info:active{background-color:#2aabd2;border-color:#28a4c9}.btn-info.disabled,.btn-info.disabled.active,.btn-info.disabled.focus,.btn-info.disabled:active,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled],.btn-info[disabled].active,.btn-info[disabled].focus,.btn-info[disabled]:active,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info,fieldset[disabled] .btn-info.active,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:active,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#2aabd2;background-image:none}.btn-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#eb9316));background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#e38d13}.btn-warning:focus,.btn-warning:hover{background-color:#eb9316;background-position:0 -15px}.btn-warning.active,.btn-warning:active{background-color:#eb9316;border-color:#e38d13}.btn-warning.disabled,.btn-warning.disabled.active,.btn-warning.disabled.focus,.btn-warning.disabled:active,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled],.btn-warning[disabled].active,.btn-warning[disabled].focus,.btn-warning[disabled]:active,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning,fieldset[disabled] .btn-warning.active,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:active,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#eb9316;background-image:none}.btn-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c12e2a));background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#b92c28}.btn-danger:focus,.btn-danger:hover{background-color:#c12e2a;background-position:0 -15px}.btn-danger.active,.btn-danger:active{background-color:#c12e2a;border-color:#b92c28}.btn-danger.disabled,.btn-danger.disabled.active,.btn-danger.disabled.focus,.btn-danger.disabled:active,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled],.btn-danger[disabled].active,.btn-danger[disabled].focus,.btn-danger[disabled]:active,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger,fieldset[disabled] .btn-danger.active,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:active,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#c12e2a;background-image:none}.img-thumbnail,.thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{background-color:#e8e8e8;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{background-color:#2e6da4;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-o-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#f8f8f8));background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075)}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-o-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dbdbdb),to(#e2e2e2));background-image:linear-gradient(to bottom,#dbdbdb 0,#e2e2e2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.075);box-shadow:inset 0 3px 9px rgba(0,0,0,.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-o-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#3c3c3c),to(#222));background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-o-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#080808),to(#0f0f0f));background-image:linear-gradient(to bottom,#080808 0,#0f0f0f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.25);box-shadow:inset 0 3px 9px rgba(0,0,0,.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,.25)}.navbar-fixed-bottom,.navbar-fixed-top,.navbar-static-top{border-radius:0}@media (max-width:767px){.navbar .navbar-nav .open .dropdown-menu>.active>a,.navbar .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}}.alert{text-shadow:0 1px 0 rgba(255,255,255,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#c8e5bc));background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);background-repeat:repeat-x;border-color:#b2dba1}.alert-info{background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#b9def0));background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);background-repeat:repeat-x;border-color:#9acfea}.alert-warning{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#f8efc0));background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);background-repeat:repeat-x;border-color:#f5e79e}.alert-danger{background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-o-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#e7c3c3));background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);background-repeat:repeat-x;border-color:#dca7a7}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#f5f5f5));background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x}.progress-bar{background-image:-webkit-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-o-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#286090));background-image:linear-gradient(to bottom,#337ab7 0,#286090 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);background-repeat:repeat-x}.progress-bar-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#449d44));background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);background-repeat:repeat-x}.progress-bar-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#31b0d5));background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);background-repeat:repeat-x}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#ec971f));background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);background-repeat:repeat-x}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c9302c));background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);background-repeat:repeat-x}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{text-shadow:0 -1px 0 #286090;background-image:-webkit-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2b669a));background-image:linear-gradient(to bottom,#337ab7 0,#2b669a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);background-repeat:repeat-x;border-color:#2b669a}.list-group-item.active .badge,.list-group-item.active:focus .badge,.list-group-item.active:hover .badge{text-shadow:none}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.05);box-shadow:0 1px 2px rgba(0,0,0,.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#d0e9c6));background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);background-repeat:repeat-x}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#c4e3f3));background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);background-repeat:repeat-x}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#faf2cc));background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);background-repeat:repeat-x}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-o-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#ebcccc));background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);background-repeat:repeat-x}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#e8e8e8),to(#f5f5f5));background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x;border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1)} -------------------------------------------------------------------------------- /assets/bootstrap-3.3.5/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeskyblue/gohttpserver/df32f57005d9f02ee08a89f237a711316b992376/assets/bootstrap-3.3.5/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /assets/bootstrap-3.3.5/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeskyblue/gohttpserver/df32f57005d9f02ee08a89f237a711316b992376/assets/bootstrap-3.3.5/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /assets/bootstrap-3.3.5/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeskyblue/gohttpserver/df32f57005d9f02ee08a89f237a711316b992376/assets/bootstrap-3.3.5/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /assets/bootstrap-3.3.5/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeskyblue/gohttpserver/df32f57005d9f02ee08a89f237a711316b992376/assets/bootstrap-3.3.5/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /assets/bootstrap-3.3.5/js/npm.js: -------------------------------------------------------------------------------- 1 | // This file is autogenerated via the `commonjs` Grunt task. You can require() this file in a CommonJS environment. 2 | require('../../js/transition.js') 3 | require('../../js/alert.js') 4 | require('../../js/button.js') 5 | require('../../js/carousel.js') 6 | require('../../js/collapse.js') 7 | require('../../js/dropdown.js') 8 | require('../../js/modal.js') 9 | require('../../js/tooltip.js') 10 | require('../../js/popover.js') 11 | require('../../js/scrollspy.js') 12 | require('../../js/tab.js') 13 | require('../../js/affix.js') -------------------------------------------------------------------------------- /assets/css/dropzone.css: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * Copyright (c) 2012 Matias Meno 4 | */ 5 | @-webkit-keyframes passing-through { 6 | 0% { 7 | opacity: 0; 8 | -webkit-transform: translateY(40px); 9 | -moz-transform: translateY(40px); 10 | -ms-transform: translateY(40px); 11 | -o-transform: translateY(40px); 12 | transform: translateY(40px); } 13 | 30%, 70% { 14 | opacity: 1; 15 | -webkit-transform: translateY(0px); 16 | -moz-transform: translateY(0px); 17 | -ms-transform: translateY(0px); 18 | -o-transform: translateY(0px); 19 | transform: translateY(0px); } 20 | 100% { 21 | opacity: 0; 22 | -webkit-transform: translateY(-40px); 23 | -moz-transform: translateY(-40px); 24 | -ms-transform: translateY(-40px); 25 | -o-transform: translateY(-40px); 26 | transform: translateY(-40px); } } 27 | @-moz-keyframes passing-through { 28 | 0% { 29 | opacity: 0; 30 | -webkit-transform: translateY(40px); 31 | -moz-transform: translateY(40px); 32 | -ms-transform: translateY(40px); 33 | -o-transform: translateY(40px); 34 | transform: translateY(40px); } 35 | 30%, 70% { 36 | opacity: 1; 37 | -webkit-transform: translateY(0px); 38 | -moz-transform: translateY(0px); 39 | -ms-transform: translateY(0px); 40 | -o-transform: translateY(0px); 41 | transform: translateY(0px); } 42 | 100% { 43 | opacity: 0; 44 | -webkit-transform: translateY(-40px); 45 | -moz-transform: translateY(-40px); 46 | -ms-transform: translateY(-40px); 47 | -o-transform: translateY(-40px); 48 | transform: translateY(-40px); } } 49 | @keyframes passing-through { 50 | 0% { 51 | opacity: 0; 52 | -webkit-transform: translateY(40px); 53 | -moz-transform: translateY(40px); 54 | -ms-transform: translateY(40px); 55 | -o-transform: translateY(40px); 56 | transform: translateY(40px); } 57 | 30%, 70% { 58 | opacity: 1; 59 | -webkit-transform: translateY(0px); 60 | -moz-transform: translateY(0px); 61 | -ms-transform: translateY(0px); 62 | -o-transform: translateY(0px); 63 | transform: translateY(0px); } 64 | 100% { 65 | opacity: 0; 66 | -webkit-transform: translateY(-40px); 67 | -moz-transform: translateY(-40px); 68 | -ms-transform: translateY(-40px); 69 | -o-transform: translateY(-40px); 70 | transform: translateY(-40px); } } 71 | @-webkit-keyframes slide-in { 72 | 0% { 73 | opacity: 0; 74 | -webkit-transform: translateY(40px); 75 | -moz-transform: translateY(40px); 76 | -ms-transform: translateY(40px); 77 | -o-transform: translateY(40px); 78 | transform: translateY(40px); } 79 | 30% { 80 | opacity: 1; 81 | -webkit-transform: translateY(0px); 82 | -moz-transform: translateY(0px); 83 | -ms-transform: translateY(0px); 84 | -o-transform: translateY(0px); 85 | transform: translateY(0px); } } 86 | @-moz-keyframes slide-in { 87 | 0% { 88 | opacity: 0; 89 | -webkit-transform: translateY(40px); 90 | -moz-transform: translateY(40px); 91 | -ms-transform: translateY(40px); 92 | -o-transform: translateY(40px); 93 | transform: translateY(40px); } 94 | 30% { 95 | opacity: 1; 96 | -webkit-transform: translateY(0px); 97 | -moz-transform: translateY(0px); 98 | -ms-transform: translateY(0px); 99 | -o-transform: translateY(0px); 100 | transform: translateY(0px); } } 101 | @keyframes slide-in { 102 | 0% { 103 | opacity: 0; 104 | -webkit-transform: translateY(40px); 105 | -moz-transform: translateY(40px); 106 | -ms-transform: translateY(40px); 107 | -o-transform: translateY(40px); 108 | transform: translateY(40px); } 109 | 30% { 110 | opacity: 1; 111 | -webkit-transform: translateY(0px); 112 | -moz-transform: translateY(0px); 113 | -ms-transform: translateY(0px); 114 | -o-transform: translateY(0px); 115 | transform: translateY(0px); } } 116 | @-webkit-keyframes pulse { 117 | 0% { 118 | -webkit-transform: scale(1); 119 | -moz-transform: scale(1); 120 | -ms-transform: scale(1); 121 | -o-transform: scale(1); 122 | transform: scale(1); } 123 | 10% { 124 | -webkit-transform: scale(1.1); 125 | -moz-transform: scale(1.1); 126 | -ms-transform: scale(1.1); 127 | -o-transform: scale(1.1); 128 | transform: scale(1.1); } 129 | 20% { 130 | -webkit-transform: scale(1); 131 | -moz-transform: scale(1); 132 | -ms-transform: scale(1); 133 | -o-transform: scale(1); 134 | transform: scale(1); } } 135 | @-moz-keyframes pulse { 136 | 0% { 137 | -webkit-transform: scale(1); 138 | -moz-transform: scale(1); 139 | -ms-transform: scale(1); 140 | -o-transform: scale(1); 141 | transform: scale(1); } 142 | 10% { 143 | -webkit-transform: scale(1.1); 144 | -moz-transform: scale(1.1); 145 | -ms-transform: scale(1.1); 146 | -o-transform: scale(1.1); 147 | transform: scale(1.1); } 148 | 20% { 149 | -webkit-transform: scale(1); 150 | -moz-transform: scale(1); 151 | -ms-transform: scale(1); 152 | -o-transform: scale(1); 153 | transform: scale(1); } } 154 | @keyframes pulse { 155 | 0% { 156 | -webkit-transform: scale(1); 157 | -moz-transform: scale(1); 158 | -ms-transform: scale(1); 159 | -o-transform: scale(1); 160 | transform: scale(1); } 161 | 10% { 162 | -webkit-transform: scale(1.1); 163 | -moz-transform: scale(1.1); 164 | -ms-transform: scale(1.1); 165 | -o-transform: scale(1.1); 166 | transform: scale(1.1); } 167 | 20% { 168 | -webkit-transform: scale(1); 169 | -moz-transform: scale(1); 170 | -ms-transform: scale(1); 171 | -o-transform: scale(1); 172 | transform: scale(1); } } 173 | .dropzone, .dropzone * { 174 | box-sizing: border-box; } 175 | 176 | .dropzone { 177 | min-height: 150px; 178 | border: 2px solid rgba(0, 0, 0, 0.3); 179 | background: white; 180 | padding: 20px 20px; } 181 | .dropzone.dz-clickable { 182 | cursor: pointer; } 183 | .dropzone.dz-clickable * { 184 | cursor: default; } 185 | .dropzone.dz-clickable .dz-message, .dropzone.dz-clickable .dz-message * { 186 | cursor: pointer; } 187 | .dropzone.dz-started .dz-message { 188 | display: none; } 189 | .dropzone.dz-drag-hover { 190 | border-style: solid; } 191 | .dropzone.dz-drag-hover .dz-message { 192 | opacity: 0.5; } 193 | .dropzone .dz-message { 194 | text-align: center; 195 | margin: 2em 0; } 196 | .dropzone .dz-preview { 197 | position: relative; 198 | display: inline-block; 199 | vertical-align: top; 200 | margin: 16px; 201 | min-height: 100px; } 202 | .dropzone .dz-preview:hover { 203 | z-index: 1000; } 204 | .dropzone .dz-preview:hover .dz-details { 205 | opacity: 1; } 206 | .dropzone .dz-preview.dz-file-preview .dz-image { 207 | border-radius: 20px; 208 | background: #999; 209 | background: linear-gradient(to bottom, #eee, #ddd); } 210 | .dropzone .dz-preview.dz-file-preview .dz-details { 211 | opacity: 1; } 212 | .dropzone .dz-preview.dz-image-preview { 213 | background: white; } 214 | .dropzone .dz-preview.dz-image-preview .dz-details { 215 | -webkit-transition: opacity 0.2s linear; 216 | -moz-transition: opacity 0.2s linear; 217 | -ms-transition: opacity 0.2s linear; 218 | -o-transition: opacity 0.2s linear; 219 | transition: opacity 0.2s linear; } 220 | .dropzone .dz-preview .dz-remove { 221 | font-size: 14px; 222 | text-align: center; 223 | display: block; 224 | cursor: pointer; 225 | border: none; } 226 | .dropzone .dz-preview .dz-remove:hover { 227 | text-decoration: underline; } 228 | .dropzone .dz-preview:hover .dz-details { 229 | opacity: 1; } 230 | .dropzone .dz-preview .dz-details { 231 | z-index: 20; 232 | position: absolute; 233 | top: 0; 234 | left: 0; 235 | opacity: 0; 236 | font-size: 13px; 237 | min-width: 100%; 238 | max-width: 100%; 239 | padding: 2em 1em; 240 | text-align: center; 241 | color: rgba(0, 0, 0, 0.9); 242 | line-height: 150%; } 243 | .dropzone .dz-preview .dz-details .dz-size { 244 | margin-bottom: 1em; 245 | font-size: 16px; } 246 | .dropzone .dz-preview .dz-details .dz-filename { 247 | white-space: nowrap; } 248 | .dropzone .dz-preview .dz-details .dz-filename:hover span { 249 | border: 1px solid rgba(200, 200, 200, 0.8); 250 | background-color: rgba(255, 255, 255, 0.8); } 251 | .dropzone .dz-preview .dz-details .dz-filename:not(:hover) { 252 | overflow: hidden; 253 | text-overflow: ellipsis; } 254 | .dropzone .dz-preview .dz-details .dz-filename:not(:hover) span { 255 | border: 1px solid transparent; } 256 | .dropzone .dz-preview .dz-details .dz-filename span, .dropzone .dz-preview .dz-details .dz-size span { 257 | background-color: rgba(255, 255, 255, 0.4); 258 | padding: 0 0.4em; 259 | border-radius: 3px; } 260 | .dropzone .dz-preview:hover .dz-image img { 261 | -webkit-transform: scale(1.05, 1.05); 262 | -moz-transform: scale(1.05, 1.05); 263 | -ms-transform: scale(1.05, 1.05); 264 | -o-transform: scale(1.05, 1.05); 265 | transform: scale(1.05, 1.05); 266 | -webkit-filter: blur(8px); 267 | filter: blur(8px); } 268 | .dropzone .dz-preview .dz-image { 269 | border-radius: 20px; 270 | overflow: hidden; 271 | width: 120px; 272 | height: 120px; 273 | position: relative; 274 | display: block; 275 | z-index: 10; } 276 | .dropzone .dz-preview .dz-image img { 277 | display: block; } 278 | .dropzone .dz-preview.dz-success .dz-success-mark { 279 | -webkit-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); 280 | -moz-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); 281 | -ms-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); 282 | -o-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); 283 | animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); } 284 | .dropzone .dz-preview.dz-error .dz-error-mark { 285 | opacity: 1; 286 | -webkit-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1); 287 | -moz-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1); 288 | -ms-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1); 289 | -o-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1); 290 | animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1); } 291 | .dropzone .dz-preview .dz-success-mark, .dropzone .dz-preview .dz-error-mark { 292 | pointer-events: none; 293 | opacity: 0; 294 | z-index: 500; 295 | position: absolute; 296 | display: block; 297 | top: 50%; 298 | left: 50%; 299 | margin-left: -27px; 300 | margin-top: -27px; } 301 | .dropzone .dz-preview .dz-success-mark svg, .dropzone .dz-preview .dz-error-mark svg { 302 | display: block; 303 | width: 54px; 304 | height: 54px; } 305 | .dropzone .dz-preview.dz-processing .dz-progress { 306 | opacity: 1; 307 | -webkit-transition: all 0.2s linear; 308 | -moz-transition: all 0.2s linear; 309 | -ms-transition: all 0.2s linear; 310 | -o-transition: all 0.2s linear; 311 | transition: all 0.2s linear; } 312 | .dropzone .dz-preview.dz-complete .dz-progress { 313 | opacity: 0; 314 | -webkit-transition: opacity 0.4s ease-in; 315 | -moz-transition: opacity 0.4s ease-in; 316 | -ms-transition: opacity 0.4s ease-in; 317 | -o-transition: opacity 0.4s ease-in; 318 | transition: opacity 0.4s ease-in; } 319 | .dropzone .dz-preview:not(.dz-processing) .dz-progress { 320 | -webkit-animation: pulse 6s ease infinite; 321 | -moz-animation: pulse 6s ease infinite; 322 | -ms-animation: pulse 6s ease infinite; 323 | -o-animation: pulse 6s ease infinite; 324 | animation: pulse 6s ease infinite; } 325 | .dropzone .dz-preview .dz-progress { 326 | opacity: 1; 327 | z-index: 1000; 328 | pointer-events: none; 329 | position: absolute; 330 | height: 16px; 331 | left: 50%; 332 | top: 50%; 333 | margin-top: -8px; 334 | width: 80px; 335 | margin-left: -40px; 336 | background: rgba(255, 255, 255, 0.9); 337 | -webkit-transform: scale(1); 338 | border-radius: 8px; 339 | overflow: hidden; } 340 | .dropzone .dz-preview .dz-progress .dz-upload { 341 | background: #333; 342 | background: linear-gradient(to bottom, #666, #444); 343 | position: absolute; 344 | top: 0; 345 | left: 0; 346 | bottom: 0; 347 | width: 0; 348 | -webkit-transition: width 300ms ease-in-out; 349 | -moz-transition: width 300ms ease-in-out; 350 | -ms-transition: width 300ms ease-in-out; 351 | -o-transition: width 300ms ease-in-out; 352 | transition: width 300ms ease-in-out; } 353 | .dropzone .dz-preview.dz-error .dz-error-message { 354 | display: block; } 355 | .dropzone .dz-preview.dz-error:hover .dz-error-message { 356 | opacity: 1; 357 | pointer-events: auto; } 358 | .dropzone .dz-preview .dz-error-message { 359 | pointer-events: none; 360 | z-index: 1000; 361 | position: absolute; 362 | display: block; 363 | display: none; 364 | opacity: 0; 365 | -webkit-transition: opacity 0.3s ease; 366 | -moz-transition: opacity 0.3s ease; 367 | -ms-transition: opacity 0.3s ease; 368 | -o-transition: opacity 0.3s ease; 369 | transition: opacity 0.3s ease; 370 | border-radius: 8px; 371 | font-size: 13px; 372 | top: 130px; 373 | left: -10px; 374 | width: 140px; 375 | background: #be2626; 376 | background: linear-gradient(to bottom, #be2626, #a92222); 377 | padding: 0.5em 1.2em; 378 | color: white; } 379 | .dropzone .dz-preview .dz-error-message:after { 380 | content: ''; 381 | position: absolute; 382 | top: -6px; 383 | left: 64px; 384 | width: 0; 385 | height: 0; 386 | border-left: 6px solid transparent; 387 | border-right: 6px solid transparent; 388 | border-bottom: 6px solid #be2626; } 389 | -------------------------------------------------------------------------------- /assets/css/github-markdown.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: octicons-link; 3 | src: url(data:font/woff;charset=utf-8;base64,d09GRgABAAAAAAZwABAAAAAACFQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABEU0lHAAAGaAAAAAgAAAAIAAAAAUdTVUIAAAZcAAAACgAAAAoAAQAAT1MvMgAAAyQAAABJAAAAYFYEU3RjbWFwAAADcAAAAEUAAACAAJThvmN2dCAAAATkAAAABAAAAAQAAAAAZnBnbQAAA7gAAACyAAABCUM+8IhnYXNwAAAGTAAAABAAAAAQABoAI2dseWYAAAFsAAABPAAAAZwcEq9taGVhZAAAAsgAAAA0AAAANgh4a91oaGVhAAADCAAAABoAAAAkCA8DRGhtdHgAAAL8AAAADAAAAAwGAACfbG9jYQAAAsAAAAAIAAAACABiATBtYXhwAAACqAAAABgAAAAgAA8ASm5hbWUAAAToAAABQgAAAlXu73sOcG9zdAAABiwAAAAeAAAAME3QpOBwcmVwAAAEbAAAAHYAAAB/aFGpk3jaTY6xa8JAGMW/O62BDi0tJLYQincXEypYIiGJjSgHniQ6umTsUEyLm5BV6NDBP8Tpts6F0v+k/0an2i+itHDw3v2+9+DBKTzsJNnWJNTgHEy4BgG3EMI9DCEDOGEXzDADU5hBKMIgNPZqoD3SilVaXZCER3/I7AtxEJLtzzuZfI+VVkprxTlXShWKb3TBecG11rwoNlmmn1P2WYcJczl32etSpKnziC7lQyWe1smVPy/Lt7Kc+0vWY/gAgIIEqAN9we0pwKXreiMasxvabDQMM4riO+qxM2ogwDGOZTXxwxDiycQIcoYFBLj5K3EIaSctAq2kTYiw+ymhce7vwM9jSqO8JyVd5RH9gyTt2+J/yUmYlIR0s04n6+7Vm1ozezUeLEaUjhaDSuXHwVRgvLJn1tQ7xiuVv/ocTRF42mNgZGBgYGbwZOBiAAFGJBIMAAizAFoAAABiAGIAznjaY2BkYGAA4in8zwXi+W2+MjCzMIDApSwvXzC97Z4Ig8N/BxYGZgcgl52BCSQKAA3jCV8CAABfAAAAAAQAAEB42mNgZGBg4f3vACQZQABIMjKgAmYAKEgBXgAAeNpjYGY6wTiBgZWBg2kmUxoDA4MPhGZMYzBi1AHygVLYQUCaawqDA4PChxhmh/8ODDEsvAwHgMKMIDnGL0x7gJQCAwMAJd4MFwAAAHjaY2BgYGaA4DAGRgYQkAHyGMF8NgYrIM3JIAGVYYDT+AEjAwuDFpBmA9KMDEwMCh9i/v8H8sH0/4dQc1iAmAkALaUKLgAAAHjaTY9LDsIgEIbtgqHUPpDi3gPoBVyRTmTddOmqTXThEXqrob2gQ1FjwpDvfwCBdmdXC5AVKFu3e5MfNFJ29KTQT48Ob9/lqYwOGZxeUelN2U2R6+cArgtCJpauW7UQBqnFkUsjAY/kOU1cP+DAgvxwn1chZDwUbd6CFimGXwzwF6tPbFIcjEl+vvmM/byA48e6tWrKArm4ZJlCbdsrxksL1AwWn/yBSJKpYbq8AXaaTb8AAHja28jAwOC00ZrBeQNDQOWO//sdBBgYGRiYWYAEELEwMTE4uzo5Zzo5b2BxdnFOcALxNjA6b2ByTswC8jYwg0VlNuoCTWAMqNzMzsoK1rEhNqByEyerg5PMJlYuVueETKcd/89uBpnpvIEVomeHLoMsAAe1Id4AAAAAAAB42oWQT07CQBTGv0JBhagk7HQzKxca2sJCE1hDt4QF+9JOS0nbaaYDCQfwCJ7Au3AHj+LO13FMmm6cl7785vven0kBjHCBhfpYuNa5Ph1c0e2Xu3jEvWG7UdPDLZ4N92nOm+EBXuAbHmIMSRMs+4aUEd4Nd3CHD8NdvOLTsA2GL8M9PODbcL+hD7C1xoaHeLJSEao0FEW14ckxC+TU8TxvsY6X0eLPmRhry2WVioLpkrbp84LLQPGI7c6sOiUzpWIWS5GzlSgUzzLBSikOPFTOXqly7rqx0Z1Q5BAIoZBSFihQYQOOBEdkCOgXTOHA07HAGjGWiIjaPZNW13/+lm6S9FT7rLHFJ6fQbkATOG1j2OFMucKJJsxIVfQORl+9Jyda6Sl1dUYhSCm1dyClfoeDve4qMYdLEbfqHf3O/AdDumsjAAB42mNgYoAAZQYjBmyAGYQZmdhL8zLdDEydARfoAqIAAAABAAMABwAKABMAB///AA8AAQAAAAAAAAAAAAAAAAABAAAAAA==) format('woff'); 4 | } 5 | 6 | .markdown-body { 7 | -ms-text-size-adjust: 100%; 8 | -webkit-text-size-adjust: 100%; 9 | color: #333; 10 | font-family: "Helvetica Neue", Helvetica, "Segoe UI", Arial, freesans, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 11 | font-size: 16px; 12 | line-height: 1.6; 13 | word-wrap: break-word; 14 | } 15 | 16 | .markdown-body a { 17 | background-color: transparent; 18 | -webkit-text-decoration-skip: objects; 19 | } 20 | 21 | .markdown-body a:active, 22 | .markdown-body a:hover { 23 | outline-width: 0; 24 | } 25 | 26 | .markdown-body strong { 27 | font-weight: inherit; 28 | } 29 | 30 | .markdown-body strong { 31 | font-weight: bolder; 32 | } 33 | 34 | .markdown-body h1 { 35 | font-size: 2em; 36 | margin: 0.67em 0; 37 | } 38 | 39 | .markdown-body img { 40 | border-style: none; 41 | } 42 | 43 | .markdown-body svg:not(:root) { 44 | overflow: hidden; 45 | } 46 | 47 | .markdown-body code, 48 | .markdown-body kbd, 49 | .markdown-body pre { 50 | font-family: monospace, monospace; 51 | font-size: 1em; 52 | } 53 | 54 | .markdown-body hr { 55 | box-sizing: content-box; 56 | height: 0; 57 | overflow: visible; 58 | } 59 | 60 | .markdown-body input { 61 | font: inherit; 62 | margin: 0; 63 | } 64 | 65 | .markdown-body input { 66 | overflow: visible; 67 | } 68 | 69 | .markdown-body button:-moz-focusring, 70 | .markdown-body [type="button"]:-moz-focusring, 71 | .markdown-body [type="reset"]:-moz-focusring, 72 | .markdown-body [type="submit"]:-moz-focusring { 73 | outline: 1px dotted ButtonText; 74 | } 75 | 76 | .markdown-body [type="checkbox"] { 77 | box-sizing: border-box; 78 | padding: 0; 79 | } 80 | 81 | .markdown-body table { 82 | border-spacing: 0; 83 | border-collapse: collapse; 84 | } 85 | 86 | .markdown-body td, 87 | .markdown-body th { 88 | padding: 0; 89 | } 90 | 91 | .markdown-body * { 92 | box-sizing: border-box; 93 | } 94 | 95 | .markdown-body input { 96 | font: 13px/1.4 Helvetica, arial, nimbussansl, liberationsans, freesans, clean, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 97 | } 98 | 99 | .markdown-body a { 100 | color: #4078c0; 101 | text-decoration: none; 102 | } 103 | 104 | .markdown-body a:hover, 105 | .markdown-body a:active { 106 | text-decoration: underline; 107 | } 108 | 109 | .markdown-body hr { 110 | height: 0; 111 | margin: 15px 0; 112 | overflow: hidden; 113 | background: transparent; 114 | border: 0; 115 | border-bottom: 1px solid #ddd; 116 | } 117 | 118 | .markdown-body hr::before { 119 | display: table; 120 | content: ""; 121 | } 122 | 123 | .markdown-body hr::after { 124 | display: table; 125 | clear: both; 126 | content: ""; 127 | } 128 | 129 | .markdown-body h1, 130 | .markdown-body h2, 131 | .markdown-body h3, 132 | .markdown-body h4, 133 | .markdown-body h5, 134 | .markdown-body h6 { 135 | margin-top: 0; 136 | margin-bottom: 0; 137 | line-height: 1.5; 138 | } 139 | 140 | .markdown-body h1 { 141 | font-size: 30px; 142 | } 143 | 144 | .markdown-body h2 { 145 | font-size: 21px; 146 | } 147 | 148 | .markdown-body h3 { 149 | font-size: 16px; 150 | } 151 | 152 | .markdown-body h4 { 153 | font-size: 14px; 154 | } 155 | 156 | .markdown-body h5 { 157 | font-size: 12px; 158 | } 159 | 160 | .markdown-body h6 { 161 | font-size: 11px; 162 | } 163 | 164 | .markdown-body p { 165 | margin-top: 0; 166 | margin-bottom: 10px; 167 | } 168 | 169 | .markdown-body blockquote { 170 | margin: 0; 171 | } 172 | 173 | .markdown-body ul, 174 | .markdown-body ol { 175 | padding-left: 0; 176 | margin-top: 0; 177 | margin-bottom: 0; 178 | } 179 | 180 | .markdown-body ol ol, 181 | .markdown-body ul ol { 182 | list-style-type: lower-roman; 183 | } 184 | 185 | .markdown-body ul ul ol, 186 | .markdown-body ul ol ol, 187 | .markdown-body ol ul ol, 188 | .markdown-body ol ol ol { 189 | list-style-type: lower-alpha; 190 | } 191 | 192 | .markdown-body dd { 193 | margin-left: 0; 194 | } 195 | 196 | .markdown-body code { 197 | font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; 198 | font-size: 12px; 199 | } 200 | 201 | .markdown-body pre { 202 | margin-top: 0; 203 | margin-bottom: 0; 204 | font: 12px Consolas, "Liberation Mono", Menlo, Courier, monospace; 205 | } 206 | 207 | .markdown-body .pl-0 { 208 | padding-left: 0 !important; 209 | } 210 | 211 | .markdown-body .pl-1 { 212 | padding-left: 3px !important; 213 | } 214 | 215 | .markdown-body .pl-2 { 216 | padding-left: 6px !important; 217 | } 218 | 219 | .markdown-body .pl-3 { 220 | padding-left: 12px !important; 221 | } 222 | 223 | .markdown-body .pl-4 { 224 | padding-left: 24px !important; 225 | } 226 | 227 | .markdown-body .pl-5 { 228 | padding-left: 36px !important; 229 | } 230 | 231 | .markdown-body .pl-6 { 232 | padding-left: 48px !important; 233 | } 234 | 235 | .markdown-body .form-select::-ms-expand { 236 | opacity: 0; 237 | } 238 | 239 | .markdown-body:before { 240 | display: table; 241 | content: ""; 242 | } 243 | 244 | .markdown-body:after { 245 | display: table; 246 | clear: both; 247 | content: ""; 248 | } 249 | 250 | .markdown-body>*:first-child { 251 | margin-top: 0 !important; 252 | } 253 | 254 | .markdown-body>*:last-child { 255 | margin-bottom: 0 !important; 256 | } 257 | 258 | .markdown-body a:not([href]) { 259 | color: inherit; 260 | text-decoration: none; 261 | } 262 | 263 | .markdown-body .anchor { 264 | display: inline-block; 265 | padding-right: 2px; 266 | margin-left: -18px; 267 | } 268 | 269 | .markdown-body .anchor:focus { 270 | outline: none; 271 | } 272 | 273 | .markdown-body h1, 274 | .markdown-body h2, 275 | .markdown-body h3, 276 | .markdown-body h4, 277 | .markdown-body h5, 278 | .markdown-body h6 { 279 | margin-top: 1em; 280 | margin-bottom: 16px; 281 | font-weight: bold; 282 | line-height: 1.4; 283 | } 284 | 285 | .markdown-body h1 .octicon-link, 286 | .markdown-body h2 .octicon-link, 287 | .markdown-body h3 .octicon-link, 288 | .markdown-body h4 .octicon-link, 289 | .markdown-body h5 .octicon-link, 290 | .markdown-body h6 .octicon-link { 291 | color: #000; 292 | vertical-align: middle; 293 | visibility: hidden; 294 | } 295 | 296 | .markdown-body h1:hover .anchor, 297 | .markdown-body h2:hover .anchor, 298 | .markdown-body h3:hover .anchor, 299 | .markdown-body h4:hover .anchor, 300 | .markdown-body h5:hover .anchor, 301 | .markdown-body h6:hover .anchor { 302 | text-decoration: none; 303 | } 304 | 305 | .markdown-body h1:hover .anchor .octicon-link, 306 | .markdown-body h2:hover .anchor .octicon-link, 307 | .markdown-body h3:hover .anchor .octicon-link, 308 | .markdown-body h4:hover .anchor .octicon-link, 309 | .markdown-body h5:hover .anchor .octicon-link, 310 | .markdown-body h6:hover .anchor .octicon-link { 311 | visibility: visible; 312 | } 313 | 314 | .markdown-body h1 { 315 | padding-bottom: 0.3em; 316 | font-size: 2.25em; 317 | line-height: 1.2; 318 | border-bottom: 1px solid #eee; 319 | } 320 | 321 | .markdown-body h1 .anchor { 322 | line-height: 1; 323 | } 324 | 325 | .markdown-body h2 { 326 | padding-bottom: 0.3em; 327 | font-size: 1.75em; 328 | line-height: 1.225; 329 | border-bottom: 1px solid #eee; 330 | } 331 | 332 | .markdown-body h2 .anchor { 333 | line-height: 1; 334 | } 335 | 336 | .markdown-body h3 { 337 | font-size: 1.5em; 338 | line-height: 1.43; 339 | } 340 | 341 | .markdown-body h3 .anchor { 342 | line-height: 1.2; 343 | } 344 | 345 | .markdown-body h4 { 346 | font-size: 1.25em; 347 | } 348 | 349 | .markdown-body h4 .anchor { 350 | line-height: 1.2; 351 | } 352 | 353 | .markdown-body h5 { 354 | font-size: 1em; 355 | } 356 | 357 | .markdown-body h5 .anchor { 358 | line-height: 1.1; 359 | } 360 | 361 | .markdown-body h6 { 362 | font-size: 1em; 363 | color: #777; 364 | } 365 | 366 | .markdown-body h6 .anchor { 367 | line-height: 1.1; 368 | } 369 | 370 | .markdown-body p, 371 | .markdown-body blockquote, 372 | .markdown-body ul, 373 | .markdown-body ol, 374 | .markdown-body dl, 375 | .markdown-body table, 376 | .markdown-body pre { 377 | margin-top: 0; 378 | margin-bottom: 16px; 379 | } 380 | 381 | .markdown-body hr { 382 | height: 4px; 383 | padding: 0; 384 | margin: 16px 0; 385 | background-color: #e7e7e7; 386 | border: 0 none; 387 | } 388 | 389 | .markdown-body ul, 390 | .markdown-body ol { 391 | padding-left: 2em; 392 | } 393 | 394 | .markdown-body ul ul, 395 | .markdown-body ul ol, 396 | .markdown-body ol ol, 397 | .markdown-body ol ul { 398 | margin-top: 0; 399 | margin-bottom: 0; 400 | } 401 | 402 | .markdown-body li>p { 403 | margin-top: 16px; 404 | } 405 | 406 | .markdown-body dl { 407 | padding: 0; 408 | } 409 | 410 | .markdown-body dl dt { 411 | padding: 0; 412 | margin-top: 16px; 413 | font-size: 1em; 414 | font-style: italic; 415 | font-weight: bold; 416 | } 417 | 418 | .markdown-body dl dd { 419 | padding: 0 16px; 420 | margin-bottom: 16px; 421 | } 422 | 423 | .markdown-body blockquote { 424 | padding: 0 15px; 425 | color: #777; 426 | border-left: 4px solid #ddd; 427 | } 428 | 429 | .markdown-body blockquote>:first-child { 430 | margin-top: 0; 431 | } 432 | 433 | .markdown-body blockquote>:last-child { 434 | margin-bottom: 0; 435 | } 436 | 437 | .markdown-body table { 438 | display: block; 439 | width: 100%; 440 | overflow: auto; 441 | word-break: normal; 442 | word-break: keep-all; 443 | } 444 | 445 | .markdown-body table th { 446 | font-weight: bold; 447 | } 448 | 449 | .markdown-body table th, 450 | .markdown-body table td { 451 | padding: 6px 13px; 452 | border: 1px solid #ddd; 453 | } 454 | 455 | .markdown-body table tr { 456 | background-color: #fff; 457 | border-top: 1px solid #ccc; 458 | } 459 | 460 | .markdown-body table tr:nth-child(2n) { 461 | background-color: #f8f8f8; 462 | } 463 | 464 | .markdown-body img { 465 | max-width: 100%; 466 | box-sizing: content-box; 467 | background-color: #fff; 468 | } 469 | 470 | .markdown-body code { 471 | padding: 0; 472 | padding-top: 0.2em; 473 | padding-bottom: 0.2em; 474 | margin: 0; 475 | font-size: 85%; 476 | background-color: rgba(0,0,0,0.04); 477 | border-radius: 3px; 478 | } 479 | 480 | .markdown-body code:before, 481 | .markdown-body code:after { 482 | letter-spacing: -0.2em; 483 | content: "\00a0"; 484 | } 485 | 486 | .markdown-body pre>code { 487 | padding: 0; 488 | margin: 0; 489 | font-size: 100%; 490 | word-break: normal; 491 | white-space: pre; 492 | background: transparent; 493 | border: 0; 494 | } 495 | 496 | .markdown-body .highlight { 497 | margin-bottom: 16px; 498 | } 499 | 500 | .markdown-body .highlight pre, 501 | .markdown-body pre { 502 | padding: 16px; 503 | overflow: auto; 504 | font-size: 85%; 505 | line-height: 1.45; 506 | background-color: #f7f7f7; 507 | border-radius: 3px; 508 | } 509 | 510 | .markdown-body .highlight pre { 511 | margin-bottom: 0; 512 | word-break: normal; 513 | } 514 | 515 | .markdown-body pre { 516 | word-wrap: normal; 517 | } 518 | 519 | .markdown-body pre code { 520 | display: inline; 521 | max-width: initial; 522 | padding: 0; 523 | margin: 0; 524 | overflow: initial; 525 | line-height: inherit; 526 | word-wrap: normal; 527 | background-color: transparent; 528 | border: 0; 529 | } 530 | 531 | .markdown-body pre code:before, 532 | .markdown-body pre code:after { 533 | content: normal; 534 | } 535 | 536 | .markdown-body kbd { 537 | display: inline-block; 538 | padding: 3px 5px; 539 | font-size: 11px; 540 | line-height: 10px; 541 | color: #555; 542 | vertical-align: middle; 543 | background-color: #fcfcfc; 544 | border: solid 1px #ccc; 545 | border-bottom-color: #bbb; 546 | border-radius: 3px; 547 | box-shadow: inset 0 -1px 0 #bbb; 548 | } 549 | 550 | .markdown-body .pl-c { 551 | color: #969896; 552 | } 553 | 554 | .markdown-body .pl-c1, 555 | .markdown-body .pl-s .pl-v { 556 | color: #0086b3; 557 | } 558 | 559 | .markdown-body .pl-e, 560 | .markdown-body .pl-en { 561 | color: #795da3; 562 | } 563 | 564 | .markdown-body .pl-s .pl-s1, 565 | .markdown-body .pl-smi { 566 | color: #333; 567 | } 568 | 569 | .markdown-body .pl-ent { 570 | color: #63a35c; 571 | } 572 | 573 | .markdown-body .pl-k { 574 | color: #a71d5d; 575 | } 576 | 577 | .markdown-body .pl-pds, 578 | .markdown-body .pl-s, 579 | .markdown-body .pl-s .pl-pse .pl-s1, 580 | .markdown-body .pl-sr, 581 | .markdown-body .pl-sr .pl-cce, 582 | .markdown-body .pl-sr .pl-sra, 583 | .markdown-body .pl-sr .pl-sre { 584 | color: #183691; 585 | } 586 | 587 | .markdown-body .pl-v { 588 | color: #ed6a43; 589 | } 590 | 591 | .markdown-body .pl-id { 592 | color: #b52a1d; 593 | } 594 | 595 | .markdown-body .pl-ii { 596 | background-color: #b52a1d; 597 | color: #f8f8f8; 598 | } 599 | 600 | .markdown-body .pl-sr .pl-cce { 601 | color: #63a35c; 602 | font-weight: bold; 603 | } 604 | 605 | .markdown-body .pl-ml { 606 | color: #693a17; 607 | } 608 | 609 | .markdown-body .pl-mh, 610 | .markdown-body .pl-mh .pl-en, 611 | .markdown-body .pl-ms { 612 | color: #1d3e81; 613 | font-weight: bold; 614 | } 615 | 616 | .markdown-body .pl-mq { 617 | color: #008080; 618 | } 619 | 620 | .markdown-body .pl-mi { 621 | color: #333; 622 | font-style: italic; 623 | } 624 | 625 | .markdown-body .pl-mb { 626 | color: #333; 627 | font-weight: bold; 628 | } 629 | 630 | .markdown-body .pl-md { 631 | background-color: #ffecec; 632 | color: #bd2c00; 633 | } 634 | 635 | .markdown-body .pl-mi1 { 636 | background-color: #eaffea; 637 | color: #55a532; 638 | } 639 | 640 | .markdown-body .pl-mdr { 641 | color: #795da3; 642 | font-weight: bold; 643 | } 644 | 645 | .markdown-body .pl-mo { 646 | color: #1d3e81; 647 | } 648 | 649 | .markdown-body kbd { 650 | display: inline-block; 651 | padding: 3px 5px; 652 | font: 11px Consolas, "Liberation Mono", Menlo, Courier, monospace; 653 | line-height: 10px; 654 | color: #555; 655 | vertical-align: middle; 656 | background-color: #fcfcfc; 657 | border: solid 1px #ccc; 658 | border-bottom-color: #bbb; 659 | border-radius: 3px; 660 | box-shadow: inset 0 -1px 0 #bbb; 661 | } 662 | 663 | .markdown-body .full-commit .btn-outline:not(:disabled):hover { 664 | color: #4078c0; 665 | border: 1px solid #4078c0; 666 | } 667 | 668 | .markdown-body :checked+.radio-label { 669 | position: relative; 670 | z-index: 1; 671 | border-color: #4078c0; 672 | } 673 | 674 | .markdown-body .octicon { 675 | display: inline-block; 676 | vertical-align: text-top; 677 | fill: currentColor; 678 | } 679 | 680 | .markdown-body .task-list-item { 681 | list-style-type: none; 682 | } 683 | 684 | .markdown-body .task-list-item+.task-list-item { 685 | margin-top: 3px; 686 | } 687 | 688 | .markdown-body .task-list-item input { 689 | margin: 0 0.2em 0.25em -1.6em; 690 | vertical-align: middle; 691 | } 692 | 693 | .markdown-body hr { 694 | border-bottom-color: #eee; 695 | } 696 | -------------------------------------------------------------------------------- /assets/css/scrollUp-image.css: -------------------------------------------------------------------------------- 1 | /* Image style */ 2 | 3 | #scrollUp { 4 | /*background-image: url("../imgs/top.png");*/ 5 | bottom: 5px; 6 | right: 20px; 7 | width: 38px; 8 | /* Width of image */ 9 | height: 38px; 10 | /* Height of image */ 11 | } 12 | 13 | #scrollUp:after { 14 | content: "Scroll to top"; 15 | } 16 | -------------------------------------------------------------------------------- /assets/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 3 | } 4 | 5 | #footer { 6 | margin-bottom: 1em; 7 | } 8 | 9 | 10 | /* Not used */ 11 | 12 | div.dropzone { 13 | display: block; 14 | /*text-align: center;*/ 15 | border: 2px dashed #666; 16 | border-radius: 5px; 17 | cursor: pointer; 18 | height: 74x; 19 | line-height: 70px; 20 | font-size: 20px; 21 | position: relative; 22 | } 23 | 24 | .qrcode-title { 25 | font-size: 0.8em; 26 | } 27 | 28 | .clearfix::after { 29 | clear: both; 30 | } 31 | 32 | #qrcodeCanvas { 33 | padding-right: 20px; 34 | } -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeskyblue/gohttpserver/df32f57005d9f02ee08a89f237a711316b992376/assets/favicon.png -------------------------------------------------------------------------------- /assets/font-awesome-4.6.3/HELP-US-OUT.txt: -------------------------------------------------------------------------------- 1 | I hope you love Font Awesome. If you've found it useful, please do me a favor and check out my latest project, 2 | Fort Awesome (https://fortawesome.com). It makes it easy to put the perfect icons on your website. Choose from our awesome, 3 | comprehensive icon sets or copy and paste your own. 4 | 5 | Please. Check it out. 6 | 7 | -Dave Gandy 8 | -------------------------------------------------------------------------------- /assets/font-awesome-4.6.3/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeskyblue/gohttpserver/df32f57005d9f02ee08a89f237a711316b992376/assets/font-awesome-4.6.3/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /assets/font-awesome-4.6.3/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeskyblue/gohttpserver/df32f57005d9f02ee08a89f237a711316b992376/assets/font-awesome-4.6.3/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /assets/font-awesome-4.6.3/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeskyblue/gohttpserver/df32f57005d9f02ee08a89f237a711316b992376/assets/font-awesome-4.6.3/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /assets/font-awesome-4.6.3/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeskyblue/gohttpserver/df32f57005d9f02ee08a89f237a711316b992376/assets/font-awesome-4.6.3/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /assets/font-awesome-4.6.3/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeskyblue/gohttpserver/df32f57005d9f02ee08a89f237a711316b992376/assets/font-awesome-4.6.3/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /assets/imgs/top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeskyblue/gohttpserver/df32f57005d9f02ee08a89f237a711316b992376/assets/imgs/top.png -------------------------------------------------------------------------------- /assets/imgs/wx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeskyblue/gohttpserver/df32f57005d9f02ee08a89f237a711316b992376/assets/imgs/wx.png -------------------------------------------------------------------------------- /assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | [[.Title]] 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 87 |
88 |
89 | 100 | 101 | 102 | 103 | 122 | 123 | 124 | 125 | 126 | 129 | 130 | 131 | 132 | 133 | 134 | 144 | 145 | 146 | 183 | 184 | 185 |
104 | 107 |
108 | 111 | 114 | 117 | 120 |
121 |
NameSizeActions
135 | 136 | 137 | {{f.name}} 138 | 139 | 140 | 143 | ~ {{f.size | formatBytes}} 147 | 159 | 182 |
186 |
187 |
188 |
189 |
190 |

191 | {{preview.filename}} 192 |

193 |
194 |
195 |
{{{preview.contentHTML }}} 196 |
197 |
198 |
199 |
200 |
201 | 202 | 223 | 224 | 243 | 244 | 259 |
260 |
261 | 264 |
265 |
266 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 287 | [[if .GoogleTrackerID ]] 288 | [[ end ]] 304 | 305 | 306 | 307 | -------------------------------------------------------------------------------- /assets/ipa-install.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | [[.Name]] install 5 | 6 | 7 | 8 | 9 | 41 | 42 | 43 | 44 | 54 | 57 | 60 | 63 | 66 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /assets/js/clipboard-1.5.12.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * clipboard.js v1.5.12 3 | * https://zenorocha.github.io/clipboard.js 4 | * 5 | * Licensed MIT © Zeno Rocha 6 | */ 7 | !function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var e;e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,e.Clipboard=t()}}(function(){var t,e,n;return function t(e,n,o){function i(a,c){if(!n[a]){if(!e[a]){var s="function"==typeof require&&require;if(!c&&s)return s(a,!0);if(r)return r(a,!0);var l=new Error("Cannot find module '"+a+"'");throw l.code="MODULE_NOT_FOUND",l}var u=n[a]={exports:{}};e[a][0].call(u.exports,function(t){var n=e[a][1][t];return i(n?n:t)},u,u.exports,t,e,n,o)}return n[a].exports}for(var r="function"==typeof require&&require,a=0;ao;o++)n[o].fn.apply(n[o].ctx,e);return this},off:function(t,e){var n=this.e||(this.e={}),o=n[t],i=[];if(o&&e)for(var r=0,a=o.length;a>r;r++)o[r].fn!==e&&o[r].fn._!==e&&i.push(o[r]);return i.length?n[t]=i:delete n[t],this}},e.exports=o},{}],8:[function(e,n,o){!function(i,r){if("function"==typeof t&&t.amd)t(["module","select"],r);else if("undefined"!=typeof o)r(n,e("select"));else{var a={exports:{}};r(a,i.select),i.clipboardAction=a.exports}}(this,function(t,e){"use strict";function n(t){return t&&t.__esModule?t:{"default":t}}function o(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}var i=n(e),r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol?"symbol":typeof t},a=function(){function t(t,e){for(var n=0;n>> 0) + 2); 9 | } 10 | 11 | function pathJoin(parts, sep) { 12 | var separator = sep || '/'; 13 | var replace = new RegExp(separator + '{1,}', 'g'); 14 | return parts.join(separator).replace(replace, separator); 15 | } 16 | 17 | function getQueryString(name) { 18 | var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)"); 19 | var r = decodeURI(window.location.search).substr(1).match(reg); 20 | if (r != null) return r[2].replace(/\+/g, ' '); 21 | return null; 22 | } 23 | 24 | function checkPathNameLegal(name) { 25 | var reg = new RegExp("[\\/:*<>|]"); 26 | var r = name.match(reg) 27 | return r == null; 28 | } 29 | 30 | function showErrorMessage(jqXHR) { 31 | let errMsg = jqXHR.getResponseHeader("x-auth-authentication-message") 32 | if (errMsg == null) { 33 | errMsg = jqXHR.responseText 34 | } 35 | alert(String(jqXHR.status).concat(":", errMsg)); 36 | console.error(errMsg) 37 | } 38 | 39 | var vm = new Vue({ 40 | el: "#app", 41 | data: { 42 | user: { 43 | email: "", 44 | name: "", 45 | }, 46 | location: window.location, 47 | breadcrumb: [], 48 | showHidden: false, 49 | previewMode: false, 50 | preview: { 51 | filename: '', 52 | filetype: '', 53 | filesize: 0, 54 | contentHTML: '', 55 | }, 56 | version: "loading", 57 | mtimeTypeFromNow: false, // or fromNow 58 | auth: {}, 59 | search: getQueryString("search"), 60 | files: [{ 61 | name: "loading ...", 62 | path: "", 63 | size: "...", 64 | type: "dir", 65 | }], 66 | myDropzone: null, 67 | }, 68 | computed: { 69 | computedFiles: function () { 70 | var that = this; 71 | that.preview.filename = null; 72 | 73 | var files = this.files.filter(function (f) { 74 | if (f.name == 'README.md') { 75 | that.preview.filename = f.name; 76 | } 77 | if (!that.showHidden && f.name.slice(0, 1) === '.') { 78 | return false; 79 | } 80 | return true; 81 | }); 82 | // console.log(this.previewFile) 83 | if (this.preview.filename) { 84 | var name = this.preview.filename; // For now only README.md 85 | console.log(pathJoin([location.pathname, 'README.md'])) 86 | $.ajax({ 87 | url: pathJoin([location.pathname, 'README.md']), 88 | method: 'GET', 89 | success: function (res) { 90 | var converter = new showdown.Converter({ 91 | tables: true, 92 | omitExtraWLInCodeBlocks: true, 93 | parseImgDimensions: true, 94 | simplifiedAutoLink: true, 95 | literalMidWordUnderscores: true, 96 | tasklists: true, 97 | ghCodeBlocks: true, 98 | smoothLivePreview: true, 99 | simplifiedAutoLink: true, 100 | strikethrough: true, 101 | }); 102 | 103 | var html = converter.makeHtml(res); 104 | that.preview.contentHTML = html; 105 | }, 106 | error: function (err) { 107 | console.log(err) 108 | } 109 | }) 110 | } 111 | 112 | return files; 113 | }, 114 | }, 115 | created: function () { 116 | $.ajax({ 117 | url: "/-/user", 118 | method: "get", 119 | dataType: "json", 120 | success: function (ret) { 121 | if (ret) { 122 | this.user.email = ret.email; 123 | this.user.name = ret.name; 124 | } 125 | }.bind(this) 126 | }) 127 | this.myDropzone = new Dropzone("#upload-form", { 128 | paramName: "file", 129 | maxFilesize: 10240, 130 | addRemoveLinks: true, 131 | init: function () { 132 | this.on("uploadprogress", function (file, progress) { 133 | // console.log("File progress", progress); 134 | }); 135 | this.on("complete", function (file) { 136 | console.log("reload file list") 137 | loadFileList() 138 | }) 139 | } 140 | }); 141 | }, 142 | methods: { 143 | getEncodePath: function (filepath) { 144 | return pathJoin([location.pathname].concat(filepath.split("/").map(v => encodeURIComponent(v)))) 145 | }, 146 | formatTime: function (timestamp) { 147 | var m = moment(timestamp); 148 | if (this.mtimeTypeFromNow) { 149 | return m.fromNow(); 150 | } 151 | return m.format('YYYY-MM-DD HH:mm:ss'); 152 | }, 153 | toggleHidden: function () { 154 | this.showHidden = !this.showHidden; 155 | }, 156 | removeAllUploads: function () { 157 | this.myDropzone.removeAllFiles(); 158 | }, 159 | parentDirectory: function (path) { 160 | return path.replace('\\', '/').split('/').slice(0, -1).join('/') 161 | }, 162 | changeParentDirectory: function (path) { 163 | var parentDir = this.parentDirectory(path); 164 | loadFileOrDir(parentDir); 165 | }, 166 | genInstallURL: function (name, noEncode) { 167 | var parts = [location.host]; 168 | var pathname = decodeURI(location.pathname); 169 | if (!name) { 170 | parts.push(pathname); 171 | } else if (getExtention(name) == "ipa") { 172 | parts.push("/-/ipa/link", pathname, encodeURIComponent(name)); 173 | } else { 174 | parts.push(pathname, name); 175 | } 176 | var urlPath = location.protocol + "//" + pathJoin(parts); 177 | return noEncode ? urlPath : encodeURI(urlPath); 178 | }, 179 | genQrcode: function (name, title) { 180 | var urlPath = this.genInstallURL(name, true); 181 | $("#qrcode-title").html(title || name || location.pathname); 182 | $("#qrcode-link").attr("href", urlPath); 183 | $('#qrcodeCanvas').empty().qrcode({ 184 | text: encodeURI(urlPath), 185 | }); 186 | 187 | $("#qrcodeRight a").attr("href", urlPath); 188 | $("#qrcode-modal").modal("show"); 189 | }, 190 | genDownloadURL: function (f) { 191 | var search = location.search; 192 | var sep = search == "" ? "?" : "&" 193 | return location.origin + this.getEncodePath(f.name) + location.search + sep + "download=true"; 194 | }, 195 | shouldHaveQrcode: function (name) { 196 | return ['apk', 'ipa'].indexOf(getExtention(name)) !== -1; 197 | }, 198 | genFileClass: function (f) { 199 | if (f.type == "dir") { 200 | if (f.name == '.git') { 201 | return 'fa-git-square'; 202 | } 203 | return "fa-folder-open"; 204 | } 205 | var ext = getExtention(f.name); 206 | switch (ext) { 207 | case "go": 208 | case "py": 209 | case "js": 210 | case "java": 211 | case "c": 212 | case "cpp": 213 | case "h": 214 | return "fa-file-code-o"; 215 | case "pdf": 216 | return "fa-file-pdf-o"; 217 | case "zip": 218 | return "fa-file-zip-o"; 219 | case "mp3": 220 | case "wav": 221 | return "fa-file-audio-o"; 222 | case "jpg": 223 | case "png": 224 | case "gif": 225 | case "jpeg": 226 | case "tiff": 227 | return "fa-file-picture-o"; 228 | case "ipa": 229 | case "dmg": 230 | return "fa-apple"; 231 | case "apk": 232 | return "fa-android"; 233 | case "exe": 234 | return "fa-windows"; 235 | } 236 | return "fa-file-text-o" 237 | }, 238 | clickFileOrDir: function (f, e) { 239 | var reqPath = this.getEncodePath(f.name) 240 | // TODO: fix here tomorrow 241 | if (f.type == "file") { 242 | // check whether the file is video 243 | var videoExtensions = ['mp4', 'webm', 'ogg', 'mov', 'avi', 'mkv']; 244 | var fileExtension = getExtention(f.name).toLowerCase(); 245 | if (videoExtensions.includes(fileExtension)) { 246 | window.location.href = '/-/video-player' + reqPath; 247 | } else { 248 | window.location.href = reqPath; 249 | } 250 | e.preventDefault() 251 | return; 252 | } 253 | loadFileOrDir(reqPath); 254 | e.preventDefault() 255 | }, 256 | changePath: function (reqPath, e) { 257 | loadFileOrDir(reqPath); 258 | e.preventDefault() 259 | }, 260 | showInfo: function (f) { 261 | console.log(f); 262 | $.ajax({ 263 | url: this.getEncodePath(f.name), 264 | data: { 265 | op: "info", 266 | }, 267 | method: "GET", 268 | success: function (res) { 269 | $("#file-info-title").text(f.name); 270 | $("#file-info-content").text(JSON.stringify(res, null, 4)); 271 | $("#file-info-modal").modal("show"); 272 | // console.log(JSON.stringify(res, null, 4)); 273 | }, 274 | error: function (jqXHR, textStatus, errorThrown) { 275 | showErrorMessage(jqXHR) 276 | } 277 | }) 278 | }, 279 | makeDirectory: function () { 280 | var name = window.prompt("current path: " + location.pathname + "\nplease enter the new directory name", "") 281 | console.log(name) 282 | if (!name) { 283 | return 284 | } 285 | if(!checkPathNameLegal(name)) { 286 | alert("Name should not contains any of \\/:*<>|") 287 | return 288 | } 289 | $.ajax({ 290 | url: this.getEncodePath(name), 291 | method: "POST", 292 | success: function (res) { 293 | console.log(res) 294 | loadFileList() 295 | }, 296 | error: function (jqXHR, textStatus, errorThrown) { 297 | showErrorMessage(jqXHR) 298 | } 299 | }) 300 | }, 301 | deletePathConfirm: function (f, e) { 302 | e.preventDefault(); 303 | if (!e.altKey) { // skip confirm when alt pressed 304 | if (!window.confirm("Delete " + f.name + " ?")) { 305 | return; 306 | } 307 | } 308 | $.ajax({ 309 | url: this.getEncodePath(f.name), 310 | method: 'DELETE', 311 | success: function (res) { 312 | loadFileList() 313 | }, 314 | error: function (jqXHR, textStatus, errorThrown) { 315 | showErrorMessage(jqXHR) 316 | } 317 | }); 318 | }, 319 | updateBreadcrumb: function (pathname) { 320 | var pathname = decodeURI(pathname || location.pathname || "/"); 321 | pathname = pathname.split('?')[0] 322 | var parts = pathname.split('/'); 323 | this.breadcrumb = []; 324 | if (pathname == "/") { 325 | return this.breadcrumb; 326 | } 327 | var i = 2; 328 | for (; i <= parts.length; i += 1) { 329 | var name = parts[i - 1]; 330 | if (!name) { 331 | continue; 332 | } 333 | var path = parts.slice(0, i).join('/'); 334 | this.breadcrumb.push({ 335 | name: name + (i == parts.length ? ' /' : ''), 336 | path: path 337 | }) 338 | } 339 | return this.breadcrumb; 340 | }, 341 | loadPreviewFile: function (filepath, e) { 342 | if (e) { 343 | e.preventDefault() // may be need a switch 344 | } 345 | var that = this; 346 | $.getJSON(pathJoin(['/-/info', location.pathname])) 347 | .then(function (res) { 348 | console.log(res); 349 | that.preview.filename = res.name; 350 | that.preview.filesize = res.size; 351 | return $.ajax({ 352 | url: '/' + res.path, 353 | dataType: 'text', 354 | }); 355 | }) 356 | .then(function (res) { 357 | console.log(res) 358 | that.preview.contentHTML = '
' + res + '
'; 359 | console.log("Finally") 360 | }) 361 | .done(function (res) { 362 | console.log("done", res) 363 | }); 364 | }, 365 | loadAll: function () { 366 | // TODO: move loadFileList here 367 | }, 368 | } 369 | }) 370 | 371 | window.onpopstate = function (event) { 372 | if (location.search.match(/\?search=/)) { 373 | location.reload(); 374 | return; 375 | } 376 | loadFileList() 377 | } 378 | 379 | function loadFileOrDir(reqPath) { 380 | let requestUri = reqPath + location.search 381 | var retObj = loadFileList(requestUri) 382 | if (retObj !== null) { 383 | retObj.done(function () { 384 | window.history.pushState({}, "", requestUri); 385 | }); 386 | } 387 | 388 | } 389 | 390 | function loadFileList(pathname) { 391 | var pathname = pathname || location.pathname + location.search; 392 | var retObj = null 393 | if (getQueryString("raw") !== "false") { // not a file preview 394 | var sep = pathname.indexOf("?") === -1 ? "?" : "&" 395 | retObj = $.ajax({ 396 | url: pathname + sep + "json=true", 397 | dataType: "json", 398 | cache: false, 399 | success: function (res) { 400 | res.files = _.sortBy(res.files, function (f) { 401 | var weight = f.type == 'dir' ? 1000 : 1; 402 | return -weight * f.mtime; 403 | }) 404 | vm.files = res.files; 405 | vm.auth = res.auth; 406 | vm.updateBreadcrumb(pathname); 407 | }, 408 | error: function (jqXHR, textStatus, errorThrown) { 409 | showErrorMessage(jqXHR) 410 | }, 411 | }); 412 | 413 | } 414 | 415 | vm.previewMode = getQueryString("raw") == "false"; 416 | if (vm.previewMode) { 417 | vm.loadPreviewFile(); 418 | } 419 | return retObj 420 | } 421 | 422 | Vue.filter('fromNow', function (value) { 423 | return moment(value).fromNow(); 424 | }) 425 | 426 | Vue.filter('formatBytes', function (value) { 427 | var bytes = parseFloat(value); 428 | if (bytes < 0) return "-"; 429 | else if (bytes < 1024) return bytes + " B"; 430 | else if (bytes < 1048576) return (bytes / 1024).toFixed(0) + " KB"; 431 | else if (bytes < 1073741824) return (bytes / 1048576).toFixed(1) + " MB"; 432 | else return (bytes / 1073741824).toFixed(1) + " GB"; 433 | }) 434 | 435 | $(function () { 436 | $.scrollUp({ 437 | scrollText: '', // text are defined in css 438 | }); 439 | 440 | // For page first loading 441 | loadFileList(location.pathname + location.search) 442 | 443 | // update version 444 | $.getJSON("/-/sysinfo", function (res) { 445 | vm.version = res.version; 446 | }) 447 | 448 | var clipboard = new Clipboard('.btn'); 449 | clipboard.on('success', function (e) { 450 | console.info('Action:', e.action); 451 | console.info('Text:', e.text); 452 | console.info('Trigger:', e.trigger); 453 | $(e.trigger) 454 | .tooltip('show') 455 | .mouseleave(function () { 456 | $(this).tooltip('hide'); 457 | }) 458 | 459 | e.clearSelection(); 460 | }); 461 | }); 462 | -------------------------------------------------------------------------------- /assets/js/jquery.qrcode.js: -------------------------------------------------------------------------------- 1 | (function( $ ){ 2 | $.fn.qrcode = function(options) { 3 | // if options is string, 4 | if( typeof options === 'string' ){ 5 | options = { text: options }; 6 | } 7 | 8 | // set default values 9 | // typeNumber < 1 for automatic calculation 10 | options = $.extend( {}, { 11 | render : "canvas", 12 | width : 256, 13 | height : 256, 14 | typeNumber : -1, 15 | correctLevel : QRErrorCorrectLevel.H, 16 | background : "#ffffff", 17 | foreground : "#000000" 18 | }, options); 19 | 20 | var createCanvas = function(){ 21 | // create the qrcode itself 22 | var qrcode = new QRCode(options.typeNumber, options.correctLevel); 23 | qrcode.addData(options.text); 24 | qrcode.make(); 25 | 26 | // create canvas element 27 | var canvas = document.createElement('canvas'); 28 | canvas.width = options.width; 29 | canvas.height = options.height; 30 | var ctx = canvas.getContext('2d'); 31 | 32 | // compute tileW/tileH based on options.width/options.height 33 | var tileW = options.width / qrcode.getModuleCount(); 34 | var tileH = options.height / qrcode.getModuleCount(); 35 | 36 | // draw in the canvas 37 | for( var row = 0; row < qrcode.getModuleCount(); row++ ){ 38 | for( var col = 0; col < qrcode.getModuleCount(); col++ ){ 39 | ctx.fillStyle = qrcode.isDark(row, col) ? options.foreground : options.background; 40 | var w = (Math.ceil((col+1)*tileW) - Math.floor(col*tileW)); 41 | var h = (Math.ceil((row+1)*tileW) - Math.floor(row*tileW)); 42 | ctx.fillRect(Math.round(col*tileW),Math.round(row*tileH), w, h); 43 | } 44 | } 45 | // return just built canvas 46 | return canvas; 47 | } 48 | 49 | // from Jon-Carlos Rivera (https://github.com/imbcmdth) 50 | var createTable = function(){ 51 | // create the qrcode itself 52 | var qrcode = new QRCode(options.typeNumber, options.correctLevel); 53 | qrcode.addData(options.text); 54 | qrcode.make(); 55 | 56 | // create table element 57 | var $table = $('
') 58 | .css("width", options.width+"px") 59 | .css("height", options.height+"px") 60 | .css("border", "0px") 61 | .css("border-collapse", "collapse") 62 | .css('background-color', options.background); 63 | 64 | // compute tileS percentage 65 | var tileW = options.width / qrcode.getModuleCount(); 66 | var tileH = options.height / qrcode.getModuleCount(); 67 | 68 | // draw in the table 69 | for(var row = 0; row < qrcode.getModuleCount(); row++ ){ 70 | var $row = $('').css('height', tileH+"px").appendTo($table); 71 | 72 | for(var col = 0; col < qrcode.getModuleCount(); col++ ){ 73 | $('') 74 | .css('width', tileW+"px") 75 | .css('background-color', qrcode.isDark(row, col) ? options.foreground : options.background) 76 | .appendTo($row); 77 | } 78 | } 79 | // return just built canvas 80 | return $table; 81 | } 82 | 83 | 84 | return this.each(function(){ 85 | var element = options.render == "canvas" ? createCanvas() : createTable(); 86 | $(element).appendTo(this); 87 | }); 88 | }; 89 | })( jQuery ); 90 | -------------------------------------------------------------------------------- /assets/js/jquery.scrollUp.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * scrollup v2.4.1 3 | * Url: http://markgoodyear.com/labs/scrollup/ 4 | * Copyright (c) Mark Goodyear — @markgdyr — http://markgoodyear.com 5 | * License: MIT 6 | */ 7 | !function(l,o,e){"use strict";l.fn.scrollUp=function(o){l.data(e.body,"scrollUp")||(l.data(e.body,"scrollUp",!0),l.fn.scrollUp.init(o))},l.fn.scrollUp.init=function(r){var s,t,c,i,n,a,d,p=l.fn.scrollUp.settings=l.extend({},l.fn.scrollUp.defaults,r),f=!1;switch(d=p.scrollTrigger?l(p.scrollTrigger):l("",{id:p.scrollName,href:"#top"}),p.scrollTitle&&d.attr("title",p.scrollTitle),d.appendTo("body"),p.scrollImg||p.scrollTrigger||d.html(p.scrollText),d.css({display:"none",position:"fixed",zIndex:p.zIndex}),p.activeOverlay&&l("
",{id:p.scrollName+"-active"}).css({position:"absolute",top:p.scrollDistance+"px",width:"100%",borderTop:"1px dotted"+p.activeOverlay,zIndex:p.zIndex}).appendTo("body"),p.animation){case"fade":s="fadeIn",t="fadeOut",c=p.animationSpeed;break;case"slide":s="slideDown",t="slideUp",c=p.animationSpeed;break;default:s="show",t="hide",c=0}i="top"===p.scrollFrom?p.scrollDistance:l(e).height()-l(o).height()-p.scrollDistance,n=l(o).scroll(function(){l(o).scrollTop()>i?f||(d[s](c),f=!0):f&&(d[t](c),f=!1)}),p.scrollTarget?"number"==typeof p.scrollTarget?a=p.scrollTarget:"string"==typeof p.scrollTarget&&(a=Math.floor(l(p.scrollTarget).offset().top)):a=0,d.click(function(o){o.preventDefault(),l("html, body").animate({scrollTop:a},p.scrollSpeed,p.easingType)})},l.fn.scrollUp.defaults={scrollName:"scrollUp",scrollDistance:300,scrollFrom:"top",scrollSpeed:300,easingType:"linear",animation:"fade",animationSpeed:200,scrollTrigger:!1,scrollTarget:!1,scrollText:"Scroll to top",scrollTitle:!1,scrollImg:!1,activeOverlay:!1,zIndex:2147483647},l.fn.scrollUp.destroy=function(r){l.removeData(e.body,"scrollUp"),l("#"+l.fn.scrollUp.settings.scrollName).remove(),l("#"+l.fn.scrollUp.settings.scrollName+"-active").remove(),l.fn.jquery.split(".")[1]>=7?l(o).off("scroll",r):l(o).unbind("scroll",r)},l.scrollUp=l.fn.scrollUp}(jQuery,window,document); -------------------------------------------------------------------------------- /assets/js/ua-parser.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UAParser.js v0.7.3 3 | * Lightweight JavaScript-based User-Agent string parser 4 | * https://github.com/faisalman/ua-parser-js 5 | * 6 | * Copyright © 2012-2014 Faisal Salman 7 | * Dual licensed under GPLv2 & MIT 8 | */ 9 | (function(window,undefined){"use strict";var LIBVERSION="0.7.3",EMPTY="",UNKNOWN="?",FUNC_TYPE="function",UNDEF_TYPE="undefined",OBJ_TYPE="object",MAJOR="major",MODEL="model",NAME="name",TYPE="type",VENDOR="vendor",VERSION="version",ARCHITECTURE="architecture",CONSOLE="console",MOBILE="mobile",TABLET="tablet",SMARTTV="smarttv",WEARABLE="wearable",EMBEDDED="embedded";var util={extend:function(regexes,extensions){for(var i in extensions){if("browser cpu device engine os".indexOf(i)!==-1&&extensions[i].length%2===0){regexes[i]=extensions[i].concat(regexes[i])}}return regexes},has:function(str1,str2){if(typeof str1==="string"){return str2.toLowerCase().indexOf(str1.toLowerCase())!==-1}},lowerize:function(str){return str.toLowerCase()}};var mapper={rgx:function(){var result,i=0,j,k,p,q,matches,match,args=arguments;while(i0){if(q.length==2){if(typeof q[1]==FUNC_TYPE){result[q[0]]=q[1].call(this,match)}else{result[q[0]]=q[1]}}else if(q.length==3){if(typeof q[1]===FUNC_TYPE&&!(q[1].exec&&q[1].test)){result[q[0]]=match?q[1].call(this,match,q[2]):undefined}else{result[q[0]]=match?match.replace(q[1],q[2]):undefined}}else if(q.length==4){result[q[0]]=match?q[3].call(this,match.replace(q[1],q[2])):undefined}}else{result[q]=match?match:undefined}}}}i+=2}return result},str:function(str,map){for(var i in map){if(typeof map[i]===OBJ_TYPE&&map[i].length>0){for(var j=0;j=0&&o>i;i+=n){var a=u?u[i]:i;e=r(e,t[a],a,t)}return e}return function(r,e,u,i){e=b(e,i,4);var o=!k(r)&&m.keys(r),a=(o||r).length,c=n>0?0:a-1;return arguments.length<3&&(u=r[o?o[c]:c],c+=n),t(r,e,u,o,c,a)}}function t(n){return function(t,r,e){r=x(r,e);for(var u=O(t),i=n>0?0:u-1;i>=0&&u>i;i+=n)if(r(t[i],i,t))return i;return-1}}function r(n,t,r){return function(e,u,i){var o=0,a=O(e);if("number"==typeof i)n>0?o=i>=0?i:Math.max(i+a,o):a=i>=0?Math.min(i+1,a):i+a+1;else if(r&&i&&a)return i=r(e,u),e[i]===u?i:-1;if(u!==u)return i=t(l.call(e,o,a),m.isNaN),i>=0?i+o:-1;for(i=n>0?o:a-1;i>=0&&a>i;i+=n)if(e[i]===u)return i;return-1}}function e(n,t){var r=I.length,e=n.constructor,u=m.isFunction(e)&&e.prototype||a,i="constructor";for(m.has(n,i)&&!m.contains(t,i)&&t.push(i);r--;)i=I[r],i in n&&n[i]!==u[i]&&!m.contains(t,i)&&t.push(i)}var u=this,i=u._,o=Array.prototype,a=Object.prototype,c=Function.prototype,f=o.push,l=o.slice,s=a.toString,p=a.hasOwnProperty,h=Array.isArray,v=Object.keys,g=c.bind,y=Object.create,d=function(){},m=function(n){return n instanceof m?n:this instanceof m?void(this._wrapped=n):new m(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=m),exports._=m):u._=m,m.VERSION="1.8.3";var b=function(n,t,r){if(t===void 0)return n;switch(null==r?3:r){case 1:return function(r){return n.call(t,r)};case 2:return function(r,e){return n.call(t,r,e)};case 3:return function(r,e,u){return n.call(t,r,e,u)};case 4:return function(r,e,u,i){return n.call(t,r,e,u,i)}}return function(){return n.apply(t,arguments)}},x=function(n,t,r){return null==n?m.identity:m.isFunction(n)?b(n,t,r):m.isObject(n)?m.matcher(n):m.property(n)};m.iteratee=function(n,t){return x(n,t,1/0)};var _=function(n,t){return function(r){var e=arguments.length;if(2>e||null==r)return r;for(var u=1;e>u;u++)for(var i=arguments[u],o=n(i),a=o.length,c=0;a>c;c++){var f=o[c];t&&r[f]!==void 0||(r[f]=i[f])}return r}},j=function(n){if(!m.isObject(n))return{};if(y)return y(n);d.prototype=n;var t=new d;return d.prototype=null,t},w=function(n){return function(t){return null==t?void 0:t[n]}},A=Math.pow(2,53)-1,O=w("length"),k=function(n){var t=O(n);return"number"==typeof t&&t>=0&&A>=t};m.each=m.forEach=function(n,t,r){t=b(t,r);var e,u;if(k(n))for(e=0,u=n.length;u>e;e++)t(n[e],e,n);else{var i=m.keys(n);for(e=0,u=i.length;u>e;e++)t(n[i[e]],i[e],n)}return n},m.map=m.collect=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=Array(u),o=0;u>o;o++){var a=e?e[o]:o;i[o]=t(n[a],a,n)}return i},m.reduce=m.foldl=m.inject=n(1),m.reduceRight=m.foldr=n(-1),m.find=m.detect=function(n,t,r){var e;return e=k(n)?m.findIndex(n,t,r):m.findKey(n,t,r),e!==void 0&&e!==-1?n[e]:void 0},m.filter=m.select=function(n,t,r){var e=[];return t=x(t,r),m.each(n,function(n,r,u){t(n,r,u)&&e.push(n)}),e},m.reject=function(n,t,r){return m.filter(n,m.negate(x(t)),r)},m.every=m.all=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=0;u>i;i++){var o=e?e[i]:i;if(!t(n[o],o,n))return!1}return!0},m.some=m.any=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=0;u>i;i++){var o=e?e[i]:i;if(t(n[o],o,n))return!0}return!1},m.contains=m.includes=m.include=function(n,t,r,e){return k(n)||(n=m.values(n)),("number"!=typeof r||e)&&(r=0),m.indexOf(n,t,r)>=0},m.invoke=function(n,t){var r=l.call(arguments,2),e=m.isFunction(t);return m.map(n,function(n){var u=e?t:n[t];return null==u?u:u.apply(n,r)})},m.pluck=function(n,t){return m.map(n,m.property(t))},m.where=function(n,t){return m.filter(n,m.matcher(t))},m.findWhere=function(n,t){return m.find(n,m.matcher(t))},m.max=function(n,t,r){var e,u,i=-1/0,o=-1/0;if(null==t&&null!=n){n=k(n)?n:m.values(n);for(var a=0,c=n.length;c>a;a++)e=n[a],e>i&&(i=e)}else t=x(t,r),m.each(n,function(n,r,e){u=t(n,r,e),(u>o||u===-1/0&&i===-1/0)&&(i=n,o=u)});return i},m.min=function(n,t,r){var e,u,i=1/0,o=1/0;if(null==t&&null!=n){n=k(n)?n:m.values(n);for(var a=0,c=n.length;c>a;a++)e=n[a],i>e&&(i=e)}else t=x(t,r),m.each(n,function(n,r,e){u=t(n,r,e),(o>u||1/0===u&&1/0===i)&&(i=n,o=u)});return i},m.shuffle=function(n){for(var t,r=k(n)?n:m.values(n),e=r.length,u=Array(e),i=0;e>i;i++)t=m.random(0,i),t!==i&&(u[i]=u[t]),u[t]=r[i];return u},m.sample=function(n,t,r){return null==t||r?(k(n)||(n=m.values(n)),n[m.random(n.length-1)]):m.shuffle(n).slice(0,Math.max(0,t))},m.sortBy=function(n,t,r){return t=x(t,r),m.pluck(m.map(n,function(n,r,e){return{value:n,index:r,criteria:t(n,r,e)}}).sort(function(n,t){var r=n.criteria,e=t.criteria;if(r!==e){if(r>e||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.index-t.index}),"value")};var F=function(n){return function(t,r,e){var u={};return r=x(r,e),m.each(t,function(e,i){var o=r(e,i,t);n(u,e,o)}),u}};m.groupBy=F(function(n,t,r){m.has(n,r)?n[r].push(t):n[r]=[t]}),m.indexBy=F(function(n,t,r){n[r]=t}),m.countBy=F(function(n,t,r){m.has(n,r)?n[r]++:n[r]=1}),m.toArray=function(n){return n?m.isArray(n)?l.call(n):k(n)?m.map(n,m.identity):m.values(n):[]},m.size=function(n){return null==n?0:k(n)?n.length:m.keys(n).length},m.partition=function(n,t,r){t=x(t,r);var e=[],u=[];return m.each(n,function(n,r,i){(t(n,r,i)?e:u).push(n)}),[e,u]},m.first=m.head=m.take=function(n,t,r){return null==n?void 0:null==t||r?n[0]:m.initial(n,n.length-t)},m.initial=function(n,t,r){return l.call(n,0,Math.max(0,n.length-(null==t||r?1:t)))},m.last=function(n,t,r){return null==n?void 0:null==t||r?n[n.length-1]:m.rest(n,Math.max(0,n.length-t))},m.rest=m.tail=m.drop=function(n,t,r){return l.call(n,null==t||r?1:t)},m.compact=function(n){return m.filter(n,m.identity)};var S=function(n,t,r,e){for(var u=[],i=0,o=e||0,a=O(n);a>o;o++){var c=n[o];if(k(c)&&(m.isArray(c)||m.isArguments(c))){t||(c=S(c,t,r));var f=0,l=c.length;for(u.length+=l;l>f;)u[i++]=c[f++]}else r||(u[i++]=c)}return u};m.flatten=function(n,t){return S(n,t,!1)},m.without=function(n){return m.difference(n,l.call(arguments,1))},m.uniq=m.unique=function(n,t,r,e){m.isBoolean(t)||(e=r,r=t,t=!1),null!=r&&(r=x(r,e));for(var u=[],i=[],o=0,a=O(n);a>o;o++){var c=n[o],f=r?r(c,o,n):c;t?(o&&i===f||u.push(c),i=f):r?m.contains(i,f)||(i.push(f),u.push(c)):m.contains(u,c)||u.push(c)}return u},m.union=function(){return m.uniq(S(arguments,!0,!0))},m.intersection=function(n){for(var t=[],r=arguments.length,e=0,u=O(n);u>e;e++){var i=n[e];if(!m.contains(t,i)){for(var o=1;r>o&&m.contains(arguments[o],i);o++);o===r&&t.push(i)}}return t},m.difference=function(n){var t=S(arguments,!0,!0,1);return m.filter(n,function(n){return!m.contains(t,n)})},m.zip=function(){return m.unzip(arguments)},m.unzip=function(n){for(var t=n&&m.max(n,O).length||0,r=Array(t),e=0;t>e;e++)r[e]=m.pluck(n,e);return r},m.object=function(n,t){for(var r={},e=0,u=O(n);u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},m.findIndex=t(1),m.findLastIndex=t(-1),m.sortedIndex=function(n,t,r,e){r=x(r,e,1);for(var u=r(t),i=0,o=O(n);o>i;){var a=Math.floor((i+o)/2);r(n[a])i;i++,n+=r)u[i]=n;return u};var E=function(n,t,r,e,u){if(!(e instanceof t))return n.apply(r,u);var i=j(n.prototype),o=n.apply(i,u);return m.isObject(o)?o:i};m.bind=function(n,t){if(g&&n.bind===g)return g.apply(n,l.call(arguments,1));if(!m.isFunction(n))throw new TypeError("Bind must be called on a function");var r=l.call(arguments,2),e=function(){return E(n,e,t,this,r.concat(l.call(arguments)))};return e},m.partial=function(n){var t=l.call(arguments,1),r=function(){for(var e=0,u=t.length,i=Array(u),o=0;u>o;o++)i[o]=t[o]===m?arguments[e++]:t[o];for(;e=e)throw new Error("bindAll must be passed function names");for(t=1;e>t;t++)r=arguments[t],n[r]=m.bind(n[r],n);return n},m.memoize=function(n,t){var r=function(e){var u=r.cache,i=""+(t?t.apply(this,arguments):e);return m.has(u,i)||(u[i]=n.apply(this,arguments)),u[i]};return r.cache={},r},m.delay=function(n,t){var r=l.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},m.defer=m.partial(m.delay,m,1),m.throttle=function(n,t,r){var e,u,i,o=null,a=0;r||(r={});var c=function(){a=r.leading===!1?0:m.now(),o=null,i=n.apply(e,u),o||(e=u=null)};return function(){var f=m.now();a||r.leading!==!1||(a=f);var l=t-(f-a);return e=this,u=arguments,0>=l||l>t?(o&&(clearTimeout(o),o=null),a=f,i=n.apply(e,u),o||(e=u=null)):o||r.trailing===!1||(o=setTimeout(c,l)),i}},m.debounce=function(n,t,r){var e,u,i,o,a,c=function(){var f=m.now()-o;t>f&&f>=0?e=setTimeout(c,t-f):(e=null,r||(a=n.apply(i,u),e||(i=u=null)))};return function(){i=this,u=arguments,o=m.now();var f=r&&!e;return e||(e=setTimeout(c,t)),f&&(a=n.apply(i,u),i=u=null),a}},m.wrap=function(n,t){return m.partial(t,n)},m.negate=function(n){return function(){return!n.apply(this,arguments)}},m.compose=function(){var n=arguments,t=n.length-1;return function(){for(var r=t,e=n[t].apply(this,arguments);r--;)e=n[r].call(this,e);return e}},m.after=function(n,t){return function(){return--n<1?t.apply(this,arguments):void 0}},m.before=function(n,t){var r;return function(){return--n>0&&(r=t.apply(this,arguments)),1>=n&&(t=null),r}},m.once=m.partial(m.before,2);var M=!{toString:null}.propertyIsEnumerable("toString"),I=["valueOf","isPrototypeOf","toString","propertyIsEnumerable","hasOwnProperty","toLocaleString"];m.keys=function(n){if(!m.isObject(n))return[];if(v)return v(n);var t=[];for(var r in n)m.has(n,r)&&t.push(r);return M&&e(n,t),t},m.allKeys=function(n){if(!m.isObject(n))return[];var t=[];for(var r in n)t.push(r);return M&&e(n,t),t},m.values=function(n){for(var t=m.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=n[t[u]];return e},m.mapObject=function(n,t,r){t=x(t,r);for(var e,u=m.keys(n),i=u.length,o={},a=0;i>a;a++)e=u[a],o[e]=t(n[e],e,n);return o},m.pairs=function(n){for(var t=m.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=[t[u],n[t[u]]];return e},m.invert=function(n){for(var t={},r=m.keys(n),e=0,u=r.length;u>e;e++)t[n[r[e]]]=r[e];return t},m.functions=m.methods=function(n){var t=[];for(var r in n)m.isFunction(n[r])&&t.push(r);return t.sort()},m.extend=_(m.allKeys),m.extendOwn=m.assign=_(m.keys),m.findKey=function(n,t,r){t=x(t,r);for(var e,u=m.keys(n),i=0,o=u.length;o>i;i++)if(e=u[i],t(n[e],e,n))return e},m.pick=function(n,t,r){var e,u,i={},o=n;if(null==o)return i;m.isFunction(t)?(u=m.allKeys(o),e=b(t,r)):(u=S(arguments,!1,!1,1),e=function(n,t,r){return t in r},o=Object(o));for(var a=0,c=u.length;c>a;a++){var f=u[a],l=o[f];e(l,f,o)&&(i[f]=l)}return i},m.omit=function(n,t,r){if(m.isFunction(t))t=m.negate(t);else{var e=m.map(S(arguments,!1,!1,1),String);t=function(n,t){return!m.contains(e,t)}}return m.pick(n,t,r)},m.defaults=_(m.allKeys,!0),m.create=function(n,t){var r=j(n);return t&&m.extendOwn(r,t),r},m.clone=function(n){return m.isObject(n)?m.isArray(n)?n.slice():m.extend({},n):n},m.tap=function(n,t){return t(n),n},m.isMatch=function(n,t){var r=m.keys(t),e=r.length;if(null==n)return!e;for(var u=Object(n),i=0;e>i;i++){var o=r[i];if(t[o]!==u[o]||!(o in u))return!1}return!0};var N=function(n,t,r,e){if(n===t)return 0!==n||1/n===1/t;if(null==n||null==t)return n===t;n instanceof m&&(n=n._wrapped),t instanceof m&&(t=t._wrapped);var u=s.call(n);if(u!==s.call(t))return!1;switch(u){case"[object RegExp]":case"[object String]":return""+n==""+t;case"[object Number]":return+n!==+n?+t!==+t:0===+n?1/+n===1/t:+n===+t;case"[object Date]":case"[object Boolean]":return+n===+t}var i="[object Array]"===u;if(!i){if("object"!=typeof n||"object"!=typeof t)return!1;var o=n.constructor,a=t.constructor;if(o!==a&&!(m.isFunction(o)&&o instanceof o&&m.isFunction(a)&&a instanceof a)&&"constructor"in n&&"constructor"in t)return!1}r=r||[],e=e||[];for(var c=r.length;c--;)if(r[c]===n)return e[c]===t;if(r.push(n),e.push(t),i){if(c=n.length,c!==t.length)return!1;for(;c--;)if(!N(n[c],t[c],r,e))return!1}else{var f,l=m.keys(n);if(c=l.length,m.keys(t).length!==c)return!1;for(;c--;)if(f=l[c],!m.has(t,f)||!N(n[f],t[f],r,e))return!1}return r.pop(),e.pop(),!0};m.isEqual=function(n,t){return N(n,t)},m.isEmpty=function(n){return null==n?!0:k(n)&&(m.isArray(n)||m.isString(n)||m.isArguments(n))?0===n.length:0===m.keys(n).length},m.isElement=function(n){return!(!n||1!==n.nodeType)},m.isArray=h||function(n){return"[object Array]"===s.call(n)},m.isObject=function(n){var t=typeof n;return"function"===t||"object"===t&&!!n},m.each(["Arguments","Function","String","Number","Date","RegExp","Error"],function(n){m["is"+n]=function(t){return s.call(t)==="[object "+n+"]"}}),m.isArguments(arguments)||(m.isArguments=function(n){return m.has(n,"callee")}),"function"!=typeof/./&&"object"!=typeof Int8Array&&(m.isFunction=function(n){return"function"==typeof n||!1}),m.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},m.isNaN=function(n){return m.isNumber(n)&&n!==+n},m.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"===s.call(n)},m.isNull=function(n){return null===n},m.isUndefined=function(n){return n===void 0},m.has=function(n,t){return null!=n&&p.call(n,t)},m.noConflict=function(){return u._=i,this},m.identity=function(n){return n},m.constant=function(n){return function(){return n}},m.noop=function(){},m.property=w,m.propertyOf=function(n){return null==n?function(){}:function(t){return n[t]}},m.matcher=m.matches=function(n){return n=m.extendOwn({},n),function(t){return m.isMatch(t,n)}},m.times=function(n,t,r){var e=Array(Math.max(0,n));t=b(t,r,1);for(var u=0;n>u;u++)e[u]=t(u);return e},m.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))},m.now=Date.now||function(){return(new Date).getTime()};var B={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`"},T=m.invert(B),R=function(n){var t=function(t){return n[t]},r="(?:"+m.keys(n).join("|")+")",e=RegExp(r),u=RegExp(r,"g");return function(n){return n=null==n?"":""+n,e.test(n)?n.replace(u,t):n}};m.escape=R(B),m.unescape=R(T),m.result=function(n,t,r){var e=null==n?void 0:n[t];return e===void 0&&(e=r),m.isFunction(e)?e.call(n):e};var q=0;m.uniqueId=function(n){var t=++q+"";return n?n+t:t},m.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var K=/(.)^/,z={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},D=/\\|'|\r|\n|\u2028|\u2029/g,L=function(n){return"\\"+z[n]};m.template=function(n,t,r){!t&&r&&(t=r),t=m.defaults({},t,m.templateSettings);var e=RegExp([(t.escape||K).source,(t.interpolate||K).source,(t.evaluate||K).source].join("|")+"|$","g"),u=0,i="__p+='";n.replace(e,function(t,r,e,o,a){return i+=n.slice(u,a).replace(D,L),u=a+t.length,r?i+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'":e?i+="'+\n((__t=("+e+"))==null?'':__t)+\n'":o&&(i+="';\n"+o+"\n__p+='"),t}),i+="';\n",t.variable||(i="with(obj||{}){\n"+i+"}\n"),i="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+i+"return __p;\n";try{var o=new Function(t.variable||"obj","_",i)}catch(a){throw a.source=i,a}var c=function(n){return o.call(this,n,m)},f=t.variable||"obj";return c.source="function("+f+"){\n"+i+"}",c},m.chain=function(n){var t=m(n);return t._chain=!0,t};var P=function(n,t){return n._chain?m(t).chain():t};m.mixin=function(n){m.each(m.functions(n),function(t){var r=m[t]=n[t];m.prototype[t]=function(){var n=[this._wrapped];return f.apply(n,arguments),P(this,r.apply(m,n))}})},m.mixin(m),m.each(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=o[n];m.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!==n&&"splice"!==n||0!==r.length||delete r[0],P(this,r)}}),m.each(["concat","join","slice"],function(n){var t=o[n];m.prototype[n]=function(){return P(this,t.apply(this._wrapped,arguments))}}),m.prototype.value=function(){return this._wrapped},m.prototype.valueOf=m.prototype.toJSON=m.prototype.value,m.prototype.toString=function(){return""+this._wrapped},"function"==typeof define&&define.amd&&define("underscore",[],function(){return m})}).call(this); 6 | //# sourceMappingURL=underscore-min.map -------------------------------------------------------------------------------- /assets/themes/black.css: -------------------------------------------------------------------------------- 1 | body {} 2 | 3 | td>a { 4 | color: black; 5 | } 6 | 7 | .navbar { 8 | border-radius: 0px; 9 | background-color: #101010; 10 | border-color: #101010; 11 | } 12 | 13 | .navbar .navbar-brand { 14 | color: #9d9d9d; 15 | } 16 | 17 | .navbar .navbar-brand:hover { 18 | color: #fff; 19 | } 20 | 21 | .navbar-default ul.navbar-nav>li>a:hover { 22 | color: #fff; 23 | } 24 | -------------------------------------------------------------------------------- /assets/themes/cyan.css: -------------------------------------------------------------------------------- 1 | body {} 2 | 3 | td>a:hover { 4 | color: #4cc0cf; 5 | font-weight: normal; 6 | } 7 | 8 | td>a { 9 | color: rgb(51, 51, 51); 10 | } 11 | 12 | a:hover { 13 | font-weight: bold; 14 | } 15 | 16 | .navbar { 17 | background-color: #00BCD4; 18 | border-color: #0097A7; 19 | } 20 | 21 | .navbar .navbar-brand { 22 | color: white; 23 | } 24 | 25 | .navbar { 26 | border-radius: 0px; 27 | } 28 | 29 | .navbar-default ul.navbar-nav>li>a { 30 | color: white; 31 | } 32 | -------------------------------------------------------------------------------- /assets/themes/green.css: -------------------------------------------------------------------------------- 1 | body {} 2 | 3 | td>a:hover { 4 | color: #1fa67a; 5 | font-weight: normal; 6 | } 7 | 8 | td>a { 9 | color: rgb(51, 51, 51); 10 | } 11 | 12 | a:hover { 13 | font-weight: bold; 14 | } 15 | 16 | .navbar { 17 | background-color: #1b926c; 18 | border-color: #1fa67a; 19 | } 20 | 21 | .navbar .navbar-brand { 22 | color: white; 23 | } 24 | 25 | .navbar { 26 | border-radius: 0px; 27 | } 28 | 29 | .navbar-default ul.navbar-nav>li>a { 30 | color: white; 31 | } 32 | -------------------------------------------------------------------------------- /assets/video-player.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Video Player - [[.FileName]] 7 | 33 | 34 | 35 |
36 | 40 |
41 | 42 | 48 | 49 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash - 2 | 3 | set -eu 4 | 5 | VERSION=$(git describe --abbrev=0 --tags) 6 | REVCNT=$(git rev-list --count HEAD) 7 | DEVCNT=$(git rev-list --count $VERSION) 8 | if test $REVCNT != $DEVCNT 9 | then 10 | VERSION="$VERSION.dev$(expr $REVCNT - $DEVCNT)" 11 | fi 12 | echo "VER: $VERSION" 13 | 14 | GITCOMMIT=$(git rev-parse HEAD) 15 | BUILDTIME=$(date -u +%Y/%m/%d-%H:%M:%S) 16 | 17 | LDFLAGS="-X main.VERSION=$VERSION -X main.BUILDTIME=$BUILDTIME -X main.GITCOMMIT=$GITCOMMIT" 18 | if [[ -n "${EX_LDFLAGS:-""}" ]] 19 | then 20 | LDFLAGS="$LDFLAGS $EX_LDFLAGS" 21 | fi 22 | 23 | build() { 24 | echo "$1 $2 ..." 25 | GOOS=$1 GOARCH=$2 go build \ 26 | -ldflags "$LDFLAGS" \ 27 | -o dist/gohttpserver-${3:-""} 28 | } 29 | 30 | build linux arm linux-arm 31 | build darwin amd64 mac-amd64 32 | build linux amd64 linux-amd64 33 | build linux 386 linux-386 34 | build windows amd64 win-amd64.exe 35 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.16 2 | WORKDIR /app/gohttpserver 3 | ADD . /app/gohttpserver 4 | RUN CGO_ENABLED=0 GOOS=linux go build -ldflags '-X main.VERSION=docker' -o gohttpserver 5 | 6 | FROM debian:stable 7 | WORKDIR /app 8 | RUN mkdir -p /app/public 9 | RUN apt-get update && apt-get install -y ca-certificates 10 | VOLUME /app/public 11 | ADD assets ./assets 12 | COPY --from=0 /app/gohttpserver/gohttpserver . 13 | EXPOSE 8000 14 | ENTRYPOINT [ "/app/gohttpserver", "--root=/app/public" ] 15 | CMD [] 16 | -------------------------------------------------------------------------------- /docker/Dockerfile.alpine: -------------------------------------------------------------------------------- 1 | FROM golang:1.16.0-alpine as build 2 | 3 | WORKDIR /app 4 | COPY go.mod . 5 | COPY go.sum . 6 | RUN go mod download 7 | # Binary Compression 8 | RUN apk add upx --no-cache 9 | 10 | # Build app and pack using upx 11 | ADD . /app 12 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags '-s -w -X main.VERSION=docker' -o gohttpserver && upx gohttpserver 13 | 14 | FROM alpine:3 15 | WORKDIR /app 16 | ADD assets /usr/local/bin/assets 17 | COPY --from=build /app/gohttpserver /usr/local/bin/gohttpserver 18 | EXPOSE 8000 19 | ENTRYPOINT [ "/usr/local/bin/gohttpserver" ] 20 | -------------------------------------------------------------------------------- /docker/Dockerfile.armhf: -------------------------------------------------------------------------------- 1 | FROM golang:1.16 2 | WORKDIR /appsrc/gohttpserver 3 | ADD . /appsrc/gohttpserver 4 | RUN GOOS=linux GOARCH=arm go build -ldflags '-X main.VERSION=docker' -o gohttpserver . 5 | 6 | FROM multiarch/debian-debootstrap:armhf-stretch 7 | WORKDIR /app 8 | RUN mkdir -p /app/public 9 | RUN apt-get update && apt-get install -y ca-certificates 10 | VOLUME /app/public 11 | ADD assets ./assets 12 | COPY --from=0 /appsrc/gohttpserver/gohttpserver . 13 | EXPOSE 8000 14 | ENTRYPOINT [ "/app/gohttpserver", "--root=/app/public" ] 15 | CMD [] 16 | -------------------------------------------------------------------------------- /docker/push_images: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # article: https://lantian.pub/article/modify-computer/build-arm-docker-image-on-x86-docker-hub-travis-automatic-build.lantian 4 | 5 | set -ex 6 | 7 | docker run --rm --privileged multiarch/qemu-user-static:register --reset 8 | echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin 9 | 10 | IMAGE_NAME="gohttpserver" 11 | 12 | # arm linux for respberry 13 | docker build -t $DOCKER_USERNAME/$IMAGE_NAME:armhf -f docker/Dockerfile.armhf . 14 | 15 | # x86 linux 16 | docker build -t $DOCKER_USERNAME/$IMAGE_NAME:latest -f docker/Dockerfile . 17 | 18 | docker push $DOCKER_USERNAME/$IMAGE_NAME -------------------------------------------------------------------------------- /docker/push_manifest: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | 4 | # push manifest 5 | if [[ ! -d $HOME/.docker ]] 6 | then 7 | mkdir $HOME/.docker 8 | fi 9 | 10 | set -ex 11 | 12 | if test $(uname) = "Linux" 13 | then 14 | sed -i '/experimental/d' $HOME/.docker/config.json 15 | sed -i '1a"experimental": "enabled",' $HOME/.docker/config.json 16 | fi 17 | 18 | docker manifest create codeskyblue/gohttpserver \ 19 | codeskyblue/gohttpserver:latest \ 20 | codeskyblue/gohttpserver:armhf 21 | docker manifest annotate codeskyblue/gohttpserver \ 22 | codeskyblue/gohttpserver:latest --os linux --arch amd64 23 | docker manifest annotate codeskyblue/gohttpserver \ 24 | codeskyblue/gohttpserver:armhf --os linux --arch arm --variant v7 25 | docker manifest push codeskyblue/gohttpserver 26 | 27 | # check again 28 | docker run mplatform/mquery codeskyblue/gohttpserver 29 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/codeskyblue/gohttpserver 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/alecthomas/kingpin v2.2.6+incompatible 7 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc // indirect 8 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf // indirect 9 | github.com/codeskyblue/dockerignore v0.0.0-20151214070507-de82dee623d9 10 | github.com/codeskyblue/go-accesslog v0.0.0-20171215023101-6188d3bd9371 11 | github.com/codeskyblue/openid-go v0.0.0-20160923065855-0d30842b2fb4 12 | github.com/fork2fix/go-plist v0.0.0-20181126021357-36960be5e636 13 | github.com/go-yaml/yaml v2.1.0+incompatible 14 | github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d 15 | github.com/gorilla/handlers v1.4.0 16 | github.com/gorilla/mux v1.6.2 17 | github.com/gorilla/sessions v1.1.3 18 | github.com/pkg/errors v0.8.0 // indirect 19 | github.com/shogo82148/androidbinary v0.0.0-20180627093851-01c4bfa8b3b5 20 | github.com/smartystreets/goconvey v1.6.4 // indirect 21 | github.com/stretchr/testify v1.3.0 22 | golang.org/x/net v0.23.0 // indirect 23 | golang.org/x/text v0.14.0 24 | howett.net/plist v0.0.0-20201203080718-1454fab16a06 // indirect 25 | ) 26 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alecthomas/kingpin v2.2.6+incompatible h1:5svnBTFgJjZvGKyYBtMB0+m5wvrbUHiqye8wRJMlnYI= 2 | github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE= 3 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= 4 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 5 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= 6 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 7 | github.com/codeskyblue/dockerignore v0.0.0-20151214070507-de82dee623d9 h1:c9axcChJwkLuSl9AvwTHi8jiBa6+VX4gGgERhABgv2E= 8 | github.com/codeskyblue/dockerignore v0.0.0-20151214070507-de82dee623d9/go.mod h1:XNZkUhPf+qgRnhY/ecS3B73ODJ2NXCzDMJHXM069IMg= 9 | github.com/codeskyblue/go-accesslog v0.0.0-20171215023101-6188d3bd9371 h1:dEBIvaVFaP2Uc9QA6J41qWxE5NfEnDWEBk+kWv5nK5k= 10 | github.com/codeskyblue/go-accesslog v0.0.0-20171215023101-6188d3bd9371/go.mod h1:sgXnVxxZ1u72GAzc9s1SzpuPMxBDKfTg6F2PvDrPSJU= 11 | github.com/codeskyblue/openid-go v0.0.0-20160923065855-0d30842b2fb4 h1:66lzN78lwccK+BPztRgBiWCYzhlerQEVOh2oeBksu5I= 12 | github.com/codeskyblue/openid-go v0.0.0-20160923065855-0d30842b2fb4/go.mod h1:K/hSCtAHvnE9aM+LsYgVmgzPNFuWFdx6i9t6/3jNrZQ= 13 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 14 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/fork2fix/go-plist v0.0.0-20181126021357-36960be5e636 h1:ESUdS2eb8LyDQfboYyFBwAL+rqYhnTZ15ntw8BLsd9g= 16 | github.com/fork2fix/go-plist v0.0.0-20181126021357-36960be5e636/go.mod h1:v6KRhgoO1QKamoeuZ7yHqZIP8p6j9k41Tb0jCyOEmr4= 17 | github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o= 18 | github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= 19 | github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d h1:lBXNCxVENCipq4D1Is42JVOP4eQjlB8TQ6H69Yx5J9Q= 20 | github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= 21 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 22 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 23 | github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= 24 | github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= 25 | github.com/gorilla/handlers v1.4.0 h1:XulKRWSQK5uChr4pEgSE4Tc/OcmnU9GJuSwdog/tZsA= 26 | github.com/gorilla/handlers v1.4.0/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= 27 | github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk= 28 | github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 29 | github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= 30 | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= 31 | github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU= 32 | github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= 33 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 34 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 35 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 36 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 37 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 38 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 39 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 40 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 41 | github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= 42 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 43 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 44 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 45 | github.com/shogo82148/androidbinary v0.0.0-20180627093851-01c4bfa8b3b5 h1:bXRaUWl3Afe3F9YR5NU1U3UB5zjCHlu4im5p3J/LUYk= 46 | github.com/shogo82148/androidbinary v0.0.0-20180627093851-01c4bfa8b3b5/go.mod h1:05AjXWPWLdTIl9+REKhSmTeoJ6Wz5e9ir0Q0NRxCIKo= 47 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= 48 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 49 | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= 50 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 51 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 52 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 53 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 54 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 55 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 56 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 57 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 58 | golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= 59 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 60 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 61 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 62 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 63 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 64 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 65 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 66 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 67 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 68 | golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= 69 | golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= 70 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 71 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 72 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 73 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 74 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 75 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 76 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 77 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 78 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 79 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 80 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 81 | golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 82 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 83 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 84 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 85 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 86 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 87 | golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= 88 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 89 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 90 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 91 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 92 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 93 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 94 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 95 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 96 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 97 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 98 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 99 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 100 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 101 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 102 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 103 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 104 | gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= 105 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 106 | howett.net/plist v0.0.0-20201203080718-1454fab16a06 h1:QDxUo/w2COstK1wIBYpzQlHX/NqaQTcf9jyz347nI58= 107 | howett.net/plist v0.0.0-20201203080718-1454fab16a06/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= 108 | -------------------------------------------------------------------------------- /httpstaticserver.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "html/template" 9 | "io" 10 | "io/ioutil" 11 | "log" 12 | "mime" 13 | "net/http" 14 | "net/url" 15 | "os" 16 | "path/filepath" 17 | "strconv" 18 | "strings" 19 | "sync" 20 | "time" 21 | 22 | "regexp" 23 | 24 | "github.com/go-yaml/yaml" 25 | "github.com/gorilla/mux" 26 | "github.com/shogo82148/androidbinary/apk" 27 | ) 28 | 29 | const YAMLCONF = ".ghs.yml" 30 | 31 | type ApkInfo struct { 32 | PackageName string `json:"packageName"` 33 | MainActivity string `json:"mainActivity"` 34 | Version struct { 35 | Code int `json:"code"` 36 | Name string `json:"name"` 37 | } `json:"version"` 38 | } 39 | 40 | type IndexFileItem struct { 41 | Path string 42 | Info os.FileInfo 43 | } 44 | 45 | type Directory struct { 46 | size map[string]int64 47 | mutex *sync.RWMutex 48 | } 49 | 50 | type HTTPStaticServer struct { 51 | Root string 52 | Prefix string 53 | Upload bool 54 | Delete bool 55 | Title string 56 | Theme string 57 | PlistProxy string 58 | GoogleTrackerID string 59 | AuthType string 60 | DeepPathMaxDepth int 61 | NoIndex bool 62 | 63 | indexes []IndexFileItem 64 | m *mux.Router 65 | bufPool sync.Pool // use sync.Pool caching buf to reduce gc ratio 66 | } 67 | 68 | func NewHTTPStaticServer(root string, noIndex bool) *HTTPStaticServer { 69 | // if root == "" { 70 | // root = "./" 71 | // } 72 | // root = filepath.ToSlash(root) 73 | root = filepath.ToSlash(filepath.Clean(root)) 74 | if !strings.HasSuffix(root, "/") { 75 | root = root + "/" 76 | } 77 | log.Printf("root path: %s\n", root) 78 | m := mux.NewRouter() 79 | s := &HTTPStaticServer{ 80 | Root: root, 81 | Theme: "black", 82 | m: m, 83 | bufPool: sync.Pool{ 84 | New: func() interface{} { return make([]byte, 32*1024) }, 85 | }, 86 | NoIndex: noIndex, 87 | } 88 | 89 | if !noIndex { 90 | go func() { 91 | time.Sleep(1 * time.Second) 92 | for { 93 | startTime := time.Now() 94 | log.Println("Started making search index") 95 | s.makeIndex() 96 | log.Printf("Completed search index in %v", time.Since(startTime)) 97 | //time.Sleep(time.Second * 1) 98 | time.Sleep(time.Minute * 10) 99 | } 100 | }() 101 | } 102 | 103 | // routers for Apple *.ipa 104 | m.HandleFunc("/-/ipa/plist/{path:.*}", s.hPlist) 105 | m.HandleFunc("/-/ipa/link/{path:.*}", s.hIpaLink) 106 | m.HandleFunc("/-/video-player/{path:.*}", s.hVideoPlayer) 107 | 108 | m.HandleFunc("/{path:.*}", s.hIndex).Methods("GET", "HEAD") 109 | m.HandleFunc("/{path:.*}", s.hUploadOrMkdir).Methods("POST") 110 | m.HandleFunc("/{path:.*}", s.hDelete).Methods("DELETE") 111 | return s 112 | } 113 | 114 | func (s *HTTPStaticServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { 115 | s.m.ServeHTTP(w, r) 116 | } 117 | 118 | // Return real path with Seperator(/) 119 | func (s *HTTPStaticServer) getRealPath(r *http.Request) string { 120 | path := mux.Vars(r)["path"] 121 | if !strings.HasPrefix(path, "/") { 122 | path = "/" + path 123 | } 124 | path = filepath.Clean(path) // prevent .. for safe issues 125 | relativePath, err := filepath.Rel(s.Prefix, path) 126 | if err != nil { 127 | relativePath = path 128 | } 129 | realPath := filepath.Join(s.Root, relativePath) 130 | return filepath.ToSlash(realPath) 131 | } 132 | 133 | func (s *HTTPStaticServer) hIndex(w http.ResponseWriter, r *http.Request) { 134 | path := mux.Vars(r)["path"] 135 | realPath := s.getRealPath(r) 136 | if r.FormValue("json") == "true" { 137 | s.hJSONList(w, r) 138 | return 139 | } 140 | 141 | if r.FormValue("op") == "info" { 142 | s.hInfo(w, r) 143 | return 144 | } 145 | 146 | if r.FormValue("op") == "archive" { 147 | s.hZip(w, r) 148 | return 149 | } 150 | 151 | log.Println("GET", path, realPath) 152 | if r.FormValue("raw") == "false" || isDir(realPath) { 153 | if r.Method == "HEAD" { 154 | return 155 | } 156 | renderHTML(w, "assets/index.html", s) 157 | } else { 158 | if filepath.Base(path) == YAMLCONF { 159 | auth := s.readAccessConf(realPath) 160 | if !auth.Delete { 161 | http.Error(w, "Security warning, not allowed to read", http.StatusForbidden) 162 | return 163 | } 164 | } 165 | if r.FormValue("download") == "true" { 166 | w.Header().Set("Content-Disposition", "attachment; filename="+strconv.Quote(filepath.Base(path))) 167 | } 168 | http.ServeFile(w, r, realPath) 169 | } 170 | } 171 | 172 | func (s *HTTPStaticServer) hDelete(w http.ResponseWriter, req *http.Request) { 173 | path := mux.Vars(req)["path"] 174 | realPath := s.getRealPath(req) 175 | // path = filepath.Clean(path) // for safe reason, prevent path contain .. 176 | auth := s.readAccessConf(realPath) 177 | if !auth.canDelete(req) { 178 | http.Error(w, "Delete forbidden", http.StatusForbidden) 179 | return 180 | } 181 | 182 | // TODO: path safe check 183 | err := os.RemoveAll(realPath) 184 | if err != nil { 185 | pathErr, ok := err.(*os.PathError) 186 | if ok { 187 | http.Error(w, pathErr.Op+" "+path+": "+pathErr.Err.Error(), 500) 188 | } else { 189 | http.Error(w, err.Error(), 500) 190 | } 191 | return 192 | } 193 | w.Write([]byte("Success")) 194 | } 195 | 196 | func (s *HTTPStaticServer) hUploadOrMkdir(w http.ResponseWriter, req *http.Request) { 197 | dirpath := s.getRealPath(req) 198 | 199 | // check auth 200 | auth := s.readAccessConf(dirpath) 201 | if !auth.canUpload(req) { 202 | http.Error(w, "Upload forbidden", http.StatusForbidden) 203 | return 204 | } 205 | 206 | file, header, err := req.FormFile("file") 207 | 208 | if _, err := os.Stat(dirpath); os.IsNotExist(err) { 209 | if err := os.MkdirAll(dirpath, os.ModePerm); err != nil { 210 | log.Println("Create directory:", err) 211 | http.Error(w, "Directory create "+err.Error(), http.StatusInternalServerError) 212 | return 213 | } 214 | } 215 | 216 | if file == nil { // only mkdir 217 | w.Header().Set("Content-Type", "application/json;charset=utf-8") 218 | json.NewEncoder(w).Encode(map[string]interface{}{ 219 | "success": true, 220 | "destination": dirpath, 221 | }) 222 | return 223 | } 224 | 225 | if err != nil { 226 | log.Println("Parse form file:", err) 227 | http.Error(w, err.Error(), http.StatusInternalServerError) 228 | return 229 | } 230 | defer func() { 231 | file.Close() 232 | req.MultipartForm.RemoveAll() // Seen from go source code, req.MultipartForm not nil after call FormFile(..) 233 | }() 234 | 235 | filename := req.FormValue("filename") 236 | if filename == "" { 237 | filename = header.Filename 238 | } 239 | if err := checkFilename(filename); err != nil { 240 | http.Error(w, err.Error(), http.StatusForbidden) 241 | return 242 | } 243 | 244 | dstPath := filepath.Join(dirpath, filename) 245 | 246 | // Large file (>32MB) will store in tmp directory 247 | // The quickest operation is call os.Move instead of os.Copy 248 | // Note: it seems not working well, os.Rename might be failed 249 | 250 | var copyErr error 251 | // if osFile, ok := file.(*os.File); ok && fileExists(osFile.Name()) { 252 | // tmpUploadPath := osFile.Name() 253 | // osFile.Close() // Windows can not rename opened file 254 | // log.Printf("Move %s -> %s", tmpUploadPath, dstPath) 255 | // copyErr = os.Rename(tmpUploadPath, dstPath) 256 | // } else { 257 | dst, err := os.Create(dstPath) 258 | if err != nil { 259 | log.Println("Create file:", err) 260 | http.Error(w, "File create "+err.Error(), http.StatusInternalServerError) 261 | return 262 | } 263 | 264 | // Note: very large size file might cause poor performance 265 | // _, copyErr = io.Copy(dst, file) 266 | buf := s.bufPool.Get().([]byte) 267 | defer s.bufPool.Put(buf) 268 | _, copyErr = io.CopyBuffer(dst, file, buf) 269 | dst.Close() 270 | // } 271 | if copyErr != nil { 272 | log.Println("Handle upload file:", err) 273 | http.Error(w, err.Error(), http.StatusInternalServerError) 274 | return 275 | } 276 | 277 | w.Header().Set("Content-Type", "application/json;charset=utf-8") 278 | 279 | if req.FormValue("unzip") == "true" { 280 | err = unzipFile(dstPath, dirpath) 281 | os.Remove(dstPath) 282 | message := "success" 283 | if err != nil { 284 | message = err.Error() 285 | } 286 | json.NewEncoder(w).Encode(map[string]interface{}{ 287 | "success": err == nil, 288 | "description": message, 289 | }) 290 | return 291 | } 292 | 293 | json.NewEncoder(w).Encode(map[string]interface{}{ 294 | "success": true, 295 | "destination": dstPath, 296 | }) 297 | } 298 | 299 | type FileJSONInfo struct { 300 | Name string `json:"name"` 301 | Type string `json:"type"` 302 | Size int64 `json:"size"` 303 | Path string `json:"path"` 304 | ModTime int64 `json:"mtime"` 305 | Extra interface{} `json:"extra,omitempty"` 306 | } 307 | 308 | // path should be absolute 309 | func parseApkInfo(path string) (ai *ApkInfo) { 310 | defer func() { 311 | if err := recover(); err != nil { 312 | log.Println("parse-apk-info panic:", err) 313 | } 314 | }() 315 | apkf, err := apk.OpenFile(path) 316 | if err != nil { 317 | return 318 | } 319 | ai = &ApkInfo{} 320 | ai.MainActivity, _ = apkf.MainActivity() 321 | ai.PackageName = apkf.PackageName() 322 | ai.Version.Code = apkf.Manifest().VersionCode 323 | ai.Version.Name = apkf.Manifest().VersionName 324 | return 325 | } 326 | 327 | func (s *HTTPStaticServer) hInfo(w http.ResponseWriter, r *http.Request) { 328 | path := mux.Vars(r)["path"] 329 | relPath := s.getRealPath(r) 330 | 331 | fi, err := os.Stat(relPath) 332 | if err != nil { 333 | http.Error(w, err.Error(), 500) 334 | return 335 | } 336 | fji := &FileJSONInfo{ 337 | Name: fi.Name(), 338 | Size: fi.Size(), 339 | Path: path, 340 | ModTime: fi.ModTime().UnixNano() / 1e6, 341 | } 342 | ext := filepath.Ext(path) 343 | switch ext { 344 | case ".md": 345 | fji.Type = "markdown" 346 | case ".apk": 347 | fji.Type = "apk" 348 | fji.Extra = parseApkInfo(relPath) 349 | case "": 350 | fji.Type = "dir" 351 | default: 352 | fji.Type = "text" 353 | } 354 | data, _ := json.Marshal(fji) 355 | w.Header().Set("Content-Type", "application/json") 356 | w.Write(data) 357 | } 358 | 359 | func (s *HTTPStaticServer) hZip(w http.ResponseWriter, r *http.Request) { 360 | CompressToZip(w, s.getRealPath(r)) 361 | } 362 | 363 | func (s *HTTPStaticServer) hUnzip(w http.ResponseWriter, r *http.Request) { 364 | vars := mux.Vars(r) 365 | zipPath, path := vars["zip_path"], vars["path"] 366 | ctype := mime.TypeByExtension(filepath.Ext(path)) 367 | if ctype != "" { 368 | w.Header().Set("Content-Type", ctype) 369 | } 370 | err := ExtractFromZip(filepath.Join(s.Root, zipPath), path, w) 371 | if err != nil { 372 | http.Error(w, err.Error(), 500) 373 | return 374 | } 375 | } 376 | 377 | func combineURL(r *http.Request, path string) *url.URL { 378 | return &url.URL{ 379 | Scheme: r.URL.Scheme, 380 | Host: r.Host, 381 | Path: path, 382 | } 383 | } 384 | 385 | func (s *HTTPStaticServer) hPlist(w http.ResponseWriter, r *http.Request) { 386 | path := mux.Vars(r)["path"] 387 | // rename *.plist to *.ipa 388 | if filepath.Ext(path) == ".plist" { 389 | path = path[0:len(path)-6] + ".ipa" 390 | } 391 | 392 | relPath := s.getRealPath(r) 393 | plinfo, err := parseIPA(relPath) 394 | if err != nil { 395 | http.Error(w, err.Error(), 500) 396 | return 397 | } 398 | 399 | scheme := "http" 400 | if r.TLS != nil { 401 | scheme = "https" 402 | } 403 | baseURL := &url.URL{ 404 | Scheme: scheme, 405 | Host: r.Host, 406 | } 407 | data, err := generateDownloadPlist(baseURL, path, plinfo) 408 | if err != nil { 409 | http.Error(w, err.Error(), 500) 410 | return 411 | } 412 | w.Header().Set("Content-Type", "text/xml") 413 | w.Write(data) 414 | } 415 | 416 | func (s *HTTPStaticServer) hIpaLink(w http.ResponseWriter, r *http.Request) { 417 | path := mux.Vars(r)["path"] 418 | var plistUrl string 419 | 420 | if r.URL.Scheme == "https" { 421 | plistUrl = combineURL(r, "/-/ipa/plist/"+path).String() 422 | } else if s.PlistProxy != "" { 423 | httpPlistLink := "http://" + r.Host + "/-/ipa/plist/" + path 424 | url, err := s.genPlistLink(httpPlistLink) 425 | if err != nil { 426 | http.Error(w, err.Error(), 500) 427 | return 428 | } 429 | plistUrl = url 430 | } else { 431 | http.Error(w, "500: Server should be https:// or provide valid plistproxy", 500) 432 | return 433 | } 434 | 435 | w.Header().Set("Content-Type", "text/html") 436 | log.Println("PlistURL:", plistUrl) 437 | renderHTML(w, "assets/ipa-install.html", map[string]string{ 438 | "Name": filepath.Base(path), 439 | "PlistLink": plistUrl, 440 | }) 441 | } 442 | 443 | func (s *HTTPStaticServer) genPlistLink(httpPlistLink string) (plistUrl string, err error) { 444 | // Maybe need a proxy, a little slowly now. 445 | pp := s.PlistProxy 446 | if pp == "" { 447 | pp = defaultPlistProxy 448 | } 449 | resp, err := http.Get(httpPlistLink) 450 | if err != nil { 451 | return 452 | } 453 | defer resp.Body.Close() 454 | 455 | data, _ := ioutil.ReadAll(resp.Body) 456 | retData, err := http.Post(pp, "text/xml", bytes.NewBuffer(data)) 457 | if err != nil { 458 | return 459 | } 460 | defer retData.Body.Close() 461 | 462 | jsonData, _ := ioutil.ReadAll(retData.Body) 463 | var ret map[string]string 464 | if err = json.Unmarshal(jsonData, &ret); err != nil { 465 | return 466 | } 467 | plistUrl = pp + "/" + ret["key"] 468 | return 469 | } 470 | 471 | func (s *HTTPStaticServer) hFileOrDirectory(w http.ResponseWriter, r *http.Request) { 472 | http.ServeFile(w, r, s.getRealPath(r)) 473 | } 474 | 475 | type HTTPFileInfo struct { 476 | Name string `json:"name"` 477 | Path string `json:"path"` 478 | Type string `json:"type"` 479 | Size int64 `json:"size"` 480 | ModTime int64 `json:"mtime"` 481 | } 482 | 483 | type AccessTable struct { 484 | Regex string `yaml:"regex"` 485 | Allow bool `yaml:"allow"` 486 | } 487 | 488 | type UserControl struct { 489 | Email string 490 | // Access bool 491 | Upload bool 492 | Delete bool 493 | Token string 494 | } 495 | 496 | type AccessConf struct { 497 | Upload bool `yaml:"upload" json:"upload"` 498 | Delete bool `yaml:"delete" json:"delete"` 499 | Users []UserControl `yaml:"users" json:"users"` 500 | AccessTables []AccessTable `yaml:"accessTables"` 501 | } 502 | 503 | var reCache = make(map[string]*regexp.Regexp) 504 | 505 | func (c *AccessConf) canAccess(fileName string) bool { 506 | for _, table := range c.AccessTables { 507 | pattern, ok := reCache[table.Regex] 508 | if !ok { 509 | pattern, _ = regexp.Compile(table.Regex) 510 | reCache[table.Regex] = pattern 511 | } 512 | // skip wrong format regex 513 | if pattern == nil { 514 | continue 515 | } 516 | if pattern.MatchString(fileName) { 517 | return table.Allow 518 | } 519 | } 520 | return true 521 | } 522 | 523 | func (c *AccessConf) canDelete(r *http.Request) bool { 524 | session, err := store.Get(r, defaultSessionName) 525 | if err != nil { 526 | return c.Delete 527 | } 528 | val := session.Values["user"] 529 | if val == nil { 530 | return c.Delete 531 | } 532 | userInfo := val.(*UserInfo) 533 | for _, rule := range c.Users { 534 | if rule.Email == userInfo.Email { 535 | return rule.Delete 536 | } 537 | } 538 | return c.Delete 539 | } 540 | 541 | func (c *AccessConf) canUploadByToken(token string) bool { 542 | for _, rule := range c.Users { 543 | if rule.Token == token { 544 | return rule.Upload 545 | } 546 | } 547 | return c.Upload 548 | } 549 | 550 | func (c *AccessConf) canUpload(r *http.Request) bool { 551 | token := r.FormValue("token") 552 | if token != "" { 553 | return c.canUploadByToken(token) 554 | } 555 | session, err := store.Get(r, defaultSessionName) 556 | if err != nil { 557 | return c.Upload 558 | } 559 | val := session.Values["user"] 560 | if val == nil { 561 | return c.Upload 562 | } 563 | userInfo := val.(*UserInfo) 564 | 565 | for _, rule := range c.Users { 566 | if rule.Email == userInfo.Email { 567 | return rule.Upload 568 | } 569 | } 570 | return c.Upload 571 | } 572 | 573 | func (s *HTTPStaticServer) hJSONList(w http.ResponseWriter, r *http.Request) { 574 | requestPath := mux.Vars(r)["path"] 575 | realPath := s.getRealPath(r) 576 | search := r.FormValue("search") 577 | auth := s.readAccessConf(realPath) 578 | auth.Upload = auth.canUpload(r) 579 | auth.Delete = auth.canDelete(r) 580 | maxDepth := s.DeepPathMaxDepth 581 | 582 | // path string -> info os.FileInfo 583 | fileInfoMap := make(map[string]os.FileInfo, 0) 584 | 585 | if search != "" { 586 | results := s.findIndex(search) 587 | if len(results) > 50 { // max 50 588 | results = results[:50] 589 | } 590 | for _, item := range results { 591 | if filepath.HasPrefix(item.Path, requestPath) { 592 | fileInfoMap[item.Path] = item.Info 593 | } 594 | } 595 | } else { 596 | infos, err := ioutil.ReadDir(realPath) 597 | if err != nil { 598 | http.Error(w, err.Error(), 500) 599 | return 600 | } 601 | for _, info := range infos { 602 | fileInfoMap[filepath.Join(requestPath, info.Name())] = info 603 | } 604 | } 605 | 606 | // turn file list -> json 607 | lrs := make([]HTTPFileInfo, 0) 608 | for path, info := range fileInfoMap { 609 | if !auth.canAccess(info.Name()) { 610 | continue 611 | } 612 | lr := HTTPFileInfo{ 613 | Name: info.Name(), 614 | Path: path, 615 | ModTime: info.ModTime().UnixNano() / 1e6, 616 | } 617 | if search != "" { 618 | name, err := filepath.Rel(requestPath, path) 619 | if err != nil { 620 | log.Println(requestPath, path, err) 621 | } 622 | lr.Name = filepath.ToSlash(name) // fix for windows 623 | } 624 | if info.IsDir() { 625 | name := deepPath(realPath, info.Name(), maxDepth) 626 | lr.Name = name 627 | lr.Path = filepath.Join(filepath.Dir(path), name) 628 | lr.Type = "dir" 629 | lr.Size = s.historyDirSize(lr.Path) 630 | } else { 631 | lr.Type = "file" 632 | lr.Size = info.Size() // formatSize(info) 633 | } 634 | lrs = append(lrs, lr) 635 | } 636 | 637 | data, _ := json.Marshal(map[string]interface{}{ 638 | "files": lrs, 639 | "auth": auth, 640 | }) 641 | w.Header().Set("Content-Type", "application/json") 642 | w.Write(data) 643 | } 644 | 645 | var dirInfoSize = Directory{size: make(map[string]int64), mutex: &sync.RWMutex{}} 646 | 647 | func (s *HTTPStaticServer) makeIndex() error { 648 | var indexes = make([]IndexFileItem, 0) 649 | var err = filepath.Walk(s.Root, func(path string, info os.FileInfo, err error) error { 650 | if err != nil { 651 | log.Printf("WARN: Visit path: %s error: %v", strconv.Quote(path), err) 652 | return filepath.SkipDir 653 | // return err 654 | } 655 | if info.IsDir() { 656 | return nil 657 | } 658 | 659 | path, _ = filepath.Rel(s.Root, path) 660 | path = filepath.ToSlash(path) 661 | indexes = append(indexes, IndexFileItem{path, info}) 662 | return nil 663 | }) 664 | s.indexes = indexes 665 | return err 666 | } 667 | 668 | func (s *HTTPStaticServer) historyDirSize(dir string) int64 { 669 | dirInfoSize.mutex.RLock() 670 | size, ok := dirInfoSize.size[dir] 671 | dirInfoSize.mutex.RUnlock() 672 | 673 | if ok { 674 | return size 675 | } 676 | 677 | for _, fitem := range s.indexes { 678 | if filepath.HasPrefix(fitem.Path, dir) { 679 | size += fitem.Info.Size() 680 | } 681 | } 682 | 683 | dirInfoSize.mutex.Lock() 684 | dirInfoSize.size[dir] = size 685 | dirInfoSize.mutex.Unlock() 686 | 687 | return size 688 | } 689 | 690 | func (s *HTTPStaticServer) findIndex(text string) []IndexFileItem { 691 | ret := make([]IndexFileItem, 0) 692 | for _, item := range s.indexes { 693 | ok := true 694 | // search algorithm, space for AND 695 | for _, keyword := range strings.Fields(text) { 696 | needContains := true 697 | if strings.HasPrefix(keyword, "-") { 698 | needContains = false 699 | keyword = keyword[1:] 700 | } 701 | if keyword == "" { 702 | continue 703 | } 704 | ok = (needContains == strings.Contains(strings.ToLower(item.Path), strings.ToLower(keyword))) 705 | if !ok { 706 | break 707 | } 708 | } 709 | if ok { 710 | ret = append(ret, item) 711 | } 712 | } 713 | return ret 714 | } 715 | 716 | func (s *HTTPStaticServer) defaultAccessConf() AccessConf { 717 | return AccessConf{ 718 | Upload: s.Upload, 719 | Delete: s.Delete, 720 | } 721 | } 722 | 723 | func (s *HTTPStaticServer) readAccessConf(realPath string) (ac AccessConf) { 724 | relativePath, err := filepath.Rel(s.Root, realPath) 725 | if err != nil || relativePath == "." || relativePath == "" { // actually relativePath is always "." if root == realPath 726 | ac = s.defaultAccessConf() 727 | realPath = s.Root 728 | } else { 729 | parentPath := filepath.Dir(realPath) 730 | ac = s.readAccessConf(parentPath) 731 | } 732 | if isFile(realPath) { 733 | realPath = filepath.Dir(realPath) 734 | } 735 | cfgFile := filepath.Join(realPath, YAMLCONF) 736 | data, err := ioutil.ReadFile(cfgFile) 737 | if err != nil { 738 | if os.IsNotExist(err) { 739 | return 740 | } 741 | log.Printf("Err read .ghs.yml: %v", err) 742 | } 743 | err = yaml.Unmarshal(data, &ac) 744 | if err != nil { 745 | log.Printf("Err format .ghs.yml: %v", err) 746 | } 747 | return 748 | } 749 | 750 | func deepPath(basedir, name string, maxDepth int) string { 751 | // loop max 5, incase of for loop not finished 752 | for depth := 0; depth <= maxDepth; depth += 1 { 753 | finfos, err := ioutil.ReadDir(filepath.Join(basedir, name)) 754 | if err != nil || len(finfos) != 1 { 755 | break 756 | } 757 | if finfos[0].IsDir() { 758 | name = filepath.ToSlash(filepath.Join(name, finfos[0].Name())) 759 | } else { 760 | break 761 | } 762 | } 763 | return name 764 | } 765 | 766 | func assetsContent(name string) string { 767 | fd, err := Assets.Open(name) 768 | if err != nil { 769 | panic(err) 770 | } 771 | data, err := ioutil.ReadAll(fd) 772 | if err != nil { 773 | panic(err) 774 | } 775 | return string(data) 776 | } 777 | 778 | // TODO: I need to read more abouthtml/template 779 | var ( 780 | funcMap template.FuncMap 781 | ) 782 | 783 | func init() { 784 | funcMap = template.FuncMap{ 785 | "title": strings.Title, 786 | "urlhash": func(path string) string { 787 | httpFile, err := Assets.Open(path) 788 | if err != nil { 789 | return path + "#no-such-file" 790 | } 791 | info, err := httpFile.Stat() 792 | if err != nil { 793 | return path + "#stat-error" 794 | } 795 | return fmt.Sprintf("%s?t=%d", path, info.ModTime().Unix()) 796 | }, 797 | } 798 | } 799 | 800 | var ( 801 | _tmpls = make(map[string]*template.Template) 802 | ) 803 | 804 | func renderHTML(w http.ResponseWriter, name string, v interface{}) { 805 | if t, ok := _tmpls[name]; ok { 806 | t.Execute(w, v) 807 | return 808 | } 809 | t := template.Must(template.New(name).Funcs(funcMap).Delims("[[", "]]").Parse(assetsContent(name))) 810 | _tmpls[name] = t 811 | t.Execute(w, v) 812 | } 813 | 814 | func checkFilename(name string) error { 815 | if strings.ContainsAny(name, "\\/:*<>|") { 816 | return errors.New("Name should not contains \\/:*<>|") 817 | } 818 | return nil 819 | } 820 | 821 | func (s *HTTPStaticServer) hVideoPlayer(w http.ResponseWriter, r *http.Request) { 822 | path := mux.Vars(r)["path"] 823 | realPath := s.getRealPath(r) 824 | extension := strings.ToLower(strings.TrimPrefix(filepath.Ext(path), ".")) 825 | 826 | if _, err := os.Stat(realPath); os.IsNotExist(err) { 827 | http.Error(w, "File not found", http.StatusNotFound) 828 | return 829 | } 830 | 831 | fileName := filepath.Base(path) 832 | 833 | scheme := "http" 834 | if r.TLS != nil { 835 | scheme = "https" 836 | } 837 | videoURL := fmt.Sprintf("%s://%s/%s", scheme, r.Host, path) 838 | 839 | renderHTML(w, "assets/video-player.html", map[string]interface{}{ 840 | "FileName": fileName, 841 | "VideoURL": videoURL, 842 | "Extension": extension, 843 | }) 844 | } 845 | -------------------------------------------------------------------------------- /ipa.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "archive/zip" 5 | "bytes" 6 | "errors" 7 | "io" 8 | "io/ioutil" 9 | "net/url" 10 | "path/filepath" 11 | "regexp" 12 | 13 | goplist "github.com/fork2fix/go-plist" 14 | //goplist "github.com/DHowett/go-plist" 15 | ) 16 | 17 | func parseIpaIcon(path string) (data []byte, err error) { 18 | iconPattern := regexp.MustCompile(`(?i)^Payload/[^/]*/icon\.png$`) 19 | r, err := zip.OpenReader(path) 20 | if err != nil { 21 | return 22 | } 23 | defer r.Close() 24 | 25 | var zfile *zip.File 26 | for _, file := range r.File { 27 | if iconPattern.MatchString(file.Name) { 28 | zfile = file 29 | break 30 | } 31 | } 32 | if zfile == nil { 33 | err = errors.New("icon.png file not found") 34 | return 35 | } 36 | plreader, err := zfile.Open() 37 | if err != nil { 38 | return 39 | } 40 | defer plreader.Close() 41 | return ioutil.ReadAll(plreader) 42 | } 43 | 44 | func parseIPA(path string) (plinfo *plistBundle, err error) { 45 | plistre := regexp.MustCompile(`^Payload/[^/]*/Info\.plist$`) 46 | r, err := zip.OpenReader(path) 47 | if err != nil { 48 | return 49 | } 50 | defer r.Close() 51 | 52 | var plfile *zip.File 53 | for _, file := range r.File { 54 | if plistre.MatchString(file.Name) { 55 | plfile = file 56 | break 57 | } 58 | } 59 | if plfile == nil { 60 | err = errors.New("Info.plist file not found") 61 | return 62 | } 63 | plreader, err := plfile.Open() 64 | if err != nil { 65 | return 66 | } 67 | defer plreader.Close() 68 | buf := make([]byte, plfile.FileInfo().Size()) 69 | _, err = io.ReadFull(plreader, buf) 70 | if err != nil { 71 | return 72 | } 73 | dec := goplist.NewDecoder(bytes.NewReader(buf)) 74 | plinfo = new(plistBundle) 75 | err = dec.Decode(plinfo) 76 | return 77 | } 78 | 79 | type plistBundle struct { 80 | CFBundleIdentifier string `plist:"CFBundleIdentifier"` 81 | CFBundleVersion string `plist:"CFBundleVersion"` 82 | CFBundleDisplayName string `plist:"CFBundleDisplayName"` 83 | CFBundleName string `plist:"CFBundleName"` 84 | CFBundleIconFile string `plist:"CFBundleIconFile"` 85 | CFBundleIcons struct { 86 | CFBundlePrimaryIcon struct { 87 | CFBundleIconFiles []string `plist:"CFBundleIconFiles"` 88 | } `plist:"CFBundlePrimaryIcon"` 89 | } `plist:"CFBundleIcons"` 90 | } 91 | 92 | // ref: https://gist.github.com/frischmilch/b15d81eabb67925642bd#file_manifest.plist 93 | type plAsset struct { 94 | Kind string `plist:"kind"` 95 | URL string `plist:"url"` 96 | } 97 | 98 | type plItem struct { 99 | Assets []*plAsset `plist:"assets"` 100 | Metadata struct { 101 | BundleIdentifier string `plist:"bundle-identifier"` 102 | BundleVersion string `plist:"bundle-version"` 103 | Kind string `plist:"kind"` 104 | Title string `plist:"title"` 105 | } `plist:"metadata"` 106 | } 107 | 108 | type downloadPlist struct { 109 | Items []*plItem `plist:"items"` 110 | } 111 | 112 | func generateDownloadPlist(baseURL *url.URL, ipaPath string, plinfo *plistBundle) ([]byte, error) { 113 | dp := new(downloadPlist) 114 | item := new(plItem) 115 | baseURL.Path = ipaPath 116 | ipaUrl := baseURL.String() 117 | item.Assets = append(item.Assets, &plAsset{ 118 | Kind: "software-package", 119 | URL: ipaUrl, 120 | }) 121 | 122 | iconFiles := plinfo.CFBundleIcons.CFBundlePrimaryIcon.CFBundleIconFiles 123 | if iconFiles != nil && len(iconFiles) > 0 { 124 | baseURL.Path = "/-/unzip/" + ipaPath + "/-/**/" + iconFiles[0] + ".png" 125 | imgUrl := baseURL.String() 126 | item.Assets = append(item.Assets, &plAsset{ 127 | Kind: "display-image", 128 | URL: imgUrl, 129 | }) 130 | } 131 | 132 | item.Metadata.Kind = "software" 133 | 134 | item.Metadata.BundleIdentifier = plinfo.CFBundleIdentifier 135 | item.Metadata.BundleVersion = plinfo.CFBundleVersion 136 | item.Metadata.Title = plinfo.CFBundleName 137 | if item.Metadata.Title == "" { 138 | item.Metadata.Title = filepath.Base(ipaUrl) 139 | } 140 | 141 | dp.Items = append(dp.Items, item) 142 | data, err := goplist.MarshalIndent(dp, goplist.XMLFormat, " ") 143 | return data, err 144 | } 145 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "crypto/sha256" 6 | "crypto/subtle" 7 | "encoding/json" 8 | "fmt" 9 | "io/ioutil" 10 | "log" 11 | "net" 12 | "net/http" 13 | "net/url" 14 | "os" 15 | "regexp" 16 | "runtime" 17 | "strconv" 18 | "strings" 19 | "text/template" 20 | 21 | "github.com/alecthomas/kingpin" 22 | accesslog "github.com/codeskyblue/go-accesslog" 23 | "github.com/go-yaml/yaml" 24 | "github.com/goji/httpauth" 25 | "github.com/gorilla/handlers" 26 | "github.com/gorilla/mux" 27 | ) 28 | 29 | type Configure struct { 30 | Conf *os.File `yaml:"-"` 31 | Addr string `yaml:"addr"` 32 | Port int `yaml:"port"` 33 | Root string `yaml:"root"` 34 | Prefix string `yaml:"prefix"` 35 | HTTPAuth string `yaml:"httpauth"` 36 | Cert string `yaml:"cert"` 37 | Key string `yaml:"key"` 38 | Theme string `yaml:"theme"` 39 | XHeaders bool `yaml:"xheaders"` 40 | Upload bool `yaml:"upload"` 41 | Delete bool `yaml:"delete"` 42 | PlistProxy string `yaml:"plistproxy"` 43 | Title string `yaml:"title"` 44 | Debug bool `yaml:"debug"` 45 | GoogleTrackerID string `yaml:"google-tracker-id"` 46 | Auth struct { 47 | Type string `yaml:"type"` // openid|http|github 48 | OpenID string `yaml:"openid"` 49 | HTTP []string `yaml:"http"` 50 | ID string `yaml:"id"` // for oauth2 51 | Secret string `yaml:"secret"` // for oauth2 52 | } `yaml:"auth"` 53 | DeepPathMaxDepth int `yaml:"deep-path-max-depth"` 54 | NoIndex bool `yaml:"no-index"` 55 | } 56 | 57 | type httpLogger struct{} 58 | 59 | func (l httpLogger) Log(record accesslog.LogRecord) { 60 | log.Printf("%s - %s %d %s", record.Ip, record.Method, record.Status, record.Uri) 61 | } 62 | 63 | var ( 64 | defaultPlistProxy = "https://plistproxy.herokuapp.com/plist" 65 | defaultOpenID = "https://login.netease.com/openid" 66 | gcfg = Configure{} 67 | logger = httpLogger{} 68 | 69 | VERSION = "unknown" 70 | BUILDTIME = "unknown time" 71 | GITCOMMIT = "unknown git commit" 72 | SITE = "https://github.com/codeskyblue/gohttpserver" 73 | ) 74 | 75 | func versionMessage() string { 76 | t := template.Must(template.New("version").Parse(`GoHTTPServer 77 | Version: {{.Version}} 78 | Go version: {{.GoVersion}} 79 | OS/Arch: {{.OSArch}} 80 | Git commit: {{.GitCommit}} 81 | Built: {{.Built}} 82 | Site: {{.Site}}`)) 83 | buf := bytes.NewBuffer(nil) 84 | t.Execute(buf, map[string]interface{}{ 85 | "Version": VERSION, 86 | "GoVersion": runtime.Version(), 87 | "OSArch": runtime.GOOS + "/" + runtime.GOARCH, 88 | "GitCommit": GITCOMMIT, 89 | "Built": BUILDTIME, 90 | "Site": SITE, 91 | }) 92 | return buf.String() 93 | } 94 | 95 | func parseFlags() error { 96 | // initial default conf 97 | gcfg.Root = "./" 98 | gcfg.Port = 8000 99 | gcfg.Addr = "" 100 | gcfg.Theme = "black" 101 | gcfg.PlistProxy = defaultPlistProxy 102 | gcfg.Auth.OpenID = defaultOpenID 103 | gcfg.GoogleTrackerID = "UA-81205425-2" 104 | gcfg.Title = "Go HTTP File Server" 105 | gcfg.DeepPathMaxDepth = 5 106 | gcfg.NoIndex = false 107 | 108 | kingpin.HelpFlag.Short('h') 109 | kingpin.Version(versionMessage()) 110 | kingpin.Flag("conf", "config file path, yaml format").FileVar(&gcfg.Conf) 111 | kingpin.Flag("root", "root directory, default ./").Short('r').StringVar(&gcfg.Root) 112 | kingpin.Flag("prefix", "url prefix, eg /foo").StringVar(&gcfg.Prefix) 113 | kingpin.Flag("port", "listen port, default 8000").IntVar(&gcfg.Port) 114 | kingpin.Flag("addr", "listen address, eg 127.0.0.1:8000").Short('a').StringVar(&gcfg.Addr) 115 | kingpin.Flag("cert", "tls cert.pem path").StringVar(&gcfg.Cert) 116 | kingpin.Flag("key", "tls key.pem path").StringVar(&gcfg.Key) 117 | kingpin.Flag("auth-type", "Auth type ").StringVar(&gcfg.Auth.Type) 118 | kingpin.Flag("auth-http", "HTTP basic auth (ex: user:pass)").StringsVar(&gcfg.Auth.HTTP) 119 | kingpin.Flag("auth-openid", "OpenID auth identity url").StringVar(&gcfg.Auth.OpenID) 120 | kingpin.Flag("theme", "web theme, one of ").StringVar(&gcfg.Theme) 121 | kingpin.Flag("upload", "enable upload support").BoolVar(&gcfg.Upload) 122 | kingpin.Flag("delete", "enable delete support").BoolVar(&gcfg.Delete) 123 | kingpin.Flag("xheaders", "used when behide nginx").BoolVar(&gcfg.XHeaders) 124 | kingpin.Flag("debug", "enable debug mode").BoolVar(&gcfg.Debug) 125 | kingpin.Flag("plistproxy", "plist proxy when server is not https").Short('p').StringVar(&gcfg.PlistProxy) 126 | kingpin.Flag("title", "server title").StringVar(&gcfg.Title) 127 | kingpin.Flag("google-tracker-id", "set to empty to disable it").StringVar(&gcfg.GoogleTrackerID) 128 | kingpin.Flag("deep-path-max-depth", "set to -1 to not combine dirs").IntVar(&gcfg.DeepPathMaxDepth) 129 | kingpin.Flag("no-index", "disable indexing").BoolVar(&gcfg.NoIndex) 130 | 131 | kingpin.Parse() // first parse conf 132 | 133 | if gcfg.Conf != nil { 134 | defer func() { 135 | kingpin.Parse() // command line priority high than conf 136 | }() 137 | ymlData, err := ioutil.ReadAll(gcfg.Conf) 138 | if err != nil { 139 | return err 140 | } 141 | return yaml.Unmarshal(ymlData, &gcfg) 142 | } 143 | return nil 144 | } 145 | 146 | func fixPrefix(prefix string) string { 147 | prefix = regexp.MustCompile(`/*$`).ReplaceAllString(prefix, "") 148 | if !strings.HasPrefix(prefix, "/") { 149 | prefix = "/" + prefix 150 | } 151 | if prefix == "/" { 152 | prefix = "" 153 | } 154 | return prefix 155 | } 156 | 157 | func cors(next http.Handler) http.Handler { 158 | // access control and CORS middleware 159 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 160 | w.Header().Set("Access-Control-Allow-Origin", "*") 161 | w.Header().Set("Access-Control-Allow-Methods", "*") 162 | w.Header().Set("Access-Control-Allow-Headers", "*") 163 | if r.Method == "OPTIONS" { 164 | return 165 | } 166 | next.ServeHTTP(w, r) 167 | }) 168 | } 169 | 170 | func multiBasicAuth(auths []string) func(http.Handler) http.Handler { 171 | userPassMap := make(map[string]string) 172 | for _, auth := range auths { 173 | userpass := strings.SplitN(auth, ":", 2) 174 | if len(userpass) == 2 { 175 | userPassMap[userpass[0]] = userpass[1] 176 | } 177 | } 178 | return httpauth.BasicAuth(httpauth.AuthOptions{ 179 | Realm: "Restricted", 180 | AuthFunc: func(user, pass string, request *http.Request) bool { 181 | password, ok := userPassMap[user] 182 | if !ok { 183 | return false 184 | } 185 | givenPass := sha256.Sum256([]byte(pass)) 186 | requiredPass := sha256.Sum256([]byte(password)) 187 | return subtle.ConstantTimeCompare(givenPass[:], requiredPass[:]) == 1 188 | }, 189 | }) 190 | } 191 | 192 | func main() { 193 | if err := parseFlags(); err != nil { 194 | log.Fatal(err) 195 | } 196 | if gcfg.Debug { 197 | data, _ := yaml.Marshal(gcfg) 198 | fmt.Printf("--- config ---\n%s\n", string(data)) 199 | } 200 | log.SetFlags(log.Lshortfile | log.LstdFlags) 201 | 202 | // make sure prefix matches: ^/.*[^/]$ 203 | gcfg.Prefix = fixPrefix(gcfg.Prefix) 204 | if gcfg.Prefix != "" { 205 | log.Printf("url prefix: %s", gcfg.Prefix) 206 | } 207 | 208 | ss := NewHTTPStaticServer(gcfg.Root, gcfg.NoIndex) 209 | ss.Prefix = gcfg.Prefix 210 | ss.Theme = gcfg.Theme 211 | ss.Title = gcfg.Title 212 | ss.GoogleTrackerID = gcfg.GoogleTrackerID 213 | ss.Upload = gcfg.Upload 214 | ss.Delete = gcfg.Delete 215 | ss.AuthType = gcfg.Auth.Type 216 | ss.DeepPathMaxDepth = gcfg.DeepPathMaxDepth 217 | 218 | if gcfg.PlistProxy != "" { 219 | u, err := url.Parse(gcfg.PlistProxy) 220 | if err != nil { 221 | log.Fatal(err) 222 | } 223 | u.Scheme = "https" 224 | ss.PlistProxy = u.String() 225 | } 226 | if ss.PlistProxy != "" { 227 | log.Printf("plistproxy: %s", strconv.Quote(ss.PlistProxy)) 228 | } 229 | 230 | var hdlr http.Handler = ss 231 | 232 | hdlr = accesslog.NewLoggingHandler(hdlr, logger) 233 | 234 | // HTTP Basic Authentication 235 | switch gcfg.Auth.Type { 236 | case "http": 237 | hdlr = multiBasicAuth(gcfg.Auth.HTTP)(hdlr) 238 | case "openid": 239 | handleOpenID(gcfg.Auth.OpenID, false) // FIXME(ssx): set secure default to false 240 | // case "github": 241 | // handleOAuth2ID(gcfg.Auth.Type, gcfg.Auth.ID, gcfg.Auth.Secret) // FIXME(ssx): set secure default to false 242 | case "oauth2-proxy": 243 | handleOauth2() 244 | } 245 | 246 | // CORS 247 | hdlr = cors(hdlr) 248 | 249 | if gcfg.XHeaders { 250 | hdlr = handlers.ProxyHeaders(hdlr) 251 | } 252 | 253 | mainRouter := mux.NewRouter() 254 | router := mainRouter 255 | if gcfg.Prefix != "" { 256 | router = mainRouter.PathPrefix(gcfg.Prefix).Subrouter() 257 | mainRouter.Handle(gcfg.Prefix, hdlr) 258 | mainRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 259 | http.Redirect(w, r, gcfg.Prefix, http.StatusTemporaryRedirect) 260 | }) 261 | } 262 | 263 | router.PathPrefix("/-/assets/").Handler(http.StripPrefix(gcfg.Prefix+"/-/", http.FileServer(Assets))) 264 | router.HandleFunc("/-/sysinfo", func(w http.ResponseWriter, r *http.Request) { 265 | data, _ := json.Marshal(map[string]interface{}{ 266 | "version": VERSION, 267 | }) 268 | w.Header().Set("Content-Type", "application/json") 269 | w.Header().Set("Content-Length", fmt.Sprintf("%d", len(data))) 270 | w.Write(data) 271 | }) 272 | router.PathPrefix("/").Handler(hdlr) 273 | 274 | if gcfg.Addr == "" { 275 | gcfg.Addr = fmt.Sprintf(":%d", gcfg.Port) 276 | } 277 | if !strings.Contains(gcfg.Addr, ":") { 278 | gcfg.Addr = ":" + gcfg.Addr 279 | } 280 | _, port, _ := net.SplitHostPort(gcfg.Addr) 281 | log.Printf("listening on %s, local address http://%s:%s\n", strconv.Quote(gcfg.Addr), getLocalIP(), port) 282 | 283 | srv := &http.Server{ 284 | Handler: mainRouter, 285 | Addr: gcfg.Addr, 286 | } 287 | 288 | var err error 289 | if gcfg.Key != "" && gcfg.Cert != "" { 290 | err = srv.ListenAndServeTLS(gcfg.Cert, gcfg.Key) 291 | } else { 292 | err = srv.ListenAndServe() 293 | } 294 | log.Fatal(err) 295 | } 296 | -------------------------------------------------------------------------------- /oauth2-proxy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "net/url" 7 | ) 8 | 9 | func handleOauth2() { 10 | http.HandleFunc("/-/user", func(w http.ResponseWriter, r *http.Request) { 11 | fullNameMap, _ := url.ParseQuery(r.Header.Get("X-Auth-Request-Fullname")) 12 | var fullName string 13 | for k := range fullNameMap { 14 | fullName = k 15 | break 16 | } 17 | user := &UserInfo{ 18 | Email: r.Header.Get("X-Auth-Request-Email"), 19 | Name: fullName, 20 | NickName: r.Header.Get("X-Auth-Request-User"), 21 | } 22 | 23 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 24 | data, _ := json.Marshal(user) 25 | w.Write(data) 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /openid-login.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/gob" 5 | "encoding/json" 6 | "io" 7 | "log" 8 | "net/http" 9 | "strings" 10 | 11 | openid "github.com/codeskyblue/openid-go" 12 | "github.com/gorilla/sessions" 13 | ) 14 | 15 | var ( 16 | nonceStore = openid.NewSimpleNonceStore() 17 | discoveryCache = openid.NewSimpleDiscoveryCache() 18 | store = sessions.NewCookieStore([]byte("something-very-secret")) 19 | defaultSessionName = "ghs-session" 20 | ) 21 | 22 | type UserInfo struct { 23 | Id string `json:"id"` 24 | Email string `json:"email"` 25 | Name string `json:"name"` 26 | NickName string `json:"nickName"` 27 | } 28 | 29 | type M map[string]interface{} 30 | 31 | func init() { 32 | gob.Register(&UserInfo{}) 33 | gob.Register(&M{}) 34 | } 35 | 36 | func handleOpenID(loginUrl string, secure bool) { 37 | http.HandleFunc("/-/login", func(w http.ResponseWriter, r *http.Request) { 38 | nextUrl := r.FormValue("next") 39 | referer := r.Referer() 40 | if nextUrl == "" && strings.Contains(referer, "://"+r.Host) { 41 | nextUrl = referer 42 | } 43 | scheme := "http" 44 | if r.URL.Scheme != "" { 45 | scheme = r.URL.Scheme 46 | } 47 | log.Println("Scheme:", scheme) 48 | if url, err := openid.RedirectURL(loginUrl, 49 | scheme+"://"+r.Host+"/-/openidcallback?next="+nextUrl, ""); err == nil { 50 | http.Redirect(w, r, url, 303) 51 | } else { 52 | log.Println("Should not got error here:", err) 53 | } 54 | }) 55 | 56 | http.HandleFunc("/-/openidcallback", func(w http.ResponseWriter, r *http.Request) { 57 | id, err := openid.Verify("http://"+r.Host+r.URL.String(), discoveryCache, nonceStore) 58 | if err != nil { 59 | io.WriteString(w, "Authentication check failed.") 60 | return 61 | } 62 | session, err := store.Get(r, defaultSessionName) 63 | if err != nil { 64 | http.Error(w, err.Error(), http.StatusInternalServerError) 65 | return 66 | } 67 | user := &UserInfo{ 68 | Id: id, 69 | Email: r.FormValue("openid.sreg.email"), 70 | Name: r.FormValue("openid.sreg.fullname"), 71 | NickName: r.FormValue("openid.sreg.nickname"), 72 | } 73 | session.Values["user"] = user 74 | if err := session.Save(r, w); err != nil { 75 | log.Println("session save error:", err) 76 | } 77 | 78 | nextUrl := r.FormValue("next") 79 | if nextUrl == "" { 80 | nextUrl = "/" 81 | } 82 | http.Redirect(w, r, nextUrl, 302) 83 | }) 84 | 85 | http.HandleFunc("/-/user", func(w http.ResponseWriter, r *http.Request) { 86 | session, err := store.Get(r, defaultSessionName) 87 | if err != nil { 88 | http.Error(w, err.Error(), http.StatusInternalServerError) 89 | return 90 | } 91 | val := session.Values["user"] 92 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 93 | data, _ := json.Marshal(val) 94 | w.Write(data) 95 | }) 96 | 97 | http.HandleFunc("/-/logout", func(w http.ResponseWriter, r *http.Request) { 98 | session, err := store.Get(r, defaultSessionName) 99 | if err != nil { 100 | http.Error(w, err.Error(), http.StatusInternalServerError) 101 | return 102 | } 103 | delete(session.Values, "user") 104 | session.Options.MaxAge = -1 105 | nextUrl := r.FormValue("next") 106 | _ = session.Save(r, w) 107 | if nextUrl == "" { 108 | nextUrl = r.Referer() 109 | } 110 | http.Redirect(w, r, nextUrl, 302) 111 | }) 112 | } 113 | -------------------------------------------------------------------------------- /res.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "html/template" 4 | 5 | var ( 6 | tmpl *template.Template 7 | templates = map[string]string{ 8 | "index": "res/index.tmpl.html", 9 | "ipa-install": "res/ipa-install.tmpl.html", 10 | } 11 | ) 12 | 13 | func ParseTemplate(name string, content string) { 14 | if tmpl == nil { 15 | tmpl = template.New(name) 16 | } 17 | var t *template.Template 18 | if tmpl.Name() == name { 19 | t = tmpl 20 | } else { 21 | t = tmpl.New(name) 22 | } 23 | template.Must(t.New(name).Delims("[[", "]]").Parse(content)) 24 | } 25 | -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | ## File: scripts/proxy.py 2 | Proxy request 3 | 4 | ## Dependencies 5 | need tornado, `pip install tornado` 6 | 7 | ## How to use 8 | Start server `python proxy.py --port 8100` 9 | 10 | http GET localhost:8100/proxy/www.baidu.com 11 | -------------------------------------------------------------------------------- /scripts/proxy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | import time 5 | import hashlib 6 | 7 | import tornado 8 | import tornado.ioloop 9 | import tornado.web 10 | from tornado import gen, httpclient, ioloop 11 | from tornado.options import define, options 12 | 13 | 14 | define("port", default=8200, help="Run server on a specific port", type=int) 15 | 16 | class MainHandler(tornado.web.RequestHandler): 17 | def get(self): 18 | self.write("Hello, proxy") 19 | 20 | 21 | class ProxyHandler(tornado.web.RequestHandler): 22 | http_client = httpclient.AsyncHTTPClient() 23 | 24 | @gen.coroutine 25 | def get(self, url): 26 | print time.strftime('%Y-%m-%d %H:%M:%S'), 'PROXY http://' + url 27 | response = yield self.http_client.fetch('http://'+url) #www.google.com') 28 | # print response.body 29 | if (response.error and not 30 | isinstance(response.error, tornado.httpclient.HTTPError)): 31 | self.set_status(500) 32 | self.write('Internal server error:\n' + str(response.error)) 33 | else: 34 | self.set_status(response.code, response.reason) 35 | for header, v in response.headers.get_all(): 36 | if header not in ('Content-Length', 'Transfer-Encoding', 'Content-Encoding', 'Connection'): 37 | self.set_header(header, v) # some header appear multiple times, eg 'Set-Cookie' 38 | if response.body: 39 | self.set_header('Content-Length', len(response.body)) 40 | self.write(response.body) 41 | self.finish() 42 | 43 | class PlistStoreHandler(tornado.web.RequestHandler): 44 | db = {} 45 | 46 | def post(self): 47 | body = self.request.body 48 | if len(body) > 5000: 49 | self.set_status(500) 50 | self.finish("request body too long") 51 | m = hashlib.md5() 52 | m.update(body) 53 | key = m.hexdigest()[8:16] 54 | self.db[key] = body 55 | self.write({'key': key}) 56 | 57 | def get(self): 58 | key = self.get_argument('key') 59 | value = self.db.get(key) 60 | if value is None: 61 | raise tornado.web.HTTPError(404) 62 | self.set_header('Content-Type', 'text/xml') 63 | self.finish(value) 64 | 65 | 66 | def make_app(debug=True): 67 | return tornado.web.Application([ 68 | (r"/", MainHandler), 69 | (r"/proxy/(.*)", ProxyHandler), 70 | (r"/plist", PlistStoreHandler), 71 | ], debug=debug) 72 | 73 | 74 | if __name__ == "__main__": 75 | app = make_app() 76 | tornado.options.parse_command_line() 77 | app.listen(options.port) 78 | ioloop.IOLoop.current().start() 79 | -------------------------------------------------------------------------------- /testdata/README.md: -------------------------------------------------------------------------------- 1 | ## This is title 2 | 1. [x] task1 3 | 1. [ ] list2 4 | 5 | ## Table 6 | this will produce this: 7 | 8 | | Tables | Are | Cool | 9 | | ------------- |:-------------:| -----:| 10 | | **col 3 is** | right-aligned | $1600 | 11 | | col 2 is | *centered* | $12 | 12 | | zebra stripes | ~~are neat~~ | $1 | 13 | 14 | https://github.com/codeskyblue/gohttpserver -------------------------------------------------------------------------------- /testdata/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | addr: ":4000" 3 | title: "hello world" 4 | theme: green 5 | debug: true 6 | xheaders: true 7 | cors: true 8 | -------------------------------------------------------------------------------- /testdata/deep1/deep2/deep3/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeskyblue/gohttpserver/df32f57005d9f02ee08a89f237a711316b992376/testdata/deep1/deep2/deep3/.gitkeep -------------------------------------------------------------------------------- /testdata/deletable/.ghs.yml: -------------------------------------------------------------------------------- 1 | upload: true 2 | delete: true 3 | accessTables: 4 | - regex: block.file 5 | allow: false 6 | - regex: visual.file 7 | allow: true -------------------------------------------------------------------------------- /testdata/deletable/block.file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeskyblue/gohttpserver/df32f57005d9f02ee08a89f237a711316b992376/testdata/deletable/block.file -------------------------------------------------------------------------------- /testdata/deletable/other.file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeskyblue/gohttpserver/df32f57005d9f02ee08a89f237a711316b992376/testdata/deletable/other.file -------------------------------------------------------------------------------- /testdata/deletable/visual.file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeskyblue/gohttpserver/df32f57005d9f02ee08a89f237a711316b992376/testdata/deletable/visual.file -------------------------------------------------------------------------------- /testdata/filetypes/code.go: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeskyblue/gohttpserver/df32f57005d9f02ee08a89f237a711316b992376/testdata/filetypes/code.go -------------------------------------------------------------------------------- /testdata/filetypes/gohttpserver.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeskyblue/gohttpserver/df32f57005d9f02ee08a89f237a711316b992376/testdata/filetypes/gohttpserver.gif -------------------------------------------------------------------------------- /testdata/filetypes/image.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeskyblue/gohttpserver/df32f57005d9f02ee08a89f237a711316b992376/testdata/filetypes/image.jpeg -------------------------------------------------------------------------------- /testdata/filetypes/image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeskyblue/gohttpserver/df32f57005d9f02ee08a89f237a711316b992376/testdata/filetypes/image.jpg -------------------------------------------------------------------------------- /testdata/filetypes/image.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeskyblue/gohttpserver/df32f57005d9f02ee08a89f237a711316b992376/testdata/filetypes/image.pdf -------------------------------------------------------------------------------- /testdata/filetypes/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeskyblue/gohttpserver/df32f57005d9f02ee08a89f237a711316b992376/testdata/filetypes/image.png -------------------------------------------------------------------------------- /testdata/filetypes/image.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeskyblue/gohttpserver/df32f57005d9f02ee08a89f237a711316b992376/testdata/filetypes/image.tiff -------------------------------------------------------------------------------- /testdata/filetypes/page.html: -------------------------------------------------------------------------------- 1 | 2 | Haha 3 | 4 |

Hello world

5 | 6 | 7 | -------------------------------------------------------------------------------- /testdata/filetypes/script.js: -------------------------------------------------------------------------------- 1 | document.write("Hello world!") -------------------------------------------------------------------------------- /testdata/filetypes/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: red; 3 | } 4 | 5 | -------------------------------------------------------------------------------- /testdata/test.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeskyblue/gohttpserver/df32f57005d9f02ee08a89f237a711316b992376/testdata/test.zip -------------------------------------------------------------------------------- /testdata/uploadable/.ghs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | upload: false 3 | users: 4 | - email: "user@example.com" 5 | upload: true 6 | delete: true 7 | token: 123456 -------------------------------------------------------------------------------- /testdata/uploadable/sub-upload/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeskyblue/gohttpserver/df32f57005d9f02ee08a89f237a711316b992376/testdata/uploadable/sub-upload/.gitkeep -------------------------------------------------------------------------------- /testdata/中文路径/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeskyblue/gohttpserver/df32f57005d9f02ee08a89f237a711316b992376/testdata/中文路径/.gitkeep -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net" 5 | "net/http" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | ) 10 | 11 | // func formatSize(file os.FileInfo) string { 12 | // if file.IsDir() { 13 | // return "-" 14 | // } 15 | // size := file.Size() 16 | // switch { 17 | // case size > 1024*1024: 18 | // return fmt.Sprintf("%.1f MB", float64(size)/1024/1024) 19 | // case size > 1024: 20 | // return fmt.Sprintf("%.1f KB", float64(size)/1024) 21 | // default: 22 | // return strconv.Itoa(int(size)) + " B" 23 | // } 24 | // return "" 25 | // } 26 | 27 | func getRealIP(req *http.Request) string { 28 | xip := req.Header.Get("X-Real-IP") 29 | if xip == "" { 30 | xip = strings.Split(req.RemoteAddr, ":")[0] 31 | } 32 | return xip 33 | } 34 | 35 | func SublimeContains(s, substr string) bool { 36 | rs, rsubstr := []rune(s), []rune(substr) 37 | if len(rsubstr) > len(rs) { 38 | return false 39 | } 40 | 41 | var ok = true 42 | var i, j = 0, 0 43 | for ; i < len(rsubstr); i++ { 44 | found := -1 45 | for ; j < len(rs); j++ { 46 | if rsubstr[i] == rs[j] { 47 | found = j 48 | break 49 | } 50 | } 51 | if found == -1 { 52 | ok = false 53 | break 54 | } 55 | j += 1 56 | } 57 | return ok 58 | } 59 | 60 | // getLocalIP returns the non loopback local IP of the host 61 | func getLocalIP() string { 62 | addrs, err := net.InterfaceAddrs() 63 | if err != nil { 64 | return "" 65 | } 66 | for _, address := range addrs { 67 | // check the address type and if it is not a loopback the display it 68 | if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { 69 | if ipnet.IP.To4() != nil { 70 | return ipnet.IP.String() 71 | } 72 | } 73 | } 74 | return "" 75 | } 76 | 77 | func fileExists(path string) bool { 78 | info, err := os.Stat(path) 79 | if err != nil { 80 | return false 81 | } 82 | return !info.IsDir() 83 | } 84 | 85 | // Convert path to normal paths 86 | func cleanPath(path string) string { 87 | return filepath.ToSlash(filepath.Clean(path)) 88 | } 89 | 90 | func isFile(path string) bool { 91 | info, err := os.Stat(path) 92 | return err == nil && info.Mode().IsRegular() 93 | } 94 | 95 | func isDir(path string) bool { 96 | info, err := os.Stat(path) 97 | return err == nil && info.Mode().IsDir() 98 | } 99 | -------------------------------------------------------------------------------- /utils_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func TestSublimeContains(t *testing.T) { 6 | tests := []struct { 7 | text string 8 | substr string 9 | pass bool 10 | }{ 11 | {"hello", "lo", true}, 12 | {"abcdefg", "cf", true}, 13 | {"abcdefg", "a", true}, 14 | {"abcdefg", "b", true}, 15 | {"abcdefg", "cfa", false}, 16 | {"abcdefg", "aa", false}, 17 | {"世界", "a", false}, 18 | {"Hello 世界", "界", true}, 19 | {"Hello 世界", "elo", true}, 20 | } 21 | for _, v := range tests { 22 | res := SublimeContains(v.text, v.substr) 23 | if res != v.pass { 24 | t.Fatalf("Failed: %v - res:%v", v, res) 25 | } 26 | } 27 | } 28 | 29 | func TestCleanPath(t *testing.T) { 30 | tests := []struct { 31 | orig string 32 | expect string 33 | }{ 34 | // {"C:\\hello", "C:/hello"}, // Only works in windows 35 | {"", "."}, 36 | {"//../foo", "/foo"}, 37 | {"/../../", "/"}, 38 | {"/hello/world/..", "/hello"}, 39 | {"/..", "/"}, 40 | {"/foo/..", "/"}, 41 | {"/-/foo", "/-/foo"}, 42 | } 43 | for _, v := range tests { 44 | res := cleanPath(v.orig) 45 | if res != v.expect { 46 | t.Fatalf("Clean path(%v) expect(%v) but got(%v)", v.orig, v.expect, res) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /zip.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "archive/zip" 5 | "bytes" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "net/http" 10 | "os" 11 | "path/filepath" 12 | "runtime" 13 | "strconv" 14 | "strings" 15 | 16 | dkignore "github.com/codeskyblue/dockerignore" 17 | "golang.org/x/text/encoding/simplifiedchinese" 18 | ) 19 | 20 | type Zip struct { 21 | *zip.Writer 22 | } 23 | 24 | func sanitizedName(filename string) string { 25 | if len(filename) > 1 && filename[1] == ':' && 26 | runtime.GOOS == "windows" { 27 | filename = filename[2:] 28 | } 29 | filename = strings.TrimLeft(strings.Replace(filename, `\`, "/", -1), `/`) 30 | filename = filepath.ToSlash(filename) 31 | filename = filepath.Clean(filename) 32 | return filename 33 | } 34 | 35 | func statFile(filename string) (info os.FileInfo, reader io.ReadCloser, err error) { 36 | info, err = os.Lstat(filename) 37 | if err != nil { 38 | return 39 | } 40 | // content 41 | if info.Mode()&os.ModeSymlink != 0 { 42 | var target string 43 | target, err = os.Readlink(filename) 44 | if err != nil { 45 | return 46 | } 47 | reader = ioutil.NopCloser(bytes.NewBuffer([]byte(target))) 48 | } else if !info.IsDir() { 49 | reader, err = os.Open(filename) 50 | if err != nil { 51 | return 52 | } 53 | } else { 54 | reader = ioutil.NopCloser(bytes.NewBuffer(nil)) 55 | } 56 | return 57 | } 58 | 59 | func (z *Zip) Add(relpath, abspath string) error { 60 | info, rdc, err := statFile(abspath) 61 | if err != nil { 62 | return err 63 | } 64 | defer rdc.Close() 65 | 66 | hdr, err := zip.FileInfoHeader(info) 67 | if err != nil { 68 | return err 69 | } 70 | hdr.Name = sanitizedName(relpath) 71 | if info.IsDir() { 72 | hdr.Name += "/" 73 | } 74 | hdr.Method = zip.Deflate // compress method 75 | writer, err := z.CreateHeader(hdr) 76 | if err != nil { 77 | return err 78 | } 79 | _, err = io.Copy(writer, rdc) 80 | return err 81 | } 82 | 83 | func CompressToZip(w http.ResponseWriter, rootDir string) { 84 | rootDir = filepath.Clean(rootDir) 85 | zipFileName := filepath.Base(rootDir) + ".zip" 86 | 87 | w.Header().Set("Content-Type", "application/zip") 88 | w.Header().Set("Content-Disposition", `attachment; filename="`+zipFileName+`"`) 89 | 90 | zw := &Zip{Writer: zip.NewWriter(w)} 91 | defer zw.Close() 92 | 93 | filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error { 94 | zipPath := path[len(rootDir):] 95 | if info.Name() == YAMLCONF { // ignore .ghs.yml for security 96 | return nil 97 | } 98 | return zw.Add(zipPath, path) 99 | }) 100 | } 101 | 102 | func ExtractFromZip(zipFile, path string, w io.Writer) (err error) { 103 | cf, err := zip.OpenReader(zipFile) 104 | if err != nil { 105 | return 106 | } 107 | defer cf.Close() 108 | 109 | rd := ioutil.NopCloser(bytes.NewBufferString(path)) 110 | patterns, err := dkignore.ReadIgnore(rd) 111 | if err != nil { 112 | return 113 | } 114 | 115 | for _, file := range cf.File { 116 | matched, _ := dkignore.Matches(file.Name, patterns) 117 | if !matched { 118 | continue 119 | } 120 | rc, er := file.Open() 121 | if er != nil { 122 | err = er 123 | return 124 | } 125 | defer rc.Close() 126 | _, err = io.Copy(w, rc) 127 | if err != nil { 128 | return 129 | } 130 | return 131 | } 132 | return fmt.Errorf("File %s not found", strconv.Quote(path)) 133 | } 134 | 135 | func unzipFile(filename, dest string) error { 136 | zr, err := zip.OpenReader(filename) 137 | if err != nil { 138 | return err 139 | } 140 | defer zr.Close() 141 | 142 | if dest == "" { 143 | dest = filepath.Dir(filename) 144 | } 145 | 146 | for _, f := range zr.File { 147 | rc, err := f.Open() 148 | if err != nil { 149 | return err 150 | } 151 | defer rc.Close() 152 | 153 | // ignore .ghs.yml 154 | filename := sanitizedName(f.Name) 155 | if filepath.Base(filename) == ".ghs.yml" { 156 | continue 157 | } 158 | fpath := filepath.Join(dest, filename) 159 | 160 | // filename maybe GBK or UTF-8 161 | // Ref: https://studygolang.com/articles/3114 162 | if f.Flags&(1<<11) == 0 { // GBK 163 | tr := simplifiedchinese.GB18030.NewDecoder() 164 | fpathUtf8, err := tr.String(fpath) 165 | if err == nil { 166 | fpath = fpathUtf8 167 | } 168 | } 169 | 170 | if f.FileInfo().IsDir() { 171 | os.MkdirAll(fpath, os.ModePerm) 172 | continue 173 | } 174 | 175 | os.MkdirAll(filepath.Dir(fpath), os.ModePerm) 176 | outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) 177 | if err != nil { 178 | return err 179 | } 180 | _, err = io.Copy(outFile, rc) 181 | outFile.Close() 182 | 183 | if err != nil { 184 | return err 185 | } 186 | } 187 | return nil 188 | } 189 | -------------------------------------------------------------------------------- /zip_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestExtractFromZip(t *testing.T) { 11 | buf := bytes.NewBuffer(nil) 12 | err := ExtractFromZip("testdata/test.zip", "**/foo.txt", buf) 13 | assert.Nil(t, err) 14 | t.Log("Content: " + buf.String()) 15 | } 16 | 17 | //func TestUnzipTo(t *testing.T){ 18 | // err := unzipFile("testdata.zip", "./tmp") 19 | // assert.Nil(t, err) 20 | //} 21 | --------------------------------------------------------------------------------