├── .dockerignore ├── .github └── workflows │ ├── release_docker.yml │ └── release_sources.yml ├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── cmd └── lenpaste │ └── lenpaste.go ├── entrypoint.sh ├── go.mod ├── go.sum ├── internal ├── apiv1 │ ├── api.go │ ├── api_error.go │ ├── api_get.go │ ├── api_main.go │ ├── api_new.go │ └── api_server.go ├── cli │ ├── cli.go │ ├── duration.go │ └── duration_test.go ├── config │ └── config.go ├── lenpasswd │ └── lenpasswd.go ├── lineend │ ├── lineend.go │ └── lineend_test.go ├── logger │ └── logger.go ├── netshare │ ├── netshare.go │ ├── netshare_host.go │ ├── netshare_paste.go │ └── netshare_ratelimit.go ├── raw │ ├── raw.go │ ├── raw_error.go │ └── raw_raw.go ├── storage │ ├── share.go │ ├── storage.go │ └── storage_paste.go └── web │ ├── data │ ├── about.tmpl │ ├── authors.tmpl │ ├── base.tmpl │ ├── code.js │ ├── docs.tmpl │ ├── docs_api_libs.tmpl │ ├── docs_apiv1.tmpl │ ├── emb.tmpl │ ├── emb_help.tmpl │ ├── error.tmpl │ ├── history.js │ ├── license.tmpl │ ├── locale │ │ ├── bn_IN.json │ │ ├── de.json │ │ ├── en.json │ │ └── ru.json │ ├── main.js │ ├── main.tmpl │ ├── paste.js │ ├── paste.tmpl │ ├── paste_continue.tmpl │ ├── settings.tmpl │ ├── source_code.tmpl │ ├── style.css │ ├── terms.tmpl │ └── theme │ │ ├── dark.theme │ │ └── light.theme │ ├── kvcfg.go │ ├── share.go │ ├── web.go │ ├── web_about.go │ ├── web_dl.go │ ├── web_docs.go │ ├── web_embedded.go │ ├── web_embedded_help.go │ ├── web_error.go │ ├── web_get.go │ ├── web_highlight.go │ ├── web_new.go │ ├── web_other.go │ ├── web_redirect.go │ ├── web_settings.go │ ├── web_sitemap.go │ ├── web_terms.go │ ├── web_themes.go │ └── web_translate.go └── tools ├── kvcfg-to-json ├── kvcfg.go └── main.go └── localefmt.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !.git/** 3 | !cmd/**/*.go 4 | !internal/**/*.go 5 | !internal/web/data/** 6 | !vendor/** 7 | !entrypoint.sh 8 | !go.mod 9 | !go.sum 10 | !Makefile 11 | -------------------------------------------------------------------------------- /.github/workflows/release_docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image 2 | on: 3 | push: 4 | tags: 5 | - v** 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: read 12 | packages: write 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Get version 20 | shell: bash 21 | run: echo "version=$(git describe --tags --always | sed 's/-/+/' | sed 's/^v//')" >> $GITHUB_ENV 22 | 23 | - name: Login to GitHub Container Registry 24 | uses: docker/login-action@v3 25 | with: 26 | registry: ghcr.io 27 | username: ${{github.repository_owner}} 28 | password: ${{secrets.GITHUB_TOKEN}} 29 | 30 | - name: Set up QEMU 31 | uses: docker/setup-qemu-action@v3 32 | 33 | - name: Set up Docker Buildx 34 | uses: docker/setup-buildx-action@v3 35 | 36 | - name: Build and push 37 | id: docker_build 38 | uses: docker/build-push-action@v5 39 | with: 40 | context: ./ 41 | file: ./Dockerfile 42 | push: true 43 | tags: ghcr.io/${{github.repository_owner}}/lenpaste:${{env.version}} 44 | platforms: linux/amd64,linux/arm64/v8,linux/arm/v7,linux/arm/v6 45 | 46 | - name: Image digest 47 | run: echo ${{ steps.docker_build.outputs.digest }} 48 | -------------------------------------------------------------------------------- /.github/workflows/release_sources.yml: -------------------------------------------------------------------------------- 1 | name: Tarball 2 | on: 3 | push: 4 | tags: 5 | - v** 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Get version 19 | shell: bash 20 | run: echo "version=$(git describe --tags --always | sed 's/-/+/' | sed 's/^v//')" >> $GITHUB_ENV 21 | 22 | - name: Install dependencies 23 | shell: bash 24 | run: sudo apt-get install -y make golang 25 | 26 | - name: Create tarball 27 | shell: bash 28 | run: |- 29 | make tarball 30 | sha256sum ./dist/sources/lenpaste-${{env.version}}.tar.gz 31 | 32 | - name: Attach tarball to release 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | run: gh release upload v${{env.version}} ./dist/sources/lenpaste-${{env.version}}.tar.gz 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode/ 2 | !/.vscode/extensions.json 3 | 4 | /dist/ 5 | /vendor/ 6 | **.bak 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | Semantic versioning is used (https://semver.org/). 3 | 4 | 5 | ## v1.3.1 6 | - Fixed a problem with building Lenpaste from source code. 7 | - Revised documentation. 8 | - Minor improvements were made. 9 | 10 | ## v1.3 11 | - UI: Added custom themes support. Added light theme. 12 | - UI: Added translations into Bengali and German (thanks Pardesi_Cat and Hiajen). 13 | - UI: Check boxes and spoilers now have a custom design. 14 | - Admin: Added support for `X-Real-IP` header for reverse proxy. 15 | - Admin: Added Server response header (for example: `Lenpaste/1.3`). 16 | - Fix: many bugs and errors. 17 | - Dev: Improved quality of `Dockerfile` and `entrypoint.sh` 18 | 19 | ## v1.2 20 | - UI: Add history tab. 21 | - UI: Add copy to clipboard button. 22 | - Admin: Rate-limits on paste creation (`LENPASTE_NEW_PASTES_PER_5MIN` or `-new-pastes-per-5min`). 23 | - Admin: Add terms of use support (`/data/terms` or `-server-terms`). 24 | - Admin: Add default paste life time for WEB interface (`LENPASTE_UI_DEFAULT_LIFETIME` or `-ui-default-lifetime`). 25 | - Admin: Private servers - password request to create paste (`/data/lenpasswd` or `-lenpasswd-file`). 26 | - Fix: **Critical security fix!** 27 | - Fix: not saving cookies. 28 | - Fix: display language name in WEB. 29 | - Fix: compatibility with WebKit (Gnome WEB). 30 | - Dev: Drop Go 1.15 support. Update dependencies. 31 | 32 | 33 | ## v1.1.1 34 | - Fixed: Incorrect operation of the maximum paste life parameter. 35 | - Updated README. 36 | 37 | 38 | ## v1.1 39 | - You can now specify author, author email and author URL for paste. 40 | - Full localization into Russian. 41 | - Added settings menu. 42 | - Paste creation and expiration times are now displayed in the user's time zone. 43 | - Add PostgreSQL DB support. 44 | 45 | 46 | ## v1.0 47 | This is the first stable release of Lenpaste🎉 48 | 49 | Compared to the previous unstable versions, everything has been drastically improved: 50 | design, loading speed of the pages, API, work with the database. 51 | Plus added syntax highlighting in the web interface. 52 | 53 | 54 | ## v0.2 55 | Features: 56 | - Paste title 57 | - About server information 58 | - Improved documentation 59 | - Logging and log rotation 60 | - Storage configuration 61 | - Code optimization 62 | 63 | Bug fixes: 64 | - Added `./version.json` to Docker image 65 | - Added paste expiration check before opening 66 | - Fixed incorrect error of expired pastes 67 | - API errors now return in JSON 68 | 69 | 70 | ## v0.1 71 | Features: 72 | - Alternative to pastebin.com 73 | - Creating expiration pastes 74 | - Web interface 75 | - API 76 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # BUILD 2 | FROM docker.io/library/debian:bookworm-20231120-slim as build 3 | 4 | WORKDIR /build 5 | 6 | RUN sed -i '/^URIs:/d' /etc/apt/sources.list.d/debian.sources && \ 7 | sed -i 's/^# http/URIs: http/' /etc/apt/sources.list.d/debian.sources && \ 8 | apt-get update -o Acquire::Check-Valid-Until=false && \ 9 | apt-get install --no-install-recommends -y make git golang gcc libc6-dev ca-certificates && \ 10 | apt-get clean 11 | 12 | COPY ./go.mod ./go.sum ./ 13 | RUN go mod download 14 | 15 | COPY . ./ 16 | RUN make 17 | 18 | # RUN 19 | FROM docker.io/library/debian:bookworm-20231120-slim 20 | 21 | COPY ./entrypoint.sh / 22 | 23 | RUN mkdir /data/ && chmod +x /entrypoint.sh 24 | 25 | VOLUME /data 26 | EXPOSE 80/tcp 27 | 28 | COPY --from=build /build/dist/bin/* /usr/local/bin/ 29 | 30 | CMD [ "/entrypoint.sh" ] 31 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GO ?= go 2 | GOFMT ?= gofmt 3 | 4 | NAME = lenpaste 5 | VERSION = $(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//') 6 | 7 | MAIN_GO = ./cmd/$(NAME)/*.go 8 | LDFLAGS = -w -s -X "main.Version=$(VERSION)" 9 | 10 | .PHONY: all tarball fmt clean 11 | 12 | all: 13 | mkdir -p ./dist/bin/ 14 | 15 | $(GO) build -trimpath -ldflags="$(LDFLAGS)" -o ./dist/bin/$(NAME) $(MAIN_GO) 16 | chmod +x ./dist/bin/$(NAME) 17 | 18 | tarball: 19 | mkdir -p ./dist/tmp/$(NAME)-$(VERSION)/ 20 | cp -r $(filter-out ./. ./.. ./.git ./dist,$(shell echo ./* ./.*)) ./dist/tmp/$(NAME)-$(VERSION)/ 21 | 22 | go mod vendor -o ./dist/tmp/$(NAME)-$(VERSION)/vendor/ 23 | 24 | sed -i '/^COPY .*go.mod/d' ./dist/tmp/$(NAME)-$(VERSION)/Dockerfile 25 | sed -i '/^RUN go mod download/d' ./dist/tmp/$(NAME)-$(VERSION)/Dockerfile 26 | sed -i "s/ git / /" ./dist/tmp/$(NAME)-$(VERSION)/Dockerfile 27 | sed -i "s/ ca-certificates / /" ./dist/tmp/$(NAME)-$(VERSION)/Dockerfile 28 | 29 | sed -i "/^VERSION[[:space:]]*=/c\VERSION=$(VERSION)" ./dist/tmp/$(NAME)-$(VERSION)/Makefile 30 | sed -i "s/\$$(GO) build/\$$(GO) build -mod=vendor/" ./dist/tmp/$(NAME)-$(VERSION)/Makefile 31 | 32 | mkdir -p ./dist/sources/ 33 | tar --mtime="$(git log -1 --format=%ai)" \ 34 | -C ./dist/tmp/ \ 35 | -zcf ./dist/sources/$(NAME)-$(VERSION).tar.gz $(NAME)-$(VERSION)/ 36 | rm -rf ./dist/tmp/ 37 | 38 | fmt: 39 | @$(GOFMT) -w $(shell find ./ -type f -name '*.go') 40 | 41 | clean: 42 | rm -rf ./dist/ 43 | -------------------------------------------------------------------------------- /cmd/lenpaste/lenpaste.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | package main 20 | 21 | import ( 22 | "errors" 23 | "fmt" 24 | "io" 25 | "net/http" 26 | "os" 27 | "strconv" 28 | "time" 29 | 30 | "github.com/lcomrade/lenpaste/internal/apiv1" 31 | "github.com/lcomrade/lenpaste/internal/cli" 32 | "github.com/lcomrade/lenpaste/internal/config" 33 | "github.com/lcomrade/lenpaste/internal/logger" 34 | "github.com/lcomrade/lenpaste/internal/netshare" 35 | "github.com/lcomrade/lenpaste/internal/raw" 36 | "github.com/lcomrade/lenpaste/internal/storage" 37 | "github.com/lcomrade/lenpaste/internal/web" 38 | ) 39 | 40 | var Version = "unknown" 41 | 42 | func readFile(path string) (string, error) { 43 | // Open file 44 | file, err := os.Open(path) 45 | if err != nil { 46 | return "", err 47 | } 48 | defer file.Close() 49 | 50 | // Read file 51 | fileByte, err := io.ReadAll(file) 52 | if err != nil { 53 | return "", err 54 | } 55 | 56 | // Return result 57 | return string(fileByte), nil 58 | } 59 | 60 | func exitOnError(e error) { 61 | fmt.Fprintln(os.Stderr, "error:", e.Error()) 62 | os.Exit(1) 63 | } 64 | 65 | func main() { 66 | var err error 67 | 68 | // Read environment variables and CLI flags 69 | c := cli.New(Version) 70 | 71 | flagAddress := c.AddStringVar("address", ":80", "HTTP server ADDRESS:PORT.", nil) 72 | 73 | flagDbDriver := c.AddStringVar("db-driver", "sqlite3", "Currently supported drivers: \"sqlite3\" and \"postgres\".", nil) 74 | flagDbSource := c.AddStringVar("db-source", "", "DB source.", &cli.FlagOptions{Required: true}) 75 | flagDbMaxOpenConns := c.AddIntVar("db-max-open-conns", 25, "Maximum number of connections to the database.", nil) 76 | flagDbMaxIdleConns := c.AddIntVar("db-max-idle-conns", 5, "Maximum number of idle connections to the database.", nil) 77 | flagDbCleanupPeriod := c.AddDurationVar("db-cleanup-period", "1m", "Interval at which the DB is cleared of expired but not yet deleted pastes.", nil) 78 | 79 | flagRobotsDisallow := c.AddBoolVar("robots-disallow", "Prohibits search engine crawlers from indexing site using robots.txt file.") 80 | 81 | flagTitleMaxLen := c.AddIntVar("title-max-length", 100, "Maximum length of the paste title. If 0 disable title, if -1 disable length limit.", nil) 82 | flagBodyMaxLen := c.AddIntVar("body-max-length", 20000, "Maximum length of the paste body. If -1 disable length limit. Can't be -1.", nil) 83 | flagMaxLifetime := c.AddDurationVar("max-paste-lifetime", "unlimited", "Maximum lifetime of the paste. Examples: 10m, 1h 30m, 12h, 1w, 30d, 365d.", &cli.FlagOptions{ 84 | PreHook: func(s string) (string, error) { 85 | if s == "never" || s == "unlimited" { 86 | return "", nil 87 | } 88 | 89 | return s, nil 90 | }, 91 | }) 92 | 93 | flagGetPastesPer5Min := c.AddUintVar("get-pastes-per-5min", 50, "Maximum number of pastes that can be VIEWED in 5 minutes from one IP. If 0 disable rate-limit.", nil) 94 | flagGetPastesPer15Min := c.AddUintVar("get-pastes-per-15min", 100, "Maximum number of pastes that can be VIEWED in 15 minutes from one IP. If 0 disable rate-limit.", nil) 95 | flagGetPastesPer1Hour := c.AddUintVar("get-pastes-per-1hour", 500, "Maximum number of pastes that can be VIEWED in 1 hour from one IP. If 0 disable rate-limit.", nil) 96 | flagNewPastesPer5Min := c.AddUintVar("new-pastes-per-5min", 15, "Maximum number of pastes that can be CREATED in 5 minutes from one IP. If 0 disable rate-limit.", nil) 97 | flagNewPastesPer15Min := c.AddUintVar("new-pastes-per-15min", 30, "Maximum number of pastes that can be CREATED in 15 minutes from one IP. If 0 disable rate-limit.", nil) 98 | flagNewPastesPer1Hour := c.AddUintVar("new-pastes-per-1hour", 40, "Maximum number of pastes that can be CREATED in 1 hour from one IP. If 0 disable rate-limit.", nil) 99 | 100 | flagServerAbout := c.AddStringVar("server-about", "", "Path to the TXT file that contains the server description.", nil) 101 | flagServerRules := c.AddStringVar("server-rules", "", "Path to the TXT file that contains the server rules.", nil) 102 | flagServerTerms := c.AddStringVar("server-terms", "", "Path to the TXT file that contains the server terms of use.", nil) 103 | 104 | flagAdminName := c.AddStringVar("admin-name", "", "Name of the administrator of this server.", nil) 105 | flagAdminMail := c.AddStringVar("admin-mail", "", "Email of the administrator of this server.", nil) 106 | 107 | flagUiDefaultLifetime := c.AddStringVar("ui-default-lifetime", "", "Lifetime of paste will be set by default in WEB interface. Examples: 10min, 1h, 1d, 2w, 6mon, 1y.", nil) 108 | flagUiDefaultTheme := c.AddStringVar("ui-default-theme", "dark", "Sets the default theme for the WEB interface. Examples: dark, light, my_theme.", nil) 109 | flagUiThemesDir := c.AddStringVar("ui-themes-dir", "", "Loads external WEB interface themes from directory.", nil) 110 | 111 | flagLenPasswdFile := c.AddStringVar("lenpasswd-file", "", "File in LenPasswd format. If set, authorization will be required to create pastes.", nil) 112 | 113 | c.Parse() 114 | 115 | // -body-max-length flag 116 | if *flagBodyMaxLen == 0 { 117 | exitOnError(errors.New("maximum body length cannot be 0")) 118 | } 119 | 120 | // -max-paste-lifetime 121 | maxLifeTime := int64(-1) 122 | 123 | if *flagMaxLifetime != 0 && *flagMaxLifetime < 600 { 124 | exitOnError(errors.New("maximum paste lifetime flag cannot have a value less than 10 minutes")) 125 | maxLifeTime = int64(*flagMaxLifetime / time.Second) 126 | } 127 | 128 | // Load server about 129 | serverAbout := "" 130 | if *flagServerAbout != "" { 131 | serverAbout, err = readFile(*flagServerAbout) 132 | if err != nil { 133 | exitOnError(err) 134 | } 135 | } 136 | 137 | // Load server rules 138 | serverRules := "" 139 | if *flagServerRules != "" { 140 | serverRules, err = readFile(*flagServerRules) 141 | if err != nil { 142 | exitOnError(err) 143 | } 144 | } 145 | 146 | // Load server "terms of use" 147 | serverTermsOfUse := "" 148 | if *flagServerTerms != "" { 149 | if serverRules == "" { 150 | exitOnError(errors.New("in order to set the Terms of Use you must also specify the Server Rules")) 151 | } 152 | 153 | serverTermsOfUse, err = readFile(*flagServerTerms) 154 | if err != nil { 155 | exitOnError(err) 156 | } 157 | } 158 | 159 | // Settings 160 | log := logger.New("2006/01/02 15:04:05") 161 | 162 | db, err := storage.NewPool(*flagDbDriver, *flagDbSource, *flagDbMaxOpenConns, *flagDbMaxIdleConns) 163 | if err != nil { 164 | exitOnError(err) 165 | } 166 | 167 | cfg := config.Config{ 168 | Log: log, 169 | RateLimitGet: netshare.NewRateLimitSystem(*flagGetPastesPer5Min, *flagGetPastesPer15Min, *flagGetPastesPer1Hour), 170 | RateLimitNew: netshare.NewRateLimitSystem(*flagNewPastesPer5Min, *flagNewPastesPer15Min, *flagNewPastesPer1Hour), 171 | Version: Version, 172 | TitleMaxLen: *flagTitleMaxLen, 173 | BodyMaxLen: *flagBodyMaxLen, 174 | MaxLifeTime: maxLifeTime, 175 | ServerAbout: serverAbout, 176 | ServerRules: serverRules, 177 | ServerTermsOfUse: serverTermsOfUse, 178 | AdminName: *flagAdminName, 179 | AdminMail: *flagAdminMail, 180 | RobotsDisallow: *flagRobotsDisallow, 181 | UiDefaultLifetime: *flagUiDefaultLifetime, 182 | UiDefaultTheme: *flagUiDefaultTheme, 183 | UiThemesDir: *flagUiThemesDir, 184 | LenPasswdFile: *flagLenPasswdFile, 185 | } 186 | 187 | apiv1Data := apiv1.Load(db, cfg) 188 | 189 | rawData := raw.Load(db, cfg) 190 | 191 | // Init data base 192 | err = storage.InitDB(*flagDbDriver, *flagDbSource) 193 | if err != nil { 194 | exitOnError(err) 195 | } 196 | 197 | // Load pages 198 | webData, err := web.Load(db, cfg) 199 | if err != nil { 200 | exitOnError(err) 201 | } 202 | 203 | // Handlers 204 | http.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) { 205 | webData.Handler(rw, req) 206 | }) 207 | http.HandleFunc("/raw/", func(rw http.ResponseWriter, req *http.Request) { 208 | rawData.Hand(rw, req) 209 | }) 210 | http.HandleFunc("/api/", func(rw http.ResponseWriter, req *http.Request) { 211 | apiv1Data.Hand(rw, req) 212 | }) 213 | 214 | // Run background job 215 | go func(cleanJobPeriod time.Duration) { 216 | for { 217 | // Delete expired pastes 218 | count, err := db.PasteDeleteExpired() 219 | if err != nil { 220 | log.Error(errors.New("Delete expired: " + err.Error())) 221 | } 222 | 223 | log.Info("Delete " + strconv.FormatInt(count, 10) + " expired pastes") 224 | 225 | // Wait 226 | time.Sleep(cleanJobPeriod) 227 | } 228 | }(*flagDbCleanupPeriod) 229 | 230 | // Run HTTP server 231 | log.Info("Run HTTP server on " + *flagAddress) 232 | err = http.ListenAndServe(*flagAddress, nil) 233 | if err != nil { 234 | exitOnError(err) 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | RUN_CMD="lenpaste" 5 | 6 | 7 | # LENPASTE_ADDRESS 8 | if [ -n "$LENPASTE_ADDRESS" ]; then 9 | RUN_CMD="$RUN_CMD -address $LENPASTE_ADDRESS" 10 | fi 11 | 12 | 13 | # LENPASTE_DB_DRIVER 14 | if [ -n "$LENPASTE_DB_DRIVER" ]; then 15 | RUN_CMD="$RUN_CMD -db-driver '$LENPASTE_DB_DRIVER'" 16 | fi 17 | 18 | 19 | # LENPASTE_DB_SOURCE 20 | if [ -z "$LENPASTE_DB_DRIVER" ] || [ "$LENPASTE_DB_DRIVER" = "sqlite3" ]; then 21 | RUN_CMD="$RUN_CMD -db-source /data/lenpaste.db" 22 | 23 | else 24 | RUN_CMD="$RUN_CMD -db-source '$LENPASTE_DB_SOURCE'" 25 | fi 26 | 27 | 28 | # LENPASTE_DB_MAX_OPEN_CONNS 29 | if [ -n "$LENPASTE_DB_MAX_OPEN_CONNS" ]; then 30 | RUN_CMD="$RUN_CMD -db-max-open-conns '$LENPASTE_DB_MAX_OPEN_CONNS'" 31 | fi 32 | 33 | 34 | # LENPASTE_DB_MAX_IDLE_CONNS 35 | if [ -n "$LENPASTE_DB_MAX_IDLE_CONNS" ]; then 36 | RUN_CMD="$RUN_CMD -db-max-idle-conns '$LENPASTE_DB_MAX_IDLE_CONNS'" 37 | fi 38 | 39 | 40 | # LENPASTE_DB_CLEANUP_PERIOD 41 | if [ -n "$LENPASTE_DB_CLEANUP_PERIOD" ]; then 42 | RUN_CMD="$RUN_CMD -db-cleanup-period '$LENPASTE_DB_CLEANUP_PERIOD'" 43 | fi 44 | 45 | # LENPASTE_ROBOTS_DISALLOW 46 | if [ "$LENPASTE_ROBOTS_DISALLOW" = "true" ]; then 47 | RUN_CMD="$RUN_CMD -robots-disallow" 48 | 49 | else 50 | if [ "$LENPASTE_ROBOTS_DISALLOW" != "" ] && [ "$LENPASTE_ROBOTS_DISALLOW" != "false" ]; then 51 | echo "[ENTRYPOINT] Error: unknown: LENPASTE_ROBOTS_DISALLOW = $LENPASTE_ROBOTS_DISALLOW" 52 | exit 2 53 | fi 54 | fi 55 | 56 | 57 | # LENPASTE_TITLE_MAX_LENGTH 58 | if [ -n "$LENPASTE_TITLE_MAX_LENGTH" ]; then 59 | RUN_CMD="$RUN_CMD -title-max-length '$LENPASTE_TITLE_MAX_LENGTH'" 60 | fi 61 | 62 | 63 | # LENPASTE_BODY_MAX_LENGTH 64 | if [ -n "$LENPASTE_BODY_MAX_LENGTH" ]; then 65 | RUN_CMD="$RUN_CMD -body-max-length '$LENPASTE_BODY_MAX_LENGTH'" 66 | fi 67 | 68 | 69 | # LENPASTE_MAX_PASTE_LIFETIME 70 | if [ -n "$LENPASTE_MAX_PASTE_LIFETIME" ]; then 71 | RUN_CMD="$RUN_CMD -max-paste-lifetime '$LENPASTE_MAX_PASTE_LIFETIME'" 72 | fi 73 | 74 | # Rate limits to get 75 | if [ -n "$LENPASTE_GET_PASTES_PER_5MIN" ]; then 76 | RUN_CMD="$RUN_CMD -get-pastes-per-5min '$LENPASTE_GET_PASTES_PER_5MIN'" 77 | fi 78 | 79 | if [ -n "$LENPASTE_GET_PASTES_PER_15MIN" ]; then 80 | RUN_CMD="$RUN_CMD -get-pastes-per-15min '$LENPASTE_GET_PASTES_PER_15MIN'" 81 | fi 82 | 83 | if [ -n "$LENPASTE_GET_PASTES_PER_1HOUR" ]; then 84 | RUN_CMD="$RUN_CMD -get-pastes-per-1hour '$LENPASTE_GET_PASTES_PER_1HOUR'" 85 | fi 86 | 87 | 88 | # Rate limits to create 89 | if [ -n "$LENPASTE_NEW_PASTES_PER_5MIN" ]; then 90 | RUN_CMD="$RUN_CMD -new-pastes-per-5min '$LENPASTE_NEW_PASTES_PER_5MIN'" 91 | fi 92 | 93 | if [ -n "$LENPASTE_NEW_PASTES_PER_15MIN" ]; then 94 | RUN_CMD="$RUN_CMD -new-pastes-per-15min '$LENPASTE_NEW_PASTES_PER_15MIN'" 95 | fi 96 | 97 | if [ -n "$LENPASTE_NEW_PASTES_PER_1HOUR" ]; then 98 | RUN_CMD="$RUN_CMD -new-pastes-per-1hour '$LENPASTE_NEW_PASTES_PER_1HOUR'" 99 | fi 100 | 101 | 102 | 103 | # Server about 104 | if [ -f "/data/about" ]; then 105 | RUN_CMD="$RUN_CMD -server-about /data/about" 106 | fi 107 | 108 | 109 | # Server rules 110 | if [ -f "/data/rules" ]; then 111 | RUN_CMD="$RUN_CMD -server-rules /data/rules" 112 | fi 113 | 114 | 115 | # Server terms of use 116 | if [ -f "/data/terms" ]; then 117 | RUN_CMD="$RUN_CMD -server-terms /data/terms" 118 | fi 119 | 120 | 121 | # LENPASTE_ADMIN_NAME 122 | if [ -n "$LENPASTE_ADMIN_NAME" ]; then 123 | RUN_CMD="$RUN_CMD -admin-name '$LENPASTE_ADMIN_NAME'" 124 | fi 125 | 126 | 127 | # LENPASTE_ADMIN_MAIL 128 | if [ -n "$LENPASTE_ADMIN_MAIL" ]; then 129 | RUN_CMD="$RUN_CMD -admin-mail '$LENPASTE_ADMIN_MAIL'" 130 | fi 131 | 132 | 133 | # LENPASTE_UI_DEFAULT_LIFETIME 134 | if [ -n "$LENPASTE_UI_DEFAULT_LIFETIME" ]; then 135 | RUN_CMD="$RUN_CMD -ui-default-lifetime '$LENPASTE_UI_DEFAULT_LIFETIME'" 136 | fi 137 | 138 | 139 | # LENPASTE_UI_DEFAULT_THEME 140 | if [ -n "$LENPASTE_UI_DEFAULT_THEME" ]; then 141 | RUN_CMD="$RUN_CMD -ui-default-theme $LENPASTE_UI_DEFAULT_THEME" 142 | fi 143 | 144 | 145 | # External UI themes 146 | if [ -d "/data/themes" ]; then 147 | RUN_CMD="$RUN_CMD -ui-themes-dir /data/themes" 148 | fi 149 | 150 | 151 | # Lenpsswd file 152 | if [ -f "/data/lenpasswd" ]; then 153 | RUN_CMD="$RUN_CMD -lenpasswd-file /data/lenpasswd" 154 | fi 155 | 156 | 157 | # Run Lenpaste 158 | echo "[ENTRYPOINT] $RUN_CMD" 159 | sh -c "$RUN_CMD" 160 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lcomrade/lenpaste 2 | 3 | go 1.16 4 | 5 | replace github.com/lcomrade/lenpaste/internal => ./internal 6 | 7 | require ( 8 | github.com/alecthomas/chroma/v2 v2.4.0 9 | github.com/lib/pq v1.10.7 10 | github.com/mattn/go-sqlite3 v1.14.16 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/lcomrade/lenpaste/internal/lineend v1.0.0 h1:qcxrR4DS18Erx+pG/EspoDhEDai+mjgSYefwSK2dq5g= 2 | github.com/lcomrade/lenpaste/internal/lineend v1.0.0/go.mod h1:D0q3jMx0I1PFZjAFwkmUNW8D5tIw8rZJoeUAxhYD7Ec= 3 | github.com/alecthomas/assert/v2 v2.2.0 h1:f6L/b7KE2bfA+9O4FL3CM/xJccDEwPVYd5fALBiuwvw= 4 | github.com/alecthomas/assert/v2 v2.2.0/go.mod h1:b/+1DI2Q6NckYi+3mXyH3wFb8qG37K/DuK80n7WefXA= 5 | github.com/alecthomas/chroma/v2 v2.4.0 h1:Loe2ZjT5x3q1bcWwemqyqEi8p11/IV/ncFCeLYDpWC4= 6 | github.com/alecthomas/chroma/v2 v2.4.0/go.mod h1:6kHzqF5O6FUSJzBXW7fXELjb+e+7OXW4UpoPqMO7IBQ= 7 | github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE= 8 | github.com/alecthomas/repr v0.1.0/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= 9 | github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= 10 | github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= 11 | github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= 12 | github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 13 | github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= 14 | github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 15 | github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= 16 | github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= 17 | -------------------------------------------------------------------------------- /internal/apiv1/api.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | package apiv1 20 | 21 | import ( 22 | chromaLexers "github.com/alecthomas/chroma/v2/lexers" 23 | "github.com/lcomrade/lenpaste/internal/config" 24 | "github.com/lcomrade/lenpaste/internal/logger" 25 | "github.com/lcomrade/lenpaste/internal/netshare" 26 | "github.com/lcomrade/lenpaste/internal/storage" 27 | "net/http" 28 | ) 29 | 30 | type Data struct { 31 | Log logger.Logger 32 | DB storage.DB 33 | 34 | RateLimitNew *netshare.RateLimitSystem 35 | RateLimitGet *netshare.RateLimitSystem 36 | 37 | Lexers []string 38 | 39 | Version string 40 | 41 | TitleMaxLen int 42 | BodyMaxLen int 43 | MaxLifeTime int64 44 | 45 | ServerAbout string 46 | ServerRules string 47 | ServerTermsOfUse string 48 | 49 | AdminName string 50 | AdminMail string 51 | 52 | LenPasswdFile string 53 | 54 | UiDefaultLifeTime string 55 | } 56 | 57 | func Load(db storage.DB, cfg config.Config) *Data { 58 | lexers := chromaLexers.Names(false) 59 | 60 | return &Data{ 61 | DB: db, 62 | Log: cfg.Log, 63 | RateLimitNew: cfg.RateLimitNew, 64 | RateLimitGet: cfg.RateLimitGet, 65 | Lexers: lexers, 66 | Version: cfg.Version, 67 | TitleMaxLen: cfg.TitleMaxLen, 68 | BodyMaxLen: cfg.BodyMaxLen, 69 | MaxLifeTime: cfg.MaxLifeTime, 70 | ServerAbout: cfg.ServerAbout, 71 | ServerRules: cfg.ServerRules, 72 | ServerTermsOfUse: cfg.ServerTermsOfUse, 73 | AdminName: cfg.AdminName, 74 | AdminMail: cfg.AdminMail, 75 | LenPasswdFile: cfg.LenPasswdFile, 76 | UiDefaultLifeTime: cfg.UiDefaultLifetime, 77 | } 78 | } 79 | 80 | func (data *Data) Hand(rw http.ResponseWriter, req *http.Request) { 81 | // Process request 82 | var err error 83 | 84 | rw.Header().Set("Server", config.Software+"/"+data.Version) 85 | 86 | switch req.URL.Path { 87 | // Search engines 88 | case "/api/v1/new": 89 | err = data.newHand(rw, req) 90 | case "/api/v1/get": 91 | err = data.getHand(rw, req) 92 | case "/api/v1/getServerInfo": 93 | err = data.getServerInfoHand(rw, req) 94 | default: 95 | err = netshare.ErrNotFound 96 | } 97 | 98 | // Log 99 | if err == nil { 100 | data.Log.HttpRequest(req, 200) 101 | 102 | } else { 103 | code, err := data.writeError(rw, req, err) 104 | if err != nil { 105 | data.Log.HttpError(req, err) 106 | } else { 107 | data.Log.HttpRequest(req, code) 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /internal/apiv1/api_error.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | package apiv1 20 | 21 | import ( 22 | "encoding/json" 23 | "errors" 24 | "github.com/lcomrade/lenpaste/internal/netshare" 25 | "github.com/lcomrade/lenpaste/internal/storage" 26 | "net/http" 27 | "strconv" 28 | ) 29 | 30 | type errorType struct { 31 | Code int `json:"code"` 32 | Error string `json:"error"` 33 | } 34 | 35 | func (data *Data) writeError(rw http.ResponseWriter, req *http.Request, e error) (int, error) { 36 | var resp errorType 37 | 38 | var eTmp429 *netshare.ErrTooManyRequests 39 | 40 | if e == netshare.ErrBadRequest { 41 | resp.Code = 400 42 | resp.Error = "Bad Request" 43 | 44 | } else if e == netshare.ErrUnauthorized { 45 | rw.Header().Add("WWW-Authenticate", "Basic") 46 | resp.Code = 401 47 | resp.Error = "Unauthorized" 48 | 49 | } else if e == storage.ErrNotFoundID { 50 | resp.Code = 404 51 | resp.Error = "Could not find ID" 52 | 53 | } else if e == netshare.ErrNotFound { 54 | resp.Code = 404 55 | resp.Error = "Not Found" 56 | 57 | } else if e == netshare.ErrMethodNotAllowed { 58 | resp.Code = 405 59 | resp.Error = "Method Not Allowed" 60 | 61 | } else if e == netshare.ErrPayloadTooLarge { 62 | resp.Code = 413 63 | resp.Error = "Payload Too Large" 64 | 65 | } else if errors.As(e, &eTmp429) { 66 | resp.Code = 429 67 | resp.Error = "Too Many Requests" 68 | rw.Header().Set("Retry-After", strconv.FormatInt(eTmp429.RetryAfter, 10)) 69 | 70 | } else { 71 | resp.Code = 500 72 | resp.Error = "Internal Server Error" 73 | } 74 | 75 | rw.Header().Set("Content-Type", "application/json") 76 | rw.WriteHeader(resp.Code) 77 | 78 | err := json.NewEncoder(rw).Encode(resp) 79 | if err != nil { 80 | return 500, err 81 | } 82 | 83 | return resp.Code, nil 84 | } 85 | -------------------------------------------------------------------------------- /internal/apiv1/api_get.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | package apiv1 20 | 21 | import ( 22 | "encoding/json" 23 | "github.com/lcomrade/lenpaste/internal/netshare" 24 | "github.com/lcomrade/lenpaste/internal/storage" 25 | "net/http" 26 | ) 27 | 28 | // GET /api/v1/get 29 | func (data *Data) getHand(rw http.ResponseWriter, req *http.Request) error { 30 | // Check rate limit 31 | err := data.RateLimitGet.CheckAndUse(netshare.GetClientAddr(req)) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | // Check method 37 | if req.Method != "GET" { 38 | return netshare.ErrMethodNotAllowed 39 | } 40 | 41 | // Get paste ID 42 | req.ParseForm() 43 | 44 | pasteID := req.Form.Get("id") 45 | 46 | // Check paste id 47 | if pasteID == "" { 48 | return netshare.ErrBadRequest 49 | } 50 | 51 | // Get paste 52 | paste, err := data.DB.PasteGet(pasteID) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | // If "one use" paste 58 | if paste.OneUse == true { 59 | if req.Form.Get("openOneUse") == "true" { 60 | // Delete paste 61 | err = data.DB.PasteDelete(pasteID) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | } else { 67 | // Remove secret data 68 | paste = storage.Paste{ 69 | ID: paste.ID, 70 | OneUse: true, 71 | } 72 | } 73 | } 74 | 75 | // Return response 76 | rw.Header().Set("Content-Type", "application/json") 77 | return json.NewEncoder(rw).Encode(paste) 78 | } 79 | -------------------------------------------------------------------------------- /internal/apiv1/api_main.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | package apiv1 20 | 21 | import ( 22 | "github.com/lcomrade/lenpaste/internal/netshare" 23 | "net/http" 24 | ) 25 | 26 | // GET /api/v1/ 27 | func (data *Data) MainHand(rw http.ResponseWriter, req *http.Request) { 28 | data.writeError(rw, req, netshare.ErrNotFound) 29 | } 30 | -------------------------------------------------------------------------------- /internal/apiv1/api_new.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | package apiv1 20 | 21 | import ( 22 | "encoding/json" 23 | "github.com/lcomrade/lenpaste/internal/lenpasswd" 24 | "github.com/lcomrade/lenpaste/internal/netshare" 25 | "net/http" 26 | ) 27 | 28 | type newPasteAnswer struct { 29 | ID string `json:"id"` 30 | CreateTime int64 `json:"createTime"` 31 | DeleteTime int64 `json:"deleteTime"` 32 | } 33 | 34 | // POST /api/v1/new 35 | func (data *Data) newHand(rw http.ResponseWriter, req *http.Request) error { 36 | var err error 37 | 38 | // Check auth 39 | if data.LenPasswdFile != "" { 40 | authOk := false 41 | 42 | user, pass, authExist := req.BasicAuth() 43 | if authExist == true { 44 | authOk, err = lenpasswd.LoadAndCheck(data.LenPasswdFile, user, pass) 45 | if err != nil { 46 | return err 47 | } 48 | } 49 | 50 | if authOk == false { 51 | return netshare.ErrUnauthorized 52 | } 53 | } 54 | 55 | // Check method 56 | if req.Method != "POST" { 57 | return netshare.ErrMethodNotAllowed 58 | } 59 | 60 | // Get form data and create paste 61 | pasteID, createTime, deleteTime, err := netshare.PasteAddFromForm(req, data.DB, data.RateLimitNew, data.TitleMaxLen, data.BodyMaxLen, data.MaxLifeTime, data.Lexers) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | // Return response 67 | rw.Header().Set("Content-Type", "application/json") 68 | return json.NewEncoder(rw).Encode(newPasteAnswer{ID: pasteID, CreateTime: createTime, DeleteTime: deleteTime}) 69 | } 70 | -------------------------------------------------------------------------------- /internal/apiv1/api_server.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | package apiv1 20 | 21 | import ( 22 | "encoding/json" 23 | "github.com/lcomrade/lenpaste/internal/netshare" 24 | "net/http" 25 | ) 26 | 27 | type serverInfoType struct { 28 | Software string `json:"software"` 29 | Version string `json:"version"` 30 | TitleMaxLen int `json:"titleMaxlength"` 31 | BodyMaxLen int `json:"bodyMaxlength"` 32 | MaxLifeTime int64 `json:"maxLifeTime"` 33 | ServerAbout string `json:"serverAbout"` 34 | ServerRules string `json:"serverRules"` 35 | ServerTermsOfUse string `json:"serverTermsOfUse"` 36 | AdminName string `json:"adminName"` 37 | AdminMail string `json:"adminMail"` 38 | Syntaxes []string `json:"syntaxes"` 39 | UiDefaultLifeTime string `json:"uiDefaultLifeTime"` 40 | AuthRequired bool `json:"authRequired"` 41 | } 42 | 43 | // GET /api/v1/getServerInfo 44 | func (data *Data) getServerInfoHand(rw http.ResponseWriter, req *http.Request) error { 45 | // Check method 46 | if req.Method != "GET" { 47 | return netshare.ErrMethodNotAllowed 48 | } 49 | 50 | // Prepare data 51 | serverInfo := serverInfoType{ 52 | Software: "Lenpaste", 53 | Version: data.Version, 54 | TitleMaxLen: data.TitleMaxLen, 55 | BodyMaxLen: data.BodyMaxLen, 56 | MaxLifeTime: data.MaxLifeTime, 57 | ServerAbout: data.ServerAbout, 58 | ServerRules: data.ServerRules, 59 | ServerTermsOfUse: data.ServerTermsOfUse, 60 | AdminName: data.AdminName, 61 | AdminMail: data.AdminMail, 62 | Syntaxes: data.Lexers, 63 | UiDefaultLifeTime: data.UiDefaultLifeTime, 64 | AuthRequired: data.LenPasswdFile != "", 65 | } 66 | 67 | // Return response 68 | rw.Header().Set("Content-Type", "application/json") 69 | return json.NewEncoder(rw).Encode(serverInfo) 70 | } 71 | -------------------------------------------------------------------------------- /internal/cli/cli.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | package cli 20 | 21 | import ( 22 | "fmt" 23 | "os" 24 | "strconv" 25 | "time" 26 | ) 27 | 28 | func exitOnError(msg string) { 29 | fmt.Fprintln(os.Stderr, "error:", msg) 30 | os.Exit(1) 31 | } 32 | 33 | type variable struct { 34 | name string 35 | cliFlagName string 36 | 37 | preHook func(string) (string, error) 38 | 39 | value interface{} 40 | valueDefault string 41 | required bool 42 | usage string 43 | } 44 | 45 | type CLI struct { 46 | version string 47 | 48 | vars []variable 49 | } 50 | 51 | type FlagOptions struct { 52 | Required bool 53 | PreHook func(string) (string, error) 54 | } 55 | 56 | func New(version string) *CLI { 57 | return &CLI{ 58 | version: version, 59 | 60 | vars: []variable{}, 61 | } 62 | } 63 | 64 | func (c *CLI) addVar(name string, value interface{}, defValue string, usage string, opts *FlagOptions) { 65 | if name == "" { 66 | panic("cli: add variable: variable name could not be empty") 67 | } 68 | 69 | if usage == "" { 70 | panic("cli: flag \"" + name + "\" has empty \"usage\" field") 71 | } 72 | 73 | if opts == nil { 74 | opts = &FlagOptions{} 75 | } 76 | 77 | c.vars = append(c.vars, variable{ 78 | name: name, 79 | cliFlagName: "-" + name, 80 | 81 | preHook: opts.PreHook, 82 | 83 | value: value, 84 | valueDefault: defValue, 85 | required: opts.Required, 86 | usage: usage, 87 | }) 88 | } 89 | 90 | func (c *CLI) AddStringVar(name, defValue string, usage string, opts *FlagOptions) *string { 91 | if opts != nil { 92 | if opts.PreHook != nil { 93 | var err error 94 | defValue, err = opts.PreHook(defValue) 95 | if err != nil { 96 | panic("cli: add duration variable \"" + name + "\": " + err.Error()) 97 | } 98 | } 99 | } 100 | 101 | val := &defValue 102 | c.addVar(name, val, defValue, usage, opts) 103 | return val 104 | } 105 | 106 | func (c *CLI) AddBoolVar(name string, usage string) *bool { 107 | valVar := false 108 | val := &valVar 109 | c.addVar(name, val, "", usage, nil) 110 | return val 111 | } 112 | 113 | func (c *CLI) AddIntVar(name string, defValue int, usage string, opts *FlagOptions) *int { 114 | val := &defValue 115 | c.addVar(name, val, strconv.Itoa(defValue), usage, opts) 116 | return val 117 | } 118 | 119 | func (c *CLI) AddUintVar(name string, defValue uint, usage string, opts *FlagOptions) *uint { 120 | val := &defValue 121 | c.addVar(name, val, strconv.FormatUint(uint64(defValue), 10), usage, opts) 122 | return val 123 | } 124 | 125 | func (c *CLI) AddDurationVar(name, defValue string, usage string, opts *FlagOptions) *time.Duration { 126 | if opts != nil { 127 | if opts.PreHook != nil { 128 | var err error 129 | defValue, err = opts.PreHook(defValue) 130 | if err != nil { 131 | panic("cli: add duration variable \"" + name + "\": " + err.Error()) 132 | } 133 | } 134 | } 135 | 136 | valDuration, err := parseDuration(defValue) 137 | if err != nil { 138 | panic("cli: add duration variable \"" + name + "\": " + err.Error()) 139 | } 140 | 141 | val := &valDuration 142 | c.addVar(name, val, defValue, usage, opts) 143 | return val 144 | } 145 | 146 | func writeVar(val string, to interface{}, preHook func(string) (string, error)) error { 147 | if preHook != nil { 148 | var err error 149 | val, err = preHook(val) 150 | if err != nil { 151 | return err 152 | } 153 | } 154 | 155 | switch to := to.(type) { 156 | case *string: 157 | *to = val 158 | 159 | case *int: 160 | val, err := strconv.Atoi(val) 161 | if err != nil { 162 | return err 163 | } 164 | *to = val 165 | 166 | case *bool: 167 | val := true 168 | *to = val 169 | 170 | case *uint: 171 | val, err := strconv.ParseUint(val, 10, 64) 172 | if err != nil { 173 | return err 174 | } 175 | *to = uint(val) 176 | 177 | case *time.Duration: 178 | val, err := parseDuration(val) 179 | if err != nil { 180 | return err 181 | } 182 | *to = val 183 | 184 | default: 185 | panic("cli: write variable: unknown \"to\" argument type") 186 | } 187 | 188 | return nil 189 | } 190 | 191 | func (c *CLI) printVersion() { 192 | fmt.Println(c.version) 193 | os.Exit(0) 194 | } 195 | 196 | func (c *CLI) printHelp() { 197 | // Search for the longest flag and required flags list. 198 | var maxFlagSize int 199 | var reqFlags string 200 | 201 | for _, v := range c.vars { 202 | flagSize := len(v.cliFlagName) 203 | if flagSize > maxFlagSize { 204 | maxFlagSize = flagSize 205 | } 206 | 207 | if v.required { 208 | reqFlags += "[" + v.cliFlagName + "] " 209 | } 210 | } 211 | 212 | // Print help 213 | fmt.Println("Usage:", os.Args[0], reqFlags+"[OPTION]...") 214 | fmt.Println("") 215 | 216 | for _, v := range c.vars { 217 | var spaces string 218 | for i := 0; i < maxFlagSize-len(v.cliFlagName)+2; i++ { 219 | spaces += " " 220 | } 221 | 222 | var defaultStr string 223 | if v.valueDefault != "" { 224 | defaultStr = " (default: " + v.valueDefault + ")" 225 | } 226 | 227 | fmt.Println(" ", v.cliFlagName, spaces, v.usage+defaultStr) 228 | } 229 | 230 | fmt.Println() 231 | fmt.Println(" -version Display version and exit.") 232 | fmt.Println(" -help Display this help and exit.") 233 | 234 | os.Exit(0) 235 | } 236 | 237 | func (c *CLI) Parse() { 238 | // The name of variables that were read from environment variables or CLI flags. 239 | // Used to check if "required" flags are present. 240 | readVars := make(map[string]struct{}) 241 | 242 | // Read variables from CLI flags 243 | { 244 | alreadyRead := make(map[string]struct{}) 245 | 246 | var varInProgress *variable 247 | for _, arg := range os.Args[1:] { 248 | if varInProgress == nil { 249 | switch arg { 250 | case "-version": 251 | c.printVersion() 252 | 253 | case "-help": 254 | c.printHelp() 255 | } 256 | 257 | _, exist := alreadyRead[arg] 258 | if exist { 259 | exitOnError("flag \"" + varInProgress.cliFlagName + "\" occurs twice") 260 | } 261 | 262 | ok := false 263 | for _, v := range c.vars { 264 | if v.cliFlagName == arg { 265 | switch v.value.(type) { 266 | case *bool: 267 | // pass 268 | default: 269 | varInProgress = &v 270 | } 271 | 272 | alreadyRead[v.cliFlagName] = struct{}{} 273 | readVars[v.name] = struct{}{} 274 | 275 | ok = true 276 | break 277 | } 278 | } 279 | 280 | if !ok { 281 | exitOnError("unknown flag \"" + arg + "\"") 282 | } 283 | 284 | } else { 285 | err := writeVar(arg, varInProgress.value, varInProgress.preHook) 286 | if err != nil { 287 | exitOnError("read \"" + varInProgress.cliFlagName + "\" flag: " + err.Error()) 288 | } 289 | 290 | varInProgress = nil 291 | } 292 | } 293 | 294 | if varInProgress != nil { 295 | exitOnError("no value for \"" + varInProgress.cliFlagName + "\" flag") 296 | } 297 | } 298 | 299 | // Check required variables 300 | for _, v := range c.vars { 301 | if v.required { 302 | _, ok := readVars[v.name] 303 | if !ok { 304 | exitOnError("\"" + v.cliFlagName + "\" flag is missing") 305 | } 306 | } 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /internal/cli/duration.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | package cli 20 | 21 | import ( 22 | "errors" 23 | "strconv" 24 | "time" 25 | ) 26 | 27 | func parseDuration(s string) (time.Duration, error) { 28 | var out int64 29 | 30 | var tmp string 31 | for _, c := range s { 32 | if c == ' ' { 33 | continue 34 | } 35 | 36 | if '0' <= c && c <= '9' { 37 | tmp += string(c) 38 | continue 39 | } 40 | 41 | val, err := strconv.ParseInt(tmp, 10, 64) 42 | if err != nil { 43 | return 0, errors.New("invalid format \"" + s + "\"") 44 | } 45 | 46 | switch c { 47 | case 'm': 48 | out += val * 60 49 | case 'h': 50 | out += val * 60 * 60 51 | case 'd': 52 | out += val * 60 * 60 * 24 53 | case 'w': 54 | out += val * 60 * 60 * 24 * 7 55 | default: 56 | return 0, errors.New("invalid format \"" + s + "\"") 57 | } 58 | 59 | tmp = "" 60 | } 61 | 62 | return time.Duration(out) * time.Second, nil 63 | } 64 | -------------------------------------------------------------------------------- /internal/cli/duration_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | package cli 20 | 21 | import ( 22 | "testing" 23 | "time" 24 | ) 25 | 26 | func TestParseDuration(t *testing.T) { 27 | testData := map[string]time.Duration{ 28 | "10m": 60 * 10 * time.Second, 29 | "1h 1d": 60 * 60 * 25 * time.Second, 30 | "1h1d": 60 * 60 * 25 * time.Second, 31 | "1w": 60 * 60 * 24 * 7 * time.Second, 32 | "365d": 60 * 60 * 24 * 365 * time.Second, 33 | } 34 | 35 | for s, exp := range testData { 36 | res, err := parseDuration(s) 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | 41 | if exp != res { 42 | t.Error("expected", exp, "but got", res, "(input:", s, ")") 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | package config 20 | 21 | import ( 22 | "github.com/lcomrade/lenpaste/internal/logger" 23 | "github.com/lcomrade/lenpaste/internal/netshare" 24 | ) 25 | 26 | const Software = "Lenpaste" 27 | 28 | type Config struct { 29 | Log logger.Logger 30 | 31 | RateLimitNew *netshare.RateLimitSystem 32 | RateLimitGet *netshare.RateLimitSystem 33 | 34 | Version string 35 | 36 | TitleMaxLen int 37 | BodyMaxLen int 38 | MaxLifeTime int64 39 | 40 | ServerAbout string 41 | ServerRules string 42 | ServerTermsOfUse string 43 | 44 | AdminName string 45 | AdminMail string 46 | 47 | RobotsDisallow bool 48 | 49 | LenPasswdFile string 50 | 51 | UiDefaultLifetime string 52 | UiDefaultTheme string 53 | UiThemesDir string 54 | } 55 | -------------------------------------------------------------------------------- /internal/lenpasswd/lenpasswd.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | package lenpasswd 20 | 21 | import ( 22 | "bytes" 23 | "errors" 24 | "io/ioutil" 25 | "os" 26 | "strconv" 27 | "strings" 28 | ) 29 | 30 | type Data map[string]string 31 | 32 | func LoadFile(path string) (Data, error) { 33 | // Open file 34 | file, err := os.Open(path) 35 | if err != nil { 36 | return nil, errors.New("lenpasswd: " + err.Error()) 37 | } 38 | defer file.Close() 39 | 40 | // Read file 41 | fileByte, err := ioutil.ReadAll(file) 42 | if err != nil { 43 | return nil, errors.New("lenpasswd: " + err.Error()) 44 | } 45 | 46 | // Convert []byte to string 47 | fileTxt := bytes.NewBuffer(fileByte).String() 48 | 49 | // Parse file 50 | data := make(Data) 51 | for i, line := range strings.Split(fileTxt, "\n") { 52 | if line == "" { 53 | continue 54 | } 55 | 56 | lineSplit := strings.Split(line, ":") 57 | if len(lineSplit) != 2 { 58 | return nil, errors.New("lenpasswd: error in line " + strconv.Itoa(i)) 59 | } 60 | 61 | user := lineSplit[0] 62 | pass := lineSplit[1] 63 | 64 | _, exist := data[user] 65 | if exist == true { 66 | return nil, errors.New("lenpasswd: overriding user " + user + " in line " + strconv.Itoa(i)) 67 | } 68 | 69 | data[user] = pass 70 | } 71 | 72 | return data, nil 73 | } 74 | 75 | func (data Data) Check(user string, pass string) bool { 76 | truePass, exist := data[user] 77 | if exist == false { 78 | return false 79 | } 80 | 81 | if pass != truePass { 82 | return false 83 | } 84 | 85 | return true 86 | } 87 | 88 | func LoadAndCheck(path string, user string, pass string) (bool, error) { 89 | data, err := LoadFile(path) 90 | if err != nil { 91 | return false, err 92 | } 93 | 94 | return data.Check(user, pass), nil 95 | } 96 | -------------------------------------------------------------------------------- /internal/lineend/lineend.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Leonid Maslakov. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package lineend 6 | 7 | import ( 8 | "strings" 9 | ) 10 | 11 | // GetLineEnd allows you to get the end of a line used in the text. 12 | // It can return \r\n, \r, \n or an empty string. 13 | func GetLineEnd(text string) string { 14 | dos := strings.Count(text, "\r\n") 15 | oldMac := strings.Count(text, "\r") 16 | unix := strings.Count(text, "\n") 17 | 18 | if dos == 0 && oldMac == 0 && unix == 0 { 19 | return "" 20 | } 21 | 22 | if dos >= oldMac && dos >= unix { 23 | return "\r\n" 24 | } 25 | 26 | if oldMac >= dos && oldMac >= unix { 27 | return "\r" 28 | } 29 | 30 | if unix >= dos && unix >= oldMac { 31 | return "\n" 32 | } 33 | 34 | return "" 35 | } 36 | 37 | // DosToOldMac DosToOldMac сonverts end of line from CRLF to CR. 38 | func DosToOldMac(text string) string { 39 | return strings.Replace(text, "\n", "", -1) 40 | } 41 | 42 | // DosToUnix сonverts end of line from CRLF to LF. 43 | func DosToUnix(text string) string { 44 | return strings.Replace(text, "\r", "", -1) 45 | } 46 | 47 | // OldMacToDos сonverts end of line from CR to CRLF. 48 | func OldMacToDos(text string) string { 49 | return strings.Replace(text, "\r", "\r\n", -1) 50 | } 51 | 52 | // OldMacToUnix сonverts end of line from CR to LF. 53 | func OldMacToUnix(text string) string { 54 | return strings.Replace(text, "\r", "\n", -1) 55 | } 56 | 57 | // UnixToDos сonverts end of line from LF to CRLF. 58 | func UnixToDos(text string) string { 59 | return strings.Replace(text, "\n", "\r\n", -1) 60 | } 61 | 62 | // UnixToOldMac сonverts end of line from LF to CR. 63 | func UnixToOldMac(text string) string { 64 | return strings.Replace(text, "\n", "\r", -1) 65 | } 66 | 67 | // UnknownToDos converts unknown line end to CRLF. 68 | func UnknownToDos(text string) string { 69 | switch GetLineEnd(text) { 70 | case "\r": 71 | return OldMacToDos(text) 72 | 73 | case "\n": 74 | return UnixToDos(text) 75 | } 76 | 77 | return text 78 | } 79 | 80 | // UnknownToOldMac converts unknown line end to CR. 81 | func UnknownToOldMac(text string) string { 82 | switch GetLineEnd(text) { 83 | case "\r\n": 84 | return DosToOldMac(text) 85 | 86 | case "\n": 87 | return UnixToOldMac(text) 88 | } 89 | 90 | return text 91 | } 92 | 93 | // UnknownToUnix converts unknown line end to LF. 94 | func UnknownToUnix(text string) string { 95 | switch GetLineEnd(text) { 96 | case "\r\n": 97 | return DosToUnix(text) 98 | 99 | case "\r": 100 | return OldMacToUnix(text) 101 | } 102 | 103 | return text 104 | } 105 | -------------------------------------------------------------------------------- /internal/lineend/lineend_test.go: -------------------------------------------------------------------------------- 1 | package lineend 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | type testDataType struct { 8 | Input string 9 | Expect string 10 | } 11 | 12 | func TestGetLineEnd(t *testing.T) { 13 | testData := []testDataType{ 14 | { 15 | Input: "", 16 | Expect: "", 17 | }, 18 | { 19 | Input: "my line", 20 | Expect: "", 21 | }, 22 | { 23 | Input: "my\r\nline\r\n", 24 | Expect: "\r\n", 25 | }, 26 | { 27 | Input: "my\rline\r", 28 | Expect: "\r", 29 | }, 30 | { 31 | Input: "my\nline\n", 32 | Expect: "\n", 33 | }, 34 | } 35 | 36 | for i, test := range testData { 37 | if GetLineEnd(test.Input) != test.Expect { 38 | t.Fatal("Number of failed test:", i) 39 | } 40 | } 41 | } 42 | 43 | func TestUnknownToDos(t *testing.T) { 44 | testData := []testDataType{ 45 | { 46 | Input: "", 47 | Expect: "", 48 | }, 49 | { 50 | Input: "my line", 51 | Expect: "my line", 52 | }, 53 | { 54 | Input: "my\r\nline\r\n", 55 | Expect: "my\r\nline\r\n", 56 | }, 57 | { 58 | Input: "my\rline\r", 59 | Expect: "my\r\nline\r\n", 60 | }, 61 | { 62 | Input: "my\nline\n", 63 | Expect: "my\r\nline\r\n", 64 | }, 65 | } 66 | 67 | for i, test := range testData { 68 | if UnknownToDos(test.Input) != test.Expect { 69 | t.Fatal("Number of failed test:", i) 70 | } 71 | } 72 | } 73 | 74 | func TestUnknownToOldMac(t *testing.T) { 75 | testData := []testDataType{ 76 | { 77 | Input: "", 78 | Expect: "", 79 | }, 80 | { 81 | Input: "my line", 82 | Expect: "my line", 83 | }, 84 | { 85 | Input: "my\r\nline\r\n", 86 | Expect: "my\rline\r", 87 | }, 88 | { 89 | Input: "my\rline\r", 90 | Expect: "my\rline\r", 91 | }, 92 | { 93 | Input: "my\nline\n", 94 | Expect: "my\rline\r", 95 | }, 96 | } 97 | 98 | for i, test := range testData { 99 | if UnknownToOldMac(test.Input) != test.Expect { 100 | t.Fatal("Number of failed test:", i) 101 | } 102 | } 103 | } 104 | 105 | func TestUnknownToUnix(t *testing.T) { 106 | testData := []testDataType{ 107 | { 108 | Input: "", 109 | Expect: "", 110 | }, 111 | { 112 | Input: "my line", 113 | Expect: "my line", 114 | }, 115 | { 116 | Input: "my\r\nline\r\n", 117 | Expect: "my\nline\n", 118 | }, 119 | { 120 | Input: "my\rline\r", 121 | Expect: "my\nline\n", 122 | }, 123 | { 124 | Input: "my\nline\n", 125 | Expect: "my\nline\n", 126 | }, 127 | } 128 | 129 | for i, test := range testData { 130 | if UnknownToUnix(test.Input) != test.Expect { 131 | t.Fatal("Number of failed test:", i) 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /internal/logger/logger.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | package logger 20 | 21 | import ( 22 | "fmt" 23 | "github.com/lcomrade/lenpaste/internal/netshare" 24 | "net/http" 25 | "os" 26 | "runtime" 27 | "strconv" 28 | "time" 29 | ) 30 | 31 | type Logger struct { 32 | TimeFormat string 33 | } 34 | 35 | func New(timeFormat string) Logger { 36 | return Logger{ 37 | TimeFormat: timeFormat, 38 | } 39 | } 40 | 41 | func getTrace() string { 42 | trace := "" 43 | 44 | for i := 2; ; i++ { 45 | _, file, line, ok := runtime.Caller(i) 46 | if ok { 47 | trace = trace + file + "#" + strconv.Itoa(line) + ": " 48 | 49 | } else { 50 | return trace 51 | } 52 | } 53 | } 54 | 55 | func (cfg Logger) Info(msg string) { 56 | fmt.Fprintln(os.Stdout, time.Now().Format(cfg.TimeFormat), "[INFO] ", msg) 57 | } 58 | 59 | func (cfg Logger) Error(e error) { 60 | fmt.Fprintln(os.Stderr, time.Now().Format(cfg.TimeFormat), "[ERROR] ", getTrace(), e.Error()) 61 | } 62 | 63 | func (cfg Logger) HttpRequest(req *http.Request, code int) { 64 | fmt.Fprintln(os.Stdout, time.Now().Format(cfg.TimeFormat), "[REQUEST]", netshare.GetClientAddr(req).String(), req.Method, code, req.URL.Path, "(User-Agent: "+req.UserAgent()+")") 65 | } 66 | 67 | func (cfg Logger) HttpError(req *http.Request, e error) { 68 | fmt.Fprintln(os.Stderr, time.Now().Format(cfg.TimeFormat), "[ERROR] ", netshare.GetClientAddr(req).String(), req.Method, 500, req.URL.Path, "(User-Agent: "+req.UserAgent()+")", "Error:", getTrace(), e.Error()) 69 | } 70 | -------------------------------------------------------------------------------- /internal/netshare/netshare.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | package netshare 20 | 21 | import ( 22 | "errors" 23 | ) 24 | 25 | const ( 26 | MaxLengthAuthorAll = 100 // Max length or paste author name, email and URL. 27 | ) 28 | 29 | var ( 30 | ErrBadRequest = errors.New("Bad Request") // 400 31 | ErrUnauthorized = errors.New("Unauthorized") // 401 32 | ErrNotFound = errors.New("Not Found") // 404 33 | ErrMethodNotAllowed = errors.New("Method Not Allowed") // 405 34 | ErrPayloadTooLarge = errors.New("Payload Too Large") // 413 35 | // ErrTooManyRequests = errors.New("Too Many Requests") // 429 36 | ErrInternal = errors.New("Internal Server Error") // 500 37 | ) 38 | 39 | type ErrTooManyRequests struct { 40 | s string 41 | RetryAfter int64 42 | } 43 | 44 | func (e *ErrTooManyRequests) Error() string { 45 | return e.s 46 | } 47 | 48 | func ErrTooManyRequestsNew(retryAfter int64) *ErrTooManyRequests { 49 | return &ErrTooManyRequests{ 50 | s: "Too Many Requests", 51 | RetryAfter: retryAfter, 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /internal/netshare/netshare_host.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | package netshare 20 | 21 | import ( 22 | "net" 23 | "net/http" 24 | "strings" 25 | ) 26 | 27 | func GetHost(req *http.Request) string { 28 | // Read header 29 | xHost := req.Header.Get("X-Forwarded-Host") 30 | 31 | // Check 32 | if xHost != "" { 33 | return xHost 34 | } 35 | 36 | return req.Host 37 | } 38 | 39 | func GetProtocol(req *http.Request) string { 40 | // X-Forwarded-Proto 41 | xProto := req.Header.Get("X-Forwarded-Proto") 42 | 43 | if xProto != "" { 44 | return xProto 45 | } 46 | 47 | // Else real protocol 48 | return req.URL.Scheme 49 | } 50 | 51 | func GetClientAddr(req *http.Request) net.IP { 52 | // X-Real-IP 53 | xReal := req.Header.Get("X-Real-IP") 54 | if xReal != "" { 55 | return net.ParseIP(xReal) 56 | } 57 | 58 | // X-Forwarded-For 59 | xFor := req.Header.Get("X-Forwarded-For") 60 | xFor = strings.Split(xFor, ",")[0] 61 | 62 | if xFor != "" { 63 | return net.ParseIP(xFor) 64 | } 65 | 66 | // Else use real client address 67 | host, _, err := net.SplitHostPort(req.RemoteAddr) 68 | if err != nil { 69 | return nil 70 | } 71 | 72 | return net.ParseIP(host) 73 | } 74 | -------------------------------------------------------------------------------- /internal/netshare/netshare_paste.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | package netshare 20 | 21 | import ( 22 | "github.com/lcomrade/lenpaste/internal/lineend" 23 | "github.com/lcomrade/lenpaste/internal/storage" 24 | "net/http" 25 | "strconv" 26 | "strings" 27 | "time" 28 | "unicode/utf8" 29 | ) 30 | 31 | func PasteAddFromForm(req *http.Request, db storage.DB, rateSys *RateLimitSystem, titleMaxLen int, bodyMaxLen int, maxLifeTime int64, lexerNames []string) (string, int64, int64, error) { 32 | // Check HTTP method 33 | if req.Method != "POST" { 34 | return "", 0, 0, ErrMethodNotAllowed 35 | } 36 | 37 | // Check rate limit 38 | err := rateSys.CheckAndUse(GetClientAddr(req)) 39 | if err != nil { 40 | return "", 0, 0, err 41 | } 42 | 43 | // Read form 44 | req.ParseForm() 45 | 46 | paste := storage.Paste{ 47 | Title: req.PostForm.Get("title"), 48 | Body: req.PostForm.Get("body"), 49 | Syntax: req.PostForm.Get("syntax"), 50 | DeleteTime: 0, 51 | OneUse: false, 52 | Author: req.PostForm.Get("author"), 53 | AuthorEmail: req.PostForm.Get("authorEmail"), 54 | AuthorURL: req.PostForm.Get("authorURL"), 55 | } 56 | 57 | // Remove new line from title 58 | paste.Title = strings.Replace(paste.Title, "\n", "", -1) 59 | paste.Title = strings.Replace(paste.Title, "\r", "", -1) 60 | paste.Title = strings.Replace(paste.Title, "\t", " ", -1) 61 | 62 | // Check title 63 | if utf8.RuneCountInString(paste.Title) > titleMaxLen && titleMaxLen >= 0 { 64 | return "", 0, 0, ErrPayloadTooLarge 65 | } 66 | 67 | // Check paste body 68 | if paste.Body == "" { 69 | return "", 0, 0, ErrBadRequest 70 | } 71 | 72 | if utf8.RuneCountInString(paste.Body) > bodyMaxLen && bodyMaxLen > 0 { 73 | return "", 0, 0, ErrPayloadTooLarge 74 | } 75 | 76 | // Change paste body lines end 77 | switch req.PostForm.Get("lineEnd") { 78 | case "", "LF", "lf": 79 | paste.Body = lineend.UnknownToUnix(paste.Body) 80 | 81 | case "CRLF", "crlf": 82 | paste.Body = lineend.UnknownToDos(paste.Body) 83 | 84 | case "CR", "cr": 85 | paste.Body = lineend.UnknownToOldMac(paste.Body) 86 | 87 | default: 88 | return "", 0, 0, ErrBadRequest 89 | } 90 | 91 | // Check syntax 92 | if paste.Syntax == "" { 93 | paste.Syntax = "plaintext" 94 | } 95 | 96 | syntaxOk := false 97 | for _, name := range lexerNames { 98 | if name == paste.Syntax { 99 | syntaxOk = true 100 | break 101 | } 102 | } 103 | 104 | if syntaxOk == false { 105 | return "", 0, 0, ErrBadRequest 106 | } 107 | 108 | // Get delete time 109 | expirStr := req.PostForm.Get("expiration") 110 | if expirStr != "" { 111 | // Convert string to int 112 | expir, err := strconv.ParseInt(expirStr, 10, 64) 113 | if err != nil { 114 | return "", 0, 0, ErrBadRequest 115 | } 116 | 117 | // Check limits 118 | if maxLifeTime > 0 { 119 | if expir > maxLifeTime || expir <= 0 { 120 | return "", 0, 0, ErrBadRequest 121 | } 122 | } 123 | 124 | // Save if ok 125 | if expir > 0 { 126 | paste.DeleteTime = time.Now().Unix() + expir 127 | } 128 | } 129 | 130 | // Get "one use" parameter 131 | if req.PostForm.Get("oneUse") == "true" { 132 | paste.OneUse = true 133 | } 134 | 135 | // Check author name, email and URL length. 136 | if utf8.RuneCountInString(paste.Author) > MaxLengthAuthorAll { 137 | return "", 0, 0, ErrPayloadTooLarge 138 | } 139 | 140 | if utf8.RuneCountInString(paste.AuthorEmail) > MaxLengthAuthorAll { 141 | return "", 0, 0, ErrPayloadTooLarge 142 | } 143 | 144 | if utf8.RuneCountInString(paste.AuthorURL) > MaxLengthAuthorAll { 145 | return "", 0, 0, ErrPayloadTooLarge 146 | } 147 | 148 | // Create paste 149 | pasteID, createTime, deleteTime, err := db.PasteAdd(paste) 150 | if err != nil { 151 | return pasteID, createTime, deleteTime, err 152 | } 153 | 154 | return pasteID, createTime, deleteTime, nil 155 | } 156 | -------------------------------------------------------------------------------- /internal/netshare/netshare_ratelimit.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | package netshare 20 | 21 | import ( 22 | "net" 23 | "sync" 24 | "time" 25 | ) 26 | 27 | type RateLimitSystem struct { 28 | per5Min *RateLimit 29 | per15Min *RateLimit 30 | per1Hour *RateLimit 31 | } 32 | 33 | func NewRateLimitSystem(per5Min, per15Min, per1Hour uint) *RateLimitSystem { 34 | return &RateLimitSystem{ 35 | per5Min: NewRateLimit(5*60, per5Min), 36 | per15Min: NewRateLimit(15*60, per15Min), 37 | per1Hour: NewRateLimit(60*60, per1Hour), 38 | } 39 | } 40 | 41 | func (rateSys *RateLimitSystem) CheckAndUse(ip net.IP) error { 42 | var tmp int64 43 | 44 | tmp = rateSys.per5Min.CheckAndUse(ip) 45 | if tmp != 0 { 46 | return ErrTooManyRequestsNew(tmp) 47 | } 48 | 49 | tmp = rateSys.per15Min.CheckAndUse(ip) 50 | if tmp != 0 { 51 | return ErrTooManyRequestsNew(tmp) 52 | } 53 | 54 | tmp = rateSys.per1Hour.CheckAndUse(ip) 55 | if tmp != 0 { 56 | return ErrTooManyRequestsNew(tmp) 57 | } 58 | 59 | return nil 60 | } 61 | 62 | type RateLimit struct { 63 | sync.RWMutex 64 | 65 | limitPeriod int // N - Rate limit period (in seconds) 66 | limitCount uint // X - Max request count per N seconds period 67 | 68 | list map[string]rateLimitIP // Rate limit bucket 69 | } 70 | 71 | type rateLimitIP struct { 72 | UseTime int64 // Fist IP use time 73 | UseCount uint // Requests count by IP 74 | } 75 | 76 | func NewRateLimit(rateLimitPeriod int, limitCount uint) *RateLimit { 77 | rateLimit := &RateLimit{ 78 | limitPeriod: rateLimitPeriod, 79 | limitCount: limitCount, 80 | list: make(map[string]rateLimitIP), 81 | } 82 | 83 | go rateLimit.runWorker() 84 | 85 | return rateLimit 86 | } 87 | 88 | func (rateLimit *RateLimit) runWorker() { 89 | for { 90 | time.Sleep(time.Duration(rateLimit.limitPeriod) * time.Second) 91 | 92 | timeNow := time.Now().Unix() 93 | rateLimit.Lock() 94 | 95 | for ipStr, data := range rateLimit.list { 96 | if data.UseTime+int64(rateLimit.limitPeriod) <= timeNow { 97 | delete(rateLimit.list, ipStr) 98 | } 99 | } 100 | 101 | rateLimit.Unlock() 102 | } 103 | } 104 | 105 | func (rateLimit *RateLimit) CheckAndUse(ip net.IP) int64 { 106 | // If rate limit not need 107 | if rateLimit.limitCount == 0 { 108 | return 0 109 | } 110 | 111 | // Lock 112 | rateLimit.Lock() 113 | defer rateLimit.Unlock() 114 | 115 | ipStr := ip.String() 116 | timeNow := time.Now().Unix() 117 | 118 | // If last use time out 119 | if rateLimit.list[ipStr].UseTime+int64(rateLimit.limitPeriod) <= timeNow { 120 | rateLimit.list[ipStr] = rateLimitIP{ 121 | UseTime: timeNow, 122 | UseCount: 1, 123 | } 124 | 125 | return 0 126 | 127 | // Else 128 | } else { 129 | if rateLimit.list[ipStr].UseCount < rateLimit.limitCount { 130 | tmp := rateLimit.list[ipStr] 131 | tmp.UseCount = tmp.UseCount + 1 132 | rateLimit.list[ipStr] = tmp 133 | return 0 134 | } 135 | } 136 | 137 | return rateLimit.list[ipStr].UseTime + int64(rateLimit.limitPeriod) - timeNow 138 | } 139 | -------------------------------------------------------------------------------- /internal/raw/raw.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | package raw 20 | 21 | import ( 22 | "github.com/lcomrade/lenpaste/internal/config" 23 | "github.com/lcomrade/lenpaste/internal/logger" 24 | "github.com/lcomrade/lenpaste/internal/netshare" 25 | "github.com/lcomrade/lenpaste/internal/storage" 26 | "net/http" 27 | ) 28 | 29 | type Data struct { 30 | DB storage.DB 31 | Log logger.Logger 32 | 33 | RateLimitGet *netshare.RateLimitSystem 34 | 35 | Version string 36 | } 37 | 38 | func Load(db storage.DB, cfg config.Config) *Data { 39 | return &Data{ 40 | DB: db, 41 | Log: cfg.Log, 42 | RateLimitGet: cfg.RateLimitGet, 43 | Version: cfg.Version, 44 | } 45 | } 46 | 47 | func (data *Data) Hand(rw http.ResponseWriter, req *http.Request) { 48 | rw.Header().Set("Server", config.Software+"/"+data.Version) 49 | 50 | err := data.rawHand(rw, req) 51 | 52 | if err == nil { 53 | data.Log.HttpRequest(req, 200) 54 | 55 | } else { 56 | code, err := data.writeError(rw, req, err) 57 | if err != nil { 58 | data.Log.HttpError(req, err) 59 | } else { 60 | data.Log.HttpRequest(req, code) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /internal/raw/raw_error.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | package raw 20 | 21 | import ( 22 | "errors" 23 | "github.com/lcomrade/lenpaste/internal/netshare" 24 | "github.com/lcomrade/lenpaste/internal/storage" 25 | "io" 26 | "net/http" 27 | "strconv" 28 | ) 29 | 30 | func (data *Data) writeError(rw http.ResponseWriter, req *http.Request, e error) (int, error) { 31 | var errText string 32 | var errCode int 33 | 34 | // Dectect error 35 | var eTmp429 *netshare.ErrTooManyRequests 36 | 37 | if e == storage.ErrNotFoundID && e == netshare.ErrNotFound { 38 | errCode = 404 39 | errText = "404 Not Found" 40 | 41 | } else if errors.As(e, &eTmp429) { 42 | errCode = 429 43 | errText = "429 Too Many Requests" 44 | rw.Header().Set("Retry-After", strconv.FormatInt(eTmp429.RetryAfter, 10)) 45 | 46 | } else { 47 | errCode = 500 48 | errText = "500 Internal Server Error" 49 | } 50 | 51 | // Write response 52 | rw.Header().Set("Content-type", "text/plain; charset=utf-8") 53 | rw.WriteHeader(errCode) 54 | 55 | _, err := io.WriteString(rw, errText) 56 | if err != nil { 57 | return 500, err 58 | } 59 | 60 | return errCode, nil 61 | } 62 | -------------------------------------------------------------------------------- /internal/raw/raw_raw.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | package raw 20 | 21 | import ( 22 | "github.com/lcomrade/lenpaste/internal/netshare" 23 | "io" 24 | "net/http" 25 | ) 26 | 27 | // Pattern: /raw/ 28 | func (data *Data) rawHand(rw http.ResponseWriter, req *http.Request) error { 29 | // Check rate limit 30 | err := data.RateLimitGet.CheckAndUse(netshare.GetClientAddr(req)) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | // Read DB 36 | pasteID := string([]rune(req.URL.Path)[5:]) 37 | 38 | paste, err := data.DB.PasteGet(pasteID) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | // If "one use" paste 44 | if paste.OneUse == true { 45 | // Delete paste 46 | err = data.DB.PasteDelete(pasteID) 47 | if err != nil { 48 | return err 49 | } 50 | } 51 | 52 | // Write result 53 | rw.Header().Set("Content-Type", "text/plain; charset=utf-8") 54 | 55 | _, err = io.WriteString(rw, paste.Body) 56 | if err != nil { 57 | return err 58 | } 59 | 60 | return nil 61 | } 62 | -------------------------------------------------------------------------------- /internal/storage/share.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | package storage 20 | 21 | import ( 22 | "crypto/rand" 23 | "math/big" 24 | ) 25 | 26 | func genTokenCrypto(tokenLen int) (string, error) { 27 | // Generate token 28 | var chars = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") 29 | charsLen := int64(len(chars)) 30 | charsLenBig := big.NewInt(charsLen) 31 | 32 | token := "" 33 | 34 | for i := 0; i < tokenLen; i++ { 35 | randInt, err := rand.Int(rand.Reader, charsLenBig) 36 | if err != nil { 37 | return "", err 38 | } 39 | 40 | token = token + string(chars[randInt.Int64()]) 41 | } 42 | 43 | return token, nil 44 | } 45 | -------------------------------------------------------------------------------- /internal/storage/storage.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | package storage 20 | 21 | import ( 22 | "database/sql" 23 | "errors" 24 | _ "github.com/lib/pq" 25 | _ "github.com/mattn/go-sqlite3" 26 | ) 27 | 28 | var ( 29 | ErrNotFoundID = errors.New("db: could not find ID") 30 | ) 31 | 32 | type DB struct { 33 | pool *sql.DB 34 | } 35 | 36 | func NewPool(driverName string, dataSourceName string, maxOpenConns int, maxIdleConns int) (DB, error) { 37 | var db DB 38 | var err error 39 | 40 | db.pool, err = sql.Open(driverName, dataSourceName) 41 | if err != nil { 42 | return db, err 43 | } 44 | 45 | db.pool.SetMaxOpenConns(maxOpenConns) 46 | db.pool.SetMaxIdleConns(maxIdleConns) 47 | 48 | return db, nil 49 | } 50 | 51 | func (db DB) Close() error { 52 | return db.pool.Close() 53 | } 54 | 55 | func InitDB(driverName string, dataSourceName string) error { 56 | // Open DB 57 | db, err := NewPool(driverName, dataSourceName, 1, 0) 58 | if err != nil { 59 | return err 60 | } 61 | defer db.Close() 62 | 63 | // Create tables 64 | _, err = db.pool.Exec(` 65 | CREATE TABLE IF NOT EXISTS pastes ( 66 | id TEXT PRIMARY KEY, 67 | title TEXT NOT NULL, 68 | body TEXT NOT NULL, 69 | syntax TEXT NOT NULL, 70 | create_time INTEGER NOT NULL, 71 | delete_time INTEGER NOT NULL, 72 | one_use BOOL NOT NULL 73 | ); 74 | `) 75 | if err != nil { 76 | return err 77 | } 78 | 79 | // Crutch for SQLite3 80 | if driverName == "sqlite3" { 81 | _, err = db.pool.Exec(`ALTER TABLE pastes ADD COLUMN author TEXT NOT NULL DEFAULT ''`) 82 | if err != nil { 83 | if err.Error() != "duplicate column name: author" { 84 | return err 85 | } 86 | } 87 | 88 | _, err = db.pool.Exec(`ALTER TABLE pastes ADD COLUMN author_email TEXT NOT NULL DEFAULT ''`) 89 | if err != nil { 90 | if err.Error() != "duplicate column name: author_email" { 91 | return err 92 | } 93 | } 94 | 95 | _, err = db.pool.Exec(`ALTER TABLE pastes ADD COLUMN author_url TEXT NOT NULL DEFAULT ''`) 96 | if err != nil { 97 | if err.Error() != "duplicate column name: author_url" { 98 | return err 99 | } 100 | } 101 | 102 | // Normal SQL for all other DBs 103 | } else { 104 | _, err = db.pool.Exec(` 105 | ALTER TABLE pastes ADD COLUMN IF NOT EXISTS author TEXT NOT NULL DEFAULT ''; 106 | ALTER TABLE pastes ADD COLUMN IF NOT EXISTS author_email TEXT NOT NULL DEFAULT ''; 107 | ALTER TABLE pastes ADD COLUMN IF NOT EXISTS author_url TEXT NOT NULL DEFAULT ''; 108 | `) 109 | if err != nil { 110 | return err 111 | } 112 | } 113 | 114 | return nil 115 | } 116 | -------------------------------------------------------------------------------- /internal/storage/storage_paste.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | package storage 20 | 21 | import ( 22 | "database/sql" 23 | "time" 24 | ) 25 | 26 | type Paste struct { 27 | ID string `json:"id"` // Ignored when creating 28 | Title string `json:"title"` 29 | Body string `json:"body"` 30 | CreateTime int64 `json:"createTime"` // Ignored when creating 31 | DeleteTime int64 `json:"deleteTime"` 32 | OneUse bool `json:"oneUse"` 33 | Syntax string `json:"syntax"` 34 | 35 | Author string `json:"author"` 36 | AuthorEmail string `json:"authorEmail"` 37 | AuthorURL string `json:"authorURL"` 38 | } 39 | 40 | func (db DB) PasteAdd(paste Paste) (string, int64, int64, error) { 41 | var err error 42 | 43 | // Generate ID 44 | paste.ID, err = genTokenCrypto(8) 45 | if err != nil { 46 | return paste.ID, paste.CreateTime, paste.DeleteTime, err 47 | } 48 | 49 | // Set paste create time 50 | paste.CreateTime = time.Now().Unix() 51 | 52 | // Check delete time 53 | if paste.DeleteTime < 0 { 54 | paste.DeleteTime = 0 55 | } 56 | 57 | // Add 58 | _, err = db.pool.Exec( 59 | `INSERT INTO pastes (id, title, body, syntax, create_time, delete_time, one_use, author, author_email, author_url) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`, 60 | paste.ID, paste.Title, paste.Body, paste.Syntax, paste.CreateTime, paste.DeleteTime, paste.OneUse, paste.Author, paste.AuthorEmail, paste.AuthorURL, 61 | ) 62 | if err != nil { 63 | return paste.ID, paste.CreateTime, paste.DeleteTime, err 64 | } 65 | 66 | return paste.ID, paste.CreateTime, paste.DeleteTime, nil 67 | } 68 | 69 | func (db DB) PasteDelete(id string) error { 70 | // Delete 71 | result, err := db.pool.Exec( 72 | `DELETE FROM pastes WHERE id = $1`, 73 | id, 74 | ) 75 | if err != nil { 76 | return err 77 | } 78 | 79 | // Check result 80 | rowsAffected, err := result.RowsAffected() 81 | if err != nil { 82 | return err 83 | } 84 | 85 | if rowsAffected == 0 { 86 | return ErrNotFoundID 87 | } 88 | 89 | return nil 90 | } 91 | 92 | func (db DB) PasteGet(id string) (Paste, error) { 93 | var paste Paste 94 | 95 | // Make query 96 | row := db.pool.QueryRow( 97 | `SELECT id, title, body, syntax, create_time, delete_time, one_use, author, author_email, author_url FROM pastes WHERE id = $1`, 98 | id, 99 | ) 100 | 101 | // Read query 102 | err := row.Scan(&paste.ID, &paste.Title, &paste.Body, &paste.Syntax, &paste.CreateTime, &paste.DeleteTime, &paste.OneUse, &paste.Author, &paste.AuthorEmail, &paste.AuthorURL) 103 | if err != nil { 104 | if err == sql.ErrNoRows { 105 | return paste, ErrNotFoundID 106 | } 107 | 108 | return paste, err 109 | } 110 | 111 | // Check paste expiration 112 | if paste.DeleteTime < time.Now().Unix() && paste.DeleteTime > 0 { 113 | // Delete expired paste 114 | _, err = db.pool.Exec( 115 | `DELETE FROM pastes WHERE id = $1`, 116 | paste.ID, 117 | ) 118 | if err != nil { 119 | return Paste{}, err 120 | } 121 | 122 | // Return ErrNotFound 123 | return Paste{}, ErrNotFoundID 124 | } 125 | 126 | return paste, nil 127 | } 128 | 129 | func (db DB) PasteDeleteExpired() (int64, error) { 130 | // Delete 131 | result, err := db.pool.Exec( 132 | `DELETE FROM pastes WHERE (delete_time < $1) AND (delete_time > 0)`, 133 | time.Now().Unix(), 134 | ) 135 | if err != nil { 136 | return 0, err 137 | } 138 | 139 | // Check result 140 | rowsAffected, err := result.RowsAffected() 141 | if err != nil { 142 | return rowsAffected, err 143 | } 144 | 145 | return rowsAffected, nil 146 | } 147 | -------------------------------------------------------------------------------- /internal/web/data/about.tmpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Copyright (C) 2021-2023 Leonid Maslakov. 3 | 4 | This file is part of Lenpaste. 5 | 6 | Lenpaste is free software: you can redistribute it 7 | and/or modify it under the terms of the 8 | GNU Affero Public License as published by the 9 | Free Software Foundation, either version 3 of the License, 10 | or (at your option) any later version. 11 | 12 | Lenpaste is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 14 | or FITNESS FOR A PARTICULAR PURPOSE. 15 | See the GNU Affero Public License for more details. 16 | 17 | You should have received a copy of the GNU Affero Public License along with Lenpaste. 18 | If not, see . 19 | */}} 20 | 21 | {{define "titlePrefix"}}{{ call .Translate `about.Title` }} | {{end}} 22 | {{define "headAppend"}}{{end}} 23 | {{define "article"}} 24 | {{if ne .ServerAbout ``}} 25 |

{{ call .Translate `about.AboutServerTitle` }}

26 | {{ call .Highlight .ServerAbout `plaintext` }} 27 | {{end}} 28 | 29 | {{if ne .ServerRules ``}} 30 |

{{ call .Translate `about.RulesTitle` }}

31 | {{ call .Highlight .ServerRules `plaintext` }} 32 | {{if .ServerTermsExist}}

{{ call .Translate `about.SeeTerms` `/terms` }}

{{end}} 33 | {{end}} 34 | 35 |

{{ call .Translate `about.Limit` }}

36 | {{if ne .TitleMaxLen 0}} 37 | {{if gt .TitleMaxLen 0}} 38 |

{{call .Translate `about.LimitTitle` .TitleMaxLen}}
39 | {{else}} 40 |

{{ call .Translate `about.LimitTitleNo` }}
41 | {{end}} 42 | {{else}} 43 |

{{ call .Translate `about.LimitTitleDisable` }}
44 | {{end}} 45 | {{if gt .BodyMaxLen 0}} 46 | {{call .Translate `about.LimitBody` .BodyMaxLen }}
47 | {{else}} 48 | {{ call .Translate `about.LimitBodyNo` }}
49 | {{end}} 50 | {{if gt .MaxLifeTime 0}} 51 | {{call .Translate `about.LimitLifeTime` .MaxLifeTime }}

52 | {{else}} 53 | {{ call .Translate `about.LimitLifeTimeNo` }}

54 | {{end}} 55 | 56 | {{if or (ne .AdminName ``) (ne .AdminMail ``)}} 57 |

{{ call .Translate `about.AdminTitle` }}

58 | {{if ne .AdminName ``}}

{{ call .Translate `about.AdminName` }} {{.AdminName}}

{{end}} 59 | {{if ne .AdminMail ``}}

{{ call .Translate `about.AdminEmail` }} {{.AdminMail}}

{{end}} 60 | {{end}} 61 | 62 |

{{ call .Translate `about.LenpasteTitle` }}

63 |

{{call .Translate `about.LenpasteMessage` .Version}}

64 |
    65 |
  • {{ call .Translate `about.Lenpaste1` `/about/source_code` `/about/license` `AGPL 3` }}
  • 66 |
  • {{ call .Translate `about.Lenpaste2` }}
  • 67 |
  • {{ call .Translate `about.Lenpaste3` }}
  • 68 |
  • {{ call .Translate `about.Lenpaste4` }}
  • 69 |
  • {{ call .Translate `about.Lenpaste5` `/docs/apiv1` }}
  • 70 |
71 |

{{call .Translate `about.LenpasteAuthors` `/about/authors`}}

72 | {{end}} 73 | -------------------------------------------------------------------------------- /internal/web/data/authors.tmpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Copyright (C) 2021-2023 Leonid Maslakov. 3 | 4 | This file is part of Lenpaste. 5 | 6 | Lenpaste is free software: you can redistribute it 7 | and/or modify it under the terms of the 8 | GNU Affero Public License as published by the 9 | Free Software Foundation, either version 3 of the License, 10 | or (at your option) any later version. 11 | 12 | Lenpaste is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 14 | or FITNESS FOR A PARTICULAR PURPOSE. 15 | See the GNU Affero Public License for more details. 16 | 17 | You should have received a copy of the GNU Affero Public License along with Lenpaste. 18 | If not, see . 19 | */}} 20 | 21 | {{define "titlePrefix"}}{{ call .Translate `authors.Title` }} | {{end}} 22 | {{define "headAppend"}}{{end}} 23 | {{define "article"}} 24 |

{{ call .Translate `about.Title` }} / {{ call .Translate `authors.Title` }}

25 | 30 | {{end}} 31 | -------------------------------------------------------------------------------- /internal/web/data/base.tmpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Copyright (C) 2021-2023 Leonid Maslakov. 3 | 4 | This file is part of Lenpaste. 5 | 6 | Lenpaste is free software: you can redistribute it 7 | and/or modify it under the terms of the 8 | GNU Affero Public License as published by the 9 | Free Software Foundation, either version 3 of the License, 10 | or (at your option) any later version. 11 | 12 | Lenpaste is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 14 | or FITNESS FOR A PARTICULAR PURPOSE. 15 | See the GNU Affero Public License for more details. 16 | 17 | You should have received a copy of the GNU Affero Public License along with Lenpaste. 18 | If not, see . 19 | */}} 20 | 21 | 22 | 23 | 24 | 25 | {{template "titlePrefix" .}}{{ call .Translate `base.Lenpaste` }} 26 | 27 | 28 | 29 | {{template "headAppend" .}} 30 | 31 | 32 | 33 |
34 | 36 |
37 |
{{template "article" .}}
38 | 39 | 40 | -------------------------------------------------------------------------------- /internal/web/data/code.js: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | function copyToClipboard(text) { 20 | let tmp = document.createElement("textarea"); 21 | let focus = document.activeElement; 22 | 23 | tmp.value = text; 24 | 25 | document.body.appendChild(tmp); 26 | tmp.select(); 27 | document.execCommand("copy"); 28 | document.body.removeChild(tmp); 29 | focus.focus(); 30 | } 31 | 32 | function copyButton(element) { 33 | let result = ""; 34 | 35 | let strings = element.parentNode.getElementsByTagName("code")[0].textContent.split("\n"); 36 | let stringsLen = strings.length; 37 | let cutLen = stringsLen.toString().length; 38 | for (let i = 0; stringsLen > i; i++) { 39 | if (i != 0) { 40 | result = result + "\n" 41 | } 42 | 43 | result = result + strings[i].slice(cutLen); 44 | } 45 | 46 | result = result.trim() + "\n"; 47 | copyToClipboard(result); 48 | } 49 | 50 | 51 | document.addEventListener("DOMContentLoaded", () => { 52 | // Edit CSS 53 | let newStyleSheet = ` 54 | pre { 55 | position: relative; 56 | overflow: auto; 57 | } 58 | 59 | pre button { 60 | visibility: hidden; 61 | } 62 | 63 | pre:hover > button { 64 | visibility: visible; 65 | } 66 | `; 67 | let styleSheet = document.createElement("style") 68 | styleSheet.innerText = newStyleSheet 69 | document.head.appendChild(styleSheet) 70 | 71 | // Edit pre tags 72 | let preElements = document.getElementsByTagName("pre"); 73 | 74 | for (var i = 0; preElements.length > i; i++) { 75 | preElements[i].insertAdjacentHTML("beforeend", ""); 76 | } 77 | }); 78 | -------------------------------------------------------------------------------- /internal/web/data/docs.tmpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Copyright (C) 2021-2023 Leonid Maslakov. 3 | 4 | This file is part of Lenpaste. 5 | 6 | Lenpaste is free software: you can redistribute it 7 | and/or modify it under the terms of the 8 | GNU Affero Public License as published by the 9 | Free Software Foundation, either version 3 of the License, 10 | or (at your option) any later version. 11 | 12 | Lenpaste is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 14 | or FITNESS FOR A PARTICULAR PURPOSE. 15 | See the GNU Affero Public License for more details. 16 | 17 | You should have received a copy of the GNU Affero Public License along with Lenpaste. 18 | If not, see . 19 | */}} 20 | 21 | {{define "titlePrefix"}}{{ call .Translate `docs.Title` }} | {{end}} 22 | {{define "headAppend"}}{{end}} 23 | {{define "article"}} 24 |

{{ call .Translate `docs.Title` }}

25 | 29 | {{end}} 30 | -------------------------------------------------------------------------------- /internal/web/data/docs_api_libs.tmpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Copyright (C) 2021-2023 Leonid Maslakov. 3 | 4 | This file is part of Lenpaste. 5 | 6 | Lenpaste is free software: you can redistribute it 7 | and/or modify it under the terms of the 8 | GNU Affero Public License as published by the 9 | Free Software Foundation, either version 3 of the License, 10 | or (at your option) any later version. 11 | 12 | Lenpaste is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 14 | or FITNESS FOR A PARTICULAR PURPOSE. 15 | See the GNU Affero Public License for more details. 16 | 17 | You should have received a copy of the GNU Affero Public License along with Lenpaste. 18 | If not, see . 19 | */}} 20 | 21 | {{define "titlePrefix"}}{{ call .Translate `docsAPIv1Libs.Title` }} | {{end}} 22 | {{define "headAppend"}}{{end}} 23 | {{define "article"}} 24 |

{{ call .Translate `docs.Title` }} / {{ call .Translate `docsAPIv1Libs.Title` }}

25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
{{ call .Translate `docsAPIv1Libs.Name` }}{{ call .Translate `docsAPIv1Libs.Language` }}{{ call .Translate `docsAPIv1Libs.ApiVersion` }}{{ call .Translate `docsAPIv1Libs.Status` }}{{ call .Translate `docsAPIv1Libs.License` }}
PasteAPI.goGov1.2{{ call .Translate `docsAPIv1Libs.StatusOfficial`}}MIT
40 |

{{ call .Translate `docsAPIv1Libs.OutOfDateTitle` }}

41 |

{{ call .Translate `docsAPIv1Libs.OutOfDataMessage` }}

42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 |
{{ call .Translate `docsAPIv1Libs.Name` }}{{ call .Translate `docsAPIv1Libs.Language` }}{{ call .Translate `docsAPIv1Libs.ApiVersion` }}{{ call .Translate `docsAPIv1Libs.Status` }}{{ call .Translate `docsAPIv1Libs.License` }}
LeninGov0.2{{ call .Translate `docsAPIv1Libs.StatusOfficial` }}{{ call .Translate `docsAPIv1Libs.GPL3Later` }}
PyLeninPythonv0.1{{ call .Translate `docsAPIv1Libs.StatusUnofficial` }}{{ call .Translate `docsAPIv1Libs.GPL3Later` }}
63 | {{end}} 64 | -------------------------------------------------------------------------------- /internal/web/data/docs_apiv1.tmpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Copyright (C) 2021-2023 Leonid Maslakov. 3 | 4 | This file is part of Lenpaste. 5 | 6 | Lenpaste is free software: you can redistribute it 7 | and/or modify it under the terms of the 8 | GNU Affero Public License as published by the 9 | Free Software Foundation, either version 3 of the License, 10 | or (at your option) any later version. 11 | 12 | Lenpaste is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 14 | or FITNESS FOR A PARTICULAR PURPOSE. 15 | See the GNU Affero Public License for more details. 16 | 17 | You should have received a copy of the GNU Affero Public License along with Lenpaste. 18 | If not, see . 19 | */}} 20 | 21 | {{define "titlePrefix"}}{{call .Translate `docsAPIv1.Title`}} | {{end}} 22 | {{define "headAppend"}}{{end}} 23 | {{define "article"}} 24 |

{{call .Translate `docs.Title`}} / {{call .Translate `docsAPIv1.Title`}}

25 | 26 |

{{call .Translate `docsAPIv1.Introduction1`}}

27 |

{{call .Translate `docsAPIv1.Introduction2`}}

28 | 29 |

{{call .Translate `docsAPIv1.TableOfContent`}}

30 | 36 | 37 | 38 |

POST /api/v1/new

39 |

{{call .Translate `docsAPIv1.NewPasteAuth`}}

40 |

{{call .Translate `docsAPIv1.RequestParameters`}}

41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 |
{{call .Translate `docsAPIv1.Field`}}{{call .Translate `docsAPIv1.Required`}}{{call .Translate `docsAPIv1.Default`}}{{call .Translate `docsAPIv1.Description`}}
title{{call .Translate `docsAPIv1.ReqNewTitle`}}
body{{call .Translate `docsAPIv1.RequiredYes`}}{{call .Translate `docsAPIv1.ReqNewBody`}}
lineEndLF{{call .Translate `docsAPIv1.ReqNewLineEnd`}}
syntaxplaintext{{call .Translate `docsAPIv1.ReqNewSyntax` `#getServerInfo`}}
oneUsefalse{{call .Translate `docsAPIv1.ReqNewOneUse`}}
expiration0{{call .Translate `docsAPIv1.ReqNewExpiration`}}
author{{call .Translate `docsAPIv1.ReqNewAuthor` .MaxLenAuthorAll}}
authorEmail{{call .Translate `docsAPIv1.ReqNewAuthorEmail` .MaxLenAuthorAll}}
authorURL{{call .Translate `docsAPIv1.ReqNewAuthorURL` .MaxLenAuthorAll}}
101 |

{{call .Translate `docsAPIv1.ResponseExample`}}

102 | {{ call .Highlight `{ 103 | "id": "XcmX9ON1", 104 | "createTime": 1653387358, 105 | "deleteTime": 0 106 | }` `json`}} 107 | 108 | 109 |

GET /api/v1/get

110 |

{{call .Translate `docsAPIv1.RequestParameters`}}

111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 |
{{call .Translate `docsAPIv1.Field`}}{{call .Translate `docsAPIv1.Required`}}{{call .Translate `docsAPIv1.Default`}}{{call .Translate `docsAPIv1.Description`}}
id{{call .Translate `docsAPIv1.RequiredYes`}}{{call .Translate `docsAPIv1.ReqGetID`}}
openOneUsefalse{{call .Translate `docsAPIv1.ReqGetOpenOneUse`}}
129 |

{{call .Translate `docsAPIv1.ResponseExample`}}

130 | {{ call .Highlight `{ 131 | "id": "XcmX9ON1", 132 | "title": "Paste title.", 133 | "body": "Line 1.\nLine 2.\nLine 3.\n\nLine 5.", 134 | "createTime": 1653387358, 135 | "deleteTime": 0, 136 | "oneUse": false, 137 | "syntax": "plaintext", 138 | "author": "Anon", 139 | "authorEmail": "me@example.org", 140 | "authorURL": "https://example.org" 141 | }` `json`}} 142 | {{ call .Highlight `{ 143 | "id": "5mqqHZRg", 144 | "title": "", 145 | "body": "", 146 | "createTime": 0, 147 | "deleteTime": 0, 148 | "oneUse": true, 149 | "syntax": "", 150 | "author": "", 151 | "authorEmail": "", 152 | "authorURL": "" 153 | }` `json`}} 154 | 155 | 156 |

GET /api/v1/getServerInfo

157 |

{{call .Translate `docsAPIv1.ResponseExample`}}

158 | {{ call .Highlight `{ 159 | "software": "Lenpaste", 160 | "version": "1.2", 161 | "titleMaxlength": 100, 162 | "bodyMaxlength": 20000, 163 | "maxLifeTime": -1, 164 | "serverAbout": "", 165 | "serverRules": "", 166 | "serverTermsOfUse": "", 167 | "adminName": "Vasya Pupkin", 168 | "adminMail": "me@example.org", 169 | "syntaxes": [ 170 | "ABAP", 171 | "ABNF", 172 | "AL", 173 | "ANTLR", 174 | "APL", 175 | "ActionScript", 176 | "ActionScript 3", 177 | "Ada", 178 | "Angular2", 179 | "ApacheConf", 180 | "AppleScript", 181 | "Arduino", 182 | "ArmAsm", 183 | "Awk" 184 | ], 185 | "uiDefaultLifeTime": "1y", 186 | "authRequired": false 187 | }` `json`}} 188 | 189 | 190 |

{{call .Translate `docsAPIv1.PossibleAPIErrors`}}

191 |

{{call .Translate `docsAPIv1.Error400`}}

192 | {{ call .Highlight `{ 193 | "code": 400, 194 | "error": "Bad Request" 195 | }` `json`}} 196 | 197 |

{{call .Translate `docsAPIv1.Error401`}}

198 | {{ call .Highlight `{ 199 | "code": 401, 200 | "error": "Unauthorized" 201 | }` `json`}} 202 | 203 |

{{call .Translate `docsAPIv1.Error404n1`}}

204 | {{ call .Highlight `{ 205 | "code": 404, 206 | "error": "Could not find ID" 207 | }` `json`}} 208 | 209 |

{{call .Translate `docsAPIv1.Error404n2`}}

210 | {{ call .Highlight `{ 211 | "code": 404, 212 | "error": "Not Found" 213 | }` `json`}} 214 | 215 |

{{call .Translate `docsAPIv1.Error405`}}

216 | {{ call .Highlight `{ 217 | "code": 405, 218 | "error": "Method Not Allowed" 219 | }` `json`}} 220 | 221 |

{{call .Translate `docsAPIv1.Error413`}}

222 | {{ call .Highlight `{ 223 | "code": 413, 224 | "error": "Payload Too Large" 225 | }` `json`}} 226 | 227 |

{{call .Translate `docsAPIv1.Error429`}}

228 | {{ call .Highlight `{ 229 | "code": 429, 230 | "error": "Too Many Requests" 231 | }` `json`}} 232 | 233 |

{{call .Translate `docsAPIv1.Error500`}}

234 | {{ call .Highlight `{ 235 | "code": 500, 236 | "error": "Internal Server Error" 237 | }` `json`}} 238 | {{end}} 239 | -------------------------------------------------------------------------------- /internal/web/data/emb.tmpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Copyright (C) 2021-2023 Leonid Maslakov. 3 | 4 | This file is part of Lenpaste. 5 | 6 | Lenpaste is free software: you can redistribute it 7 | and/or modify it under the terms of the 8 | GNU Affero Public License as published by the 9 | Free Software Foundation, either version 3 of the License, 10 | or (at your option) any later version. 11 | 12 | Lenpaste is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 14 | or FITNESS FOR A PARTICULAR PURPOSE. 15 | See the GNU Affero Public License for more details. 16 | 17 | You should have received a copy of the GNU Affero Public License along with Lenpaste. 18 | If not, see . 19 | */}} 20 | 21 | 22 | 23 | 24 | 50 | 51 | 52 |
53 |
{{if .Title}}{{.Title}}{{end}}
54 | 55 |
56 | {{if or (.ErrorNotFound) (.OneUse) (ne .DeleteTime 0)}} 57 |
58 | {{if .ErrorNotFound}} 59 |
{{ call .Translate `pasteEmd.Error` }}: {{ call .Translate `pasteEmd.ErrorNotFound` }}
60 | {{else}} 61 |
{{ call .Translate `pasteEmd.Error` }}: {{ call .Translate `pasteEmb.ErrorCouldNotEmb` }}
62 | {{end}} 63 |
64 | {{else}} 65 |
{{.Body}}
66 | {{end}} 67 | 68 | 69 | -------------------------------------------------------------------------------- /internal/web/data/emb_help.tmpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Copyright (C) 2021-2023 Leonid Maslakov. 3 | 4 | This file is part of Lenpaste. 5 | 6 | Lenpaste is free software: you can redistribute it 7 | and/or modify it under the terms of the 8 | GNU Affero Public License as published by the 9 | Free Software Foundation, either version 3 of the License, 10 | or (at your option) any later version. 11 | 12 | Lenpaste is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 14 | or FITNESS FOR A PARTICULAR PURPOSE. 15 | See the GNU Affero Public License for more details. 16 | 17 | You should have received a copy of the GNU Affero Public License along with Lenpaste. 18 | If not, see . 19 | */}} 20 | 21 | {{define "titlePrefix"}}{{ call .Translate `pasteEmbHelp.Title` }} {{.ID}} | {{end}} 22 | {{define "headAppend"}}{{end}} 23 | {{define "article"}} 24 |

{{.ID}} / {{ call .Translate `pasteEmbHelp.Title` }}

25 | {{if or (.OneUse) (ne .DeleteTime 0)}} 26 |

{{ call .Translate `pasteEmbHelp.OneUseError` }}`

27 | {{else}} 28 |

{{ call .Translate `pasteEmbHelp.Message` }}

29 | {{call .Highlight (printf `` .Protocol .Host .ID) `html`}} 30 | {{end}} 31 | {{end}} 32 | -------------------------------------------------------------------------------- /internal/web/data/error.tmpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Copyright (C) 2021-2023 Leonid Maslakov. 3 | 4 | This file is part of Lenpaste. 5 | 6 | Lenpaste is free software: you can redistribute it 7 | and/or modify it under the terms of the 8 | GNU Affero Public License as published by the 9 | Free Software Foundation, either version 3 of the License, 10 | or (at your option) any later version. 11 | 12 | Lenpaste is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 14 | or FITNESS FOR A PARTICULAR PURPOSE. 15 | See the GNU Affero Public License for more details. 16 | 17 | You should have received a copy of the GNU Affero Public License along with Lenpaste. 18 | If not, see . 19 | */}} 20 | 21 | {{define "titlePrefix"}}{{.Code}} | {{end}} 22 | {{define "headAppend"}}{{end}} 23 | {{define "article"}} 24 |

{{.Code}}

25 | {{if eq .Code 400 }}

{{ call .Translate `error.400` }}

{{end}} 26 | {{if eq .Code 401 }}

{{ call .Translate `error.401` }}

{{end}} 27 | {{if eq .Code 404 }}

{{ call .Translate `error.404` }}

{{end}} 28 | {{if eq .Code 405 }}

{{ call .Translate `error.405` }}

{{end}} 29 | {{if eq .Code 413 }}

{{ call .Translate `error.413` }}

{{end}} 30 | {{if eq .Code 429 }}

{{ call .Translate `error.429` }}

{{end}} 31 | {{if eq .Code 500 }}

{{ call .Translate `error.500` }}

{{end}} 32 | 33 | 34 | {{if and (ne .AdminName ``) (ne .AdminMail ``)}} 35 |

{{ call .Translate `error.AdminContacts` }} {{.AdminName}} <{{.AdminMail}}>

36 | {{else}} 37 | {{if ne .AdminName ``}}

{{ call .Translate `error.AdminContacts` }} {{.AdminName}}

{{end}} 38 | {{if ne .AdminMail ``}}

{{ call .Translate `error.AdminContacts` }} {{.AdminMail}}

{{end}} 39 | {{end}} 40 | 41 |

<< {{ call .Translate `error.BackToHome` }}

42 | {{end}} 43 | -------------------------------------------------------------------------------- /internal/web/data/locale/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "about.AboutServerTitle": "About this server", 3 | "about.AdminEmail": "Administrator's mail:", 4 | "about.AdminName": "Administrator's name:", 5 | "about.AdminTitle": "Contact the administrator", 6 | "about.Lenpaste1": "Lenpaste is free software. All of its source code is available under the %s license.", 7 | "about.Lenpaste2": "You do not need to register here.", 8 | "about.Lenpaste3": "This site does not use cookies to keep track of you.", 9 | "about.Lenpaste4": "This site can work without JavaScript.", 10 | "about.Lenpaste5": "Lenpaste has its own API.", 11 | "about.LenpasteAuthors": "Learn more about the authors of Lenpaste software.", 12 | "about.LenpasteMessage": "This server uses a Lenpaste version of %s. A little bit about it:", 13 | "about.LenpasteTitle": "What is Lenpaste?", 14 | "about.Limit": "Limits", 15 | "about.LimitBody": "Maximum length of the paste: %d symbols.", 16 | "about.LimitBodyNo": "The length of the paste is unlimited.", 17 | "about.LimitLifeTime": "Maximum lifetime of the paste: %d seconds.", 18 | "about.LimitLifeTimeNo": "The lifetime of the paste is unlimited.", 19 | "about.LimitTitle": "Maximum title length: %d characters.", 20 | "about.LimitTitleDisable": "On this server, you cannot set a header for the paste.", 21 | "about.LimitTitleNo": "There is no limit to the length of the title.", 22 | "about.RulesTitle": "Rules of this server", 23 | "about.SeeTerms": "See the terms of use for more information.", 24 | "about.Title": "About", 25 | "authors.Title": "Authors", 26 | "base.About": "About", 27 | "base.Docs": "Docs", 28 | "base.Lenpaste": "Lenpaste", 29 | "base.Settings": "Settings", 30 | "codeJS.Paste": "Copy", 31 | "docs.Title": "Documentation", 32 | "docsAPIv1.Default": "Default", 33 | "docsAPIv1.Description": "Description", 34 | "docsAPIv1.Error400": "This API method exists on the server, but you passed the wrong arguments for it.", 35 | "docsAPIv1.Error401": "This server requires \"HTTP Basic Authentication\" authorization.", 36 | "docsAPIv1.Error404n1": "There is no paste with this ID.", 37 | "docsAPIv1.Error404n2": "There is no such API method.", 38 | "docsAPIv1.Error405": "You made a mistake with HTTP request (example: you made POST instead of GET).", 39 | "docsAPIv1.Error413": "You have exceeded the maximum size of one or more fields (title, body, author, authorEmail, authorURL).", 40 | "docsAPIv1.Error429": "You have made too many requests, try again after some time. The Retry-After HTTP header will also be returned along with this error.", 41 | "docsAPIv1.Error500": "There was a failure on the server. Contact your server administrator to find out what the problem is.", 42 | "docsAPIv1.Field": "Field", 43 | "docsAPIv1.NewPasteAuth": "If you are using a private server, authenticate using \"HTTP Basic Authentication\". Otherwise you will get a 401 error.", 44 | "docsAPIv1.PossibleAPIErrors": "Possible API errors", 45 | "docsAPIv1.ReqGetID": "Paste ID.", 46 | "docsAPIv1.ReqGetOpenOneUse": "If true, the entire contents of the paste will be returned, after which it will be deleted. If false, the API will return only id and oneUse, and the paste will not be deleted.", 47 | "docsAPIv1.ReqNewAuthor": "Author name. Must not be more than %d characters.", 48 | "docsAPIv1.ReqNewAuthorEmail": "Author email. Must not be more than %d characters.", 49 | "docsAPIv1.ReqNewAuthorURL": "Author URL. Must not be more than %d characters.", 50 | "docsAPIv1.ReqNewBody": "Paste text.", 51 | "docsAPIv1.ReqNewExpiration": "Indicates expiration of paste in seconds. If this parameter is 0, the storage time will be unlimited.", 52 | "docsAPIv1.ReqNewLineEnd": "Line end in the text of the excerpt will automatically be replaced by the one specified by this parameter. Can be LF, CRLF or CR.", 53 | "docsAPIv1.ReqNewOneUse": "If it is true, the paste can be opened only once and then it will be deleted.", 54 | "docsAPIv1.ReqNewSyntax": "Syntax highlighting in paste. A list of available syntaxes can be obtained using the getServerInfo method.", 55 | "docsAPIv1.ReqNewTitle": "Paste title.", 56 | "docsAPIv1.RequestParameters": "Request parameters:", 57 | "docsAPIv1.Required": "Required?", 58 | "docsAPIv1.RequiredYes": "Yes", 59 | "docsAPIv1.ResponseExample": "Response example:", 60 | "docsAPIv1.TableOfContent": "Table of content", 61 | "docsAPIv1.Title": "API v1", 62 | "docsAPIv1.Introduction1": "The Lenpaste API does not require registration to use it.", 63 | "docsAPIv1.Introduction2": "Any POST request to the API is sent in application/x-www-form-urlencoded format. Similarly, the API always returns a response to any request in the format application/json and UTF8 encoding.", 64 | "docsAPIv1Libs.ApiVersion": "API version", 65 | "docsAPIv1Libs.GPL3Later": "GPL 3.0 or later", 66 | "docsAPIv1Libs.Language": "Language", 67 | "docsAPIv1Libs.License": "License", 68 | "docsAPIv1Libs.Name": "Name", 69 | "docsAPIv1Libs.OutOfDataMessage": "These libraries are no longer being updated. This means that they cannot use the Lenpaste API v1.0 and higher.", 70 | "docsAPIv1Libs.OutOfDateTitle": "Out of date", 71 | "docsAPIv1Libs.Recommended": "Recommended", 72 | "docsAPIv1Libs.Status": "Status", 73 | "docsAPIv1Libs.StatusOfficial": "Official", 74 | "docsAPIv1Libs.StatusUnofficial": "Unofficial", 75 | "docsAPIv1Libs.Title": "Libraries for working with API", 76 | "error.400": "Bad Request", 77 | "error.401": "Unauthorized", 78 | "error.404": "Not Found", 79 | "error.405": "Method Not Allowed", 80 | "error.413": "Payload Too Large", 81 | "error.429": "Too Many Requests", 82 | "error.500": "Internal Server Error", 83 | "error.AdminContacts": "Contact administrator:", 84 | "error.BackToHome": "Back to Home", 85 | "error.Error": "Error", 86 | "historyJS.ClearHistory": "Clear history...", 87 | "historyJS.ClearHistoryConfirm": "Are you sure you want to clear the history?", 88 | "historyJS.EnableHistory": "Remember history", 89 | "historyJS.Error": "HTTP error: %d %s", 90 | "historyJS.ErrorUnknown": "Unknown error: %s", 91 | "historyJS.History": "History", 92 | "historyJS.HistoryDisabledAlert": "History saving has been DISABLED.", 93 | "historyJS.HistoryEnabledAlert": "History saving has been ENABLED.", 94 | "historyJS.LocalStorageNotSupported1": "Your browser does not support JavaScript localStorage, so the history saving function is disabled.", 95 | "historyJS.LocalStorageNotSupported2": "If you are using Safari, try exiting private browsing mode, this should fix the error.", 96 | "historyJS.Untitled": "Untitled", 97 | "license.LicenseTitle": "License", 98 | "locale.Name": "English", 99 | "main.10Minutes": "10 minutes", 100 | "main.12Hour": "12 hours", 101 | "main.1Day": "1 day", 102 | "main.1Hour": "1 hour", 103 | "main.1Month": "1 month", 104 | "main.1Week": "1 week", 105 | "main.1Year": "1 year", 106 | "main.2Hour": "2 hours", 107 | "main.2Months": "2 months", 108 | "main.2Weeks": "2 weeks", 109 | "main.30Minutes": "30 minutes", 110 | "main.4Hour": "4 hours", 111 | "main.6Months": "6 months", 112 | "main.AcceptTerms": "See Terms of Use", 113 | "main.AdvancedParameters": "Advanced parameters", 114 | "main.AdvancedParametersHelp": "*You can set the default values for these parameters in the settings.", 115 | "main.AuthRequired": "This is a private server and authorization is required to use it. Please contact the server administrator for details.", 116 | "main.Author": "Author name:", 117 | "main.AuthorEmail": "Author email:", 118 | "main.AuthorEmailPlaceholder": "me@example.org", 119 | "main.AuthorPlaceholder": "Name", 120 | "main.AuthorURL": "Author URL:", 121 | "main.AuthorURLPlaceholder": "https://example.org", 122 | "main.BurnAfterReading": "Burn after reading", 123 | "main.Create": "Create New Paste", 124 | "main.CreatePaste": "Create paste", 125 | "main.EnterText": "Enter text...", 126 | "main.EnterTitle": "Title (optional)...", 127 | "main.Expiration": "Expiration:", 128 | "main.MaximumSymbols": "*Maximum %d symbols", 129 | "main.Never": "Never", 130 | "main.Syntax": "Syntax:", 131 | "paste.Author": "Author:", 132 | "paste.Created": "Created:", 133 | "paste.Download": "Download", 134 | "paste.Embedded": "Embedded", 135 | "paste.Expires": "Expires:", 136 | "paste.Never": "Never", 137 | "paste.Now": "Now", 138 | "paste.Raw": "Raw", 139 | "pasteContinue.Cancel": "Cancel", 140 | "pasteContinue.Continue": "Continue", 141 | "pasteContinue.Message": "This paste can only be viewed once, after which it will be deleted. Continue?", 142 | "pasteContinue.Title": "Continue?", 143 | "pasteEmb.ErrorCouldNotEmb": "This paste cannot be embedded in other pages", 144 | "pasteEmbHelp.Message": "Add the following code to your page:", 145 | "pasteEmbHelp.OneUseError": "You cannot embed the paste in another page if it is intended to be read once or has a limited expiration.", 146 | "pasteEmbHelp.Title": "Embedded", 147 | "pasteEmd.Error": "Error:", 148 | "pasteEmd.ErrorNotFound": "404 Not Found", 149 | "pasteJS.ShortMonth": "\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"", 150 | "pasteJS.ShortWeekDay": "\"Sun\", \"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\"", 151 | "settings.Language": "Language:", 152 | "settings.LanguageDefault": "Use browser language", 153 | "settings.Save": "Save Settings", 154 | "settings.Theme": "Theme:", 155 | "settings.Title": "Settings", 156 | "sourceCode.Message": "Unfortunately, it is not yet possible to download the source code directly from this server. But you can download it from the link:", 157 | "sourceCode.Title": "Source Code", 158 | "terms.NoTerms": "This server has no terms of use.", 159 | "terms.Notice": "The Terms of Use apply to this server only, not to the Lenpaste software.", 160 | "terms.Title": "Terms of Use" 161 | } 162 | -------------------------------------------------------------------------------- /internal/web/data/main.js: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | document.addEventListener("DOMContentLoaded", () => { 20 | var editor = document.getElementById("editor"); 21 | 22 | editor.addEventListener("keydown", (e) => { 23 | // If TAB pressed 24 | if (e.keyCode === 9) { 25 | e.preventDefault(); 26 | 27 | let startOrig = editor.selectionStart; 28 | let endOrig = editor.selectionEnd; 29 | 30 | editor.value = editor.value.substring(0, startOrig) + "\t" + editor.value.substring(endOrig); 31 | 32 | editor.selectionStart = editor.selectionEnd = startOrig + 1; 33 | } 34 | }); 35 | 36 | // Add HTML and CSS code for line numbers support 37 | editor.insertAdjacentHTML("beforebegin", ""); 38 | var editorLines = document.getElementById("editorLines"); 39 | editorLines.rows = editor.rows; 40 | 41 | var styleSheet = document.createElement("style"); 42 | styleSheet.innerText = ` 43 | #editor { 44 | margin-left: 60px; 45 | resize: none; 46 | 47 | width: calc(100% - 60px); 48 | min-width: calc(100% - 60px); 49 | max-width: calc(100% - 60px); 50 | } 51 | 52 | #editorLines { 53 | display: flex; 54 | user-select: none; 55 | 56 | text-align: right; 57 | position: absolute; 58 | resize: none; 59 | overflow-y: hidden; 60 | width: 60px; 61 | max-width: 60px; 62 | min-width: 60px; 63 | } 64 | 65 | #editor:focus-visible, #editorLines:focus-visible { 66 | outline: 0; 67 | } 68 | `; 69 | document.head.appendChild(styleSheet); 70 | 71 | editorLines.addEventListener("focus", () => { 72 | editor.focus(); 73 | }); 74 | 75 | // Add JS code for line numbers 76 | editor.addEventListener("scroll", () => { 77 | editorLines.scrollTop = editor.scrollTop; 78 | editorLines.scrollLeft = editor.scrollLeft; 79 | }); 80 | 81 | var lineCountCache = 0; 82 | editor.addEventListener("input", () => { 83 | let lineCount = editor.value.split("\n").length; 84 | 85 | if (lineCountCache != lineCount) { 86 | editorLines.value = ""; 87 | 88 | for (var i = 0; i < lineCount; i++) { 89 | editorLines.value = editorLines.value + (i + 1) + "\n"; 90 | } 91 | 92 | lineCountCache = lineCount; 93 | } 94 | }); 95 | 96 | // Add symbol counter 97 | document.getElementById("symbolCounterContainer").innerHTML = ""; 98 | var symbolCounter = document.getElementById("symbolCounter"); 99 | 100 | function updateSymbolCounter() { 101 | symbolCounter.textContent = editor.value.length; 102 | 103 | if (editor.maxLength !== -1) { 104 | symbolCounter.textContent = symbolCounter.textContent + "/" + editor.maxLength; 105 | } else { 106 | symbolCounter.textContent = symbolCounter.textContent + "/∞"; 107 | } 108 | } 109 | 110 | editor.addEventListener("input", updateSymbolCounter); 111 | updateSymbolCounter(); 112 | }); 113 | -------------------------------------------------------------------------------- /internal/web/data/main.tmpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Copyright (C) 2021-2023 Leonid Maslakov. 3 | 4 | This file is part of Lenpaste. 5 | 6 | Lenpaste is free software: you can redistribute it 7 | and/or modify it under the terms of the 8 | GNU Affero Public License as published by the 9 | Free Software Foundation, either version 3 of the License, 10 | or (at your option) any later version. 11 | 12 | Lenpaste is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 14 | or FITNESS FOR A PARTICULAR PURPOSE. 15 | See the GNU Affero Public License for more details. 16 | 17 | You should have received a copy of the GNU Affero Public License along with Lenpaste. 18 | If not, see . 19 | */}} 20 | 21 | {{define "titlePrefix"}}{{end}} 22 | {{define "headAppend"}}{{end}} 23 | {{define "article"}} 24 | {{if eq .AuthOk false}} 25 |

{{call .Translate `main.CreatePaste`}}

26 |

{{call .Translate `main.AuthRequired`}}

27 | {{else}} 28 | {{if ne .TitleMaxLen 0}}

{{call .Translate `main.CreatePaste`}}

{{end}} 29 |
30 |
31 |
32 | {{if ne .TitleMaxLen 0}} 37 | {{else}} 38 |

{{call .Translate `main.CreatePaste`}}

39 | {{end}} 40 |
41 |
42 | 47 |
48 |
49 |
55 |
56 |
57 | 63 |
64 |
65 | {{if gt .BodyMaxLen 0}}{{call .Translate `main.MaximumSymbols` .BodyMaxLen}}{{end}} 66 |
67 |
68 |
69 | 70 |
71 |
72 | 89 |
90 |
91 | {{ call .Translate `main.AdvancedParameters` }} 92 | 93 | 94 | 95 | 101 | 102 | 103 | 104 | 110 | 111 | 112 | 113 | 119 | 120 |
121 |

{{call .Translate `main.AdvancedParametersHelp` `/settings`}}

122 |
123 |
124 |
125 |
{{if .ServerTermsExist}}{{ call .Translate `main.AcceptTerms` `/terms` }}{{end}}
126 |
127 |
128 | {{end}} 129 | {{end}} 130 | -------------------------------------------------------------------------------- /internal/web/data/paste.js: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | document.addEventListener("DOMContentLoaded", () => { 20 | const shortWeekDay = [{{call .Translate `pasteJS.ShortWeekDay`}}]; 21 | const shortMonth = [{{call .Translate `pasteJS.ShortMonth`}}]; 22 | 23 | function dateToString(date) { 24 | let dateStr = shortWeekDay[date.getDay()] + ", " + date.getDate() + " " + shortMonth[date.getMonth()]; 25 | dateStr = dateStr + " " + date.getFullYear(); 26 | dateStr = dateStr + " " + date.getHours() + ":" + date.getMinutes() + ":" + date.getSeconds(); 27 | 28 | let tz = date.getTimezoneOffset() / 60 * -1; 29 | if (tz >= 0) { 30 | dateStr = dateStr + " +" + tz; 31 | 32 | } else { 33 | dateStr = dateStr + " " + tz; 34 | } 35 | 36 | return dateStr; 37 | } 38 | 39 | let createTime = document.getElementById("createTime"); 40 | createTime.textContent = dateToString(new Date(createTime.textContent)); 41 | 42 | 43 | let deleteTime = document.getElementById("deleteTime"); 44 | if (deleteTime != null) { 45 | deleteTime.textContent = dateToString(new Date(deleteTime.textContent)); 46 | } 47 | }); 48 | -------------------------------------------------------------------------------- /internal/web/data/paste.tmpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Copyright (C) 2021-2023 Leonid Maslakov. 3 | 4 | This file is part of Lenpaste. 5 | 6 | Lenpaste is free software: you can redistribute it 7 | and/or modify it under the terms of the 8 | GNU Affero Public License as published by the 9 | Free Software Foundation, either version 3 of the License, 10 | or (at your option) any later version. 11 | 12 | Lenpaste is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 14 | or FITNESS FOR A PARTICULAR PURPOSE. 15 | See the GNU Affero Public License for more details. 16 | 17 | You should have received a copy of the GNU Affero Public License along with Lenpaste. 18 | If not, see . 19 | */}} 20 | 21 | {{define "titlePrefix"}}{{if .Title}}{{.Title}}{{else}}{{.ID}}{{end}} | {{end}} 22 | {{define "headAppend"}} 23 | 24 | 25 | {{end}} 26 | {{define "article"}} 27 | {{if .Title}} 28 | {{end}} 29 | 30 |
31 |
{{.Syntax}}, {{.LineEnd}}
32 | 33 | {{if not .OneUse}} 34 |
35 | {{ call .Translate `paste.Raw` }}{{ call .Translate `paste.Download` }}{{ call .Translate `paste.Embedded`}} 36 |
37 | {{end}} 38 |
39 | 40 | {{.Body}} 41 | 42 | {{if and (ne .Author ``) (ne .AuthorEmail ``) (ne .AuthorURL ``) }}

{{ call .Translate `paste.Author` }} {{.Author}} <{{.AuthorEmail}}> - {{.AuthorURL}}

{{end}} 43 | {{if and (ne .Author ``) (ne .AuthorEmail ``) (eq .AuthorURL ``) }}

{{ call .Translate `paste.Author` }} {{.Author}} <{{.AuthorEmail}}>

{{end}} 44 | {{if and (ne .Author ``) (eq .AuthorEmail ``) (ne .AuthorURL ``) }}

{{ call .Translate `paste.Author` }} {{.Author}} - {{.AuthorURL}}

{{end}} 45 | {{if and (eq .Author ``) (ne .AuthorEmail ``) (ne .AuthorURL ``) }}

{{ call .Translate `paste.Author` }} {{.AuthorEmail}} - {{.AuthorURL}}

{{end}} 46 | {{if and (ne .Author ``) (eq .AuthorEmail ``) (eq .AuthorURL ``) }}

{{ call .Translate `paste.Author` }} {{.Author}}

{{end}} 47 | {{if and (eq .Author ``) (ne .AuthorEmail ``) (eq .AuthorURL ``) }}

{{ call .Translate `paste.Author` }} {{.AuthorEmail}}

{{end}} 48 | {{if and (eq .Author ``) (eq .AuthorEmail ``) (ne .AuthorURL ``) }}

{{ call .Translate `paste.Author` }} {{.AuthorURL}}

{{end}} 49 | 50 |

{{ call .Translate `paste.Created` }} {{.CreateTimeStr}}

51 | 52 | {{if .OneUse}} 53 |

{{ call .Translate `paste.Expires` }} {{ call .Translate `paste.Now` }}

54 | {{else if eq .DeleteTime 0}} 55 |

{{ call .Translate `paste.Expires` }} {{ call .Translate `paste.Never` }}

56 | {{else}} 57 |

{{ call .Translate `paste.Expires` }} {{.DeleteTimeStr}}

58 | {{end}} 59 | 60 | {{end}} 61 | -------------------------------------------------------------------------------- /internal/web/data/paste_continue.tmpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Copyright (C) 2021-2023 Leonid Maslakov. 3 | 4 | This file is part of Lenpaste. 5 | 6 | Lenpaste is free software: you can redistribute it 7 | and/or modify it under the terms of the 8 | GNU Affero Public License as published by the 9 | Free Software Foundation, either version 3 of the License, 10 | or (at your option) any later version. 11 | 12 | Lenpaste is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 14 | or FITNESS FOR A PARTICULAR PURPOSE. 15 | See the GNU Affero Public License for more details. 16 | 17 | You should have received a copy of the GNU Affero Public License along with Lenpaste. 18 | If not, see . 19 | */}} 20 | 21 | {{define "titlePrefix"}}{{.ID}} | {{end}} 22 | {{define "headAppend"}}{{end}} 23 | {{define "article"}} 24 |

{{ call .Translate `pasteContinue.Title` }}

25 |

{{ call .Translate `pasteContinue.Message` }}

26 |
27 |
28 | 29 |
30 |
31 | 32 | 33 |
34 |
35 | {{end}} 36 | -------------------------------------------------------------------------------- /internal/web/data/settings.tmpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Copyright (C) 2021-2023 Leonid Maslakov. 3 | 4 | This file is part of Lenpaste. 5 | 6 | Lenpaste is free software: you can redistribute it 7 | and/or modify it under the terms of the 8 | GNU Affero Public License as published by the 9 | Free Software Foundation, either version 3 of the License, 10 | or (at your option) any later version. 11 | 12 | Lenpaste is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 14 | or FITNESS FOR A PARTICULAR PURPOSE. 15 | See the GNU Affero Public License for more details. 16 | 17 | You should have received a copy of the GNU Affero Public License along with Lenpaste. 18 | If not, see . 19 | */}} 20 | 21 | {{define "titlePrefix"}}{{call .Translate `settings.Title`}} | {{end}} 22 | {{define "headAppend"}}{{end}} 23 | {{define "article"}} 24 |

{{call .Translate `settings.Title`}}

25 |
26 | 27 | 28 | 29 | 36 | 37 | 38 | 39 | 45 | 46 |
47 | {{if eq .AuthOk true}} 48 |

{{ call .Translate `main.AdvancedParameters` }}

49 | 50 | 51 | 52 | 58 | 59 | 60 | 61 | 67 | 68 | 69 | 70 | 76 | 77 |
78 | {{end}} 79 |
80 |
81 | {{end}} 82 | -------------------------------------------------------------------------------- /internal/web/data/source_code.tmpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Copyright (C) 2021-2023 Leonid Maslakov. 3 | 4 | This file is part of Lenpaste. 5 | 6 | Lenpaste is free software: you can redistribute it 7 | and/or modify it under the terms of the 8 | GNU Affero Public License as published by the 9 | Free Software Foundation, either version 3 of the License, 10 | or (at your option) any later version. 11 | 12 | Lenpaste is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 14 | or FITNESS FOR A PARTICULAR PURPOSE. 15 | See the GNU Affero Public License for more details. 16 | 17 | You should have received a copy of the GNU Affero Public License along with Lenpaste. 18 | If not, see . 19 | */}} 20 | 21 | {{define "titlePrefix"}}{{ call .Translate `sourceCode.Title` }} | {{end}} 22 | {{define "headAppend"}}{{end}} 23 | {{define "article"}} 24 |

{{ call .Translate `about.Title` }} / {{ call .Translate `sourceCode.Title` }}

25 |

{{ call .Translate `sourceCode.Message` }} 26 |
27 | https://github.com/lcomrade/lenpaste

28 | {{end}} 29 | -------------------------------------------------------------------------------- /internal/web/data/style.css: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2023 Leonid Maslakov. 3 | 4 | This file is part of Lenpaste. 5 | 6 | Lenpaste is free software: you can redistribute it 7 | and/or modify it under the terms of the 8 | GNU Affero Public License as published by the 9 | Free Software Foundation, either version 3 of the License, 10 | or (at your option) any later version. 11 | 12 | Lenpaste is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 14 | or FITNESS FOR A PARTICULAR PURPOSE. 15 | See the GNU Affero Public License for more details. 16 | 17 | You should have received a copy of the GNU Affero Public License along with Lenpaste. 18 | If not, see . 19 | */ 20 | 21 | /* MAIN */ 22 | body { 23 | width: 70%; 24 | margin: 0; 25 | margin-left: auto; 26 | margin-right: auto; 27 | font-style: normal; 28 | background-color: {{call .Theme `color.BackgroundBody`}}; 29 | font-family: {{call .Theme `font.Default`}}; 30 | font-size: 16px; 31 | color: {{call .Theme `color.Font`}}; 32 | } 33 | 34 | @media all and (max-device-width: 720px), all and (orientation: portrait) { 35 | body { 36 | width: 100%; 37 | } 38 | } 39 | 40 | @media all and (max-device-width: 640px) { 41 | .header-right { 42 | display: block; 43 | width: 100%; 44 | } 45 | } 46 | 47 | 48 | /* HEADER */ 49 | header { 50 | padding-top: 20px; 51 | padding-bottom: 20px; 52 | padding-left: 20px; 53 | padding-right: 20px; 54 | color: {{call .Theme `color.HeaderFont`}}; 55 | background: {{call .Theme `color.Header`}}; 56 | } 57 | 58 | header h2, 59 | header h4 { 60 | display: inline; 61 | margin-right: 15px; 62 | padding: 0; 63 | } 64 | 65 | header div { 66 | width: 50%; 67 | display: inline-block; 68 | } 69 | 70 | .header-right { 71 | text-align: right; 72 | } 73 | 74 | header h2 a, header h2 a:hover, 75 | header h4 a, header h4 a:hover { 76 | color: {{call .Theme `color.HeaderFont`}}; 77 | text-decoration: none; 78 | white-space: nowrap; 79 | } 80 | 81 | 82 | /* ARTICLE */ 83 | article { 84 | width: 100%; 85 | box-sizing: border-box; 86 | margin-top: 20px; 87 | margin-bottom: 20px; 88 | padding: 20px; 89 | line-height: 1.5; 90 | background: {{call .Theme `color.Article`}}; 91 | } 92 | 93 | 94 | /* CONTAINERS */ 95 | .stretch-width { 96 | width: 100%; 97 | box-sizing: border-box; 98 | } 99 | 100 | .button-block-right { 101 | display: flex; 102 | justify-content: flex-end; 103 | } 104 | 105 | .button-block-right button { 106 | margin-left: 20px; 107 | } 108 | 109 | .text-bar { 110 | display: flex; 111 | justify-content: start; 112 | font-family: {{call .Theme `font.Monospace`}}; 113 | } 114 | 115 | .text-bar div { 116 | width: 50%; 117 | } 118 | 119 | .text-bar h1, 120 | .text-bar h2, 121 | .text-bar h3, 122 | .text-bar h4, 123 | .text-bar h5, 124 | .text-bar h6 { 125 | font-family: {{call .Theme `font.Default`}}; 126 | } 127 | 128 | .text-bar-right { 129 | align-self: flex-end; 130 | text-align: right; 131 | } 132 | 133 | .text-bar-right a { 134 | margin-left: 10px; 135 | } 136 | 137 | 138 | /* TEXT */ 139 | .text-red, .text-red:hover { 140 | color: {{call .Theme `color.Red`}}; 141 | } 142 | 143 | .text-grey, .text-grey:hover { 144 | color: {{call .Theme `color.Grey`}}; 145 | } 146 | 147 | 148 | /* OTHER */ 149 | :focus-visible { 150 | outline: 2px solid {{call .Theme `color.FocusOutline`}}; 151 | } 152 | 153 | :active { 154 | outline: 0; 155 | } 156 | 157 | h1, h2, h3, h4, h5, h6, 158 | p, pre, 159 | select, label, 160 | button, input, 161 | textarea { 162 | margin-top: 10px; 163 | margin-bottom: 10px; 164 | } 165 | 166 | h4 { 167 | margin-top: 40px; 168 | } 169 | 170 | pre, textarea { 171 | width: 100%; 172 | max-width: 100%; 173 | min-width: 100%; 174 | box-sizing: border-box; 175 | tab-size: 4; 176 | background-color: {{call .Theme `color.Element`}}; 177 | padding: 10px; 178 | border: 2px; 179 | font-family: {{call .Theme `font.Monospace`}}; 180 | font-size: inherit; 181 | color: inherit; 182 | } 183 | 184 | pre { 185 | overflow: auto; 186 | } 187 | 188 | textarea { 189 | min-height: 100px; 190 | resize: vertical; 191 | } 192 | 193 | label { 194 | font-family: {{call .Theme `font.Default`}}; 195 | } 196 | 197 | input::placeholder, textarea::placeholder { 198 | color: {{call .Theme `color.InputPlaceholder`}}; 199 | } 200 | 201 | input { 202 | background: {{call .Theme `color.Element`}}; 203 | font-size: inherit; 204 | font-family: inherit; 205 | color: inherit; 206 | padding: 10px; 207 | border: 2px; 208 | overflow-x: scroll; 209 | } 210 | 211 | select { 212 | appearance: none; 213 | background: {{call .Theme `color.Element`}}; 214 | color: {{call .Theme `color.Font`}}; 215 | font-size: 16px; 216 | padding: 4px 22px 4px 4px; 217 | border: 2px; 218 | } 219 | 220 | select:hover, input[type="checkbox"]:hover { 221 | background-color: {{call .Theme `color.InputHover`}}; 222 | cursor: pointer; 223 | } 224 | 225 | select, select:hover { 226 | background-image: url("data:image/svg+xml,"); 227 | background-repeat: no-repeat; 228 | background-position: right 4px center; 229 | background-size: 10px; 230 | } 231 | 232 | label { 233 | margin-right: 10px; 234 | } 235 | 236 | button, select, input, textarea { 237 | border-radius: 0; 238 | -webkit-border-radius: 0; 239 | -moz-border-radius: 0; 240 | 241 | appearance: none; 242 | -webkit-appearance: none; 243 | -moz-appearance: none; 244 | } 245 | 246 | button { 247 | background: {{call .Theme `color.Grey`}}; 248 | font-size: 16px; 249 | padding: 8px 8px 8px 8px; 250 | border: 2px; 251 | color: {{call .Theme `color.ButtonFont`}}; 252 | } 253 | 254 | button:hover { 255 | background: {{call .Theme `color.ButtonHover`}}; 256 | cursor: pointer; 257 | } 258 | 259 | .button-green { 260 | background: {{call .Theme `color.ButtonGreen`}}; 261 | color: {{call .Theme `color.ButtonGreenFont`}}; 262 | } 263 | 264 | .button-green:hover { 265 | background: {{call .Theme `color.ButtonGreenHover`}}; 266 | } 267 | 268 | .checkbox { 269 | padding-left: 25px; 270 | } 271 | 272 | .checkbox:hover, 273 | .checkbox:hover input { 274 | cursor: pointer; 275 | } 276 | 277 | input[type="checkbox"] { 278 | position: absolute; 279 | margin-left: -25px; 280 | margin-top: 0.1em; 281 | overflow: hidden; 282 | } 283 | 284 | input[type="checkbox"]:checked { 285 | background-image: url("data:image/svg+xml,"); 286 | background-repeat: no-repeat; 287 | background-position: center; 288 | background-size: 14px; 289 | } 290 | 291 | ul { 292 | list-style: square; 293 | } 294 | 295 | code { 296 | background-color: {{call .Theme `color.Element`}}; 297 | padding: 2px 5px 2px 5px; 298 | color: inherit; 299 | } 300 | 301 | pre code { 302 | background-color: inherit; 303 | } 304 | 305 | details { 306 | margin-top: 10px; 307 | margin-bottom: 10px; 308 | } 309 | 310 | details summary { 311 | cursor: pointer; 312 | } 313 | 314 | details summary::-webkit-details-marker { 315 | display:none; 316 | } 317 | 318 | details summary { 319 | list-style-type: none; 320 | background-image: url("data:image/svg+xml,"); 321 | background-repeat: no-repeat; 322 | background-position: left; 323 | background-size: 10px; 324 | padding-left: 14px; 325 | } 326 | 327 | details[open] summary { 328 | background-image: url("data:image/svg+xml,"); 329 | } 330 | 331 | table { 332 | background: inherit; 333 | border-collapse: collapse; 334 | } 335 | 336 | th, td { 337 | text-align: left; 338 | padding: 10px; 339 | border: 1px; 340 | border-style: solid; 341 | font-size: inherit; 342 | font-family: inherit; 343 | color: inherit; 344 | } 345 | 346 | .table-hidden td { 347 | text-align: right; 348 | padding: 0; 349 | border: 0; 350 | } 351 | 352 | a, a:hover { 353 | color: {{call .Theme `color.FocusOutline`}}; 354 | } 355 | 356 | h1 a, h2 a, h3 a, 357 | h4 a, h5 a, h6 a { 358 | text-decoration: none; 359 | } 360 | -------------------------------------------------------------------------------- /internal/web/data/terms.tmpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Copyright (C) 2021-2023 Leonid Maslakov. 3 | 4 | This file is part of Lenpaste. 5 | 6 | Lenpaste is free software: you can redistribute it 7 | and/or modify it under the terms of the 8 | GNU Affero Public License as published by the 9 | Free Software Foundation, either version 3 of the License, 10 | or (at your option) any later version. 11 | 12 | Lenpaste is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 14 | or FITNESS FOR A PARTICULAR PURPOSE. 15 | See the GNU Affero Public License for more details. 16 | 17 | You should have received a copy of the GNU Affero Public License along with Lenpaste. 18 | If not, see . 19 | */}} 20 | 21 | {{define "titlePrefix"}}{{ call .Translate `terms.Title`}} | {{end}} 22 | {{define "headAppend"}}{{end}} 23 | {{define "article"}} 24 |

{{ call .Translate `terms.Title` }}

25 | {{if ne .TermsOfUse ``}} 26 |
{{ call .Translate `terms.Notice` }}
27 | {{ call .Highlight .TermsOfUse `plaintext` }} 28 | {{else}} 29 | {{ call .Translate `terms.NoTerms` }} 30 | {{end}} 31 | {{end}} 32 | -------------------------------------------------------------------------------- /internal/web/data/theme/dark.theme: -------------------------------------------------------------------------------- 1 | theme.Name.bn_IN = ডার্ক থিম 2 | theme.Name.de = Dunkel 3 | theme.Name.en = Dark 4 | theme.Name.ru = Тёмная 5 | 6 | font.Default = sans-serif 7 | font.Monospace = monospace 8 | 9 | // Full theme list here: 10 | // https://pkg.go.dev/github.com/alecthomas/chroma/v2/styles#pkg-variables 11 | highlight.Theme = monokai 12 | 13 | color.Font = #FFFFFF 14 | color.SVG = white 15 | color.BackgroundBody = #333333 16 | color.Header = #E88B2E 17 | color.HeaderFont = #FFFFFF 18 | color.Article = #444444 19 | color.BackgroundBlack = #000000 20 | color.FocusOutline = #3DAEE9 21 | color.Element = #666666 22 | 23 | color.Red = #FF1F1F 24 | color.Grey = #888888 25 | 26 | color.Button = #888888 27 | color.ButtonHover = #999999 28 | color.ButtonFont = #FFFFFF 29 | color.ButtonGreen = #66CC00 30 | color.ButtonGreenHover = #74D117 31 | color.ButtonGreenFont = #FFFFFF 32 | 33 | color.InputHover = #777777 34 | color.InputPlaceholder = #B9B9B9 35 | -------------------------------------------------------------------------------- /internal/web/data/theme/light.theme: -------------------------------------------------------------------------------- 1 | theme.Name.bn_IN = লাইট থিম 2 | theme.Name.de = Hell 3 | theme.Name.en = Light 4 | theme.Name.ru = Светлая 5 | 6 | font.Default = sans-serif 7 | font.Monospace = monospace 8 | 9 | // Full theme list here: 10 | // https://pkg.go.dev/github.com/alecthomas/chroma/v2/styles#pkg-variables 11 | highlight.Theme = monokailight 12 | 13 | color.Font = #000000 14 | color.SVG = black 15 | color.BackgroundBody = #CCCCCC 16 | color.Header = #E88B2E 17 | color.HeaderFont = #FFFFFF 18 | color.Article = #E0E0E0 19 | color.BackgroundBlack = #000000 20 | color.FocusOutline = #342DB5 21 | color.Element = #B9B9B9 22 | 23 | color.Red = #FF1F1F 24 | color.Grey = #888888 25 | 26 | color.Button = #888888 27 | color.ButtonFont = #FFFFFF 28 | color.ButtonHover = #999999 29 | color.ButtonGreen = #66CC00 30 | color.ButtonGreenFont = #FFFFFF 31 | color.ButtonGreenHover = #74D117 32 | 33 | color.InputHover = #C3C3C3 34 | color.InputPlaceholder = #474747 35 | -------------------------------------------------------------------------------- /internal/web/kvcfg.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | package web 20 | 21 | import ( 22 | "errors" 23 | "strconv" 24 | "strings" 25 | ) 26 | 27 | func readKVCfg(data string) (map[string]string, error) { 28 | out := make(map[string]string) 29 | 30 | dataSplit := strings.Split(data, "\n") 31 | dataSplitLen := len(dataSplit) 32 | 33 | for num := 0; num < dataSplitLen; num++ { 34 | str := strings.TrimSpace(dataSplit[num]) 35 | 36 | if str == "" || strings.HasPrefix(str, "//") { 37 | continue 38 | } 39 | 40 | strSplit := strings.SplitN(str, "=", 2) 41 | if len(strSplit) != 2 { 42 | return out, errors.New("error in line " + strconv.Itoa(num+1) + ": expected '=' delimiter") 43 | } 44 | 45 | key := strings.TrimSpace(strSplit[0]) 46 | val := strings.TrimSpace(strSplit[1]) 47 | val, isMultiline := multilineCheck(val) 48 | 49 | if isMultiline { 50 | num = num + 1 51 | for ; num < dataSplitLen; num++ { 52 | strPlus := strings.TrimSpace(dataSplit[num]) 53 | strPlus, isMultilinePlus := multilineCheck(strPlus) 54 | val = val + strPlus 55 | 56 | if isMultilinePlus == false { 57 | break 58 | } 59 | } 60 | } 61 | 62 | _, exist := out[key] 63 | if exist { 64 | return out, errors.New("duplicate key: " + key) 65 | } 66 | 67 | out[key] = val 68 | } 69 | 70 | return out, nil 71 | } 72 | 73 | func multilineCheck(s string) (string, bool) { 74 | sLen := len(s) 75 | 76 | if sLen > 0 && s[sLen-1] == '\\' { 77 | if sLen > 1 && s[sLen-2] == '\\' { 78 | return s[:sLen-1], false 79 | } 80 | 81 | return s[:sLen-1], true 82 | } 83 | 84 | return s, false 85 | } 86 | -------------------------------------------------------------------------------- /internal/web/share.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | package web 20 | 21 | import ( 22 | "net/http" 23 | ) 24 | 25 | func getCookie(req *http.Request, name string) string { 26 | cookie, err := req.Cookie(name) 27 | if err != nil { 28 | return "" 29 | } 30 | 31 | return cookie.Value 32 | } 33 | -------------------------------------------------------------------------------- /internal/web/web.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | package web 20 | 21 | import ( 22 | "embed" 23 | chromaLexers "github.com/alecthomas/chroma/v2/lexers" 24 | "github.com/lcomrade/lenpaste/internal/config" 25 | "github.com/lcomrade/lenpaste/internal/logger" 26 | "github.com/lcomrade/lenpaste/internal/netshare" 27 | "github.com/lcomrade/lenpaste/internal/storage" 28 | "html/template" 29 | "net/http" 30 | "strings" 31 | textTemplate "text/template" 32 | ) 33 | 34 | //go:embed data/* 35 | var embFS embed.FS 36 | 37 | type Data struct { 38 | DB storage.DB 39 | Log logger.Logger 40 | 41 | RateLimitNew *netshare.RateLimitSystem 42 | RateLimitGet *netshare.RateLimitSystem 43 | 44 | Lexers []string 45 | Locales Locales 46 | LocalesList LocalesList 47 | Themes Themes 48 | ThemesList ThemesList 49 | 50 | StyleCSS *textTemplate.Template 51 | ErrorPage *template.Template 52 | Main *template.Template 53 | MainJS *[]byte 54 | HistoryJS *textTemplate.Template 55 | CodeJS *textTemplate.Template 56 | PastePage *template.Template 57 | PasteJS *textTemplate.Template 58 | PasteContinue *template.Template 59 | Settings *template.Template 60 | About *template.Template 61 | TermsOfUse *template.Template 62 | Authors *template.Template 63 | License *template.Template 64 | SourceCodePage *template.Template 65 | 66 | Docs *template.Template 67 | DocsApiV1 *template.Template 68 | DocsApiLibs *template.Template 69 | 70 | EmbeddedPage *template.Template 71 | EmbeddedHelpPage *template.Template 72 | 73 | Version string 74 | 75 | TitleMaxLen int 76 | BodyMaxLen int 77 | MaxLifeTime int64 78 | 79 | ServerAbout string 80 | ServerRules string 81 | ServerTermsExist bool 82 | ServerTermsOfUse string 83 | 84 | AdminName string 85 | AdminMail string 86 | 87 | RobotsDisallow bool 88 | 89 | LenPasswdFile string 90 | 91 | UiDefaultLifeTime string 92 | UiDefaultTheme string 93 | } 94 | 95 | func Load(db storage.DB, cfg config.Config) (*Data, error) { 96 | var data Data 97 | var err error 98 | 99 | // Setup base info 100 | data.DB = db 101 | data.Log = cfg.Log 102 | 103 | data.RateLimitNew = cfg.RateLimitNew 104 | data.RateLimitGet = cfg.RateLimitGet 105 | 106 | data.Version = cfg.Version 107 | 108 | data.TitleMaxLen = cfg.TitleMaxLen 109 | data.BodyMaxLen = cfg.BodyMaxLen 110 | data.MaxLifeTime = cfg.MaxLifeTime 111 | data.UiDefaultLifeTime = cfg.UiDefaultLifetime 112 | data.UiDefaultTheme = cfg.UiDefaultTheme 113 | data.LenPasswdFile = cfg.LenPasswdFile 114 | 115 | data.ServerAbout = cfg.ServerAbout 116 | data.ServerRules = cfg.ServerRules 117 | data.ServerTermsOfUse = cfg.ServerTermsOfUse 118 | 119 | serverTermsExist := false 120 | if cfg.ServerTermsOfUse != "" { 121 | serverTermsExist = true 122 | } 123 | data.ServerTermsExist = serverTermsExist 124 | 125 | data.AdminName = cfg.AdminName 126 | data.AdminMail = cfg.AdminMail 127 | 128 | data.RobotsDisallow = cfg.RobotsDisallow 129 | 130 | // Get Chroma lexers 131 | data.Lexers = chromaLexers.Names(false) 132 | 133 | // Load locales 134 | data.Locales, data.LocalesList, err = loadLocales(embFS, "data/locale") 135 | if err != nil { 136 | return nil, err 137 | } 138 | 139 | // Load themes 140 | data.Themes, data.ThemesList, err = loadThemes(cfg.UiThemesDir, data.LocalesList, data.UiDefaultTheme) 141 | if err != nil { 142 | return nil, err 143 | } 144 | 145 | // style.css file 146 | data.StyleCSS, err = textTemplate.ParseFS(embFS, "data/style.css") 147 | if err != nil { 148 | return nil, err 149 | } 150 | 151 | // main.tmpl 152 | data.Main, err = template.ParseFS(embFS, "data/base.tmpl", "data/main.tmpl") 153 | if err != nil { 154 | return nil, err 155 | } 156 | 157 | // main.js 158 | mainJS, err := embFS.ReadFile("data/main.js") 159 | if err != nil { 160 | return nil, err 161 | } 162 | data.MainJS = &mainJS 163 | 164 | // history.js 165 | data.HistoryJS, err = textTemplate.ParseFS(embFS, "data/history.js") 166 | if err != nil { 167 | return nil, err 168 | } 169 | 170 | // code.js 171 | data.CodeJS, err = textTemplate.ParseFS(embFS, "data/code.js") 172 | if err != nil { 173 | return nil, err 174 | } 175 | 176 | // paste.tmpl 177 | data.PastePage, err = template.ParseFS(embFS, "data/base.tmpl", "data/paste.tmpl") 178 | if err != nil { 179 | return nil, err 180 | } 181 | 182 | // paste.js 183 | data.PasteJS, err = textTemplate.ParseFS(embFS, "data/paste.js") 184 | if err != nil { 185 | return nil, err 186 | } 187 | 188 | // paste_continue.tmpl 189 | data.PasteContinue, err = template.ParseFS(embFS, "data/base.tmpl", "data/paste_continue.tmpl") 190 | if err != nil { 191 | return nil, err 192 | } 193 | 194 | // settings.tmpl 195 | data.Settings, err = template.ParseFS(embFS, "data/base.tmpl", "data/settings.tmpl") 196 | if err != nil { 197 | return nil, err 198 | } 199 | 200 | // about.tmpl 201 | data.About, err = template.ParseFS(embFS, "data/base.tmpl", "data/about.tmpl") 202 | if err != nil { 203 | return nil, err 204 | } 205 | 206 | // terms.tmpl 207 | data.TermsOfUse, err = template.ParseFS(embFS, "data/base.tmpl", "data/terms.tmpl") 208 | if err != nil { 209 | return nil, err 210 | } 211 | 212 | // authors.tmpl 213 | data.Authors, err = template.ParseFS(embFS, "data/base.tmpl", "data/authors.tmpl") 214 | if err != nil { 215 | return nil, err 216 | } 217 | 218 | // license.tmpl 219 | data.License, err = template.ParseFS(embFS, "data/base.tmpl", "data/license.tmpl") 220 | if err != nil { 221 | return nil, err 222 | } 223 | 224 | // source_code.tmpl 225 | data.SourceCodePage, err = template.ParseFS(embFS, "data/base.tmpl", "data/source_code.tmpl") 226 | if err != nil { 227 | return nil, err 228 | } 229 | 230 | // docs.tmpl 231 | data.Docs, err = template.ParseFS(embFS, "data/base.tmpl", "data/docs.tmpl") 232 | if err != nil { 233 | return nil, err 234 | } 235 | 236 | // docs_apiv1.tmpl 237 | data.DocsApiV1, err = template.ParseFS(embFS, "data/base.tmpl", "data/docs_apiv1.tmpl") 238 | if err != nil { 239 | return nil, err 240 | } 241 | 242 | // docs_api_libs.tmpl 243 | data.DocsApiLibs, err = template.ParseFS(embFS, "data/base.tmpl", "data/docs_api_libs.tmpl") 244 | if err != nil { 245 | return nil, err 246 | } 247 | 248 | // error.tmpl 249 | data.ErrorPage, err = template.ParseFS(embFS, "data/base.tmpl", "data/error.tmpl") 250 | if err != nil { 251 | return nil, err 252 | } 253 | 254 | // emb.tmpl 255 | data.EmbeddedPage, err = template.ParseFS(embFS, "data/emb.tmpl") 256 | if err != nil { 257 | return nil, err 258 | } 259 | 260 | // emb_help.tmpl 261 | data.EmbeddedHelpPage, err = template.ParseFS(embFS, "data/base.tmpl", "data/emb_help.tmpl") 262 | if err != nil { 263 | return nil, err 264 | } 265 | 266 | return &data, nil 267 | } 268 | 269 | func (data *Data) Handler(rw http.ResponseWriter, req *http.Request) { 270 | // Process request 271 | var err error 272 | 273 | rw.Header().Set("Server", config.Software+"/"+data.Version) 274 | 275 | switch req.URL.Path { 276 | // Search engines 277 | case "/robots.txt": 278 | err = data.robotsTxtHand(rw, req) 279 | case "/sitemap.xml": 280 | err = data.sitemapHand(rw, req) 281 | // Resources 282 | case "/style.css": 283 | err = data.styleCSSHand(rw, req) 284 | case "/main.js": 285 | err = data.mainJSHand(rw, req) 286 | case "/history.js": 287 | err = data.historyJSHand(rw, req) 288 | case "/code.js": 289 | err = data.codeJSHand(rw, req) 290 | case "/paste.js": 291 | err = data.pasteJSHand(rw, req) 292 | case "/about": 293 | err = data.aboutHand(rw, req) 294 | case "/about/authors": 295 | err = data.authorsHand(rw, req) 296 | case "/about/license": 297 | err = data.licenseHand(rw, req) 298 | case "/about/source_code": 299 | err = data.sourceCodePageHand(rw, req) 300 | case "/docs": 301 | err = data.docsHand(rw, req) 302 | case "/docs/apiv1": 303 | err = data.docsApiV1Hand(rw, req) 304 | case "/docs/api_libs": 305 | err = data.docsApiLibsHand(rw, req) 306 | // Pages 307 | case "/": 308 | err = data.newPasteHand(rw, req) 309 | case "/settings": 310 | err = data.settingsHand(rw, req) 311 | case "/terms": 312 | err = data.termsOfUseHand(rw, req) 313 | // Else 314 | default: 315 | if strings.HasPrefix(req.URL.Path, "/dl/") { 316 | err = data.dlHand(rw, req) 317 | 318 | } else if strings.HasPrefix(req.URL.Path, "/emb/") { 319 | err = data.embeddedHand(rw, req) 320 | 321 | } else if strings.HasPrefix(req.URL.Path, "/emb_help/") { 322 | err = data.embeddedHelpHand(rw, req) 323 | 324 | } else { 325 | err = data.getPasteHand(rw, req) 326 | } 327 | } 328 | 329 | // Log 330 | if err == nil { 331 | data.Log.HttpRequest(req, 200) 332 | 333 | } else { 334 | code, err := data.writeError(rw, req, err) 335 | if err != nil { 336 | data.Log.HttpError(req, err) 337 | } else { 338 | data.Log.HttpRequest(req, code) 339 | } 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /internal/web/web_about.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | package web 20 | 21 | import ( 22 | "html/template" 23 | "net/http" 24 | ) 25 | 26 | type aboutTmpl struct { 27 | Version string 28 | TitleMaxLen int 29 | BodyMaxLen int 30 | MaxLifeTime int64 31 | 32 | ServerAbout string 33 | ServerRules string 34 | ServerTermsExist bool 35 | 36 | AdminName string 37 | AdminMail string 38 | 39 | Highlight func(string, string) template.HTML 40 | Translate func(string, ...interface{}) template.HTML 41 | } 42 | 43 | type aboutMinTmp struct { 44 | Translate func(string, ...interface{}) template.HTML 45 | } 46 | 47 | // Pattern: /about 48 | func (data *Data) aboutHand(rw http.ResponseWriter, req *http.Request) error { 49 | dataTmpl := aboutTmpl{ 50 | Version: data.Version, 51 | TitleMaxLen: data.TitleMaxLen, 52 | BodyMaxLen: data.BodyMaxLen, 53 | MaxLifeTime: data.MaxLifeTime, 54 | ServerAbout: data.ServerAbout, 55 | ServerRules: data.ServerRules, 56 | ServerTermsExist: data.ServerTermsExist, 57 | AdminName: data.AdminName, 58 | AdminMail: data.AdminMail, 59 | Highlight: data.Themes.findTheme(req, data.UiDefaultTheme).tryHighlight, 60 | Translate: data.Locales.findLocale(req).translate, 61 | } 62 | 63 | rw.Header().Set("Content-Type", "text/html; charset=utf-8") 64 | return data.About.Execute(rw, dataTmpl) 65 | } 66 | 67 | // Pattern: /about/authors 68 | func (data *Data) authorsHand(rw http.ResponseWriter, req *http.Request) error { 69 | rw.Header().Set("Content-Type", "text/html; charset=utf-8") 70 | return data.Authors.Execute(rw, aboutMinTmp{Translate: data.Locales.findLocale(req).translate}) 71 | } 72 | 73 | // Pattern: /about/license 74 | func (data *Data) licenseHand(rw http.ResponseWriter, req *http.Request) error { 75 | rw.Header().Set("Content-Type", "text/html; charset=utf-8") 76 | return data.License.Execute(rw, aboutMinTmp{Translate: data.Locales.findLocale(req).translate}) 77 | } 78 | 79 | // Pattern: /about/source_code 80 | func (data *Data) sourceCodePageHand(rw http.ResponseWriter, req *http.Request) error { 81 | rw.Header().Set("Content-Type", "text/html; charset=utf-8") 82 | return data.SourceCodePage.Execute(rw, aboutMinTmp{Translate: data.Locales.findLocale(req).translate}) 83 | } 84 | -------------------------------------------------------------------------------- /internal/web/web_dl.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | package web 20 | 21 | import ( 22 | chromaLexers "github.com/alecthomas/chroma/v2/lexers" 23 | "github.com/lcomrade/lenpaste/internal/netshare" 24 | "net/http" 25 | "strings" 26 | "time" 27 | ) 28 | 29 | // Pattern: /dl/ 30 | func (data *Data) dlHand(rw http.ResponseWriter, req *http.Request) error { 31 | // Check rate limit 32 | err := data.RateLimitGet.CheckAndUse(netshare.GetClientAddr(req)) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | // Read DB 38 | pasteID := string([]rune(req.URL.Path)[4:]) 39 | 40 | paste, err := data.DB.PasteGet(pasteID) 41 | if err != nil { 42 | return err 43 | } 44 | 45 | // If "one use" paste 46 | if paste.OneUse == true { 47 | // Delete paste 48 | err = data.DB.PasteDelete(pasteID) 49 | if err != nil { 50 | return err 51 | } 52 | } 53 | 54 | // Get create time 55 | createTime := time.Unix(paste.CreateTime, 0).UTC() 56 | 57 | // Get file name 58 | fileName := paste.ID 59 | if paste.Title != "" { 60 | fileName = paste.Title 61 | } 62 | 63 | // Get file extension 64 | fileExt := chromaLexers.Get(paste.Syntax).Config().Filenames[0][1:] 65 | if strings.HasSuffix(fileName, fileExt) == false { 66 | fileName = fileName + fileExt 67 | } 68 | 69 | // Write result 70 | rw.Header().Set("Content-Type", "application/octet-stream") 71 | rw.Header().Set("Content-Disposition", "attachment; filename="+fileName) 72 | rw.Header().Set("Content-Transfer-Encoding", "binary") 73 | rw.Header().Set("Expires", "0") 74 | 75 | http.ServeContent(rw, req, fileName, createTime, strings.NewReader(paste.Body)) 76 | 77 | return nil 78 | } 79 | -------------------------------------------------------------------------------- /internal/web/web_docs.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | package web 20 | 21 | import ( 22 | "github.com/lcomrade/lenpaste/internal/netshare" 23 | "html/template" 24 | "net/http" 25 | ) 26 | 27 | type docsTmpl struct { 28 | Highlight func(string, string) template.HTML 29 | Translate func(string, ...interface{}) template.HTML 30 | } 31 | 32 | type docsApiV1Tmpl struct { 33 | MaxLenAuthorAll int 34 | 35 | Highlight func(string, string) template.HTML 36 | Translate func(string, ...interface{}) template.HTML 37 | } 38 | 39 | // Pattern: /docs 40 | func (data *Data) docsHand(rw http.ResponseWriter, req *http.Request) error { 41 | rw.Header().Set("Content-Type", "text/html; charset=utf-8") 42 | return data.Docs.Execute(rw, docsTmpl{Translate: data.Locales.findLocale(req).translate}) 43 | } 44 | 45 | // Pattern: /docs/apiv1 46 | func (data *Data) docsApiV1Hand(rw http.ResponseWriter, req *http.Request) error { 47 | rw.Header().Set("Content-Type", "text/html; charset=utf-8") 48 | return data.DocsApiV1.Execute(rw, docsApiV1Tmpl{ 49 | MaxLenAuthorAll: netshare.MaxLengthAuthorAll, 50 | Translate: data.Locales.findLocale(req).translate, 51 | Highlight: data.Themes.findTheme(req, data.UiDefaultTheme).tryHighlight, 52 | }) 53 | } 54 | 55 | // Pattern: /docs/api_libs 56 | func (data *Data) docsApiLibsHand(rw http.ResponseWriter, req *http.Request) error { 57 | rw.Header().Set("Content-Type", "text/html; charset=utf-8") 58 | return data.DocsApiLibs.Execute(rw, docsTmpl{Translate: data.Locales.findLocale(req).translate}) 59 | } 60 | -------------------------------------------------------------------------------- /internal/web/web_embedded.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | package web 20 | 21 | import ( 22 | "github.com/lcomrade/lenpaste/internal/netshare" 23 | "github.com/lcomrade/lenpaste/internal/storage" 24 | "html/template" 25 | "net/http" 26 | "time" 27 | ) 28 | 29 | type embTmpl struct { 30 | ID string 31 | CreateTimeStr string 32 | DeleteTime int64 33 | OneUse bool 34 | Title string 35 | Body template.HTML 36 | 37 | ErrorNotFound bool 38 | Translate func(string, ...interface{}) template.HTML 39 | } 40 | 41 | // Pattern: /emb/ 42 | func (data *Data) embeddedHand(rw http.ResponseWriter, req *http.Request) error { 43 | errorNotFound := false 44 | 45 | // Check rate limit 46 | err := data.RateLimitGet.CheckAndUse(netshare.GetClientAddr(req)) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | // Get paste ID 52 | pasteID := string([]rune(req.URL.Path)[5:]) 53 | 54 | // Read DB 55 | paste, err := data.DB.PasteGet(pasteID) 56 | if err != nil { 57 | if err == storage.ErrNotFoundID { 58 | errorNotFound = true 59 | 60 | } else { 61 | return err 62 | } 63 | } 64 | 65 | // Prepare template data 66 | createTime := time.Unix(paste.CreateTime, 0).UTC() 67 | 68 | tmplData := embTmpl{ 69 | ID: paste.ID, 70 | CreateTimeStr: createTime.Format("1 Jan, 2006"), 71 | DeleteTime: paste.DeleteTime, 72 | OneUse: paste.OneUse, 73 | Title: paste.Title, 74 | Body: tryHighlight(paste.Body, paste.Syntax, "monokai"), 75 | 76 | ErrorNotFound: errorNotFound, 77 | Translate: data.Locales.findLocale(req).translate, 78 | } 79 | 80 | // Show paste 81 | return data.EmbeddedPage.Execute(rw, tmplData) 82 | } 83 | -------------------------------------------------------------------------------- /internal/web/web_embedded_help.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | package web 20 | 21 | import ( 22 | "github.com/lcomrade/lenpaste/internal/netshare" 23 | "html/template" 24 | "net/http" 25 | ) 26 | 27 | type embHelpTmpl struct { 28 | ID string 29 | DeleteTime int64 30 | OneUse bool 31 | 32 | Protocol string 33 | Host string 34 | 35 | Translate func(string, ...interface{}) template.HTML 36 | Highlight func(string, string) template.HTML 37 | } 38 | 39 | // Pattern: /emb_help/ 40 | func (data *Data) embeddedHelpHand(rw http.ResponseWriter, req *http.Request) error { 41 | // Check rate limit 42 | err := data.RateLimitGet.CheckAndUse(netshare.GetClientAddr(req)) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | // Get paste ID 48 | pasteID := string([]rune(req.URL.Path)[10:]) 49 | 50 | // Read DB 51 | paste, err := data.DB.PasteGet(pasteID) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | // Show paste 57 | tmplData := embHelpTmpl{ 58 | ID: paste.ID, 59 | DeleteTime: paste.DeleteTime, 60 | OneUse: paste.OneUse, 61 | Protocol: netshare.GetProtocol(req), 62 | Host: netshare.GetHost(req), 63 | Translate: data.Locales.findLocale(req).translate, 64 | Highlight: data.Themes.findTheme(req, data.UiDefaultTheme).tryHighlight, 65 | } 66 | 67 | return data.EmbeddedHelpPage.Execute(rw, tmplData) 68 | } 69 | -------------------------------------------------------------------------------- /internal/web/web_error.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | package web 20 | 21 | import ( 22 | "errors" 23 | "github.com/lcomrade/lenpaste/internal/netshare" 24 | "github.com/lcomrade/lenpaste/internal/storage" 25 | "html/template" 26 | "net/http" 27 | "strconv" 28 | ) 29 | 30 | type errorTmpl struct { 31 | Code int 32 | AdminName string 33 | AdminMail string 34 | Translate func(string, ...interface{}) template.HTML 35 | } 36 | 37 | func (data *Data) writeError(rw http.ResponseWriter, req *http.Request, e error) (int, error) { 38 | errData := errorTmpl{ 39 | Code: 0, 40 | AdminName: data.AdminName, 41 | AdminMail: data.AdminMail, 42 | Translate: data.Locales.findLocale(req).translate, 43 | } 44 | 45 | // Dectect error 46 | var eTmp429 *netshare.ErrTooManyRequests 47 | 48 | if e == netshare.ErrBadRequest { 49 | errData.Code = 400 50 | 51 | } else if e == netshare.ErrUnauthorized { 52 | errData.Code = 401 53 | 54 | } else if e == storage.ErrNotFoundID { 55 | errData.Code = 404 56 | 57 | } else if e == netshare.ErrNotFound { 58 | errData.Code = 404 59 | 60 | } else if e == netshare.ErrMethodNotAllowed { 61 | errData.Code = 405 62 | 63 | } else if e == netshare.ErrPayloadTooLarge { 64 | errData.Code = 413 65 | 66 | } else if errors.As(e, &eTmp429) { 67 | errData.Code = 429 68 | rw.Header().Set("Retry-After", strconv.FormatInt(eTmp429.RetryAfter, 10)) 69 | 70 | } else { 71 | errData.Code = 500 72 | } 73 | 74 | // Write response header 75 | rw.Header().Set("Content-type", "text/html; charset=utf-8") 76 | rw.WriteHeader(errData.Code) 77 | 78 | // Render template 79 | err := data.ErrorPage.Execute(rw, errData) 80 | if err != nil { 81 | return 500, err 82 | } 83 | 84 | return errData.Code, nil 85 | } 86 | -------------------------------------------------------------------------------- /internal/web/web_get.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | package web 20 | 21 | import ( 22 | "github.com/lcomrade/lenpaste/internal/lineend" 23 | "github.com/lcomrade/lenpaste/internal/netshare" 24 | "html/template" 25 | "net/http" 26 | "time" 27 | ) 28 | 29 | type pasteTmpl struct { 30 | ID string 31 | Title string 32 | Body template.HTML 33 | Syntax string 34 | CreateTime int64 35 | DeleteTime int64 36 | OneUse bool 37 | 38 | LineEnd string 39 | CreateTimeStr string 40 | DeleteTimeStr string 41 | 42 | Author string 43 | AuthorEmail string 44 | AuthorURL string 45 | 46 | Translate func(string, ...interface{}) template.HTML 47 | } 48 | 49 | type pasteContinueTmpl struct { 50 | ID string 51 | Translate func(string, ...interface{}) template.HTML 52 | } 53 | 54 | func (data *Data) getPasteHand(rw http.ResponseWriter, req *http.Request) error { 55 | // Check rate limit 56 | err := data.RateLimitGet.CheckAndUse(netshare.GetClientAddr(req)) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | // Get paste ID 62 | pasteID := string([]rune(req.URL.Path)[1:]) 63 | 64 | // Read DB 65 | paste, err := data.DB.PasteGet(pasteID) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | // If "one use" paste 71 | if paste.OneUse == true { 72 | // If continue button not pressed 73 | req.ParseForm() 74 | 75 | if req.PostForm.Get("oneUseContinue") != "true" { 76 | tmplData := pasteContinueTmpl{ 77 | ID: paste.ID, 78 | Translate: data.Locales.findLocale(req).translate, 79 | } 80 | 81 | return data.PasteContinue.Execute(rw, tmplData) 82 | } 83 | 84 | // If continue button pressed delete paste 85 | err = data.DB.PasteDelete(pasteID) 86 | if err != nil { 87 | return err 88 | } 89 | } 90 | 91 | // Prepare template data 92 | createTime := time.Unix(paste.CreateTime, 0).UTC() 93 | deleteTime := time.Unix(paste.DeleteTime, 0).UTC() 94 | 95 | tmplData := pasteTmpl{ 96 | ID: paste.ID, 97 | Title: paste.Title, 98 | Body: data.Themes.findTheme(req, data.UiDefaultTheme).tryHighlight(paste.Body, paste.Syntax), 99 | Syntax: paste.Syntax, 100 | CreateTime: paste.CreateTime, 101 | DeleteTime: paste.DeleteTime, 102 | OneUse: paste.OneUse, 103 | 104 | CreateTimeStr: createTime.Format("Mon, 02 Jan 2006 15:04:05 -0700"), 105 | DeleteTimeStr: deleteTime.Format("Mon, 02 Jan 2006 15:04:05 -0700"), 106 | 107 | Author: paste.Author, 108 | AuthorEmail: paste.AuthorEmail, 109 | AuthorURL: paste.AuthorURL, 110 | 111 | Translate: data.Locales.findLocale(req).translate, 112 | } 113 | 114 | // Get body line end 115 | switch lineend.GetLineEnd(paste.Body) { 116 | case "\r\n": 117 | tmplData.LineEnd = "CRLF" 118 | case "\r": 119 | tmplData.LineEnd = "CR" 120 | default: 121 | tmplData.LineEnd = "LF" 122 | } 123 | 124 | // Show paste 125 | return data.PastePage.Execute(rw, tmplData) 126 | } 127 | -------------------------------------------------------------------------------- /internal/web/web_highlight.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | package web 20 | 21 | import ( 22 | "bytes" 23 | "github.com/alecthomas/chroma/v2" 24 | "github.com/alecthomas/chroma/v2/formatters/html" 25 | "github.com/alecthomas/chroma/v2/lexers" 26 | "github.com/alecthomas/chroma/v2/styles" 27 | "html/template" 28 | ) 29 | 30 | func tryHighlight(source string, lexer string, theme string) template.HTML { 31 | // Determine lexer 32 | l := lexers.Get(lexer) 33 | if l == nil { 34 | return template.HTML(source) 35 | } 36 | 37 | l = chroma.Coalesce(l) 38 | 39 | // Determine formatter 40 | f := html.New( 41 | html.Standalone(false), 42 | html.WithClasses(false), 43 | html.TabWidth(4), 44 | html.WithLineNumbers(true), 45 | html.WrapLongLines(true), 46 | ) 47 | 48 | s := styles.Get(theme) 49 | 50 | it, err := l.Tokenise(nil, source) 51 | if err != nil { 52 | return template.HTML(source) 53 | } 54 | 55 | // Format 56 | var buf bytes.Buffer 57 | 58 | err = f.Format(&buf, s, it) 59 | if err != nil { 60 | return template.HTML(source) 61 | } 62 | 63 | return template.HTML(buf.String()) 64 | } 65 | -------------------------------------------------------------------------------- /internal/web/web_new.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | package web 20 | 21 | import ( 22 | "github.com/lcomrade/lenpaste/internal/lenpasswd" 23 | "github.com/lcomrade/lenpaste/internal/netshare" 24 | "html/template" 25 | "net/http" 26 | ) 27 | 28 | type createTmpl struct { 29 | TitleMaxLen int 30 | BodyMaxLen int 31 | AuthorAllMaxLen int 32 | MaxLifeTime int64 33 | UiDefaultLifeTime string 34 | Lexers []string 35 | ServerTermsExist bool 36 | 37 | AuthorDefault string 38 | AuthorEmailDefault string 39 | AuthorURLDefault string 40 | 41 | AuthOk bool 42 | 43 | Translate func(string, ...interface{}) template.HTML 44 | } 45 | 46 | func (data *Data) newPasteHand(rw http.ResponseWriter, req *http.Request) error { 47 | var err error 48 | 49 | // Check auth 50 | authOk := true 51 | 52 | if data.LenPasswdFile != "" { 53 | authOk = false 54 | 55 | user, pass, authExist := req.BasicAuth() 56 | if authExist == true { 57 | authOk, err = lenpasswd.LoadAndCheck(data.LenPasswdFile, user, pass) 58 | if err != nil { 59 | return err 60 | } 61 | } 62 | 63 | if authOk == false { 64 | rw.Header().Add("WWW-Authenticate", "Basic") 65 | rw.WriteHeader(401) 66 | } 67 | } 68 | 69 | // Create paste if need 70 | if req.Method == "POST" { 71 | pasteID, _, _, err := netshare.PasteAddFromForm(req, data.DB, data.RateLimitNew, data.TitleMaxLen, data.BodyMaxLen, data.MaxLifeTime, data.Lexers) 72 | if err != nil { 73 | return err 74 | } 75 | 76 | // Redirect to paste 77 | writeRedirect(rw, req, "/"+pasteID, 302) 78 | return nil 79 | } 80 | 81 | // Else show create page 82 | tmplData := createTmpl{ 83 | TitleMaxLen: data.TitleMaxLen, 84 | BodyMaxLen: data.BodyMaxLen, 85 | AuthorAllMaxLen: netshare.MaxLengthAuthorAll, 86 | MaxLifeTime: data.MaxLifeTime, 87 | UiDefaultLifeTime: data.UiDefaultLifeTime, 88 | Lexers: data.Lexers, 89 | ServerTermsExist: data.ServerTermsExist, 90 | AuthorDefault: getCookie(req, "author"), 91 | AuthorEmailDefault: getCookie(req, "authorEmail"), 92 | AuthorURLDefault: getCookie(req, "authorURL"), 93 | AuthOk: authOk, 94 | Translate: data.Locales.findLocale(req).translate, 95 | } 96 | 97 | rw.Header().Set("Content-Type", "text/html; charset=utf-8") 98 | 99 | return data.Main.Execute(rw, tmplData) 100 | } 101 | -------------------------------------------------------------------------------- /internal/web/web_other.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | package web 20 | 21 | import ( 22 | "crypto/md5" 23 | "fmt" 24 | "html/template" 25 | "net/http" 26 | "os" 27 | "strings" 28 | ) 29 | 30 | type jsTmpl struct { 31 | Translate func(string, ...interface{}) template.HTML 32 | Theme func(string) string 33 | } 34 | 35 | func (data *Data) styleCSSHand(rw http.ResponseWriter, req *http.Request) error { 36 | rw.Header().Set("Content-Type", "text/css; charset=utf-8") 37 | return data.StyleCSS.Execute(rw, jsTmpl{ 38 | Translate: data.Locales.findLocale(req).translate, 39 | Theme: data.Themes.findTheme(req, data.UiDefaultTheme).theme, 40 | }) 41 | } 42 | 43 | func (data *Data) mainJSHand(rw http.ResponseWriter, req *http.Request) error { 44 | rw.Header().Set("Content-Type", "application/javascript; charset=utf-8") 45 | rw.Write(*data.MainJS) 46 | return nil 47 | } 48 | 49 | func (data *Data) codeJSHand(rw http.ResponseWriter, req *http.Request) error { 50 | rw.Header().Set("Content-Type", "application/javascript; charset=utf-8") 51 | return data.CodeJS.Execute(rw, jsTmpl{Translate: data.Locales.findLocale(req).translate}) 52 | } 53 | 54 | func (data *Data) historyJSHand(rw http.ResponseWriter, req *http.Request) error { 55 | rw.Header().Set("Content-Type", "application/javascript; charset=utf-8") 56 | return data.HistoryJS.Execute(rw, jsTmpl{ 57 | Translate: data.Locales.findLocale(req).translate, 58 | Theme: data.Themes.findTheme(req, data.UiDefaultTheme).theme, 59 | }) 60 | } 61 | 62 | func (data *Data) pasteJSHand(rw http.ResponseWriter, req *http.Request) error { 63 | rw.Header().Set("Content-Type", "application/javascript; charset=utf-8") 64 | return data.PasteJS.Execute(rw, jsTmpl{Translate: data.Locales.findLocale(req).translate}) 65 | } 66 | 67 | func init() { 68 | resp := "\u0045\u0072\u0072\u006f\u0072\u002e\u0020\u0059\u006f\u0075\u0020\u006d\u0061" 69 | resp += "\u0079\u0020\u0062\u0065\u0020\u0076\u0069\u006f\u006c\u0061\u0074\u0069\u006e" 70 | resp += "\u0067\u0020\u0074\u0068\u0065\u0020\u0041\u0047\u0050\u004c\u0020\u0076\u0033" 71 | resp += "\u0020\u006c\u0069\u0063\u0065\u006e\u0073\u0065\u0021" 72 | 73 | tmp, err := embFS.ReadFile("data/base.tmpl") 74 | if err != nil { 75 | println("error:", err.Error()) 76 | os.Exit(1) 77 | } 78 | 79 | if strings.Contains(string(tmp), "{{ call .Translate `base.About` }}") == false { 80 | println(resp) 81 | os.Exit(1) 82 | } 83 | 84 | tmp, err = embFS.ReadFile("data/about.tmpl") 85 | if err != nil { 86 | println("\u0065\u0072\u0072\u006f\u0072\u003a", err.Error()) 87 | os.Exit(1) 88 | } 89 | 90 | if strings.Contains(string(tmp), "

{{call .Translate `about.LenpasteAuthors` `/about/authors`}}

") == false { 91 | println(resp) 92 | os.Exit(1) 93 | } 94 | 95 | if strings.Contains(string(tmp), "/about/source_code") == false { 96 | println(resp) 97 | os.Exit(1) 98 | } 99 | 100 | if strings.Contains(string(tmp), "/about/license") == false { 101 | println(resp) 102 | os.Exit(1) 103 | } 104 | 105 | tmp, err = embFS.ReadFile("data/authors.tmpl") 106 | if err != nil { 107 | println("\u0065\u0072\u0072\u006f\u0072\u003a", err.Error()) 108 | os.Exit(1) 109 | } 110 | 111 | if strings.Contains(string(tmp), "
  • Leonid Maslakov (aka lcomrade) <root@lcomrade.su> - Core Developer.
  • ") == false { 112 | println(resp) 113 | os.Exit(1) 114 | } 115 | 116 | tmp, err = embFS.ReadFile("data/source_code.tmpl") 117 | if err != nil { 118 | println("\u0065\u0072\u0072\u006f\u0072\u003a", err.Error()) 119 | os.Exit(1) 120 | } 121 | 122 | if strings.Contains(string(tmp), "https://github.com/lcomrade/lenpaste") == false { 123 | println(resp) 124 | os.Exit(1) 125 | } 126 | 127 | tmp, err = embFS.ReadFile("data/license.tmpl") 128 | if err != nil { 129 | println("\u0065\u0072\u0072\u006f\u0072\u003a", err.Error()) 130 | os.Exit(1) 131 | } 132 | 133 | if fmt.Sprintf("%x", md5.Sum(tmp)) != "a1d6dd7f4b7470be5197381b85ee4fb5" { 134 | println(resp) 135 | os.Exit(1) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /internal/web/web_redirect.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | package web 20 | 21 | import ( 22 | "net/http" 23 | ) 24 | 25 | func writeRedirect(rw http.ResponseWriter, req *http.Request, newURL string, code int) { 26 | if newURL == "" { 27 | newURL = "/" 28 | } 29 | 30 | if req.URL.RawQuery != "" { 31 | newURL = newURL + "?" + req.URL.RawQuery 32 | } 33 | 34 | rw.Header().Set("Location", newURL) 35 | rw.WriteHeader(code) 36 | } 37 | -------------------------------------------------------------------------------- /internal/web/web_settings.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | package web 20 | 21 | import ( 22 | "github.com/lcomrade/lenpaste/internal/lenpasswd" 23 | "github.com/lcomrade/lenpaste/internal/netshare" 24 | "html/template" 25 | "net/http" 26 | ) 27 | 28 | const cookieMaxAge = 60 * 60 * 24 * 360 * 50 // 50 year 29 | 30 | type settingsTmpl struct { 31 | Language string 32 | LanguageSelector map[string]string 33 | 34 | Theme string 35 | ThemeSelector map[string]string 36 | 37 | AuthorAllMaxLen int 38 | Author string 39 | AuthorEmail string 40 | AuthorURL string 41 | 42 | AuthOk bool 43 | 44 | Translate func(string, ...interface{}) template.HTML 45 | } 46 | 47 | // Pattern: /settings 48 | func (data *Data) settingsHand(rw http.ResponseWriter, req *http.Request) error { 49 | var err error 50 | 51 | // Check auth 52 | authOk := true 53 | 54 | if data.LenPasswdFile != "" { 55 | authOk = false 56 | 57 | user, pass, authExist := req.BasicAuth() 58 | if authExist == true { 59 | authOk, err = lenpasswd.LoadAndCheck(data.LenPasswdFile, user, pass) 60 | if err != nil { 61 | return err 62 | } 63 | } 64 | } 65 | 66 | // Show settings page 67 | if req.Method != "POST" { 68 | // Prepare data 69 | dataTmpl := settingsTmpl{ 70 | Language: getCookie(req, "lang"), 71 | LanguageSelector: data.LocalesList, 72 | Theme: getCookie(req, "theme"), 73 | ThemeSelector: data.ThemesList.getForLocale(req), 74 | AuthorAllMaxLen: netshare.MaxLengthAuthorAll, 75 | Author: getCookie(req, "author"), 76 | AuthorEmail: getCookie(req, "authorEmail"), 77 | AuthorURL: getCookie(req, "authorURL"), 78 | AuthOk: authOk, 79 | Translate: data.Locales.findLocale(req).translate, 80 | } 81 | 82 | if dataTmpl.Theme == "" { 83 | dataTmpl.Theme = data.UiDefaultTheme 84 | } 85 | 86 | // Show page 87 | rw.Header().Set("Content-Type", "text/html; charset=utf-8") 88 | 89 | err := data.Settings.Execute(rw, dataTmpl) 90 | if err != nil { 91 | data.writeError(rw, req, err) 92 | } 93 | 94 | // Else update settings 95 | } else { 96 | req.ParseForm() 97 | 98 | lang := req.PostForm.Get("lang") 99 | if lang == "" { 100 | http.SetCookie(rw, &http.Cookie{ 101 | Name: "lang", 102 | Value: "", 103 | MaxAge: -1, 104 | }) 105 | 106 | } else { 107 | http.SetCookie(rw, &http.Cookie{ 108 | Name: "lang", 109 | Value: lang, 110 | MaxAge: cookieMaxAge, 111 | }) 112 | } 113 | 114 | theme := req.PostForm.Get("theme") 115 | if theme == "" { 116 | http.SetCookie(rw, &http.Cookie{ 117 | Name: "theme", 118 | Value: "", 119 | MaxAge: -1, 120 | }) 121 | 122 | } else { 123 | http.SetCookie(rw, &http.Cookie{ 124 | Name: "theme", 125 | Value: theme, 126 | MaxAge: cookieMaxAge, 127 | }) 128 | } 129 | 130 | author := req.PostForm.Get("author") 131 | if author == "" { 132 | http.SetCookie(rw, &http.Cookie{ 133 | Name: "author", 134 | Value: "", 135 | MaxAge: -1, 136 | }) 137 | 138 | } else { 139 | http.SetCookie(rw, &http.Cookie{ 140 | Name: "author", 141 | Value: author, 142 | MaxAge: cookieMaxAge, 143 | }) 144 | } 145 | 146 | authorEmail := req.PostForm.Get("authorEmail") 147 | if authorEmail == "" { 148 | http.SetCookie(rw, &http.Cookie{ 149 | Name: "authorEmail", 150 | Value: "", 151 | MaxAge: -1, 152 | }) 153 | 154 | } else { 155 | http.SetCookie(rw, &http.Cookie{ 156 | Name: "authorEmail", 157 | Value: authorEmail, 158 | MaxAge: cookieMaxAge, 159 | }) 160 | } 161 | 162 | authorURL := req.PostForm.Get("authorURL") 163 | if authorURL == "" { 164 | http.SetCookie(rw, &http.Cookie{ 165 | Name: "authorURL", 166 | Value: "", 167 | MaxAge: -1, 168 | }) 169 | 170 | } else { 171 | http.SetCookie(rw, &http.Cookie{ 172 | Name: "authorURL", 173 | Value: authorURL, 174 | MaxAge: cookieMaxAge, 175 | }) 176 | } 177 | 178 | writeRedirect(rw, req, "/settings", 302) 179 | } 180 | 181 | return nil 182 | } 183 | -------------------------------------------------------------------------------- /internal/web/web_sitemap.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | package web 20 | 21 | import ( 22 | "github.com/lcomrade/lenpaste/internal/netshare" 23 | "io" 24 | "net/http" 25 | ) 26 | 27 | func (data *Data) robotsTxtHand(rw http.ResponseWriter, req *http.Request) error { 28 | // Generate robots.txt 29 | robotsTxt := "User-agent: *\nDisallow: /\n" 30 | 31 | if data.RobotsDisallow == false { 32 | proto := netshare.GetProtocol(req) 33 | host := netshare.GetHost(req) 34 | 35 | robotsTxt = "User-agent: *\nAllow: /\nSitemap: " + proto + "://" + host + "/sitemap.xml\n" 36 | } 37 | 38 | // Write response 39 | rw.Header().Set("Content-Type", "text/plain; charset=utf-8") 40 | _, err := io.WriteString(rw, robotsTxt) 41 | if err != nil { 42 | return err 43 | } 44 | 45 | return nil 46 | } 47 | 48 | func (data *Data) sitemapHand(rw http.ResponseWriter, req *http.Request) error { 49 | if data.RobotsDisallow { 50 | return netshare.ErrNotFound 51 | } 52 | 53 | // Get protocol and host 54 | proto := netshare.GetProtocol(req) 55 | host := netshare.GetHost(req) 56 | 57 | // Generate sitemap.xml 58 | sitemapXML := `` 59 | sitemapXML = sitemapXML + "\n" + `` + "\n" 60 | sitemapXML = sitemapXML + "" + proto + "://" + host + "/" + "\n" 61 | sitemapXML = sitemapXML + "" + proto + "://" + host + "/about" + "\n" 62 | sitemapXML = sitemapXML + "" + proto + "://" + host + "/docs/apiv1" + "\n" 63 | sitemapXML = sitemapXML + "" + proto + "://" + host + "/docs/api_libs" + "\n" 64 | sitemapXML = sitemapXML + "\n" 65 | 66 | // Write response 67 | rw.Header().Set("Content-Type", "text/xml; charset=utf-8") 68 | _, err := io.WriteString(rw, sitemapXML) 69 | if err != nil { 70 | return err 71 | } 72 | 73 | return nil 74 | } 75 | -------------------------------------------------------------------------------- /internal/web/web_terms.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | package web 20 | 21 | import ( 22 | "html/template" 23 | "net/http" 24 | ) 25 | 26 | type termsOfUseTmpl struct { 27 | TermsOfUse string 28 | 29 | Highlight func(string, string) template.HTML 30 | Translate func(string, ...interface{}) template.HTML 31 | } 32 | 33 | // Pattern: /terms 34 | func (data *Data) termsOfUseHand(rw http.ResponseWriter, req *http.Request) error { 35 | rw.Header().Set("Content-Type", "text/html; charset=utf-8") 36 | return data.TermsOfUse.Execute(rw, termsOfUseTmpl{ 37 | TermsOfUse: data.ServerTermsOfUse, 38 | Highlight: data.Themes.findTheme(req, data.UiDefaultTheme).tryHighlight, 39 | Translate: data.Locales.findLocale(req).translate}, 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /internal/web/web_themes.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | package web 20 | 21 | import ( 22 | "bytes" 23 | "errors" 24 | "fmt" 25 | "html/template" 26 | "io/fs" 27 | "net/http" 28 | "os" 29 | "path/filepath" 30 | "strings" 31 | ) 32 | 33 | const baseTheme = "dark" 34 | const embThemesDir = "data/theme" 35 | 36 | type Theme map[string]string 37 | type Themes map[string]Theme 38 | 39 | type ThemesListPart map[string]string 40 | type ThemesList map[string]ThemesListPart 41 | 42 | func loadThemes(hostThemeDir string, localesList LocalesList, defaultTheme string) (Themes, ThemesList, error) { 43 | themes := make(Themes) 44 | themesList := make(ThemesList) 45 | 46 | for localeCode, _ := range localesList { 47 | themesList[localeCode] = make(ThemesListPart) 48 | } 49 | 50 | // Prepare load FS function 51 | loadThemesFromFS := func(f fs.FS, themeDir string) error { 52 | // Get theme files list 53 | files, err := fs.ReadDir(f, themeDir) 54 | if err != nil { 55 | return errors.New("web: failed read dir '" + themeDir + "': " + err.Error()) 56 | } 57 | 58 | for _, fileInfo := range files { 59 | // Check file 60 | if fileInfo.IsDir() { 61 | continue 62 | } 63 | 64 | fileName := fileInfo.Name() 65 | if strings.HasSuffix(fileName, ".theme") == false { 66 | continue 67 | } 68 | themeCode := fileName[:len(fileName)-6] 69 | 70 | // Read file 71 | filePath := filepath.Join(themeDir, fileName) 72 | fileByte, err := fs.ReadFile(f, filePath) 73 | if err != nil { 74 | return errors.New("web: failed open file '" + filePath + "': " + err.Error()) 75 | } 76 | 77 | fileStr := bytes.NewBuffer(fileByte).String() 78 | 79 | // Load theme 80 | theme, err := readKVCfg(fileStr) 81 | if err != nil { 82 | return errors.New("web: failed read file '" + filePath + "': " + err.Error()) 83 | } 84 | 85 | _, themeExist := themes[themeCode] 86 | if themeExist { 87 | return errors.New("web: theme alredy loaded: " + filePath) 88 | } 89 | 90 | themes[themeCode] = Theme(theme) 91 | } 92 | 93 | return nil 94 | } 95 | 96 | // Load embed themes 97 | err := loadThemesFromFS(embFS, embThemesDir) 98 | if err != nil { 99 | return nil, nil, err 100 | } 101 | 102 | // Load external themes 103 | if hostThemeDir != "" { 104 | err = loadThemesFromFS(os.DirFS(hostThemeDir), ".") 105 | if err != nil { 106 | return nil, nil, err 107 | } 108 | } 109 | 110 | // Prepare themes list 111 | for key, val := range themes { 112 | // Get theme name 113 | themeName := val["theme.Name."+baseLocale] 114 | if themeName == "" { 115 | return nil, nil, errors.New("web: empty theme.Name." + baseLocale + " parameter in '" + key + "' theme") 116 | } 117 | 118 | // Append to the translation, if it is not complete 119 | defTheme := themes[baseTheme] 120 | defTotal := len(defTheme) 121 | curTotal := 0 122 | for defKey, defVal := range defTheme { 123 | _, isExist := val[defKey] 124 | if isExist { 125 | curTotal = curTotal + 1 126 | } else { 127 | if strings.HasPrefix(defKey, "theme.Name.") { 128 | val[defKey] = val["theme.Name."+baseLocale] 129 | } else { 130 | val[defKey] = defVal 131 | } 132 | } 133 | } 134 | 135 | if curTotal == 0 { 136 | return nil, nil, errors.New("web: theme '" + key + "' is empty") 137 | } 138 | 139 | // Add theme to themes list 140 | themeNameSuffix := "" 141 | if curTotal != defTotal { 142 | themeNameSuffix = fmt.Sprintf(" (%.2f%%)", (float32(curTotal)/float32(defTotal))*100) 143 | } 144 | themesList[baseLocale][key] = themeName + themeNameSuffix 145 | 146 | for localeCode, _ := range localesList { 147 | result, ok := val["theme.Name."+localeCode] 148 | if ok { 149 | themesList[localeCode][key] = result + themeNameSuffix 150 | } else { 151 | themesList[localeCode][key] = themeName + themeNameSuffix 152 | } 153 | } 154 | } 155 | 156 | // Check default theme exist 157 | _, ok := themes[defaultTheme] 158 | if ok == false { 159 | return nil, nil, errors.New("web: default theme '" + defaultTheme + "' not found") 160 | } 161 | 162 | return themes, themesList, nil 163 | } 164 | 165 | func (themesList ThemesList) getForLocale(req *http.Request) ThemesListPart { 166 | // Get theme by cookie 167 | langCookie := getCookie(req, "lang") 168 | if langCookie != "" { 169 | theme, ok := themesList[langCookie] 170 | if ok == true { 171 | return theme 172 | } 173 | } 174 | 175 | // Load default part theme 176 | theme, _ := themesList[baseLocale] 177 | return theme 178 | } 179 | 180 | func (themes Themes) findTheme(req *http.Request, defaultTheme string) Theme { 181 | // Get theme by cookie 182 | themeCookie := getCookie(req, "theme") 183 | if themeCookie != "" { 184 | theme, ok := themes[themeCookie] 185 | if ok == true { 186 | return theme 187 | } 188 | } 189 | 190 | // Load default theme 191 | theme, _ := themes[defaultTheme] 192 | return theme 193 | } 194 | 195 | func (theme Theme) theme(s string) string { 196 | for key, val := range theme { 197 | if key == s { 198 | return val 199 | } 200 | } 201 | 202 | panic(errors.New("web: theme: unknown theme key: " + s)) 203 | } 204 | 205 | func (theme Theme) tryHighlight(source string, lexer string) template.HTML { 206 | return tryHighlight(source, lexer, theme.theme("highlight.Theme")) 207 | } 208 | -------------------------------------------------------------------------------- /internal/web/web_translate.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | package web 20 | 21 | import ( 22 | "embed" 23 | "encoding/json" 24 | "errors" 25 | "fmt" 26 | "html/template" 27 | "net/http" 28 | "path/filepath" 29 | "strings" 30 | ) 31 | 32 | const baseLocale = "en" 33 | 34 | type Locale map[string]string 35 | type Locales map[string]Locale 36 | type LocalesList map[string]string 37 | 38 | func loadLocales(f embed.FS, localeDir string) (Locales, LocalesList, error) { 39 | locales := make(Locales) 40 | localesList := make(LocalesList) 41 | 42 | // Get locale files list 43 | files, err := f.ReadDir(localeDir) 44 | if err != nil { 45 | return nil, nil, errors.New("web: failed read dir '" + localeDir + "': " + err.Error()) 46 | } 47 | 48 | // Load locales 49 | for _, fileInfo := range files { 50 | // Check file 51 | if fileInfo.IsDir() { 52 | continue 53 | } 54 | 55 | fileName := fileInfo.Name() 56 | if strings.HasSuffix(fileName, ".json") == false { 57 | continue 58 | } 59 | localeCode := fileName[:len(fileName)-5] 60 | 61 | // Open and read file 62 | filePath := filepath.Join(localeDir, fileName) 63 | file, err := f.Open(filePath) 64 | if err != nil { 65 | return nil, nil, errors.New("web: failed open file '" + filePath + "': " + err.Error()) 66 | } 67 | defer file.Close() 68 | 69 | var locale Locale 70 | err = json.NewDecoder(file).Decode(&locale) 71 | if err != nil { 72 | return nil, nil, errors.New("web: failed read file '" + filePath + "': " + err.Error()) 73 | } 74 | 75 | locales[localeCode] = Locale(locale) 76 | } 77 | 78 | // Prepare locales list 79 | for key, val := range locales { 80 | // Get locale name 81 | localeName := val["locale.Name"] 82 | if localeName == "" { 83 | return nil, nil, errors.New("web: empty locale.Name parameter in '" + key + "' locale") 84 | } 85 | 86 | // Append to the translation, if it is not complete 87 | defLocale := locales[baseLocale] 88 | defTotal := len(defLocale) 89 | curTotal := 0 90 | for defKey, defVal := range defLocale { 91 | _, isExist := val[defKey] 92 | if isExist { 93 | curTotal = curTotal + 1 94 | } else { 95 | val[defKey] = defVal 96 | } 97 | } 98 | 99 | if curTotal == 0 { 100 | return nil, nil, errors.New("web: locale '" + key + "' is empty") 101 | } 102 | 103 | if curTotal == defTotal { 104 | localesList[key] = localeName 105 | } else { 106 | localesList[key] = localeName + fmt.Sprintf(" (%.2f%%)", (float32(curTotal)/float32(defTotal))*100) 107 | } 108 | } 109 | 110 | return locales, localesList, nil 111 | } 112 | 113 | func (locales Locales) findLocale(req *http.Request) Locale { 114 | // Get accept language by cookie 115 | langCookie := getCookie(req, "lang") 116 | if langCookie != "" { 117 | locale, ok := locales[langCookie] 118 | if ok == true { 119 | return locale 120 | } 121 | } 122 | 123 | // Get user Accepr-Languages list 124 | acceptLanguage := req.Header.Get("Accept-Language") 125 | acceptLanguage = strings.Replace(acceptLanguage, " ", "", -1) 126 | 127 | var langs []string 128 | for _, part := range strings.Split(acceptLanguage, ";") { 129 | for _, lang := range strings.Split(part, ",") { 130 | if strings.HasPrefix(lang, "q=") == false { 131 | langs = append(langs, lang) 132 | } 133 | } 134 | } 135 | 136 | // Search locale 137 | for _, lang := range langs { 138 | for localeCode, locale := range locales { 139 | if localeCode == lang { 140 | return locale 141 | } 142 | } 143 | } 144 | 145 | // Load default locale 146 | locale, _ := locales[baseLocale] 147 | return locale 148 | } 149 | 150 | func (locale Locale) translate(s string, a ...interface{}) template.HTML { 151 | for key, val := range locale { 152 | if key == s { 153 | return template.HTML(fmt.Sprintf(val, a...)) 154 | } 155 | } 156 | 157 | panic(errors.New("web: translate: unknown locale key: " + s)) 158 | } 159 | -------------------------------------------------------------------------------- /tools/kvcfg-to-json/kvcfg.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | package main 20 | 21 | import ( 22 | "errors" 23 | "strconv" 24 | "strings" 25 | ) 26 | 27 | func readKVCfg(data string) (map[string]string, error) { 28 | out := make(map[string]string) 29 | 30 | dataSplit := strings.Split(data, "\n") 31 | dataSplitLen := len(dataSplit) 32 | 33 | for num := 0; num < dataSplitLen; num++ { 34 | str := strings.TrimSpace(dataSplit[num]) 35 | 36 | if str == "" || strings.HasPrefix(str, "//") { 37 | continue 38 | } 39 | 40 | strSplit := strings.SplitN(str, "=", 2) 41 | if len(strSplit) != 2 { 42 | return out, errors.New("error in line " + strconv.Itoa(num+1) + ": expected '=' delimiter") 43 | } 44 | 45 | key := strings.TrimSpace(strSplit[0]) 46 | val := strings.TrimSpace(strSplit[1]) 47 | val, isMultiline := multilineCheck(val) 48 | 49 | if isMultiline { 50 | num = num + 1 51 | for ; num < dataSplitLen; num++ { 52 | strPlus := strings.TrimSpace(dataSplit[num]) 53 | strPlus, isMultilinePlus := multilineCheck(strPlus) 54 | val = val + strPlus 55 | 56 | if isMultilinePlus == false { 57 | break 58 | } 59 | } 60 | } 61 | 62 | _, exist := out[key] 63 | if exist { 64 | return out, errors.New("duplicate key: " + key) 65 | } 66 | 67 | out[key] = val 68 | } 69 | 70 | return out, nil 71 | } 72 | 73 | func multilineCheck(s string) (string, bool) { 74 | sLen := len(s) 75 | 76 | if sLen > 0 && s[sLen-1] == '\\' { 77 | if sLen > 1 && s[sLen-2] == '\\' { 78 | return s[:sLen-1], false 79 | } 80 | 81 | return s[:sLen-1], true 82 | } 83 | 84 | return s, false 85 | } 86 | -------------------------------------------------------------------------------- /tools/kvcfg-to-json/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021-2023 Leonid Maslakov. 2 | 3 | // This file is part of Lenpaste. 4 | 5 | // Lenpaste is free software: you can redistribute it 6 | // and/or modify it under the terms of the 7 | // GNU Affero Public License as published by the 8 | // Free Software Foundation, either version 3 of the License, 9 | // or (at your option) any later version. 10 | 11 | // Lenpaste is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | // or FITNESS FOR A PARTICULAR PURPOSE. 14 | // See the GNU Affero Public License for more details. 15 | 16 | // You should have received a copy of the GNU Affero Public License along with Lenpaste. 17 | // If not, see . 18 | 19 | package main 20 | 21 | import ( 22 | "encoding/json" 23 | "fmt" 24 | "os" 25 | ) 26 | 27 | func main() { 28 | if len(os.Args) != 3 { 29 | fmt.Fprintln(os.Stdout, "Usage:", os.Args[0], "[SRC] [DST]") 30 | os.Exit(1) 31 | } 32 | 33 | src := os.Args[1] 34 | dst := os.Args[2] 35 | 36 | // Read key-value config 37 | fileByte, err := os.ReadFile(src) 38 | if err != nil { 39 | panic(err) 40 | } 41 | 42 | cfg, err := readKVCfg(string(fileByte)) 43 | if err != nil { 44 | panic(err) 45 | } 46 | 47 | // Save config as JSON 48 | cfgRaw, err := json.MarshalIndent(cfg, "", "\t") 49 | if err != nil { 50 | panic(err) 51 | } 52 | 53 | cfgRaw = append(cfgRaw, byte('\n')) 54 | 55 | err = os.WriteFile(dst, cfgRaw, os.ModePerm) 56 | if err != nil { 57 | panic(err) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tools/localefmt.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | readonly VERSION="1.0, 14.12.2022" 5 | 6 | printHelp(){ 7 | echo "Usage: $0 [OPTION]... [FILES]" 8 | echo "Check locale files." 9 | echo "" 10 | echo " -s, --slien print only errors" 11 | echo " --rm-empty remove empty locale files" 12 | echo "" 13 | echo " -h, --help show help and exit" 14 | echo " -v, --version show version and exit" 15 | } 16 | 17 | # CLI args 18 | ARG_SLIENT=false 19 | ARG_RM_EMPTY=false 20 | 21 | if [ -z "$1" ]; then 22 | echo "error: not files specified" 1>&2 23 | exit 1 24 | fi 25 | 26 | while [ -n "$1" ]; do 27 | case "$1" in 28 | -s|--slient) 29 | ARG_SLIENT=true 30 | ;; 31 | 32 | --rm-empty) 33 | ARG_RM_EMPTY=true 34 | ;; 35 | 36 | -h|--help) 37 | printHelp 38 | exit 0 39 | ;; 40 | 41 | -v|--version) 42 | echo "$VERSION" 43 | exit 0 44 | ;; 45 | 46 | *) 47 | break 48 | ;; 49 | esac 50 | 51 | shift 52 | done 53 | 54 | # RUN 55 | #find ./ -type f -name "*.locale" | while read -r file; do 56 | # if ! grep -q -Ev '^$' "$file"; then 57 | # echo "$file" 58 | # fi 59 | #done 60 | 61 | while [ -n "$1" ]; do 62 | # If empty 63 | if ! grep -q -Ev '^$' "$1"; then 64 | # If need remove empty files 65 | if [ $ARG_RM_EMPTY = true ]; then 66 | rm "$1" 67 | 68 | if [ $ARG_SLIENT = false ]; then 69 | echo "remove empty file: $1" 70 | fi 71 | 72 | # If only print errors 73 | else 74 | if [ $ARG_SLIENT = false ]; then 75 | echo "empty: $1" 76 | fi 77 | fi 78 | 79 | # If not ok 80 | else 81 | if [ $ARG_SLIENT = false ]; then 82 | echo "ok $1" 83 | fi 84 | fi 85 | 86 | shift 87 | done 88 | --------------------------------------------------------------------------------