├── .gitignore ├── .travis.yml ├── CMakeLists.txt ├── LICENSE ├── Makefile ├── README.md ├── extras └── lines │ ├── __init__.py │ └── lines.py ├── fiche.c ├── fiche.h └── main.c /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore binaries 2 | /fiche 3 | 4 | # ignore default outpit dir 5 | code/ 6 | 7 | # ignore log files 8 | *.log 9 | 10 | # CMake build output 11 | cmake-build-debug 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: c 2 | 3 | compiler: 4 | - gcc 5 | - clang 6 | 7 | addons: 8 | apt: 9 | packages: 10 | - cppcheck 11 | - clang-3.5 12 | 13 | install: 14 | - export PYTHONUSERBASE=~/.local 15 | - easy_install --user scan-build 16 | - easy_install --user typing 17 | 18 | script: 19 | - cppcheck --enable=all --error-exitcode=1 --inconclusive main.c fiche.c 20 | - make 21 | - scan-build --status-bugs make -B 22 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.7) 2 | project(tricorder) 3 | 4 | set(CMAKE_CXX_STANDARD 11) 5 | 6 | file(GLOB SOURCE_FILES "*.c" "*.h") 7 | 8 | add_executable(tricorder ${SOURCE_FILES}) 9 | 10 | target_link_libraries(tricorder pthread) 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 solusipse 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # for debug add -g -O0 to line below 2 | CFLAGS+=-pthread -O2 -Wall -Wextra -Wpedantic -Wstrict-overflow -fno-strict-aliasing -std=gnu11 -g -O0 3 | prefix=/usr/local/bin 4 | 5 | all: 6 | ${CC} main.c fiche.c $(CFLAGS) -o fiche 7 | 8 | install: fiche 9 | install -m 0755 fiche $(prefix) 10 | 11 | clean: 12 | rm -f fiche 13 | 14 | .PHONY: clean 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | fiche [![Build Status](https://travis-ci.org/solusipse/fiche.svg?branch=master)](https://travis-ci.org/solusipse/fiche) 2 | ===== 3 | 4 | Command line pastebin for sharing terminal output. 5 | 6 | # Client-side usage 7 | 8 | Self-explanatory live examples (using public server): 9 | 10 | ``` 11 | echo just testing! | nc termbin.com 9999 12 | ``` 13 | 14 | ``` 15 | cat file.txt | nc termbin.com 9999 16 | ``` 17 | 18 | In case you installed and started fiche on localhost: 19 | 20 | ``` 21 | ls -la | nc localhost 9999 22 | ``` 23 | 24 | You will get an url to your paste as a response, e.g.: 25 | 26 | ``` 27 | http://termbin.com/ydxh 28 | ``` 29 | 30 | You can use our beautification service to get any paste colored and numbered. Just ask for it using `l.termbin.com` subdomain, e.g.: 31 | 32 | ``` 33 | http://l.termbin.com/ydxh 34 | ``` 35 | 36 | ------------------------------------------------------------------------------- 37 | 38 | ## Useful aliases 39 | 40 | You can make your life easier by adding a termbin alias to your rc file. We list some of them here: 41 | 42 | ------------------------------------------------------------------------------- 43 | 44 | ### Pure-bash alternative to netcat 45 | 46 | __Linux/macOS:__ 47 | ``` 48 | alias tb="(exec 3<>/dev/tcp/termbin.com/9999; cat >&3; cat <&3; exec 3<&-)" 49 | ``` 50 | 51 | ``` 52 | echo less typing now! | tb 53 | ``` 54 | 55 | _See [#42](https://github.com/solusipse/fiche/issues/42), [#43](https://github.com/solusipse/fiche/issues/43) for more info._ 56 | 57 | ------------------------------------------------------------------------------- 58 | 59 | ### `tb` alias 60 | 61 | __Linux (Bash):__ 62 | ``` 63 | echo 'alias tb="nc termbin.com 9999"' >> .bashrc 64 | ``` 65 | 66 | ``` 67 | echo less typing now! | tb 68 | ``` 69 | 70 | __macOS:__ 71 | 72 | ``` 73 | echo 'alias tb="nc termbin.com 9999"' >> .bash_profile 74 | ``` 75 | 76 | ``` 77 | echo less typing now! | tb 78 | ``` 79 | 80 | ------------------------------------------------------------------------------- 81 | 82 | ### Copy output to clipboard 83 | 84 | __Linux (Bash):__ 85 | ``` 86 | echo 'alias tbc="netcat termbin.com 9999 | xclip -selection c"' >> .bashrc 87 | ``` 88 | 89 | ``` 90 | echo less typing now! | tbc 91 | ``` 92 | 93 | __macOS:__ 94 | 95 | ``` 96 | echo 'alias tbc="nc termbin.com 9999 | pbcopy"' >> .bash_profile 97 | ``` 98 | 99 | ``` 100 | echo less typing now! | tbc 101 | ``` 102 | 103 | __Remember__ to reload the shell with `source ~/.bashrc` or `source ~/.bash_profile` after adding any of provided above! 104 | 105 | ------------------------------------------------------------------------------- 106 | 107 | ## Requirements 108 | To use fiche you have to have netcat installed. You probably already have it - try typing `nc` or `netcat` into your terminal! 109 | 110 | ------------------------------------------------------------------------------- 111 | 112 | # Server-side usage 113 | 114 | ## Installation 115 | 116 | 1. Clone: 117 | 118 | ``` 119 | git clone https://github.com/solusipse/fiche.git 120 | ``` 121 | 122 | 2. Build: 123 | 124 | ``` 125 | make 126 | ``` 127 | 128 | 3. Install: 129 | 130 | ``` 131 | sudo make install 132 | ``` 133 | 134 | ### Using Ports on FreeBSD 135 | 136 | To install the port: `cd /usr/ports/net/fiche/ && make install clean`. To add the package: `pkg install fiche`. 137 | 138 | _See [#86](https://github.com/solusipse/fiche/issues/86) for more info._ 139 | 140 | ------------------------------------------------------------------------------- 141 | 142 | ## Usage 143 | 144 | ``` 145 | usage: fiche [-D6epbsdSolBuw]. 146 | [-d domain] [-L listen_addr ] [-p port] [-s slug size] 147 | [-o output directory] [-B buffer size] [-u user name] 148 | [-l log file] [-b banlist] [-w whitelist] [-S] 149 | ``` 150 | 151 | These are command line arguments. You don't have to provide any of them to run the application. Default settings will be used in such case. See section below for more info. 152 | 153 | ### Settings 154 | 155 | ------------------------------------------------------------------------------- 156 | 157 | #### Output directory `-o` 158 | 159 | Relative or absolute path to the directory where you want to store user-posted pastes. 160 | 161 | ``` 162 | fiche -o ./code 163 | ``` 164 | 165 | ``` 166 | fiche -o /home/www/code/ 167 | ``` 168 | 169 | __Default value:__ `./code` 170 | 171 | ------------------------------------------------------------------------------- 172 | 173 | #### Domain `-d` 174 | 175 | This will be used as a prefix for an output received by the client. 176 | Value will be prepended with `http`. 177 | 178 | ``` 179 | fiche -d domain.com 180 | ``` 181 | 182 | ``` 183 | fiche -d subdomain.domain.com 184 | ``` 185 | 186 | ``` 187 | fiche -d subdomain.domain.com/some_directory 188 | ``` 189 | 190 | __Default value:__ `localhost` 191 | 192 | ------------------------------------------------------------------------------- 193 | 194 | #### Slug size `-s` 195 | 196 | This will force slugs to be of required length: 197 | 198 | ``` 199 | fiche -s 6 200 | ``` 201 | 202 | __Output url with default value__: `http://localhost/xxxx`, 203 | where x is a randomized character 204 | 205 | __Output url with example value 6__: `http://localhost/xxxxxx`, 206 | where x is a randomized character 207 | 208 | __Default value:__ 4 209 | 210 | ------------------------------------------------------------------------------- 211 | 212 | #### HTTPS `-S` 213 | 214 | If set, fiche returns url with https prefix instead of http 215 | 216 | ``` 217 | fiche -S 218 | ``` 219 | 220 | __Output url with this parameter__: `https://localhost/xxxx`, 221 | where x is a randomized character 222 | 223 | ------------------------------------------------------------------------------- 224 | 225 | #### User name `-u` 226 | 227 | Fiche will try to switch to the requested user on startup if any is provided. 228 | 229 | ``` 230 | fiche -u _fiche 231 | ``` 232 | 233 | __Default value:__ not set 234 | 235 | __WARNING:__ This requires that fiche is started as a root. 236 | 237 | ------------------------------------------------------------------------------- 238 | 239 | #### Buffer size `-B` 240 | 241 | This parameter defines size of the buffer used for getting data from the user. 242 | Maximum size (in bytes) of all input files is defined by this value. 243 | 244 | ``` 245 | fiche -B 2048 246 | ``` 247 | 248 | __Default value:__ 32768 249 | 250 | ------------------------------------------------------------------------------- 251 | 252 | #### Log file `-l` 253 | 254 | ``` 255 | fiche -l /home/www/fiche-log.txt 256 | ``` 257 | 258 | __Default value:__ not set 259 | 260 | __WARNING:__ this file has to be user-writable 261 | 262 | ------------------------------------------------------------------------------- 263 | 264 | #### Ban list `-b` 265 | 266 | Relative or absolute path to a file containing IP addresses of banned users. 267 | 268 | ``` 269 | fiche -b fiche-bans.txt 270 | ``` 271 | 272 | __Format of the file:__ this file should contain only addresses, one per line. 273 | 274 | __Default value:__ not set 275 | 276 | __WARNING:__ not implemented yet 277 | 278 | ------------------------------------------------------------------------------- 279 | 280 | #### White list `-w` 281 | 282 | If whitelist mode is enabled, only addresses from the list will be able 283 | to upload files. 284 | 285 | ``` 286 | fiche -w fiche-whitelist.txt 287 | ``` 288 | 289 | __Format of the file:__ this file should contain only addresses, one per line. 290 | 291 | __Default value:__ not set 292 | 293 | __WARNING:__ not implemented yet 294 | 295 | ------------------------------------------------------------------------------- 296 | 297 | ### Running as a service 298 | 299 | There's a simple systemd example: 300 | ``` 301 | [Unit] 302 | Description=FICHE-SERVER 303 | 304 | [Service] 305 | ExecStart=/usr/local/bin/fiche -d yourdomain.com -o /path/to/output -l /path/to/log -u youruser 306 | 307 | [Install] 308 | WantedBy=multi-user.target 309 | ``` 310 | 311 | __WARNING:__ In service mode you have to set output directory with `-o` parameter. 312 | 313 | ------------------------------------------------------------------------------- 314 | 315 | ### Example nginx config 316 | 317 | Fiche has no http server built-in, thus you need to setup one if you want to make files available through http. 318 | 319 | There's a sample configuration for nginx: 320 | 321 | ``` 322 | server { 323 | listen 80; 324 | server_name mysite.com www.mysite.com; 325 | charset utf-8; 326 | 327 | location / { 328 | root /home/www/code/; 329 | index index.txt index.html; 330 | } 331 | } 332 | ``` 333 | 334 | ## License 335 | 336 | Fiche is MIT licensed. 337 | -------------------------------------------------------------------------------- /extras/lines/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pi-hole/tricorder/5a158ea89390884d606d9c5fbee24937b0411254/extras/lines/__init__.py -------------------------------------------------------------------------------- /extras/lines/lines.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, abort, redirect 2 | app = Flask(__name__) 3 | 4 | import argparse, os, pygments 5 | from pygments import highlight 6 | from pygments.lexers import guess_lexer 7 | from pygments.formatters import HtmlFormatter 8 | 9 | parser = argparse.ArgumentParser() 10 | parser.add_argument("root_dir", help="Path to directory with pastes") 11 | args = parser.parse_args() 12 | 13 | 14 | @app.route('/') 15 | def main(): 16 | return redirect("http://termbin.com", code=302) 17 | 18 | 19 | @app.route('/') 20 | def beautify(slug): 21 | # Return 404 in case of urls longer than 64 chars 22 | if len(slug) > 64: 23 | abort(404) 24 | 25 | # Create path for the target dir 26 | target_dir = os.path.join(args.root_dir, slug) 27 | 28 | # Block directory traversal attempts 29 | if not target_dir.startswith(args.root_dir): 30 | abort(404) 31 | 32 | # Check if directory with requested slug exists 33 | if os.path.isdir(target_dir): 34 | target_file = os.path.join(target_dir, "index.txt") 35 | 36 | # File index.txt found inside that dir 37 | with open(target_file) as f: 38 | code = f.read() 39 | # Identify language 40 | lexer = guess_lexer(code) 41 | # Create formatter with line numbers 42 | formatter = HtmlFormatter(linenos=True, full=True) 43 | # Return parsed code 44 | return highlight(code, lexer, formatter) 45 | 46 | # Not found 47 | abort(404) 48 | 49 | 50 | if __name__ == '__main__': 51 | app.run() 52 | -------------------------------------------------------------------------------- /fiche.c: -------------------------------------------------------------------------------- 1 | /* 2 | Fiche - Command line pastebin for sharing terminal output. 3 | 4 | ------------------------------------------------------------------------------- 5 | 6 | License: MIT (http://www.opensource.org/licenses/mit-license.php) 7 | Repository: https://github.com/solusipse/fiche/ 8 | Live example: http://termbin.com 9 | 10 | ------------------------------------------------------------------------------- 11 | 12 | usage: fiche [-DepbsdolBuw]. 13 | [-D] [-e] [-d domain] [-p port] [-s slug size] 14 | [-o output directory] [-B buffer size] [-u user name] 15 | [-l log file] [-b banlist] [-w whitelist] 16 | -D option is for daemonizing fiche 17 | -e option is for using an extended character set for the URL 18 | 19 | Compile with Makefile or manually with -O2 and -pthread flags. 20 | 21 | To install use `make install` command. 22 | 23 | Use netcat to push text - example: 24 | $ cat fiche.c | nc localhost 9999 25 | 26 | ------------------------------------------------------------------------------- 27 | */ 28 | 29 | #include "fiche.h" 30 | 31 | #include 32 | #include 33 | #include 34 | #include 35 | 36 | #include 37 | #include 38 | #include 39 | #include 40 | 41 | #include 42 | #include 43 | #include 44 | #include 45 | #include 46 | #include 47 | #include 48 | #include 49 | #include 50 | 51 | 52 | /****************************************************************************** 53 | * Various declarations 54 | */ 55 | const char *Fiche_Symbols = "abcdefghijklmnopqrstuvwxyz0123456789"; 56 | 57 | 58 | /****************************************************************************** 59 | * Inner structs 60 | */ 61 | 62 | struct fiche_connection { 63 | int socket; 64 | struct sockaddr_in address; 65 | 66 | Fiche_Settings *settings; 67 | }; 68 | 69 | 70 | /****************************************************************************** 71 | * Static function declarations 72 | */ 73 | 74 | // Settings-related 75 | 76 | /** 77 | * @brief Sets domain name 78 | * @warning settings.domain has to be freed after using this function! 79 | */ 80 | static int set_domain_name(Fiche_Settings *settings); 81 | 82 | /** 83 | * @brief Changes user running this program to requested one 84 | * @warning Application has to be run as root to use this function 85 | */ 86 | static int perform_user_change(const Fiche_Settings *settings); 87 | 88 | 89 | // Server-related 90 | 91 | /** 92 | * @brief Starts server with settings provided in Fiche_Settings struct 93 | */ 94 | static int start_server(Fiche_Settings *settings); 95 | 96 | /** 97 | * @brief Dispatches incoming connections by spawning threads 98 | */ 99 | static void dispatch_connection(int socket, Fiche_Settings *settings); 100 | 101 | /** 102 | * @brief Handles connections 103 | * @remarks Is being run by dispatch_connection in separate threads 104 | * @arg args Struct fiche_connection containing connection details 105 | */ 106 | static void *handle_connection(void *args); 107 | 108 | // Server-related utils 109 | 110 | 111 | /** 112 | * @brief Generates a slug that will be used for paste creation 113 | * @warning output has to be freed after using! 114 | * 115 | * @arg output pointer to output string containing full path to directory 116 | * @arg length default or user-requested length of a slug 117 | * @arg extra_length additional length that was added to speed-up the 118 | * generation process 119 | * 120 | * This function is used in connection with create_directory function 121 | * It generates strings that are used to create a directory for 122 | * user-provided data. If directory already exists, we ask this function 123 | * to generate another slug with increased size. 124 | */ 125 | static void generate_slug(char **output, uint8_t length, uint8_t extra_length); 126 | 127 | 128 | /** 129 | * @brief Creates a directory at requested path using requested slug 130 | * @returns 0 if succeded, 1 if failed or dir already existed 131 | * 132 | * @arg output_dir root directory for all pastes 133 | * @arg slug directory name for a particular paste 134 | */ 135 | static int create_directory(char *output_dir, char *slug); 136 | 137 | 138 | /** 139 | * @brief Saves data to file at requested path 140 | * 141 | * @arg data Buffer with data received from the user 142 | * @arg path Path at which file containing data from the buffer will be created 143 | */ 144 | static int save_to_file(const Fiche_Settings *s, uint8_t *data, char *slug); 145 | 146 | 147 | // Logging-related 148 | 149 | /** 150 | * @brief Displays error messages 151 | */ 152 | static void print_error(const char *format, ...); 153 | 154 | 155 | /** 156 | * @brief Displays status messages 157 | */ 158 | static void print_status(const char *format, ...); 159 | 160 | 161 | /** 162 | * @brief Displays horizontal line 163 | */ 164 | static void print_separator(); 165 | 166 | 167 | /** 168 | * @brief Saves connection entry to the logfile 169 | */ 170 | static void log_entry(const Fiche_Settings *s, const char *ip, 171 | const char *hostname, const char *slug); 172 | 173 | 174 | /** 175 | * @brief Returns string containing current date 176 | * @warning Output has to be freed! 177 | */ 178 | static void get_date(char *buf); 179 | 180 | 181 | /** 182 | * @brief Time seed 183 | */ 184 | unsigned int seed; 185 | 186 | /****************************************************************************** 187 | * Public fiche functions 188 | */ 189 | 190 | void fiche_init(Fiche_Settings *settings) { 191 | 192 | // Initialize everything to default values 193 | // or to NULL in case of pointers 194 | 195 | struct Fiche_Settings def = { 196 | // domain 197 | "example.com", 198 | // output dir 199 | "code", 200 | // listen_addr 201 | "0.0.0.0", 202 | // port 203 | 9999, 204 | // slug length 205 | 4, 206 | // https 207 | false, 208 | // buffer length 209 | 32768, 210 | // user name 211 | NULL, 212 | // path to log file 213 | NULL, 214 | // path to banlist 215 | NULL, 216 | // path to whitelist 217 | NULL 218 | }; 219 | 220 | // Copy default settings to provided instance 221 | *settings = def; 222 | } 223 | 224 | int fiche_run(Fiche_Settings settings) { 225 | 226 | seed = time(NULL); 227 | 228 | // Display welcome message 229 | { 230 | char date[64]; 231 | get_date(date); 232 | print_status("Starting fiche on %s...", date); 233 | } 234 | 235 | // Try to set requested user 236 | if ( perform_user_change(&settings) != 0) { 237 | print_error("Was not able to change the user!"); 238 | return -1; 239 | } 240 | 241 | // Check if output directory is writable 242 | // - First we try to create it 243 | { 244 | mkdir( 245 | settings.output_dir_path, 246 | S_IRWXU | S_IRGRP | S_IROTH | S_IXOTH | S_IXGRP 247 | ); 248 | // - Then we check if we can write there 249 | if ( access(settings.output_dir_path, W_OK) != 0 ) { 250 | print_error("Output directory not writable!"); 251 | return -1; 252 | } 253 | } 254 | 255 | // Check if log file is writable (if set) 256 | if ( settings.log_file_path ) { 257 | 258 | // Create log file if it doesn't exist 259 | FILE *f = fopen(settings.log_file_path, "a+"); 260 | fclose(f); 261 | 262 | // Then check if it's accessible 263 | if ( access(settings.log_file_path, W_OK) != 0 ) { 264 | print_error("Log file not writable!"); 265 | return -1; 266 | } 267 | 268 | } 269 | 270 | // Try to set domain name 271 | if ( set_domain_name(&settings) != 0 ) { 272 | print_error("Was not able to set domain name!"); 273 | return -1; 274 | } 275 | 276 | // Main loop in this method 277 | start_server(&settings); 278 | 279 | // Perform final cleanup 280 | 281 | // This is allways allocated on the heap 282 | free(settings.domain); 283 | 284 | return 0; 285 | 286 | } 287 | 288 | 289 | /****************************************************************************** 290 | * Static functions below 291 | */ 292 | 293 | static void print_error(const char *format, ...) { 294 | va_list args; 295 | va_start(args, format); 296 | 297 | printf("[Fiche][ERROR] "); 298 | vprintf(format, args); 299 | printf("\n"); 300 | 301 | va_end(args); 302 | } 303 | 304 | 305 | static void print_status(const char *format, ...) { 306 | va_list args; 307 | va_start(args, format); 308 | 309 | printf("[Fiche][STATUS] "); 310 | vprintf(format, args); 311 | printf("\n"); 312 | 313 | va_end(args); 314 | } 315 | 316 | 317 | static void print_separator() { 318 | printf("============================================================\n"); 319 | } 320 | 321 | 322 | static void log_entry(const Fiche_Settings *s, const char *ip, 323 | const char *hostname, const char *slug) 324 | { 325 | // Logging to file not enabled, finish here 326 | if (!s->log_file_path) { 327 | return; 328 | } 329 | 330 | FILE *f = fopen(s->log_file_path, "a"); 331 | if (!f) { 332 | print_status("Was not able to save entry to the log!"); 333 | return; 334 | } 335 | 336 | char date[64]; 337 | get_date(date); 338 | 339 | // Write entry to file 340 | fprintf(f, "%s -- %s -- %s (%s)\n", slug, date, ip, hostname); 341 | fclose(f); 342 | } 343 | 344 | 345 | static void get_date(char *buf) { 346 | struct tm curtime; 347 | time_t ltime; 348 | 349 | ltime=time(<ime); 350 | localtime_r(<ime, &curtime); 351 | 352 | // Save data to provided buffer 353 | if (asctime_r(&curtime, buf) == 0) { 354 | // Couldn't get date, setting first byte of the 355 | // buffer to zero so it won't be displayed 356 | buf[0] = 0; 357 | return; 358 | } 359 | 360 | // Remove newline char 361 | buf[strlen(buf)-1] = 0; 362 | } 363 | 364 | 365 | static int set_domain_name(Fiche_Settings *settings) { 366 | 367 | char *prefix = ""; 368 | if (settings->https) { 369 | prefix = "https://"; 370 | } else { 371 | prefix = "http://"; 372 | } 373 | const int len = strlen(settings->domain) + strlen(prefix) + 1; 374 | 375 | char *b = malloc(len); 376 | if (!b) { 377 | return -1; 378 | } 379 | 380 | strcpy(b, prefix); 381 | strcat(b, settings->domain); 382 | 383 | settings->domain = b; 384 | 385 | print_status("Domain set to: %s.", settings->domain); 386 | 387 | return 0; 388 | } 389 | 390 | 391 | static int perform_user_change(const Fiche_Settings *settings) { 392 | 393 | // User change wasn't requested, finish here 394 | if (settings->user_name == NULL) { 395 | return 0; 396 | } 397 | 398 | // Check if root, if not - finish here 399 | if (getuid() != 0) { 400 | print_error("Run as root if you want to change the user!"); 401 | return -1; 402 | } 403 | 404 | // Get user details 405 | const struct passwd *userdata = getpwnam(settings->user_name); 406 | 407 | const int uid = userdata->pw_uid; 408 | const int gid = userdata->pw_gid; 409 | 410 | if (uid == -1 || gid == -1) { 411 | print_error("Could find requested user: %s!", settings->user_name); 412 | return -1; 413 | } 414 | 415 | if (setgid(gid) != 0) { 416 | print_error("Couldn't switch to requested user: %s!", settings->user_name); 417 | } 418 | 419 | if (setuid(uid) != 0) { 420 | print_error("Couldn't switch to requested user: %s!", settings->user_name); 421 | } 422 | 423 | print_status("User changed to: %s.", settings->user_name); 424 | 425 | return 0; 426 | } 427 | 428 | 429 | static int start_server(Fiche_Settings *settings) { 430 | 431 | // Perform socket creation 432 | int s = socket(AF_INET, SOCK_STREAM, 0); 433 | if (s < 0) { 434 | print_error("Couldn't create a socket!"); 435 | return -1; 436 | } 437 | 438 | // Set socket settings 439 | if ( setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &(int){ 1 } , sizeof(int)) != 0 ) { 440 | print_error("Couldn't prepare the socket!"); 441 | return -1; 442 | } 443 | 444 | // Prepare address and port handler 445 | struct sockaddr_in address; 446 | address.sin_family = AF_INET; 447 | address.sin_addr.s_addr = inet_addr(settings->listen_addr); 448 | address.sin_port = htons(settings->port); 449 | 450 | // Bind to port 451 | if ( bind(s, (struct sockaddr *) &address, sizeof(address)) != 0) { 452 | print_error("Couldn't bind to the port: %d!", settings->port); 453 | return -1; 454 | } 455 | 456 | // Start listening 457 | if ( listen(s, 128) != 0 ) { 458 | print_error("Couldn't start listening on the socket!"); 459 | return -1; 460 | } 461 | 462 | print_status("Server started listening on: %s:%d.", 463 | settings->listen_addr, settings->port); 464 | print_separator(); 465 | 466 | // Run dispatching loop 467 | while (1) { 468 | dispatch_connection(s, settings); 469 | } 470 | 471 | // Give some time for all threads to finish 472 | // NOTE: this code is reached only in testing environment 473 | // There is currently no way to kill the main thread from any thread 474 | // Something like this can be done for testing purpouses: 475 | // int i = 0; 476 | // while (i < 3) { 477 | // dispatch_connection(s, settings); 478 | // i++; 479 | // } 480 | 481 | sleep(5); 482 | 483 | return 0; 484 | } 485 | 486 | 487 | static void dispatch_connection(int socket, Fiche_Settings *settings) { 488 | 489 | // Create address structs for this socket 490 | struct sockaddr_in address; 491 | socklen_t addlen = sizeof(address); 492 | 493 | // Accept a connection and get a new socket id 494 | const int s = accept(socket, (struct sockaddr *) &address, &addlen); 495 | if (s < 0 ) { 496 | print_error("Error on accepting connection!"); 497 | return; 498 | } 499 | 500 | // Set timeout for accepted socket 501 | const struct timeval timeout = { 5, 0 }; 502 | 503 | if ( setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) != 0 ) { 504 | print_error("Couldn't set a timeout!"); 505 | } 506 | 507 | if ( setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)) != 0 ) { 508 | print_error("Couldn't set a timeout!"); 509 | } 510 | 511 | // Create an argument for the thread function 512 | struct fiche_connection *c = malloc(sizeof(*c)); 513 | if (!c) { 514 | print_error("Couldn't allocate memory for connection!"); 515 | return; 516 | } 517 | c->socket = s; 518 | c->address = address; 519 | c->settings = settings; 520 | 521 | // Spawn a new thread to handle this connection 522 | pthread_t id; 523 | 524 | if ( pthread_create(&id, NULL, &handle_connection, c) != 0 ) { 525 | print_error("Couldn't spawn a thread!"); 526 | return; 527 | } 528 | 529 | // Detach thread if created succesfully 530 | // TODO: consider using pthread_tryjoin_np 531 | pthread_detach(id); 532 | 533 | } 534 | 535 | 536 | static void *handle_connection(void *args) { 537 | 538 | // Cast args to it's previous type 539 | struct fiche_connection *c = (struct fiche_connection *) args; 540 | 541 | // Get client's IP 542 | const char *ip = inet_ntoa(c->address.sin_addr); 543 | 544 | // Get client's hostname 545 | char hostname[1024]; 546 | 547 | if (getnameinfo((struct sockaddr *)&c->address, sizeof(c->address), 548 | hostname, sizeof(hostname), NULL, 0, 0) != 0 ) { 549 | 550 | // Couldn't resolve a hostname 551 | strcpy(hostname, "n/a"); 552 | } 553 | 554 | // Print status on this connection 555 | { 556 | char date[64]; 557 | get_date(date); 558 | print_status("%s", date); 559 | 560 | print_status("Incoming connection from: %s (%s).", ip, hostname); 561 | } 562 | 563 | // Create a buffer 564 | uint8_t *buffer = malloc(c->settings->buffer_len); 565 | if (!buffer) { 566 | print_error("Couldn't allocate memory for buffer!"); 567 | close(c->socket); 568 | free(c); 569 | pthread_exit(NULL); 570 | 571 | return 0; 572 | } 573 | memset(buffer, 0, c->settings->buffer_len); 574 | 575 | const int r = recv(c->socket, buffer, c->settings->buffer_len, MSG_WAITALL); 576 | if (r <= 0) { 577 | print_error("No data received from the client!"); 578 | print_separator(); 579 | 580 | // Close the socket 581 | close(c->socket); 582 | 583 | // Cleanup 584 | free(c); 585 | free(buffer); 586 | pthread_exit(NULL); 587 | 588 | return 0; 589 | } 590 | 591 | // - Check if request was performed with a known protocol 592 | // TODO 593 | 594 | // - Check if on whitelist 595 | // TODO 596 | 597 | // - Check if on banlist 598 | // TODO 599 | 600 | // Generate slug and use it to create an url 601 | char *slug; 602 | uint8_t extra = 0; 603 | 604 | do { 605 | 606 | // Generate slugs until it's possible to create a directory 607 | // with generated slug on disk 608 | generate_slug(&slug, c->settings->slug_len, extra); 609 | 610 | // Something went wrong in slug generation, break here 611 | if (!slug) { 612 | break; 613 | } 614 | 615 | // Increment counter for additional letters needed 616 | ++extra; 617 | 618 | // If i was incremented more than 128 times, something 619 | // for sure went wrong. We are closing connection and 620 | // killing this thread in such case 621 | if (extra > 128) { 622 | print_error("Couldn't generate a valid slug!"); 623 | print_separator(); 624 | 625 | // Cleanup 626 | close(c->socket); 627 | free(c); 628 | free(buffer); 629 | free(slug); 630 | pthread_exit(NULL); 631 | return NULL; 632 | } 633 | 634 | } 635 | while(create_directory(c->settings->output_dir_path, slug) != 0); 636 | 637 | 638 | // Slug generation failed, we have to finish here 639 | if (!slug) { 640 | print_error("Couldn't generate a slug!"); 641 | print_separator(); 642 | 643 | close(c->socket); 644 | 645 | // Cleanup 646 | free(c); 647 | free(buffer); 648 | pthread_exit(NULL); 649 | return NULL; 650 | } 651 | 652 | 653 | // Save to file failed, we have to finish here 654 | if ( save_to_file(c->settings, buffer, slug) != 0 ) { 655 | print_error("Couldn't save a file!"); 656 | print_separator(); 657 | 658 | close(c->socket); 659 | 660 | // Cleanup 661 | free(c); 662 | free(buffer); 663 | free(slug); 664 | pthread_exit(NULL); 665 | return NULL; 666 | } 667 | 668 | // Write a response to the user 669 | { 670 | // Create an url (additional byte for slash and one for null byte) 671 | const size_t len = strlen(c->settings->domain) + strlen(slug) + 2; 672 | 673 | char url[len]; 674 | snprintf(url, len, "%s%s%s", c->settings->domain, "/", slug); 675 | 676 | // Send the response 677 | write(c->socket, url, len); 678 | } 679 | 680 | print_status("Received %d bytes, saved to: %s.", r, slug); 681 | print_separator(); 682 | 683 | // Log connection 684 | // TODO: log unsuccessful and rejected connections 685 | log_entry(c->settings, ip, hostname, slug); 686 | 687 | // Close the connection 688 | close(c->socket); 689 | 690 | // Perform cleanup of values used in this thread 691 | free(slug); 692 | free(c); 693 | free(buffer); 694 | 695 | pthread_exit(NULL); 696 | 697 | return NULL; 698 | } 699 | 700 | 701 | static void generate_slug(char **output, uint8_t length, uint8_t extra_length) { 702 | 703 | // Realloc buffer for slug when we want it to be bigger 704 | // This happens in case when directory with this name already 705 | // exists. To save time, we don't generate new slugs until 706 | // we spot an available one. We add another letter instead. 707 | 708 | if (extra_length > 0) { 709 | free(*output); 710 | } 711 | 712 | // Create a buffer for slug with extra_length if any 713 | *output = calloc(length + 1 + extra_length, sizeof(char)); 714 | 715 | if (*output == NULL) { 716 | return; 717 | } 718 | 719 | // Take n-th symbol from symbol table and use it for slug generation 720 | for (int i = 0; i < length + extra_length; i++) { 721 | int n = rand_r(&seed) % strlen(Fiche_Symbols); 722 | *(output[0] + sizeof(char) * i) = Fiche_Symbols[n]; 723 | } 724 | 725 | } 726 | 727 | 728 | static int create_directory(char *output_dir, char *slug) { 729 | if (!slug) { 730 | return -1; 731 | } 732 | 733 | // Additional byte is for the slash 734 | size_t len = strlen(output_dir) + strlen(slug) + 2; 735 | 736 | // Generate a path 737 | char *path = malloc(len); 738 | if (!path) { 739 | return -1; 740 | } 741 | snprintf(path, len, "%s%s%s", output_dir, "/", slug); 742 | 743 | // Create output directory, just in case 744 | mkdir(output_dir, S_IRWXU | S_IRGRP | S_IROTH | S_IXOTH | S_IXGRP); 745 | 746 | // Create slug directory 747 | const int r = mkdir( 748 | path, 749 | S_IRWXU | S_IRGRP | S_IROTH | S_IXOTH | S_IXGRP 750 | ); 751 | 752 | free(path); 753 | 754 | return r; 755 | } 756 | 757 | 758 | static int save_to_file(const Fiche_Settings *s, uint8_t *data, char *slug) { 759 | char *file_name = "index.txt"; 760 | 761 | // Additional 2 bytes are for 2 slashes 762 | size_t len = strlen(s->output_dir_path) + strlen(slug) + strlen(file_name) + 3; 763 | 764 | // Generate a path 765 | char *path = malloc(len); 766 | if (!path) { 767 | return -1; 768 | } 769 | 770 | snprintf(path, len, "%s%s%s%s%s", s->output_dir_path, "/", slug, "/", file_name); 771 | 772 | // Attempt file saving 773 | FILE *f = fopen(path, "w"); 774 | if (!f) { 775 | free(path); 776 | return -1; 777 | } 778 | 779 | // Null-terminate buffer if not null terminated already 780 | data[s->buffer_len - 1] = 0; 781 | 782 | if ( fprintf(f, "%s", data) < 0 ) { 783 | fclose(f); 784 | free(path); 785 | return -1; 786 | } 787 | 788 | // Convert to HTML to utilize color codes 789 | 790 | // Flush the file so the HTML conversion can use the data 791 | fflush(f); 792 | 793 | // Get the size of the resulting string 794 | char command_format[] = "ansi2html --font-size larger < \"%s\" > \"%s/%s/index.html\""; 795 | size_t command_len = snprintf(NULL, 0, command_format, path, s->output_dir_path, slug); 796 | 797 | // Create the string 798 | char *ansi2html_command = malloc(command_len + 1); 799 | snprintf(ansi2html_command, command_len + 1, command_format, path, s->output_dir_path, slug); 800 | 801 | // Run the command 802 | system(ansi2html_command); 803 | 804 | free(ansi2html_command); 805 | fclose(f); 806 | free(path); 807 | 808 | return 0; 809 | } 810 | -------------------------------------------------------------------------------- /fiche.h: -------------------------------------------------------------------------------- 1 | /* 2 | Fiche - Command line pastebin for sharing terminal output. 3 | 4 | ------------------------------------------------------------------------------- 5 | 6 | License: MIT (http://www.opensource.org/licenses/mit-license.php) 7 | Repository: https://github.com/solusipse/fiche/ 8 | Live example: http://termbin.com 9 | 10 | ------------------------------------------------------------------------------- 11 | 12 | usage: fiche [-DepbsdolBuw]. 13 | [-D] [-e] [-d domain] [-p port] [-s slug size] 14 | [-o output directory] [-B buffer size] [-u user name] 15 | [-l log file] [-b banlist] [-w whitelist] 16 | 17 | Use netcat to push text - example: 18 | $ cat fiche.c | nc localhost 9999 19 | 20 | ------------------------------------------------------------------------------- 21 | */ 22 | 23 | #ifndef FICHE_H 24 | #define FICHE_H 25 | 26 | #include 27 | #include 28 | 29 | 30 | /** 31 | * @brief Used as a container for fiche settings. Create before 32 | * the initialization 33 | * 34 | */ 35 | typedef struct Fiche_Settings { 36 | /** 37 | * @brief Domain used in output links 38 | */ 39 | char *domain; 40 | 41 | /** 42 | * @brief Path to directory used for storing uploaded pastes 43 | */ 44 | char *output_dir_path; 45 | 46 | /** 47 | * @brief Address on which fiche is waiting for connections 48 | */ 49 | char *listen_addr; 50 | 51 | /** 52 | * @brief Port on which fiche is waiting for connections 53 | */ 54 | uint16_t port; 55 | 56 | /** 57 | * @brief Length of a paste's name 58 | */ 59 | uint8_t slug_len; 60 | 61 | /** 62 | * @brief If set, returns url with https prefix instead of http 63 | */ 64 | bool https; 65 | 66 | /** 67 | * @brief Connection buffer length 68 | * 69 | * @remarks Length of this buffer limits max size of uploaded files 70 | */ 71 | uint32_t buffer_len; 72 | 73 | /** 74 | * @brief Name of the user that runs fiche process 75 | */ 76 | char *user_name; 77 | 78 | /** 79 | * @brief Path to the log file 80 | */ 81 | char *log_file_path; 82 | 83 | /** 84 | * @brief Path to the file with banned IPs 85 | */ 86 | char *banlist_path; 87 | 88 | /** 89 | * @brief Path to the file with whitelisted IPs 90 | */ 91 | char *whitelist_path; 92 | 93 | 94 | 95 | } Fiche_Settings; 96 | 97 | 98 | /** 99 | * @brief Initializes Fiche_Settings instance 100 | */ 101 | void fiche_init(Fiche_Settings *settings); 102 | 103 | 104 | /** 105 | * @brief Runs fiche server 106 | * 107 | * @return 0 if it was able to start, any other value otherwise 108 | */ 109 | int fiche_run(Fiche_Settings settings); 110 | 111 | 112 | /** 113 | * @brief array of symbols used in slug generation 114 | * @remarks defined in fiche.c 115 | */ 116 | extern const char *Fiche_Symbols; 117 | 118 | 119 | #endif 120 | -------------------------------------------------------------------------------- /main.c: -------------------------------------------------------------------------------- 1 | /* 2 | Fiche - Command line pastebin for sharing terminal output. 3 | 4 | ------------------------------------------------------------------------------- 5 | 6 | License: MIT (http://www.opensource.org/licenses/mit-license.php) 7 | Repository: https://github.com/solusipse/fiche/ 8 | Live example: http://termbin.com 9 | 10 | ------------------------------------------------------------------------------- 11 | 12 | usage: fiche [-DepbsdolBuw]. 13 | [-D] [-e] [-d domain] [-p port] [-s slug size] 14 | [-o output directory] [-B buffer size] [-u user name] 15 | [-l log file] [-b banlist] [-w whitelist] 16 | 17 | Use netcat to push text - example: 18 | $ cat fiche.c | nc localhost 9999 19 | 20 | ------------------------------------------------------------------------------- 21 | */ 22 | 23 | #include "fiche.h" 24 | 25 | #include 26 | #include 27 | #include 28 | #include 29 | 30 | 31 | int main(int argc, char **argv) { 32 | 33 | // Fiche settings instance 34 | Fiche_Settings fs; 35 | 36 | // Initialize settings instance to default values 37 | fiche_init(&fs); 38 | 39 | // Note: fiche_run is responsible for checking if these values 40 | // were set correctly 41 | 42 | // Note: according to getopt documentation, we don't need to 43 | // copy strings, so we decided to go with pointer approach for these 44 | 45 | // Parse input arguments 46 | int c; 47 | while ((c = getopt(argc, argv, "D6eSL:p:b:s:d:o:l:B:u:w:")) != -1) { 48 | switch (c) { 49 | 50 | // domain 51 | case 'd': 52 | { 53 | fs.domain = optarg; 54 | } 55 | break; 56 | 57 | // port 58 | case 'p': 59 | { 60 | fs.port = atoi(optarg); 61 | } 62 | break; 63 | 64 | // listen_addr 65 | case 'L': 66 | { 67 | fs.listen_addr = optarg; 68 | } 69 | break; 70 | 71 | // slug size 72 | case 's': 73 | { 74 | fs.slug_len = atoi(optarg); 75 | } 76 | break; 77 | 78 | // https 79 | case 'S': 80 | { 81 | fs.https = true; 82 | } 83 | break; 84 | 85 | // output directory path 86 | case 'o': 87 | { 88 | fs.output_dir_path = optarg; 89 | } 90 | break; 91 | 92 | // buffer size 93 | case 'B': 94 | { 95 | fs.buffer_len = atoi(optarg); 96 | } 97 | break; 98 | 99 | // user name 100 | case 'u': 101 | { 102 | fs.user_name = optarg; 103 | } 104 | break; 105 | 106 | // log file path 107 | case 'l': 108 | { 109 | fs.log_file_path = optarg; 110 | } 111 | break; 112 | 113 | // banlist file path 114 | case 'b': 115 | { 116 | fs.banlist_path = optarg; 117 | } 118 | break; 119 | 120 | // whitelist file path 121 | case 'w': 122 | { 123 | fs.whitelist_path = optarg; 124 | } 125 | break; 126 | 127 | // Display help in case of any unsupported argument 128 | default: 129 | { 130 | printf("usage: fiche [-dLpsSoBulbw].\n"); 131 | printf(" [-d domain] [-L listen_addr] [-p port] [-s slug size]\n"); 132 | printf(" [-o output directory] [-B buffer size] [-u user name]\n"); 133 | printf(" [-l log file] [-b banlist] [-w whitelist] [-S]\n"); 134 | return 0; 135 | } 136 | break; 137 | } 138 | } 139 | 140 | 141 | fiche_run(fs); 142 | 143 | 144 | return 0; 145 | } 146 | 147 | 148 | --------------------------------------------------------------------------------