fiche.c - fiche - A pastebin adjusted for gopher use | |
git clone git://vernunftzentrum.de/fiche.git | |
Log | |
Files | |
Refs | |
LICENSE | |
--- | |
fiche.c (19680B) | |
--- | |
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 <stdio.h> | |
32 #include <stdarg.h> | |
33 #include <stdlib.h> | |
34 #include <string.h> | |
35 | |
36 #include <pwd.h> | |
37 #include <time.h> | |
38 #include <unistd.h> | |
39 #include <pthread.h> | |
40 | |
41 #include <fcntl.h> | |
42 #include <netdb.h> | |
43 #include <sys/time.h> | |
44 #include <sys/stat.h> | |
45 #include <sys/types.h> | |
46 #include <arpa/inet.h> | |
47 #include <sys/socket.h> | |
48 #include <netinet/in.h> | |
49 #include <netinet/in.h> | |
50 | |
51 | |
52 /***********************************************************************… | |
53 * Various declarations | |
54 */ | |
55 const char *Fiche_Symbols = "abcdefghijklmnopqrstuvwxyz0123456789"; | |
56 | |
57 /* File handle for the log file */ | |
58 static FILE *logfile_handle = NULL; | |
59 | |
60 | |
61 /***********************************************************************… | |
62 * Inner structs | |
63 */ | |
64 | |
65 struct fiche_connection { | |
66 int socket; | |
67 struct sockaddr_in address; | |
68 | |
69 Fiche_Settings *settings; | |
70 }; | |
71 | |
72 | |
73 /***********************************************************************… | |
74 * Static function declarations | |
75 */ | |
76 | |
77 // Settings-related | |
78 | |
79 /** | |
80 * @brief Sets domain name | |
81 * @warning settings.domain has to be freed after using this function! | |
82 */ | |
83 static int set_domain_name(Fiche_Settings *settings); | |
84 | |
85 /** | |
86 * @brief Changes user running this program to requested one | |
87 * @warning Application has to be run as root to use this function | |
88 */ | |
89 static int perform_user_change(const Fiche_Settings *settings); | |
90 | |
91 | |
92 // Server-related | |
93 | |
94 /** | |
95 * @brief Starts server with settings provided in Fiche_Settings struct | |
96 */ | |
97 static int start_server(Fiche_Settings *settings); | |
98 | |
99 /** | |
100 * @brief Dispatches incoming connections by spawning threads | |
101 */ | |
102 static void dispatch_connection(int socket, Fiche_Settings *settings); | |
103 | |
104 /** | |
105 * @brief Handles connections | |
106 * @remarks Is being run by dispatch_connection in separate threads | |
107 * @arg args Struct fiche_connection containing connection details | |
108 */ | |
109 static void *handle_connection(void *args); | |
110 | |
111 // Server-related utils | |
112 | |
113 | |
114 /** | |
115 * @brief Generates a slug that will be used for paste creation | |
116 * @warning output has to be freed after using! | |
117 * | |
118 * @arg output pointer to output string containing full path to directory | |
119 * @arg length default or user-requested length of a slug | |
120 * @arg extra_length additional length that was added to speed-up the | |
121 * generation process | |
122 * | |
123 * This function is used in connection with create_directory function | |
124 * It generates strings that are used to create a directory for | |
125 * user-provided data. If directory already exists, we ask this function | |
126 * to generate another slug with increased size. | |
127 */ | |
128 static void generate_slug(char **output, uint8_t length, uint8_t extra_l… | |
129 | |
130 | |
131 /** | |
132 * @brief Creates a directory at requested path using requested slug | |
133 * @returns 0 if succeded, 1 if failed or dir already existed | |
134 * | |
135 * @arg output_dir root directory for all pastes | |
136 * @arg slug directory name for a particular paste | |
137 */ | |
138 static int create_directory(char *output_dir, char *slug); | |
139 | |
140 | |
141 /** | |
142 * @brief Saves data to file at requested path | |
143 * | |
144 * @arg data Buffer with data received from the user | |
145 * @arg path Path at which file containing data from the buffer will be … | |
146 */ | |
147 static int save_to_file(const Fiche_Settings *s, uint8_t *data, char *sl… | |
148 | |
149 | |
150 // Logging-related | |
151 | |
152 /** | |
153 * @brief Displays error messages | |
154 */ | |
155 static void print_error(const char *format, ...); | |
156 | |
157 | |
158 /** | |
159 * @brief Displays status messages | |
160 */ | |
161 static void print_status(const char *format, ...); | |
162 | |
163 | |
164 /** | |
165 * @brief Displays horizontal line | |
166 */ | |
167 static void print_separator(); | |
168 | |
169 | |
170 /** | |
171 * @brief Saves connection entry to the logfile | |
172 */ | |
173 static void log_entry(const Fiche_Settings *s, const char *ip, | |
174 const char *hostname, const char *slug); | |
175 | |
176 | |
177 /** | |
178 * @brief Returns string containing current date | |
179 * @warning Output has to be freed! | |
180 */ | |
181 static void get_date(char *buf); | |
182 | |
183 | |
184 /***********************************************************************… | |
185 * Public fiche functions | |
186 */ | |
187 | |
188 void fiche_init(Fiche_Settings *settings) { | |
189 | |
190 // Initialize everything to default values | |
191 // or to NULL in case of pointers | |
192 | |
193 struct Fiche_Settings def = { | |
194 // domain | |
195 "example.com", | |
196 // output dir | |
197 "code", | |
198 // port | |
199 9999, | |
200 // slug length | |
201 4, | |
202 // protocol prefix | |
203 "http", | |
204 // buffer length | |
205 32768, | |
206 // user name | |
207 NULL, | |
208 // path to log file | |
209 NULL, | |
210 // path to banlist | |
211 NULL, | |
212 // path to whitelist | |
213 NULL | |
214 }; | |
215 | |
216 // Copy default settings to provided instance | |
217 *settings = def; | |
218 } | |
219 | |
220 int fiche_run(Fiche_Settings settings) { | |
221 | |
222 // Check if log file is writable (if set) | |
223 if ( settings.log_file_path ) { | |
224 | |
225 // Create log file if it doesn't exist | |
226 FILE *f = fopen(settings.log_file_path, "a+"); | |
227 if (!f){ | |
228 print_error("Unable to create log file!"); | |
229 return -1; | |
230 } | |
231 | |
232 // Then check if it's accessible | |
233 if ( access(settings.log_file_path, W_OK) != 0 ) { | |
234 print_error("Log file not writable!"); | |
235 fclose(f); | |
236 return -1; | |
237 } | |
238 | |
239 logfile_handle = f; | |
240 | |
241 } | |
242 | |
243 // Display welcome message | |
244 { | |
245 char date[64]; | |
246 get_date(date); | |
247 print_status("Starting fiche on %s...", date); | |
248 } | |
249 | |
250 // Try to set requested user | |
251 if ( perform_user_change(&settings) != 0) { | |
252 print_error("Was not able to change the user!"); | |
253 return -1; | |
254 } | |
255 | |
256 // Check if output directory is writable | |
257 // - First we try to create it | |
258 { | |
259 mkdir( | |
260 settings.output_dir_path, | |
261 S_IRWXU | S_IRGRP | S_IROTH | S_IXOTH | S_IXGRP | |
262 ); | |
263 // - Then we check if we can write there | |
264 if ( access(settings.output_dir_path, W_OK) != 0 ) { | |
265 print_error("Output directory not writable!"); | |
266 return -1; | |
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 if (logfile_handle) fclose(logfile_handle); | |
274 return -1; | |
275 } | |
276 | |
277 pid_t pid = fork(); | |
278 if (pid == -1){ | |
279 char *err = strerror(0); | |
280 print_error("Unable to fork into background: %s", err); | |
281 if (logfile_handle) fclose(logfile_handle); | |
282 return -1; | |
283 } | |
284 if (pid > 0){ | |
285 //parent | |
286 if (logfile_handle) fclose(logfile_handle); | |
287 return 0; | |
288 } | |
289 | |
290 if (setsid() == -1){ | |
291 char *err = strerror(0); | |
292 print_error("Creating new session id: %s", err); | |
293 if (logfile_handle) fclose(logfile_handle); | |
294 return -1; | |
295 } | |
296 | |
297 // We are detached so close those to avoid noise | |
298 fclose(stdin); | |
299 fclose(stdout); | |
300 fclose(stderr); | |
301 | |
302 // Main loop in this method | |
303 start_server(&settings); | |
304 | |
305 // Perform final cleanup | |
306 | |
307 // This is allways allocated on the heap | |
308 free(settings.domain); | |
309 | |
310 if (logfile_handle) fclose(logfile_handle); | |
311 | |
312 return 0; | |
313 | |
314 } | |
315 | |
316 | |
317 /***********************************************************************… | |
318 * Static functions below | |
319 */ | |
320 | |
321 static void print_error(const char *format, ...) { | |
322 va_list args; | |
323 FILE *fd = logfile_handle ? logfile_handle : stderr; | |
324 | |
325 va_start(args, format); | |
326 | |
327 fprintf(fd, "[Fiche][ERROR] "); | |
328 vfprintf(fd, format, args); | |
329 fprintf(fd, "\n"); | |
330 fflush(fd); | |
331 va_end(args); | |
332 } | |
333 | |
334 | |
335 static void print_status(const char *format, ...) { | |
336 va_list args; | |
337 FILE *fd = logfile_handle ? logfile_handle : stderr; | |
338 | |
339 va_start(args, format); | |
340 | |
341 fprintf(fd, "[Fiche][STATUS] "); | |
342 vfprintf(fd, format, args); | |
343 fprintf(fd, "\n"); | |
344 fflush(fd); | |
345 va_end(args); | |
346 } | |
347 | |
348 | |
349 static void print_separator() { | |
350 FILE *fd = logfile_handle ? logfile_handle : stderr; | |
351 fprintf(fd, "=======================================================… | |
352 fflush(fd); | |
353 } | |
354 | |
355 | |
356 static void log_entry(const Fiche_Settings *s, const char *ip, | |
357 const char *hostname, const char *slug) | |
358 { | |
359 // Logging to file not enabled, finish here | |
360 if (!s->log_file_path) { | |
361 return; | |
362 } | |
363 | |
364 if (!logfile_handle) { | |
365 print_status("Was not able to save entry to the log!"); | |
366 return; | |
367 } | |
368 | |
369 char date[64]; | |
370 get_date(date); | |
371 | |
372 // Write entry to file | |
373 fprintf(logfile_handle, "%s -- %s -- %s (%s)\n", slug, date, ip, hos… | |
374 } | |
375 | |
376 | |
377 static void get_date(char *buf) { | |
378 struct tm curtime; | |
379 time_t ltime; | |
380 | |
381 ltime=time(<ime); | |
382 localtime_r(<ime, &curtime); | |
383 | |
384 // Save data to provided buffer | |
385 if (asctime_r(&curtime, buf) == 0) { | |
386 // Couldn't get date, setting first byte of the | |
387 // buffer to zero so it won't be displayed | |
388 buf[0] = 0; | |
389 return; | |
390 } | |
391 | |
392 // Remove newline char | |
393 buf[strlen(buf)-1] = 0; | |
394 } | |
395 | |
396 | |
397 static int set_domain_name(Fiche_Settings *settings) { | |
398 | |
399 const int len = strlen(settings->domain) + strlen(settings->prefix) … | |
400 | |
401 char *b = malloc(len); | |
402 if (!b) { | |
403 return -1; | |
404 } | |
405 | |
406 strlcpy(b, settings->prefix, len); | |
407 strlcat(b, "://", len); | |
408 strlcat(b, settings->domain, len); | |
409 | |
410 settings->domain = b; | |
411 | |
412 print_status("Domain set to: %s.", settings->domain); | |
413 | |
414 return 0; | |
415 } | |
416 | |
417 | |
418 static int perform_user_change(const Fiche_Settings *settings) { | |
419 | |
420 // User change wasn't requested, finish here | |
421 if (settings->user_name == NULL) { | |
422 return 0; | |
423 } | |
424 | |
425 // Check if root, if not - finish here | |
426 if (getuid() != 0) { | |
427 print_error("Run as root if you want to change the user!"); | |
428 return -1; | |
429 } | |
430 | |
431 // Get user details | |
432 const struct passwd *userdata = getpwnam(settings->user_name); | |
433 | |
434 const int uid = userdata->pw_uid; | |
435 const int gid = userdata->pw_gid; | |
436 | |
437 if (uid == -1 || gid == -1) { | |
438 print_error("Could find requested user: %s!", settings->user_nam… | |
439 return -1; | |
440 } | |
441 | |
442 if (setgid(gid) != 0) { | |
443 print_error("Couldn't switch to requested user: %s!", settings->… | |
444 } | |
445 | |
446 if (setuid(uid) != 0) { | |
447 print_error("Couldn't switch to requested user: %s!", settings->… | |
448 } | |
449 | |
450 print_status("User changed to: %s.", settings->user_name); | |
451 | |
452 return 0; | |
453 } | |
454 | |
455 | |
456 static int start_server(Fiche_Settings *settings) { | |
457 | |
458 // Perform socket creation | |
459 int s = socket(AF_INET, SOCK_STREAM, 0); | |
460 if (s < 0) { | |
461 print_error("Couldn't create a socket!"); | |
462 return -1; | |
463 } | |
464 | |
465 // Set socket settings | |
466 if ( setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &(int){ 1 } , sizeof(in… | |
467 print_error("Couldn't prepare the socket!"); | |
468 return -1; | |
469 } | |
470 | |
471 // Prepare address and port handler | |
472 struct sockaddr_in address; | |
473 address.sin_family = AF_INET; | |
474 address.sin_addr.s_addr = INADDR_ANY; | |
475 address.sin_port = htons(settings->port); | |
476 | |
477 // Bind to port | |
478 if ( bind(s, (struct sockaddr *) &address, sizeof(address)) != 0) { | |
479 print_error("Couldn't bind to the port: %d!", settings->port); | |
480 return -1; | |
481 } | |
482 | |
483 // Start listening | |
484 if ( listen(s, 128) != 0 ) { | |
485 print_error("Couldn't start listening on the socket!"); | |
486 return -1; | |
487 } | |
488 | |
489 print_status("Server started listening on port: %d.", settings->port… | |
490 print_separator(); | |
491 | |
492 // Run dispatching loop | |
493 while (1) { | |
494 dispatch_connection(s, settings); | |
495 } | |
496 | |
497 // Give some time for all threads to finish | |
498 // NOTE: this code is reached only in testing environment | |
499 // There is currently no way to kill the main thread from any thread | |
500 // Something like this can be done for testing purpouses: | |
501 // int i = 0; | |
502 // while (i < 3) { | |
503 // dispatch_connection(s, settings); | |
504 // i++; | |
505 // } | |
506 | |
507 sleep(5); | |
508 | |
509 return 0; | |
510 } | |
511 | |
512 | |
513 static void dispatch_connection(int socket, Fiche_Settings *settings) { | |
514 | |
515 // Create address structs for this socket | |
516 struct sockaddr_in address; | |
517 socklen_t addlen = sizeof(address); | |
518 | |
519 // Accept a connection and get a new socket id | |
520 const int s = accept(socket, (struct sockaddr *) &address, &addlen); | |
521 if (s < 0 ) { | |
522 print_error("Error on accepting connection!"); | |
523 return; | |
524 } | |
525 | |
526 // Set timeout for accepted socket | |
527 const struct timeval timeout = { 5, 0 }; | |
528 | |
529 if ( setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout… | |
530 print_error("Couldn't set a timeout!"); | |
531 } | |
532 | |
533 if ( setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout… | |
534 print_error("Couldn't set a timeout!"); | |
535 } | |
536 | |
537 // Create an argument for the thread function | |
538 struct fiche_connection *c = malloc(sizeof(*c)); | |
539 if (!c) { | |
540 print_error("Couldn't allocate memory!"); | |
541 return; | |
542 } | |
543 c->socket = s; | |
544 c->address = address; | |
545 c->settings = settings; | |
546 | |
547 // Spawn a new thread to handle this connection | |
548 pthread_t id; | |
549 | |
550 if ( pthread_create(&id, NULL, &handle_connection, c) != 0 ) { | |
551 print_error("Couldn't spawn a thread!"); | |
552 return; | |
553 } | |
554 | |
555 // Detach thread if created succesfully | |
556 // TODO: consider using pthread_tryjoin_np | |
557 pthread_detach(id); | |
558 | |
559 } | |
560 | |
561 | |
562 static void *handle_connection(void *args) { | |
563 | |
564 // Cast args to it's previous type | |
565 struct fiche_connection *c = (struct fiche_connection *) args; | |
566 | |
567 // Get client's IP | |
568 const char *ip = inet_ntoa(c->address.sin_addr); | |
569 | |
570 // Get client's hostname | |
571 char hostname[1024]; | |
572 | |
573 if (getnameinfo((struct sockaddr *)&c->address, sizeof(c->address), | |
574 hostname, sizeof(hostname), NULL, 0, 0) != 0 ) { | |
575 | |
576 // Couldn't resolve a hostname | |
577 strlcpy(hostname, "n/a", 1024); | |
578 } | |
579 | |
580 // Print status on this connection | |
581 { | |
582 char date[64]; | |
583 get_date(date); | |
584 print_status("%s", date); | |
585 | |
586 print_status("Incoming connection from: %s (%s).", ip, hostname); | |
587 } | |
588 | |
589 // Create a buffer | |
590 uint8_t buffer[c->settings->buffer_len]; | |
591 memset(buffer, 0, c->settings->buffer_len); | |
592 | |
593 const int r = recv(c->socket, buffer, sizeof(buffer), MSG_WAITALL); | |
594 if (r <= 0) { | |
595 print_error("No data received from the client!"); | |
596 print_separator(); | |
597 | |
598 // Close the socket | |
599 close(c->socket); | |
600 | |
601 // Cleanup | |
602 free(c); | |
603 pthread_exit(NULL); | |
604 | |
605 return 0; | |
606 } | |
607 | |
608 // - Check if request was performed with a known protocol | |
609 // TODO | |
610 | |
611 // - Check if on whitelist | |
612 // TODO | |
613 | |
614 // - Check if on banlist | |
615 // TODO | |
616 | |
617 // Generate slug and use it to create an url | |
618 char *slug; | |
619 uint8_t extra = 0; | |
620 | |
621 do { | |
622 | |
623 // Generate slugs until it's possible to create a directory | |
624 // with generated slug on disk | |
625 generate_slug(&slug, c->settings->slug_len, extra); | |
626 | |
627 // Something went wrong in slug generation, break here | |
628 if (!slug) { | |
629 break; | |
630 } | |
631 | |
632 // Increment counter for additional letters needed | |
633 ++extra; | |
634 | |
635 // If i was incremented more than 128 times, something | |
636 // for sure went wrong. We are closing connection and | |
637 // killing this thread in such case | |
638 if (extra > 128) { | |
639 print_error("Couldn't generate a valid slug!"); | |
640 print_separator(); | |
641 | |
642 // Cleanup | |
643 free(c); | |
644 free(slug); | |
645 close(c->socket); | |
646 pthread_exit(NULL); | |
647 return NULL; | |
648 } | |
649 | |
650 } | |
651 while(create_directory(c->settings->output_dir_path, slug) != 0); | |
652 | |
653 | |
654 // Slug generation failed, we have to finish here | |
655 if (!slug) { | |
656 print_error("Couldn't generate a slug!"); | |
657 print_separator(); | |
658 | |
659 close(c->socket); | |
660 | |
661 // Cleanup | |
662 free(c); | |
663 pthread_exit(NULL); | |
664 return NULL; | |
665 } | |
666 | |
667 | |
668 // Save to file failed, we have to finish here | |
669 if ( save_to_file(c->settings, buffer, slug) != 0 ) { | |
670 print_error("Couldn't save a file!"); | |
671 print_separator(); | |
672 | |
673 close(c->socket); | |
674 | |
675 // Cleanup | |
676 free(c); | |
677 free(slug); | |
678 pthread_exit(NULL); | |
679 return NULL; | |
680 } | |
681 | |
682 // Write a response to the user | |
683 { | |
684 // Create an url (additional byte for slash and one for new line) | |
685 const size_t len = strlen(c->settings->domain) + strlen(slug) + … | |
686 | |
687 char url[len]; | |
688 snprintf(url, len, "%s%s%s%s", c->settings->domain, "/", slug, "… | |
689 | |
690 // Send the response | |
691 write(c->socket, url, len); | |
692 } | |
693 | |
694 print_status("Received %d bytes, saved to: %s.", r, slug); | |
695 print_separator(); | |
696 | |
697 // Log connection | |
698 // TODO: log unsuccessful and rejected connections | |
699 log_entry(c->settings, ip, hostname, slug); | |
700 | |
701 // Close the connection | |
702 close(c->socket); | |
703 | |
704 // Perform cleanup of values used in this thread | |
705 free(slug); | |
706 free(c); | |
707 | |
708 pthread_exit(NULL); | |
709 | |
710 return NULL; | |
711 } | |
712 | |
713 | |
714 static void generate_slug(char **output, uint8_t length, uint8_t extra_l… | |
715 | |
716 // Realloc buffer for slug when we want it to be bigger | |
717 // This happens in case when directory with this name already | |
718 // exists. To save time, we don't generate new slugs until | |
719 // we spot an available one. We add another letter instead. | |
720 | |
721 if (extra_length > 0) { | |
722 free(*output); | |
723 } | |
724 | |
725 // Create a buffer for slug with extra_length if any | |
726 *output = calloc(length + 1 + extra_length, sizeof(char)); | |
727 | |
728 if (*output == NULL) { | |
729 return; | |
730 } | |
731 | |
732 // Take n-th symbol from symbol table and use it for slug generation | |
733 for (int i = 0; i < length + extra_length; i++) { | |
734 int n = arc4random() % strlen(Fiche_Symbols); | |
735 *(output[0] + sizeof(char) * i) = Fiche_Symbols[n]; | |
736 } | |
737 | |
738 } | |
739 | |
740 | |
741 static int create_directory(char *output_dir, char *slug) { | |
742 if (!slug) { | |
743 return -1; | |
744 } | |
745 | |
746 // Additional byte is for the slash | |
747 size_t len = strlen(output_dir) + strlen(slug) + 2; | |
748 | |
749 // Generate a path | |
750 char *path = malloc(len); | |
751 if (!path) { | |
752 return -1; | |
753 } | |
754 snprintf(path, len, "%s%s%s", output_dir, "/", slug); | |
755 | |
756 // Create output directory, just in case | |
757 mkdir(output_dir, S_IRWXU | S_IRGRP | S_IROTH | S_IXOTH | S_IXGRP); | |
758 | |
759 // Create slug directory | |
760 const int r = mkdir( | |
761 path, | |
762 S_IRWXU | S_IRGRP | S_IROTH | S_IXOTH | S_IXGRP | |
763 ); | |
764 | |
765 free(path); | |
766 | |
767 return r; | |
768 } | |
769 | |
770 | |
771 static int save_to_file(const Fiche_Settings *s, uint8_t *data, char *sl… | |
772 char *file_name = "paste.txt"; | |
773 | |
774 // Additional 2 bytes are for 2 slashes | |
775 size_t len = strlen(s->output_dir_path) + strlen(slug) + strlen(file… | |
776 | |
777 // Generate a path | |
778 char *path = malloc(len); | |
779 if (!path) { | |
780 return -1; | |
781 } | |
782 | |
783 snprintf(path, len, "%s%s%s%s%s", s->output_dir_path, "/", slug, "/"… | |
784 | |
785 // Attempt file saving | |
786 FILE *f = fopen(path, "w"); | |
787 if (!f) { | |
788 free(path); | |
789 return -1; | |
790 } | |
791 | |
792 // Null-terminate buffer if not null terminated already | |
793 data[s->buffer_len - 1] = 0; | |
794 | |
795 if ( fprintf(f, "%s", data) < 0 ) { | |
796 fclose(f); | |
797 free(path); | |
798 return -1; | |
799 } | |
800 | |
801 fclose(f); | |
802 free(path); | |
803 | |
804 return 0; | |
805 } |