├── .gitignore ├── nerds.png ├── .travis.yml ├── init_mysql.sql ├── store ├── store.go └── random.go ├── static ├── cmd.txt └── banner.txt ├── LICENSE ├── README.md └── nerds.go /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.swp 3 | -------------------------------------------------------------------------------- /nerds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/writeas/nerds/HEAD/nerds.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | notifications: 4 | email: false 5 | 6 | go: 7 | - 1.4 8 | -------------------------------------------------------------------------------- /init_mysql.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS `posts` ( 2 | `id` char(16) NOT NULL, 3 | `modify_token` char(32) DEFAULT NULL, 4 | `text_appearance` char(4) NOT NULL DEFAULT 'norm', 5 | `last_modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 6 | `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, 7 | PRIMARY KEY (`id`) 8 | ) ENGINE=InnoDB DEFAULT CHARSET=latin1; 9 | -------------------------------------------------------------------------------- /store/store.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "os" 7 | ) 8 | 9 | const ( 10 | // FriendlyIdLen is the length of any saved posts's filename. 11 | FriendlyIdLen = 13 12 | ) 13 | 14 | // SavePost writes the given bytes to a file with a randomly generated name in 15 | // the given directory. 16 | func SavePost(outDir string, post []byte) (string, error) { 17 | filename := generateFileName() 18 | f, err := os.Create(outDir + "/" + filename) 19 | if err != nil { 20 | return "", err 21 | } 22 | 23 | defer f.Close() 24 | 25 | out := post[:0] 26 | for _, b := range post { 27 | if b < 32 && b != 10 && b != 13 { 28 | continue 29 | } 30 | out = append(out, b) 31 | } 32 | _, err = io.Copy(f, bytes.NewReader(out)) 33 | 34 | return filename, err 35 | } 36 | 37 | func generateFileName() string { 38 | return GenerateFriendlyRandomString(FriendlyIdLen) 39 | } 40 | -------------------------------------------------------------------------------- /static/cmd.txt: -------------------------------------------------------------------------------- 1 | Welcome to cmd.write.as! 2 | 3 | ████████████████████████████████████████████████████ 4 | █ █ 5 | █ | curl -F 'w=<-' http://cmd.write.as █ 6 | █ █ 7 | ████████████████████████████████████████████████████ 8 | 9 | Simple text pasting / publishing. Run then share the link you get. 10 | 11 | No funny stuff — all posts show up on write.as once the command finishes. No 12 | sign up, no publicized URLs, no analytics. Publish what you like. 13 | 14 | This is the pre-pre-alpha of write.as, showcasing some of what you'll see later. 15 | We have many plans, all based in text/plain. Keep up to date with us @writeas__ 16 | on Twitter or get notified when we launch at https://write.as. 17 | 18 | 19 | You can also use telnet: nerds.write.as 20 | 21 | 22 | BUGS/ISSUES/YOU HACKED THE GIBSON 23 | ————————————————————————————————— 24 | Report bugs and flaws to hello@write.as. 25 | -------------------------------------------------------------------------------- /static/banner.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | .--. __.....__ 4 | _ _ |__| .-'' '. 5 | /\ \\ /.-,.--..--. .| / .-''"'-. `. 6 | `\\ //\\ //| .-. | | .' |_/ /________\ \ __ 7 | \`// \'/ | | | | |.' | | .:--.'. _ 8 | \| |/ | | | | '--. .-\ .-------------' / | \ | .' | 9 | ' | | '-| | | | \ '-.____...---,.--.`" __ | | . | / 10 | | | |__| | | `. .// \.'.''| | .'.'| |// 11 | | | | '.' `''-...... -' \\ / / | |.'.'.-' / 12 | |_| | / `'--'\ \._,\ '.' \_.' 13 | `'-' `--' `" 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Write.as 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /store/random.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "crypto/rand" 5 | ) 6 | 7 | // Generate62RandomString creates a random string with the given length 8 | // consisting of characters in [A-Za-z0-9]. 9 | func Generate62RandomString(l int) string { 10 | return GenerateRandomString("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", l) 11 | } 12 | 13 | // GenerateFriendlyRandomString creates a random string of characters with the 14 | // given length consististing of characters in [a-z0-9]. 15 | func GenerateFriendlyRandomString(l int) string { 16 | return GenerateRandomString("0123456789abcdefghijklmnopqrstuvwxyz", l) 17 | } 18 | 19 | // GenerateRandomString creates a random string of characters of the given 20 | // length from the given dictionary of possible characters. 21 | // 22 | // This example generates a hexadecimal string 6 characters long: 23 | // GenerateRandomString("0123456789abcdef", 6) 24 | func GenerateRandomString(dictionary string, l int) string { 25 | var bytes = make([]byte, l) 26 | rand.Read(bytes) 27 | for k, v := range bytes { 28 | bytes[k] = dictionary[v%byte(len(dictionary))] 29 | } 30 | return string(bytes) 31 | } 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Write.as 2 | ======== 3 | [![Build Status](https://travis-ci.org/writeas/nerds.svg)](https://travis-ci.org/writeas/nerds) [![#writeas on freenode](https://img.shields.io/badge/freenode-%23writeas-blue.svg)](http://webchat.freenode.net/?channels=writeas) [![Public Slack discussion](http://slack.write.as/badge.svg)](http://slack.write.as/) 4 | 5 | This is a simple telnet-based interface for publishing text. Users connect and paste / type what they want to publish. Upon indicating that they're finished, a link is generated to access their new post on the web. 6 | 7 | ![Write.as telnet server](https://github.com/writeas/nerds/raw/master/nerds.png) 8 | 9 | ## Try it 10 | **Or not :(**. We had to [shut it down](https://twitter.com/writeas__/status/790356847526027264) because it was getting DDoSed too much. But you can still [run it yourself](#run-it-yourself). 11 | 12 | ``` 13 | telnet nerds.write.as 14 | ``` 15 | 16 | ## Run it yourself 17 | ``` 18 | Usage: 19 | nerds [options] 20 | 21 | Options: 22 | --debug 23 | Enables garrulous debug logging. 24 | -o 25 | Directory where text files will be stored. 26 | -s 27 | Directory where required static files exist (like the banner). 28 | -h 29 | Hostname of the server to rsync saved files to. 30 | -p 31 | Port to listen on. 32 | ``` 33 | 34 | The default configuration (without any flags) is essentially: 35 | 36 | ``` 37 | nerds -o /var/write -s . -p 2323 38 | ``` 39 | 40 | ## How it works 41 | The user's input is simply written to a flat file in a given directory. To provide web access, a web server (sold separately) serves all files in this directory as `plain/text`. That's it! 42 | 43 | ## License 44 | This project is licensed under the MIT open source license. 45 | -------------------------------------------------------------------------------- /nerds.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "database/sql" 6 | "flag" 7 | "fmt" 8 | _ "github.com/go-sql-driver/mysql" 9 | "io/ioutil" 10 | "net" 11 | "os" 12 | "os/exec" 13 | 14 | "github.com/writeas/nerds/store" 15 | ) 16 | 17 | var ( 18 | banner []byte 19 | outDir string 20 | staticDir string 21 | debugging bool 22 | rsyncHost string 23 | db *sql.DB 24 | ) 25 | 26 | const ( 27 | colBlue = "\033[0;34m" 28 | colGreen = "\033[0;32m" 29 | colBGreen = "\033[1;32m" 30 | colCyan = "\033[0;36m" 31 | colBRed = "\033[1;31m" 32 | colBold = "\033[1;37m" 33 | noCol = "\033[0m" 34 | 35 | hr = "————————————————————————————————————————————————————————————————————————————————" 36 | ) 37 | 38 | func main() { 39 | // Get any arguments 40 | outDirPtr := flag.String("o", "/var/write", "Directory where text files will be stored.") 41 | staticDirPtr := flag.String("s", "./static", "Directory where required static files exist.") 42 | rsyncHostPtr := flag.String("h", "", "Hostname of the server to rsync saved files to.") 43 | portPtr := flag.Int("p", 2323, "Port to listen on.") 44 | debugPtr := flag.Bool("debug", false, "Enables garrulous debug logging.") 45 | flag.Parse() 46 | 47 | outDir = *outDirPtr 48 | staticDir = *staticDirPtr 49 | rsyncHost = *rsyncHostPtr 50 | debugging = *debugPtr 51 | 52 | fmt.Print("\nCONFIG:\n") 53 | fmt.Printf("Output directory : %s\n", outDir) 54 | fmt.Printf("Static directory : %s\n", staticDir) 55 | fmt.Printf("rsync host : %s\n", rsyncHost) 56 | fmt.Printf("Debugging enabled : %t\n\n", debugging) 57 | 58 | fmt.Print("Initializing...") 59 | var err error 60 | banner, err = ioutil.ReadFile(staticDir + "/banner.txt") 61 | if err != nil { 62 | fmt.Println(err) 63 | } 64 | fmt.Println("DONE") 65 | 66 | // Connect to database 67 | dbUser := os.Getenv("WA_USER") 68 | dbPassword := os.Getenv("WA_PASSWORD") 69 | dbName := os.Getenv("WA_DB") 70 | dbHost := os.Getenv("WA_HOST") 71 | 72 | if outDir == "" && (dbUser == "" || dbPassword == "" || dbName == "") { 73 | // Ensure parameters needed for storage (file or database) are given 74 | fmt.Println("Database user, password, or database name not set.") 75 | return 76 | } 77 | 78 | if outDir == "" { 79 | fmt.Print("Connecting to database...") 80 | db, err = sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s:3306)/%s?charset=utf8mb4", dbUser, dbPassword, dbHost, dbName)) 81 | if err != nil { 82 | fmt.Printf("\n%s\n", err) 83 | return 84 | } 85 | defer db.Close() 86 | fmt.Println("CONNECTED") 87 | } 88 | 89 | ln, err := net.Listen("tcp", fmt.Sprintf(":%d", *portPtr)) 90 | if err != nil { 91 | panic(err) 92 | } 93 | fmt.Printf("Listening on localhost:%d\n", *portPtr) 94 | 95 | for { 96 | conn, err := ln.Accept() 97 | if err != nil { 98 | fmt.Println(err) 99 | continue 100 | } 101 | 102 | go handleConnection(conn) 103 | } 104 | } 105 | 106 | func output(c net.Conn, m string) bool { 107 | _, err := c.Write([]byte(m)) 108 | if err != nil { 109 | c.Close() 110 | return false 111 | } 112 | return true 113 | } 114 | 115 | func outputBytes(c net.Conn, m []byte) bool { 116 | _, err := c.Write(m) 117 | if err != nil { 118 | c.Close() 119 | return false 120 | } 121 | return true 122 | } 123 | 124 | func handleConnection(c net.Conn) { 125 | outputBytes(c, banner) 126 | output(c, fmt.Sprintf("\n%sWelcome to write.as!%s\n", colBGreen, noCol)) 127 | output(c, fmt.Sprintf("Our telnet server is open source! https://github.com/writeas/nerds\nOptionally post with %scURL%s: http://cmd.write.as\nOr use our cross-platform %scommand-line client%s: https://write.as/cli.html\nWe won't judge if you crawl back to the GUI: https://write.as/apps\n\n", colBold, noCol, colBold, noCol)) 128 | 129 | waitForEnter(c) 130 | 131 | c.Close() 132 | 133 | fmt.Printf("Connection from %v closed.\n", c.RemoteAddr()) 134 | } 135 | 136 | func waitForEnter(c net.Conn) { 137 | b := make([]byte, 4) 138 | 139 | output(c, fmt.Sprintf("%sPress Enter to continue...%s\n", colBRed, noCol)) 140 | for { 141 | n, err := c.Read(b) 142 | 143 | if debugging { 144 | fmt.Print(b[0:n]) 145 | fmt.Printf("\n%d: %s\n", n, b[0:n]) 146 | } 147 | 148 | if bytes.IndexRune(b[0:n], '\n') > -1 { 149 | break 150 | } 151 | if err != nil || n == 0 { 152 | c.Close() 153 | break 154 | } 155 | } 156 | 157 | output(c, fmt.Sprintf("Enter anything you like.\nPress %sCtrl-D%s to publish and quit.\n%s\n", colBold, noCol, hr)) 158 | readInput(c) 159 | } 160 | 161 | func checkExit(b []byte, n int) bool { 162 | return n > 0 && bytes.IndexRune(b[0:n], '\n') == -1 163 | } 164 | 165 | func readInput(c net.Conn) { 166 | defer c.Close() 167 | 168 | b := make([]byte, 4096) 169 | 170 | var post bytes.Buffer 171 | 172 | for { 173 | n, err := c.Read(b) 174 | post.Write(b[0:n]) 175 | 176 | if debugging { 177 | fmt.Print(b[0:n]) 178 | fmt.Printf("\n%d: %s\n", n, b[0:n]) 179 | } 180 | 181 | if checkExit(b, n) { 182 | friendlyId := store.GenerateFriendlyRandomString(store.FriendlyIdLen) 183 | editToken := store.Generate62RandomString(32) 184 | 185 | if outDir == "" { 186 | _, err := db.Exec("INSERT INTO posts (id, content, modify_token, text_appearance) VALUES (?, ?, ?, 'mono')", friendlyId, post.Bytes(), editToken) 187 | if err != nil { 188 | fmt.Printf("There was an error saving: %s\n", err) 189 | output(c, "Something went terribly wrong, sorry. Try again later?\n\n") 190 | break 191 | } 192 | output(c, fmt.Sprintf("\n%s\nPosted! View at %shttps://write.as/%s%s", hr, colBlue, friendlyId, noCol)) 193 | } else { 194 | output(c, fmt.Sprintf("\n%s\nPosted to %shttp://nerds.write.as/%s%s", hr, colBlue, friendlyId, noCol)) 195 | 196 | if rsyncHost != "" { 197 | output(c, "\nPosting to secure site...") 198 | exec.Command("rsync", "-ptgou", outDir+"/"+friendlyId, rsyncHost+":").Run() 199 | output(c, fmt.Sprintf("\nPosted! View at %shttps://write.as/%s%s", colBlue, friendlyId, noCol)) 200 | } 201 | } 202 | 203 | output(c, "\nSee you later.\n\n") 204 | break 205 | } 206 | 207 | if err != nil || n == 0 { 208 | break 209 | } 210 | } 211 | } 212 | --------------------------------------------------------------------------------