2022-12-08 19:43:13 +00:00
package main
import (
2023-08-17 22:27:52 +01:00
"bytes"
2022-12-08 19:43:13 +00:00
"context"
"crypto/md5"
2023-03-17 15:27:47 +00:00
"crypto/tls"
2022-12-08 19:43:13 +00:00
"database/sql"
2022-12-13 15:59:28 +00:00
"embed"
2023-01-17 15:52:10 +00:00
"errors"
2022-12-08 19:43:13 +00:00
"flag"
"fmt"
2023-02-23 19:53:49 +00:00
"html/template"
2022-12-08 19:43:13 +00:00
"log"
2023-03-17 15:27:47 +00:00
"net"
2022-12-08 19:43:13 +00:00
"net/http"
"os"
"path/filepath"
2023-02-23 19:53:49 +00:00
"regexp"
2022-12-15 18:47:09 +00:00
"strconv"
2022-12-08 19:43:13 +00:00
"strings"
2022-12-15 18:47:09 +00:00
"time"
2022-12-08 19:43:13 +00:00
2023-02-22 14:27:53 +00:00
"github.com/go-enry/go-enry/v2"
2022-12-08 19:43:13 +00:00
"github.com/google/uuid"
2024-04-30 23:57:37 +01:00
_ "github.com/lib/pq"
2023-02-23 19:53:49 +00:00
"github.com/microcosm-cc/bluemonday"
2023-08-17 22:27:52 +01:00
"github.com/niklasfasching/go-org/org"
2023-02-23 19:53:49 +00:00
"github.com/russross/blackfriday"
2023-09-19 15:51:11 +01:00
_ "modernc.org/sqlite"
2022-12-08 19:43:13 +00:00
"tailscale.com/client/tailscale"
"tailscale.com/client/tailscale/apitype"
2023-03-17 15:27:47 +00:00
"tailscale.com/ipn"
2022-12-08 19:43:13 +00:00
"tailscale.com/tailcfg"
"tailscale.com/tsnet"
)
var (
2024-04-30 23:57:37 +01:00
hostname = flag . String ( "hostname" , envOr ( "TSNET_HOSTNAME" , "paste" ) , "hostname to use on your tailnet, TSNET_HOSTNAME in the environment" )
dataDir = flag . String ( "data-location" , dataLocation ( ) , "where data is stored, defaults to DATA_DIR or ~/.config/tailscale/paste" )
tsnetLogVerbose = flag . Bool ( "tsnet-verbose" , hasEnv ( "TSNET_VERBOSE" ) , "if set, have tsnet log verbosely to standard error" )
useFunnel = flag . Bool ( "use-funnel" , hasEnv ( "USE_FUNNEL" ) , "if set, expose individual pastes to the public internet with Funnel, USE_FUNNEL in the environment" )
hidePasteUserInfo = flag . Bool ( "hide-funnel-users" , hasEnv ( "HIDE_FUNNEL_USERS" ) , "if set, display the username and profile picture of the user who created the paste in funneled pastes" )
databaseUrl = flag . String ( "database-url" , envOr ( "DATABASE_URL" , "" ) , "optional database url if you'd rather use Postgres instead of sqlite" )
2024-05-18 20:13:09 +01:00
httpPort = flag . String ( "http-port" , envOr ( "HTTP_PORT" , "" ) , "optional http port to start an http server on, e.g for reverse proxies. will only serve funnel endpoints" )
2022-12-08 19:43:13 +00:00
//go:embed schema.sql
sqlSchema string
2022-12-13 15:59:28 +00:00
//go:embed static
staticFiles embed . FS
2023-08-22 17:43:17 +01:00
//go:embed tmpl/*.html
2022-12-13 15:59:28 +00:00
templateFiles embed . FS
2022-12-08 19:43:13 +00:00
)
2023-09-19 15:58:35 +01:00
const timeFormat = "2006-01-02 15:04"
2023-03-17 14:41:32 +00:00
func hasEnv ( name string ) bool {
_ , ok := os . LookupEnv ( name )
return ok
}
2022-12-08 19:43:13 +00:00
const formDataLimit = 64 * 1024 // 64 kilobytes (approx. 32 printed pages of text)
func dataLocation ( ) string {
2023-01-31 17:13:22 +00:00
if dir , ok := os . LookupEnv ( "DATA_DIR" ) ; ok {
return dir
}
2022-12-08 19:43:13 +00:00
dir , err := os . UserConfigDir ( )
if err != nil {
return os . Getenv ( "DATA_DIR" )
}
return filepath . Join ( dir , "tailscale" , "paste" )
}
func envOr ( key , defaultVal string ) string {
if result , ok := os . LookupEnv ( key ) ; ok {
return result
}
return defaultVal
}
2022-12-13 15:59:28 +00:00
type Server struct {
2023-01-12 16:25:34 +00:00
lc * tailscale . LocalClient // localclient to tsnet server
db * sql . DB // SQLite datastore
tmpls * template . Template // HTML templates
httpsURL string // the tailnet/public base URL of this service
2022-12-13 15:59:28 +00:00
}
2022-12-08 19:43:13 +00:00
2022-12-13 15:59:28 +00:00
func ( s * Server ) TailnetIndex ( w http . ResponseWriter , r * http . Request ) {
if r . URL . Path != "/" {
s . NotFound ( w , r )
return
}
2022-12-08 19:43:13 +00:00
2022-12-13 15:59:28 +00:00
ui , err := upsertUserInfo ( r . Context ( ) , s . db , s . lc , r . RemoteAddr )
if err != nil {
2023-01-12 16:25:34 +00:00
s . ShowError ( w , r , err , http . StatusInternalServerError )
2022-12-13 15:59:28 +00:00
return
2022-12-08 19:43:13 +00:00
}
2023-02-23 16:37:14 +00:00
q := `
SELECT p . id
, p . filename
, p . created_at
, u . display_name
FROM pastes p
INNER JOIN users u
ON p . user_id = u . id
2024-04-30 23:57:37 +01:00
ORDER BY p . created_at DESC
2023-02-23 16:37:14 +00:00
LIMIT 5
`
jpis := make ( [ ] JoinedPasteInfo , 0 , 5 )
rows , err := s . db . QueryContext ( r . Context ( ) , q )
if err != nil {
s . ShowError ( w , r , err , http . StatusInternalServerError )
return
}
defer rows . Close ( )
for rows . Next ( ) {
jpi := JoinedPasteInfo { }
err := rows . Scan ( & jpi . ID , & jpi . Filename , & jpi . CreatedAt , & jpi . PasterDisplayName )
if err != nil {
s . ShowError ( w , r , err , http . StatusInternalServerError )
return
}
2023-09-20 14:30:49 +01:00
if jpi . Filename == "" {
jpi . Filename = "untitled"
}
2023-02-23 16:37:14 +00:00
jpis = append ( jpis , jpi )
}
2023-08-22 17:43:17 +01:00
err = s . tmpls . ExecuteTemplate ( w , "create.html" , struct {
2023-02-23 16:37:14 +00:00
UserInfo * tailcfg . UserProfile
Title string
RecentPastes [ ] JoinedPasteInfo
2022-12-13 15:59:28 +00:00
} {
2023-02-23 16:37:14 +00:00
UserInfo : ui . UserProfile ,
Title : "Create new paste" ,
RecentPastes : jpis ,
2022-12-13 15:59:28 +00:00
} )
if err != nil {
log . Printf ( "%s: %v" , r . RemoteAddr , err )
2022-12-08 19:43:13 +00:00
}
2022-12-13 15:59:28 +00:00
}
2022-12-08 19:43:13 +00:00
2023-01-12 21:01:01 +00:00
func ( s * Server ) TailnetHelp ( w http . ResponseWriter , r * http . Request ) {
ui , err := upsertUserInfo ( r . Context ( ) , s . db , s . lc , r . RemoteAddr )
if err != nil {
s . ShowError ( w , r , err , http . StatusInternalServerError )
return
}
2023-08-22 17:43:17 +01:00
err = s . tmpls . ExecuteTemplate ( w , "help.html" , struct {
2023-01-12 21:01:01 +00:00
UserInfo * tailcfg . UserProfile
Title string
} {
UserInfo : ui . UserProfile ,
Title : "Help" ,
} )
if err != nil {
log . Printf ( "%s: %v" , r . RemoteAddr , err )
}
}
2022-12-13 15:59:28 +00:00
func ( s * Server ) NotFound ( w http . ResponseWriter , r * http . Request ) {
2023-08-22 17:43:17 +01:00
s . tmpls . ExecuteTemplate ( w , "notfound.html" , struct {
2022-12-15 17:37:35 +00:00
UserInfo * tailcfg . UserProfile
Title string
} {
UserInfo : nil ,
Title : "Not found" ,
} )
}
func ( s * Server ) PublicIndex ( w http . ResponseWriter , r * http . Request ) {
if r . URL . Path != "/" {
s . NotFound ( w , r )
return
2022-12-13 15:59:28 +00:00
}
2023-08-22 17:43:17 +01:00
s . tmpls . ExecuteTemplate ( w , "publicindex.html" , struct {
2022-12-13 15:59:28 +00:00
UserInfo * tailcfg . UserProfile
Title string
} {
2022-12-15 17:37:35 +00:00
UserInfo : nil ,
2022-12-13 15:59:28 +00:00
Title : "Not found" ,
} )
}
func ( s * Server ) TailnetSubmitPaste ( w http . ResponseWriter , r * http . Request ) {
userInfo , err := upsertUserInfo ( r . Context ( ) , s . db , s . lc , r . RemoteAddr )
2022-12-08 19:43:13 +00:00
if err != nil {
2022-12-13 15:59:28 +00:00
log . Printf ( "%s: %v" , r . RemoteAddr , err )
http . Error ( w , err . Error ( ) , http . StatusInternalServerError )
return
2022-12-08 19:43:13 +00:00
}
2022-12-13 15:59:28 +00:00
if strings . HasPrefix ( r . Header . Get ( "Content-Type" ) , "multipart/form-data;" ) {
err = r . ParseMultipartForm ( formDataLimit )
} else if r . Header . Get ( "Content-Type" ) == "application/x-www-form-urlencoded" {
err = r . ParseForm ( )
} else {
log . Printf ( "%s: unknown content type: %s" , r . RemoteAddr , r . Header . Get ( "Content-Type" ) )
http . Error ( w , "bad content-type, should be a form" , http . StatusBadRequest )
return
}
2022-12-08 19:43:13 +00:00
if err != nil {
2022-12-13 15:59:28 +00:00
log . Printf ( "%s: bad form: %v" , r . RemoteAddr , err )
http . Error ( w , err . Error ( ) , http . StatusBadRequest )
return
2022-12-08 19:43:13 +00:00
}
2022-12-14 18:31:08 +00:00
if ! r . Form . Has ( "filename" ) && ! r . Form . Has ( "content" ) {
log . Printf ( "%s" , r . Form . Encode ( ) )
2022-12-13 15:59:28 +00:00
log . Printf ( "%s: posted form without filename and data" , r . RemoteAddr )
http . Error ( w , "include form values filename and data" , http . StatusBadRequest )
return
}
fname := r . Form . Get ( "filename" )
2022-12-14 18:31:08 +00:00
data := r . Form . Get ( "content" )
2022-12-13 15:59:28 +00:00
id := uuid . NewString ( )
2023-09-20 14:30:49 +01:00
if fname == "" {
fname = "untitled"
}
2022-12-13 15:59:28 +00:00
q := `
INSERT INTO pastes
( id
2022-12-15 18:47:09 +00:00
, created_at
2022-12-13 15:59:28 +00:00
, user_id
, filename
, data
)
VALUES
2024-04-30 23:57:37 +01:00
( $ 1
, $ 2
, $ 3
, $ 4
, $ 5
2022-12-13 15:59:28 +00:00
) `
_ , err = s . db . ExecContext (
r . Context ( ) ,
q ,
id ,
2023-09-19 15:58:35 +01:00
time . Now ( ) . Format ( timeFormat ) ,
2022-12-13 15:59:28 +00:00
userInfo . UserProfile . ID ,
fname ,
data ,
)
2022-12-08 19:43:13 +00:00
if err != nil {
2023-01-12 16:25:34 +00:00
s . ShowError ( w , r , err , http . StatusInternalServerError )
2022-12-13 15:59:28 +00:00
return
2022-12-08 19:43:13 +00:00
}
2022-12-13 15:59:28 +00:00
log . Printf ( "new paste: %s" , id )
2022-12-08 19:43:13 +00:00
2022-12-14 18:31:08 +00:00
switch r . Header . Get ( "Accept" ) {
case "text/plain" :
w . WriteHeader ( http . StatusOK )
2023-01-12 16:25:34 +00:00
fmt . Fprintf ( w , "https://%s/paste/%s" , s . httpsURL , id )
2022-12-14 18:31:08 +00:00
default :
2023-02-23 19:53:49 +00:00
http . Redirect ( w , r , fmt . Sprintf ( "https://%s/paste/%s" , s . httpsURL , id ) , http . StatusSeeOther )
2022-12-14 18:31:08 +00:00
}
2022-12-13 15:59:28 +00:00
}
2022-12-08 19:43:13 +00:00
2023-02-23 16:37:14 +00:00
type JoinedPasteInfo struct {
ID string ` json:"id" `
Filename string ` json:"fname" `
CreatedAt string ` json:"created_at" `
PasterDisplayName string ` json:"created_by" `
}
2022-12-15 18:47:09 +00:00
func ( s * Server ) TailnetPasteIndex ( w http . ResponseWriter , r * http . Request ) {
userInfo , err := upsertUserInfo ( r . Context ( ) , s . db , s . lc , r . RemoteAddr )
if err != nil {
2023-01-12 16:25:34 +00:00
s . ShowError ( w , r , err , http . StatusInternalServerError )
2022-12-15 18:47:09 +00:00
return
}
_ = userInfo
q := `
SELECT p . id
, p . filename
, p . created_at
, u . display_name
FROM pastes p
INNER JOIN users u
ON p . user_id = u . id
2024-04-30 23:57:37 +01:00
ORDER BY p . created_at DESC
2022-12-15 18:47:09 +00:00
LIMIT 25
2024-04-30 23:57:37 +01:00
OFFSET $ 1
2022-12-15 18:47:09 +00:00
`
uq := r . URL . Query ( )
page := uq . Get ( "page" )
if page == "" {
page = "0"
}
pageNum , err := strconv . Atoi ( page )
if err != nil {
log . Printf ( "%s: invalid ?page: %s: %v" , r . RemoteAddr , page , err )
pageNum = 0
}
2023-01-17 15:52:10 +00:00
rows , err := s . db . Query ( q , clampToZero ( pageNum ) * 25 )
2022-12-15 18:47:09 +00:00
if err != nil {
2023-01-12 16:25:34 +00:00
s . ShowError ( w , r , err , http . StatusInternalServerError )
2022-12-15 18:47:09 +00:00
return
}
jpis := make ( [ ] JoinedPasteInfo , 0 , 25 )
defer rows . Close ( )
for rows . Next ( ) {
jpi := JoinedPasteInfo { }
err := rows . Scan ( & jpi . ID , & jpi . Filename , & jpi . CreatedAt , & jpi . PasterDisplayName )
if err != nil {
s . ShowError ( w , r , err , http . StatusInternalServerError )
return
}
2023-09-20 14:30:49 +01:00
if jpi . Filename == "" {
jpi . Filename = "untitled"
}
2022-12-15 18:47:09 +00:00
jpis = append ( jpis , jpi )
}
if len ( jpis ) == 0 {
2023-08-22 17:43:17 +01:00
err = s . tmpls . ExecuteTemplate ( w , "nopastes.html" , struct {
2023-01-12 16:25:34 +00:00
UserInfo * tailcfg . UserProfile
Title string
} {
UserInfo : userInfo . UserProfile ,
Title : "Pastes" ,
} )
if err != nil {
log . Printf ( "%s: %v" , r . RemoteAddr , err )
}
return
2022-12-15 18:47:09 +00:00
}
var prev , next * int
if pageNum != 0 {
i := pageNum - 1
prev = & i
}
if len ( jpis ) == 25 {
i := pageNum + 1
next = & i
}
2023-08-22 17:43:17 +01:00
err = s . tmpls . ExecuteTemplate ( w , "listpaste.html" , struct {
2022-12-15 18:47:09 +00:00
UserInfo * tailcfg . UserProfile
Title string
Pastes [ ] JoinedPasteInfo
Prev * int
Next * int
Page int
} {
UserInfo : userInfo . UserProfile ,
Title : "Pastes" ,
Pastes : jpis ,
Prev : prev ,
Next : next ,
2023-01-12 16:25:34 +00:00
Page : pageNum + 1 ,
2022-12-15 18:47:09 +00:00
} )
if err != nil {
log . Printf ( "%s: %v" , r . RemoteAddr , err )
}
}
func ( s * Server ) ShowError ( w http . ResponseWriter , r * http . Request , err error , code int ) {
w . Header ( ) . Set ( "Content-Type" , "text/html" )
w . WriteHeader ( code )
log . Printf ( "%s: %v" , r . RemoteAddr , err )
2023-08-22 17:43:17 +01:00
if err := s . tmpls . ExecuteTemplate ( w , "error.html" , struct {
2022-12-15 18:47:09 +00:00
Title , Error string
UserInfo any
} {
Title : "Oh noes!" ,
Error : err . Error ( ) ,
} ) ; err != nil {
log . Printf ( "%s: %v" , r . RemoteAddr , err )
}
}
func clampToZero ( i int ) int {
if i <= 0 {
return 0
}
return i
}
2023-01-17 15:52:10 +00:00
func ( s * Server ) TailnetDeletePost ( w http . ResponseWriter , r * http . Request ) {
ui , err := upsertUserInfo ( r . Context ( ) , s . db , s . lc , r . RemoteAddr )
if err != nil {
s . ShowError ( w , r , err , http . StatusBadRequest )
return
}
if r . Method != http . MethodGet {
http . Error ( w , "must GET" , http . StatusMethodNotAllowed )
return
}
// /api/delete/{id}
sp := strings . Split ( r . URL . Path , "/" )
if len ( sp ) != 4 {
s . ShowError ( w , r , errors . New ( "must be /api/delete/:id" ) , http . StatusBadRequest )
}
id := sp [ 3 ]
if len ( sp ) == 0 {
http . Redirect ( w , r , "/" , http . StatusTemporaryRedirect )
return
}
q := `
SELECT p . user_id
FROM pastes p
2024-04-30 23:57:37 +01:00
WHERE p . id = $ 1 `
2023-01-17 15:52:10 +00:00
row := s . db . QueryRowContext ( r . Context ( ) , q , id )
var userIDOfPaste int64
if err := row . Scan ( & userIDOfPaste ) ; err != nil {
s . ShowError ( w , r , err , http . StatusInternalServerError )
return
}
if int64 ( ui . UserProfile . ID ) != userIDOfPaste {
s . ShowError ( w , r , errors . New ( "can only delete your pastes" ) , http . StatusForbidden )
return
}
q = `
DELETE FROM pastes
2024-04-30 23:57:37 +01:00
WHERE id = $ 1 AND user_id = $ 2
2023-01-17 15:52:10 +00:00
`
if _ , err := s . db . ExecContext ( r . Context ( ) , q , id , ui . UserProfile . ID ) ; err != nil {
s . ShowError ( w , r , err , http . StatusInternalServerError )
return
}
http . Redirect ( w , r , "/" , http . StatusTemporaryRedirect )
}
2022-12-13 15:59:28 +00:00
func ( s * Server ) ShowPost ( w http . ResponseWriter , r * http . Request ) {
ui , _ := upsertUserInfo ( r . Context ( ) , s . db , s . lc , r . RemoteAddr )
var up * tailcfg . UserProfile
if ui != nil {
up = ui . UserProfile
}
2023-03-17 15:51:40 +00:00
if valAny := r . Context ( ) . Value ( privacyKey ) ; valAny != nil {
if val , ok := valAny . ( mixedCriticalityHandlerCtxKey ) ; ok {
if val == isFunnel {
up = nil
}
}
}
2022-12-13 15:59:28 +00:00
if r . Method != http . MethodGet {
http . Error ( w , "must GET" , http . StatusMethodNotAllowed )
return
}
2023-02-24 18:28:41 +00:00
pathComponents := strings . Split ( r . URL . Path , "/" )
pathComponents = pathComponents [ 2 : ]
2022-12-13 15:59:28 +00:00
2023-02-24 18:28:41 +00:00
if len ( pathComponents ) == 0 {
2022-12-13 15:59:28 +00:00
http . Redirect ( w , r , "/" , http . StatusTemporaryRedirect )
return
}
2022-12-08 19:43:13 +00:00
2023-02-24 18:28:41 +00:00
id := pathComponents [ 0 ]
2022-12-08 19:43:13 +00:00
2022-12-13 15:59:28 +00:00
q := `
2022-12-08 19:43:13 +00:00
SELECT p . filename
2022-12-15 18:47:09 +00:00
, p . created_at
2022-12-08 19:43:13 +00:00
, p . data
, u . id
, u . login_name
, u . display_name
, u . profile_pic_url
FROM pastes p
INNER JOIN users u
ON p . user_id = u . id
2024-04-30 23:57:37 +01:00
WHERE p . id = $ 1 `
2022-12-08 19:43:13 +00:00
2022-12-13 15:59:28 +00:00
row := s . db . QueryRowContext ( r . Context ( ) , q , id )
2023-01-17 15:52:10 +00:00
var fname , data , userLoginName , userDisplayName , userProfilePicURL string
var userID int64
2022-12-15 18:47:09 +00:00
var createdAt string
2022-12-08 19:43:13 +00:00
2022-12-15 18:47:09 +00:00
err := row . Scan ( & fname , & createdAt , & data , & userID , & userLoginName , & userDisplayName , & userProfilePicURL )
2022-12-13 15:59:28 +00:00
if err != nil {
2022-12-15 18:47:09 +00:00
s . ShowError ( w , r , fmt . Errorf ( "can't find paste %s: %w" , id , err ) , http . StatusInternalServerError )
2022-12-13 15:59:28 +00:00
return
}
2022-12-08 19:43:13 +00:00
2023-09-20 14:30:49 +01:00
if fname == "" {
fname = "untitled"
}
2023-08-22 17:43:17 +01:00
lang := enry . GetLanguage ( fname , [ ] byte ( data ) )
2023-02-23 19:53:49 +00:00
var rawHTML * template . HTML
2023-02-22 14:27:53 +00:00
var cssClass string
if lang != "" {
2023-02-23 19:53:49 +00:00
cssClass = fmt . Sprintf ( "lang-%s" , strings . ToLower ( lang ) )
}
2023-08-17 22:27:52 +01:00
p := bluemonday . UGCPolicy ( )
p . AllowAttrs ( "class" ) . Matching ( regexp . MustCompile ( "^language-[a-zA-Z0-9]+$" ) ) . OnElements ( "code" )
2023-02-24 18:28:41 +00:00
if lang == "Markdown" {
2023-02-23 19:53:49 +00:00
output := blackfriday . MarkdownCommon ( [ ] byte ( data ) )
sanitized := p . SanitizeBytes ( output )
raw := template . HTML ( string ( sanitized ) )
rawHTML = & raw
2023-02-22 14:27:53 +00:00
}
2023-08-17 22:27:52 +01:00
if lang == "Org" {
w := org . NewHTMLWriter ( )
w . HighlightCodeBlock = func ( source , lang string , inline bool , params map [ string ] string ) string {
sourceSanitized := p . SanitizeBytes ( [ ] byte ( source ) )
if inline {
return fmt . Sprintf ( "<code>%s</code>" , sourceSanitized )
}
return fmt . Sprintf ( "<pre class=\"language-%[1]s\"><code class=\"language-%[1]s\">%s</code></pre>" , lang , sourceSanitized )
}
output , err := org . New ( ) . Parse ( bytes . NewReader ( [ ] byte ( data ) ) , "" ) . Write ( w )
// If we fail parsing just fall back to text and log.
if err == nil {
sanitized := p . SanitizeBytes ( [ ] byte ( output ) )
raw := template . HTML ( string ( sanitized ) )
rawHTML = & raw
} else {
log . Printf ( "error parsing org file: %s" , err )
}
}
2023-02-24 18:28:41 +00:00
// If you specify a formatting option:
if len ( pathComponents ) != 1 {
switch pathComponents [ 1 ] {
// view file as plain text in browser
2023-02-24 17:13:34 +00:00
case "raw" :
w . Header ( ) . Set ( "Content-Type" , "text/plain; charset=utf-8" )
w . Header ( ) . Set ( "Content-Length" , fmt . Sprintf ( "%d" , len ( data ) ) )
w . WriteHeader ( http . StatusOK )
fmt . Fprint ( w , data )
return
2023-02-24 18:28:41 +00:00
// download file to disk (plain text view plus download hint)
2023-02-24 17:13:34 +00:00
case "dl" :
w . Header ( ) . Set ( "Content-Type" , "text/plain; charset=utf-8" )
w . Header ( ) . Set ( "Content-Disposition" , fmt . Sprintf ( "attachment; filename=%q" , fname ) )
w . Header ( ) . Set ( "Content-Length" , fmt . Sprintf ( "%d" , len ( data ) ) )
w . WriteHeader ( http . StatusOK )
fmt . Fprint ( w , data )
2023-02-23 16:45:43 +00:00
return
case "" :
2023-02-24 18:28:41 +00:00
// view markdown file with a fancy HTML rendering step
2023-08-17 22:27:52 +01:00
case "md" , "org" :
if lang != "Markdown" && lang != "Org" {
2023-02-24 17:44:46 +00:00
http . Redirect ( w , r , "/paste/" + id , http . StatusTemporaryRedirect )
2023-02-24 17:13:34 +00:00
return
}
2023-08-17 22:27:52 +01:00
title := fname
if lang == "Markdown" {
mdTitle , ok := strings . CutPrefix ( strings . Split ( strings . TrimSpace ( data ) , "\n" ) [ 0 ] , "#" )
if ok {
title = mdTitle
}
}
if lang == "Org" {
ogTitle , ok := strings . CutPrefix ( strings . Split ( strings . TrimSpace ( data ) , "\n" ) [ 0 ] , "#+title:" )
if ok {
title = ogTitle
}
2023-02-24 17:13:34 +00:00
}
2023-08-22 17:43:17 +01:00
err = s . tmpls . ExecuteTemplate ( w , "fancypost.html" , struct {
2023-02-24 17:13:34 +00:00
Title string
CreatedAt string
PasterDisplayName string
PasterProfilePicURL string
2024-03-31 17:16:30 +01:00
DisplayUser bool
2023-02-24 17:13:34 +00:00
RawHTML * template . HTML
} {
Title : title ,
CreatedAt : createdAt ,
PasterDisplayName : userDisplayName ,
PasterProfilePicURL : userProfilePicURL ,
2024-03-31 17:16:30 +01:00
DisplayUser : ! * hidePasteUserInfo ,
2023-02-24 17:13:34 +00:00
RawHTML : rawHTML ,
} )
if err != nil {
log . Printf ( "%s: %v" , r . RemoteAddr , err )
}
return
2023-02-24 18:28:41 +00:00
// otherwise, throw a 404
2023-02-24 17:13:34 +00:00
default :
s . NotFound ( w , r )
2023-02-23 16:45:43 +00:00
return
2023-02-24 17:13:34 +00:00
}
}
2023-03-17 15:51:40 +00:00
var remoteUserID = tailcfg . UserID ( 0 )
if up != nil {
remoteUserID = up . ID
}
2023-08-22 17:43:17 +01:00
err = s . tmpls . ExecuteTemplate ( w , "showpaste.html" , struct {
2022-12-13 15:59:28 +00:00
UserInfo * tailcfg . UserProfile
Title string
2022-12-15 18:47:09 +00:00
CreatedAt string
2022-12-13 15:59:28 +00:00
PasterDisplayName string
PasterProfilePicURL string
2023-01-17 15:52:10 +00:00
PasterUserID int64
2024-03-31 17:16:30 +01:00
DisplayUser bool
2023-01-17 15:52:10 +00:00
UserID int64
2022-12-13 15:59:28 +00:00
ID string
Data string
2023-02-23 19:53:49 +00:00
RawHTML * template . HTML
2023-02-22 14:27:53 +00:00
CSSClass string
2022-12-13 15:59:28 +00:00
} {
UserInfo : up ,
Title : fname ,
2022-12-15 18:47:09 +00:00
CreatedAt : createdAt ,
2022-12-13 15:59:28 +00:00
PasterDisplayName : userDisplayName ,
PasterProfilePicURL : userProfilePicURL ,
2024-03-31 17:16:30 +01:00
DisplayUser : ! * hidePasteUserInfo ,
2023-01-17 15:52:10 +00:00
PasterUserID : userID ,
2023-03-17 15:51:40 +00:00
UserID : int64 ( remoteUserID ) ,
2022-12-13 15:59:28 +00:00
ID : id ,
Data : data ,
2023-02-23 19:53:49 +00:00
RawHTML : rawHTML ,
2023-02-22 14:27:53 +00:00
CSSClass : cssClass ,
2022-12-08 19:43:13 +00:00
} )
2022-12-13 15:59:28 +00:00
if err != nil {
log . Printf ( "%s: %v" , r . RemoteAddr , err )
}
}
2022-12-08 19:43:13 +00:00
2022-12-13 15:59:28 +00:00
func main ( ) {
flag . Parse ( )
2022-12-08 19:43:13 +00:00
2022-12-13 15:59:28 +00:00
os . MkdirAll ( * dataDir , 0700 )
os . MkdirAll ( filepath . Join ( * dataDir , "tsnet" ) , 0700 )
2022-12-08 20:38:02 +00:00
2022-12-13 15:59:28 +00:00
s := & tsnet . Server {
Hostname : * hostname ,
Dir : filepath . Join ( * dataDir , "tsnet" ) ,
Logf : func ( string , ... any ) { } ,
}
2023-01-12 16:25:34 +00:00
if * tsnetLogVerbose {
s . Logf = log . Printf
}
2022-12-13 15:59:28 +00:00
if err := s . Start ( ) ; err != nil {
log . Fatal ( err )
}
2024-04-30 23:57:37 +01:00
db , err := openDB ( * dataDir , * databaseUrl )
2022-12-13 15:59:28 +00:00
if err != nil {
log . Fatal ( err )
}
defer db . Close ( )
lc , err := s . LocalClient ( )
if err != nil {
log . Fatal ( err )
}
2023-01-12 16:25:34 +00:00
// wait for tailscale to start before trying to fetch cert names
for i := 0 ; i < 60 ; i ++ {
st , err := lc . Status ( context . Background ( ) )
if err != nil {
log . Printf ( "error retrieving tailscale status; retrying: %v" , err )
} else {
if st . BackendState == "Running" {
break
}
}
time . Sleep ( time . Second )
}
ctx := context . Background ( )
httpsURL , ok := lc . ExpandSNIName ( ctx , * hostname )
if ! ok {
2023-08-22 17:43:17 +01:00
log . Println ( httpsURL )
2023-01-12 16:25:34 +00:00
log . Fatal ( "HTTPS is not enabled in the admin panel" )
}
2022-12-13 15:59:28 +00:00
ln , err := s . Listen ( "tcp" , ":80" )
if err != nil {
log . Fatal ( err )
}
2023-08-22 17:43:17 +01:00
tmpls := template . Must ( template . ParseFS ( templateFiles , "tmpl/*.html" ) )
2022-12-13 15:59:28 +00:00
2023-01-12 16:25:34 +00:00
srv := & Server { lc , db , tmpls , httpsURL }
2022-12-13 15:59:28 +00:00
2022-12-15 17:37:35 +00:00
tailnetMux := http . NewServeMux ( )
2022-12-13 15:59:28 +00:00
tailnetMux . Handle ( "/static/" , http . FileServer ( http . FS ( staticFiles ) ) )
tailnetMux . HandleFunc ( "/paste/" , srv . ShowPost )
2022-12-15 18:47:09 +00:00
tailnetMux . HandleFunc ( "/paste/list" , srv . TailnetPasteIndex )
2022-12-13 15:59:28 +00:00
tailnetMux . HandleFunc ( "/api/post" , srv . TailnetSubmitPaste )
2023-01-17 15:52:10 +00:00
tailnetMux . HandleFunc ( "/api/delete/" , srv . TailnetDeletePost )
2022-12-13 15:59:28 +00:00
tailnetMux . HandleFunc ( "/" , srv . TailnetIndex )
2023-01-12 21:01:01 +00:00
tailnetMux . HandleFunc ( "/help" , srv . TailnetHelp )
2022-12-08 19:43:13 +00:00
2022-12-15 17:37:35 +00:00
funnelMux := http . NewServeMux ( )
funnelMux . Handle ( "/static/" , http . FileServer ( http . FS ( staticFiles ) ) )
funnelMux . HandleFunc ( "/" , srv . PublicIndex )
funnelMux . HandleFunc ( "/paste/" , srv . ShowPost )
2022-12-08 19:43:13 +00:00
log . Printf ( "listening on http://%s" , * hostname )
2023-01-12 16:25:34 +00:00
go func ( ) { log . Fatal ( http . Serve ( ln , tailnetMux ) ) } ( )
2024-05-18 20:13:09 +01:00
if * httpPort != "" {
log . Printf ( "listening on :%s" , * httpPort )
go func ( ) { log . Fatal ( http . ListenAndServe ( ":" + * httpPort , funnelMux ) ) } ( )
}
2023-01-12 16:25:34 +00:00
2023-03-17 14:41:32 +00:00
if * useFunnel {
log . Println ( "trying to listen on funnel" )
2023-03-17 15:27:47 +00:00
ln , err := s . ListenFunnel ( "tcp" , ":443" )
2023-03-17 14:41:32 +00:00
if err != nil {
log . Fatalf ( "can't listen on funnel: %v" , err )
}
defer ln . Close ( )
2023-03-17 15:51:40 +00:00
log . Printf ( "listening on https://%s" , httpsURL )
2023-03-17 15:27:47 +00:00
log . Fatal ( MixedCriticalityHandler {
Public : funnelMux ,
Private : tailnetMux ,
} . Serve ( ln ) )
} else {
ln , err := s . ListenTLS ( "tcp" , ":443" )
if err != nil {
log . Fatal ( err )
}
defer ln . Close ( )
log . Printf ( "listening on https://%s" , httpsURL )
log . Fatal ( http . Serve ( ln , tailnetMux ) )
2023-03-17 14:41:32 +00:00
}
2023-03-17 15:27:47 +00:00
}
2023-03-17 14:41:32 +00:00
2023-03-17 15:27:47 +00:00
type mixedCriticalityHandlerCtxKey int
const (
privacyKey mixedCriticalityHandlerCtxKey = iota
isFunnel
isTailnet
)
type MixedCriticalityHandler struct {
Public http . Handler
Private http . Handler
}
func ( mch MixedCriticalityHandler ) Serve ( ln net . Listener ) error {
srv := & http . Server {
ConnContext : func ( ctx context . Context , c net . Conn ) context . Context {
tc , ok := c . ( * tls . Conn )
if ! ok {
return ctx
}
if _ , ok := tc . NetConn ( ) . ( * ipn . FunnelConn ) ; ok {
return context . WithValue ( ctx , privacyKey , isFunnel )
} else {
return context . WithValue ( ctx , privacyKey , isTailnet )
}
} ,
Handler : mch ,
}
return srv . Serve ( ln )
}
func ( mch MixedCriticalityHandler ) ServeHTTP ( w http . ResponseWriter , r * http . Request ) {
ctx := r . Context ( )
valAny := ctx . Value ( privacyKey )
if valAny == nil {
panic ( "incorrect context stack (value is missing)" )
}
val , ok := valAny . ( mixedCriticalityHandlerCtxKey )
if ! ok {
panic ( "incorrect context stack (value is of wrong type)" )
2023-01-12 16:25:34 +00:00
}
2023-03-17 15:27:47 +00:00
switch val {
case isFunnel :
mch . Public . ServeHTTP ( w , r )
return
case isTailnet :
mch . Private . ServeHTTP ( w , r )
return
}
panic ( "unknown security level" )
2022-12-08 19:43:13 +00:00
}
2024-04-30 23:57:37 +01:00
func openDB ( dir string , databaseUrl string ) ( * sql . DB , error ) {
dbtype := "sqlite"
dbpath := "file:" + filepath . Join ( dir , "data.db" )
if databaseUrl != "" {
dbtype = "postgres"
dbpath = databaseUrl
}
db , err := sql . Open ( dbtype , dbpath )
2023-09-19 15:51:11 +01:00
if err != nil {
return nil , err
}
2022-12-08 19:43:13 +00:00
2023-09-19 15:51:11 +01:00
err = db . Ping ( )
2022-12-08 19:43:13 +00:00
if err != nil {
return nil , err
}
2024-04-30 23:57:37 +01:00
// Enable WAL for SQLite + Litestream
if dbtype == "sqlite" {
_ , err := db . Exec ( "PRAGMA journal_mode=WAL;" )
if err != nil {
return nil , err
}
}
2023-09-19 15:51:11 +01:00
if _ , err := db . Exec ( sqlSchema ) ; err != nil {
return nil , err
}
2022-12-08 19:43:13 +00:00
return db , nil
}
func md5Hash ( inp string ) string {
h := md5 . New ( )
return fmt . Sprintf ( "%x" , h . Sum ( [ ] byte ( inp ) ) )
}
func upsertUserInfo ( ctx context . Context , db * sql . DB , lc * tailscale . LocalClient , remoteAddr string ) ( * apitype . WhoIsResponse , error ) {
userInfo , err := lc . WhoIs ( ctx , remoteAddr )
if err != nil {
return nil , err
}
if userInfo . UserProfile . LoginName == "tagged-devices" {
userInfo . UserProfile . ID = tailcfg . UserID ( userInfo . Node . ID )
userInfo . UserProfile . LoginName = userInfo . Node . Hostinfo . Hostname ( )
userInfo . UserProfile . DisplayName = fmt . Sprintf ( "tagged node %s: %s" , userInfo . Node . Hostinfo . Hostname ( ) , userInfo . Node . Tags [ 0 ] )
userInfo . UserProfile . ProfilePicURL = fmt . Sprintf ( "https://www.gravatar.com/avatar/%s" , md5Hash ( userInfo . Node . ComputedNameWithHost ) )
}
q := `
INSERT INTO users
( id
, login_name
, display_name
, profile_pic_url
)
VALUES
2024-04-30 23:57:37 +01:00
( $ 1
, $ 2
, $ 3
, $ 4
2022-12-08 19:43:13 +00:00
)
2024-04-30 23:57:37 +01:00
ON CONFLICT ( id ) DO
2022-12-08 19:43:13 +00:00
UPDATE SET
2024-04-30 23:57:37 +01:00
login_name = $ 2
, display_name = $ 3
, profile_pic_url = $ 4
2022-12-08 19:43:13 +00:00
`
_ , err = db . ExecContext (
ctx ,
q ,
userInfo . UserProfile . ID ,
userInfo . UserProfile . LoginName ,
userInfo . UserProfile . DisplayName ,
userInfo . UserProfile . ProfilePicURL ,
)
if err != nil {
return nil , err
}
return userInfo , nil
}