radio

radio.ircforever.org
git clone git://git.ircforever.org/radio
Log | Files | Refs | Submodules | README | LICENSE

commit 161a2a5a6d2eb679e010a84cf29793e1cb75ef9f
Author: libredev <libredev@ircforever.org>
Date:   Tue,  3 Jan 2023 01:13:05 +0530

initial commit

Diffstat:
A.gitignore | 1+
A.gitmodules | 3+++
ACOPYING | 121+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AMakefile | 34++++++++++++++++++++++++++++++++++
AREADME | 1+
Afooter.html | 10++++++++++
Aheader.html | 10++++++++++
Ahttp.h | 719+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amain.c | 386+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amusic.svg | 2++
Apdjson | 1+
Astyle.css | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
12 files changed, 1385 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1 @@ +index.cgi diff --git a/.gitmodules b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "pdjson"] + path = pdjson + url = https://github.com/skeeto/pdjson diff --git a/COPYING b/COPYING @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/Makefile b/Makefile @@ -0,0 +1,34 @@ +# The author(s) have dedicated this work to the public domain by waiving all +# of his or her rights to this work worldwide under copyright and patent law, +# including all related and neighboring rights, to the extent allowed by law. +# This work is provided 'as-is', without any express or implied warranty. +# See COPYING file for details. + +CC = cc +CFLAGS =\ + -g -std=c99 -Wall -Wextra -Wpedantic -Wfatal-errors\ + -Wstrict-prototypes -Wold-style-definition\ + -D_DEFAULT_SOURCE + +INSTALL_DIR=/var/www/htdocs/radio.ircforever.org + +install: index.cgi header.html footer.html style.css music.svg + doas rm -R -f $(INSTALL_DIR) + doas mkdir -p $(INSTALL_DIR) + doas cp index.cgi $(INSTALL_DIR) + doas cp index.cgi $(INSTALL_DIR)/about.cgi + doas cp header.html $(INSTALL_DIR) + doas cp footer.html $(INSTALL_DIR) + doas cp style.css $(INSTALL_DIR) + doas cp music.svg $(INSTALL_DIR) + +index.cgi: main.c pdjson/pdjson.c + $(CC) $(CFLAGS) -o $@ main.c pdjson/pdjson.c + +run: index.cgi + SCRIPT_NAME="/index.cgi" valgrind --leak-check=full --show-leak-kinds=all ./index.cgi + +clean: + rm -f index.cgi + +.PHONY: install run clean diff --git a/README b/README @@ -0,0 +1 @@ +Everything in this repository is released under CC0 1.0 Universal. diff --git a/footer.html b/footer.html @@ -0,0 +1,10 @@ + +<p class="footer"> + Except where otherwise noted, content on this site and the source code are + released under the <br> + <a href="https://creativecommons.org/publicdomain/zero/1.0/"> + CC0 1.0 Public Domain Dedication "No Rights Reserved" + </a> +</p> + +</html> diff --git a/header.html b/header.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html> + +<head> + <meta charset="utf-8"/> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title> IRCNow Radio | Free Culture Media Streaming </title> + <link rel="stylesheet" type="text/css" href="style.css"/> +</head> + diff --git a/http.h b/http.h @@ -0,0 +1,719 @@ +/* +------------------------------------------------------------------------------ + Licensing information can be found at the end of the file. +------------------------------------------------------------------------------ + +http.hpp - v1.0 - Basic HTTP protocol implementation over sockets (no https). + +Do this: + #define HTTP_IMPLEMENTATION +before you include this file in *one* C/C++ file to create the implementation. +*/ + +#ifndef http_hpp +#define http_hpp + +#define _CRT_NONSTDC_NO_DEPRECATE +#define _CRT_SECURE_NO_WARNINGS +#include <stddef.h> // for size_t +#include <stdint.h> // for uintptr_t + +typedef enum http_status_t + { + HTTP_STATUS_PENDING, + HTTP_STATUS_COMPLETED, + HTTP_STATUS_FAILED, + } http_status_t; + +typedef struct http_t + { + http_status_t status; + int status_code; + char const* reason_phrase; + char const* content_type; + size_t response_size; + void* response_data; + } http_t; + +http_t* http_get( char const* url, void* memctx ); +http_t* http_post( char const* url, void const* data, size_t size, void* memctx ); + +http_status_t http_process( http_t* http ); + +void http_release( http_t* http ); + +#endif /* http_hpp */ + +/** + +http.hpp +======== + +Basic HTTP protocol implementation over sockets (no https). + + +Example +------- + + #define HTTP_IMPLEMENTATION + #include "http.h" + + int main( int argc, char** argv ) { + http_t* request = http_get( "http://www.mattiasgustavsson.com/http_test.txt", NULL ); + if( !request ) { + printf( "Invalid request.\n" ); + return 1; + } + + http_status_t status = HTTP_STATUS_PENDING; + int prev_size = -1; + while( status == HTTP_STATUS_PENDING ) { + status = http_process( request ); + if( prev_size != (int) request->response_size ) { + printf( "%d byte(s) received.\n", (int) request->response_size ); + prev_size = (int) request->response_size; + } + } + + if( status == HTTP_STATUS_FAILED ) { + printf( "HTTP request failed (%d): %s.\n", request->status_code, request->reason_phrase ); + http_release( request ); + return 1; + } + + printf( "\nContent type: %s\n\n%s\n", request->content_type, (char const*)request->response_data ); + http_release( request ); + return 0; + } + + +API Documentation +----------------- + +http.h is a small library for making http requests from a web server. It only supports GET and POST http commands, and +is designed for when you just need a very basic way of communicating over http. http.h does not support https +connections, just plain http. + +http.h is a single-header library, and does not need any .lib files or other binaries, or any build scripts. To use +it, you just include http.h to get the API declarations. To get the definitions, you must include http.h from +*one* single C or C++ file, and #define the symbol `HTTP_IMPLEMENTATION` before you do. + + +#### Custom memory allocators + +For working memory and to store the retrieved data, http.h needs to do dynamic allocation by calling `malloc`. Programs +might want to keep track of allocations done, or use custom defined pools to allocate memory from. http.h allows +for specifying custom memory allocation functions for `malloc` and `free`. This is done with the following code: + + #define HTTP_IMPLEMENTATION + #define HTTP_MALLOC( ctx, size ) ( my_custom_malloc( ctx, size ) ) + #define HTTP_FREE( ctx, ptr ) ( my_custom_free( ctx, ptr ) ) + #include "http.h" + +where `my_custom_malloc` and `my_custom_free` are your own memory allocation/deallocation functions. The `ctx` parameter +is an optional parameter of type `void*`. When `http_get` or `http_post` is called, , you can pass in a `memctx` +parameter, which can be a pointer to anything you like, and which will be passed through as the `ctx` parameter to every +`HTTP_MALLOC`/`HTTP_FREE` call. For example, if you are doing memory tracking, you can pass a pointer to your +tracking data as `memctx`, and in your custom allocation/deallocation function, you can cast the `ctx` param back to the +right type, and access the tracking data. + +If no custom allocator is defined, http.h will default to `malloc` and `free` from the C runtime library. + + +http_get +-------- + + http_t* http_get( char const* url, void* memctx ) + +Initiates a http GET request with the specified url. `url` is a zero terminated string containing the request location, +just like you would type it in a browser, for example `http://www.mattiasgustavsson.com:80/http_test.txt`. `memctx` is a +pointer to user defined data which will be passed through to the custom HTTP_MALLOC/HTTP_FREE calls. It can be NULL if +no user defined data is needed. Returns a `http_t` instance, which needs to be passed to `http_process` to process the +request. When the request is finished (or have failed), the returned `http_t` instance needs to be released by calling +`http_release`. If the request was invalid, `http_get` returns NULL. + + +http_post +--------- + + http_t* http_post( char const* url, void const* data, size_t size, void* memctx ) + +Initiates a http POST request with the specified url. `url` is a zero terminated string containing the request location, +just like you would type it in a browser, for example `http://www.mattiasgustavsson.com:80/http_test.txt`. `data` is a +pointer to the data to be sent along as part of the request, and `size` is the number of bytes to send. `memctx` is a +pointer to user defined data which will be passed through to the custom HTTP_MALLOC/HTTP_FREE calls. It can be NULL if +no user defined data is needed. Returns a `http_t` instance, which needs to be passed to `http_process` to process the +request. When the request is finished (or have failed), the returned `http_t` instance needs to be released by calling +`http_release`. If the request was invalid, `http_post` returns NULL. + + +http_process +------------ + + http_status_t http_process( http_t* http ) + +http.h uses non-blocking sockets, so after a request have been made by calling either `http_get` or `http_post`, you +have to keep calling `http_process` for as long as it returns `HTTP_STATUS_PENDING`. You can call it from a loop which +does other work too, for example from inside a game loop or from a loop which calls `http_process` on multiple requests. +If the request fails, `http_process` returns `HTTP_STATUS_FAILED`, and the fields `status_code` and `reason_phrase` may +contain more details (for example, status code can be 404 if the requested resource was not found on the server). If the +request completes successfully, it returns `HTTP_STATUS_COMPLETED`. In this case, the `http_t` instance will contain +details about the result. `status_code` and `reason_phrase` contains the details about the result, as specified in the +HTTP protocol. `content_type` contains the MIME type for the returns resource, for example `text/html` for a normal web +page. `response_data` is the pointer to the received data, and `resonse_size` is the number of bytes it contains. In the +case when the response data is in text format, http.h ensures there is a zero terminator placed immediately after the +response data block, so it is safe to interpret the resonse data as a `char*`. Note that the data size in this case will +be the length of the data without the additional zero terminator. + + +http_release +------------ + + void http_release( http_t* http ) + +Releases the resources acquired by `http_get` or `http_post`. Should be call when you are finished with the request. + +*/ + +/* +---------------------- + IMPLEMENTATION +---------------------- +*/ + +#ifdef HTTP_IMPLEMENTATION + +#ifdef _WIN32 + #define _CRT_NONSTDC_NO_DEPRECATE + #define _CRT_SECURE_NO_WARNINGS + #pragma warning( push ) + #pragma warning( disable: 4127 ) // conditional expression is constant + #pragma warning( disable: 4255 ) // 'function' : no function prototype given: converting '()' to '(void)' + #pragma warning( disable: 4365 ) // 'action' : conversion from 'type_1' to 'type_2', signed/unsigned mismatch + #pragma warning( disable: 4574 ) // 'Identifier' is defined to be '0': did you mean to use '#if identifier'? + #pragma warning( disable: 4668 ) // 'symbol' is not defined as a preprocessor macro, replacing with '0' for 'directive' + #pragma warning( disable: 4706 ) // assignment within conditional expression + #include <winsock2.h> + #include <ws2tcpip.h> + #pragma warning( pop ) + #pragma comment (lib, "Ws2_32.lib") + #include <string.h> + #include <stdio.h> + #define HTTP_SOCKET SOCKET + #define HTTP_INVALID_SOCKET INVALID_SOCKET +#else + #include <stdlib.h> + #include <stdio.h> + #include <string.h> + #include <sys/types.h> + #include <sys/socket.h> + #include <unistd.h> + #include <errno.h> + #include <fcntl.h> + #include <netdb.h> + #define HTTP_SOCKET int + #define HTTP_INVALID_SOCKET -1 +#endif + +#ifndef HTTP_MALLOC + #define _CRT_NONSTDC_NO_DEPRECATE + #define _CRT_SECURE_NO_WARNINGS + #include <stdlib.h> + #define HTTP_MALLOC( ctx, size ) ( malloc( size ) ) + #define HTTP_FREE( ctx, ptr ) ( free( ptr ) ) +#endif + +typedef struct http_internal_t + { + /* keep this at the top!*/ + http_t http; + /* because http_internal_t* can be cast to http_t*. */ + + void* memctx; + HTTP_SOCKET socket; + int connect_pending; + int request_sent; + char address[ 256 ]; + char request_header[ 256 ]; + char* request_header_large; + void* request_data; + size_t request_data_size; + char reason_phrase[ 1024 ]; + char content_type[ 256 ]; + size_t data_size; + size_t data_capacity; + void* data; + } http_internal_t; + + +static int http_internal_parse_url( char const* url, char* address, size_t address_capacity, char* port, + size_t port_capacity, char const** resource ) + { + // make sure url starts with http:// + if( strncmp( url, "http://", 7 ) != 0 ) return 0; + url += 7; // skip http:// part of url + + size_t url_len = strlen( url ); + + // find end of address part of url + char const* address_end = strchr( url, ':' ); + if( !address_end ) address_end = strchr( url, '/' ); + if( !address_end ) address_end = url + url_len; + + // extract address + size_t address_len = (size_t)( address_end - url ); + if( address_len >= address_capacity ) return 0; + memcpy( address, url, address_len ); + address[ address_len ] = 0; + + // check if there's a port defined + char const* port_end = address_end; + if( *address_end == ':' ) + { + ++address_end; + port_end = strchr( address_end, '/' ); + if( !port_end ) port_end = address_end + strlen( address_end ); + size_t port_len = (size_t)( port_end - address_end ); + if( port_len >= port_capacity ) return 0; + memcpy( port, address_end, port_len ); + port[ port_len ] = 0; + } + else + { + // use default port number 80 + if( port_capacity <= 2 ) return 0; + strcpy( port, "80" ); + } + + + *resource = port_end; + + return 1; + } + + +HTTP_SOCKET http_internal_connect( char const* address, char const* port ) + { + // set up hints for getaddrinfo + struct addrinfo hints; + memset( &hints, 0, sizeof( hints ) ); + hints.ai_family = AF_INET; // the Internet Protocol version 4 (IPv4) address family. + hints.ai_flags = AI_PASSIVE; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; // Use Transmission Control Protocol (TCP). + + // resolve the server address and port + struct addrinfo* addri = 0; + int error = getaddrinfo( address, port, &hints, &addri) ; + if( error != 0 ) return HTTP_INVALID_SOCKET; + + // create the socket + HTTP_SOCKET sock = socket( addri->ai_family, addri->ai_socktype, addri->ai_protocol ); + if( sock == -1) + { + freeaddrinfo( addri ); + return HTTP_INVALID_SOCKET; + } + + // set socket to nonblocking mode + u_long nonblocking = 1; + #ifdef _WIN32 + int res = ioctlsocket( sock, FIONBIO, &nonblocking ); + #else + int flags = fcntl( sock, F_GETFL, 0 ); + int res = fcntl( sock, F_SETFL, flags | O_NONBLOCK ); + #endif + if( res == -1 ) + { + freeaddrinfo( addri ); + #ifdef _WIN32 + closesocket( sock ); + #else + close( sock ); + #endif + return HTTP_INVALID_SOCKET; + } + + // connect to server + if( connect( sock, addri->ai_addr, (int)addri->ai_addrlen ) == -1 ) + { + #ifdef _WIN32 + if( WSAGetLastError() != WSAEWOULDBLOCK && WSAGetLastError() != WSAEINPROGRESS ) + { + freeaddrinfo( addri ); + closesocket( sock ); + return HTTP_INVALID_SOCKET; + } + #else + if( errno != EWOULDBLOCK && errno != EINPROGRESS && errno != EAGAIN ) + { + freeaddrinfo( addri ); + close( sock ); + return HTTP_INVALID_SOCKET; + } + #endif + } + + freeaddrinfo( addri ); + return sock; + } + + +static http_internal_t* http_internal_create( size_t request_data_size, void* memctx ) + { + http_internal_t* internal = (http_internal_t*) HTTP_MALLOC( memctx, sizeof( http_internal_t ) + request_data_size ); + + internal->http.status = HTTP_STATUS_PENDING; + internal->http.status_code = 0; + internal->http.response_size = 0; + internal->http.response_data = NULL; + + internal->memctx = memctx; + internal->connect_pending = 1; + internal->request_sent = 0; + + strcpy( internal->reason_phrase, "" ); + internal->http.reason_phrase = internal->reason_phrase; + + strcpy( internal->content_type, "" ); + internal->http.content_type = internal->content_type; + + internal->data_size = 0; + internal->data_capacity = 64 * 1024; + internal->data = HTTP_MALLOC( memctx, internal->data_capacity ); + + internal->request_data = NULL; + internal->request_data_size = 0; + + return internal; + } + + +http_t* http_get( char const* url, void* memctx ) + { + #ifdef _WIN32 + WSADATA wsa_data; + if( WSAStartup( MAKEWORD( 1, 0 ), &wsa_data ) != 0 ) return NULL; + #endif + + char address[ 256 ]; + char port[ 16 ]; + char const* resource; + + if( http_internal_parse_url( url, address, sizeof( address ), port, sizeof( port ), &resource ) == 0 ) + return NULL; + + HTTP_SOCKET socket = http_internal_connect( address, port ); + if( socket == HTTP_INVALID_SOCKET ) return NULL; + + http_internal_t* internal = http_internal_create( 0, memctx ); + internal->socket = socket; + + char* request_header; + size_t request_header_len = 64 + strlen( resource ) + strlen( address ) + strlen( port ); + if( request_header_len < sizeof( internal->request_header ) ) + { + internal->request_header_large = NULL; + request_header = internal->request_header; + } + else + { + internal->request_header_large = (char*) HTTP_MALLOC( memctx, request_header_len + 1 ); + request_header = internal->request_header_large; + } + int default_http_port = (strcmp(port, "80") == 0); + sprintf( request_header, "GET %s HTTP/1.0\r\nHost: %s%s%s\r\n\r\n", resource, address, default_http_port ? "" : ":", default_http_port ? "" : port ); + + return &internal->http; + } + + +http_t* http_post( char const* url, void const* data, size_t size, void* memctx ) + { + #ifdef _WIN32 + WSADATA wsa_data; + if( WSAStartup( MAKEWORD( 1, 0 ), &wsa_data ) != 0 ) return 0; + #endif + + char address[ 256 ]; + char port[ 16 ]; + char const* resource; + + if( http_internal_parse_url( url, address, sizeof( address ), port, sizeof( port ), &resource ) == 0 ) + return NULL; + + HTTP_SOCKET socket = http_internal_connect( address, port ); + if( socket == HTTP_INVALID_SOCKET ) return NULL; + + http_internal_t* internal = http_internal_create( size, memctx ); + internal->socket = socket; + + char* request_header; + size_t request_header_len = 64 + strlen( resource ) + strlen( address ) + strlen( port ); + if( request_header_len < sizeof( internal->request_header ) ) + { + internal->request_header_large = NULL; + request_header = internal->request_header; + } + else + { + internal->request_header_large = (char*) HTTP_MALLOC( memctx, request_header_len + 1 ); + request_header = internal->request_header_large; + } + int default_http_port = (strcmp(port, "80") == 0); + sprintf( request_header, "POST %s HTTP/1.0\r\nHost: %s%s%s\r\nContent-Length: %d\r\n\r\n", resource, address, default_http_port ? "" : ":", default_http_port ? "" : port, + (int) size ); + + internal->request_data_size = size; + internal->request_data = ( internal + 1 ); + memcpy( internal->request_data, data, size ); + + return &internal->http; + } + + +http_status_t http_process( http_t* http ) + { + http_internal_t* internal = (http_internal_t*) http; + + if( http->status == HTTP_STATUS_FAILED ) return http->status; + + if( internal->connect_pending ) + { + fd_set sockets_to_check; + FD_ZERO( &sockets_to_check ); + #pragma warning( push ) + #pragma warning( disable: 4548 ) // expression before comma has no effect; expected expression with side-effect + FD_SET( internal->socket, &sockets_to_check ); + #pragma warning( pop ) + struct timeval timeout; timeout.tv_sec = 0; timeout.tv_usec = 0; + // check if socket is ready for send + if( select( (int)( internal->socket + 1 ), NULL, &sockets_to_check, NULL, &timeout ) == 1 ) + { + int opt = -1; + socklen_t len = sizeof( opt ); + if( getsockopt( internal->socket, SOL_SOCKET, SO_ERROR, (char*)( &opt ), &len) >= 0 && opt == 0 ) + internal->connect_pending = 0; // if it is, we're connected + } + } + + if( internal->connect_pending ) return http->status; + + if( !internal->request_sent ) + { + char const* request_header = internal->request_header_large ? + internal->request_header_large : internal->request_header; + if( send( internal->socket, request_header, (int) strlen( request_header ), 0 ) == -1 ) + { + http->status = HTTP_STATUS_FAILED; + return http->status; + } + if( internal->request_data_size ) + { + int res = send( internal->socket, (char const*)internal->request_data, (int) internal->request_data_size, 0 ); + if( res == -1 ) + { + http->status = HTTP_STATUS_FAILED; + return http->status; + } + } + internal->request_sent = 1; + return http->status; + } + + // check if socket is ready for recv + fd_set sockets_to_check; + FD_ZERO( &sockets_to_check ); + #pragma warning( push ) + #pragma warning( disable: 4548 ) // expression before comma has no effect; expected expression with side-effect + FD_SET( internal->socket, &sockets_to_check ); + #pragma warning( pop ) + struct timeval timeout; timeout.tv_sec = 0; timeout.tv_usec = 0; + while( select( (int)( internal->socket + 1 ), &sockets_to_check, NULL, NULL, &timeout ) == 1 ) + { + char buffer[ 4096 ]; + int size = recv( internal->socket, buffer, sizeof( buffer ), 0 ); + if( size == -1 ) + { + http->status = HTTP_STATUS_FAILED; + return http->status; + } + else if( size > 0 ) + { + size_t min_size = internal->data_size + size + 1; + if( internal->data_capacity < min_size ) + { + internal->data_capacity *= 2; + if( internal->data_capacity < min_size ) internal->data_capacity = min_size; + void* new_data = HTTP_MALLOC( memctx, internal->data_capacity ); + memcpy( new_data, internal->data, internal->data_size ); + HTTP_FREE( memctx, internal->data ); + internal->data = new_data; + } + memcpy( (void*)( ( (uintptr_t) internal->data ) + internal->data_size ), buffer, (size_t) size ); + internal->data_size += size; + } + else if( size == 0 ) + { + char const* status_line = (char const*) internal->data; + + int header_size = 0; + char const* header_end = strstr( status_line, "\r\n\r\n" ); + if( header_end ) + { + header_end += 4; + header_size = (int)( header_end - status_line ); + } + else + { + http->status = HTTP_STATUS_FAILED; + return http->status; + } + + // skip http version + status_line = strchr( status_line, ' ' ); + if( !status_line ) + { + http->status = HTTP_STATUS_FAILED; + return http->status; + } + ++status_line; + + // extract status code + char status_code[ 16 ]; + char const* status_code_end = strchr( status_line, ' ' ); + if( !status_code_end ) + { + http->status = HTTP_STATUS_FAILED; + return http->status; + } + memcpy( status_code, status_line, (size_t)( status_code_end - status_line ) ); + status_code[ status_code_end - status_line ] = 0; + status_line = status_code_end + 1; + http->status_code = atoi( status_code ); + + // extract reason phrase + char const* reason_phrase_end = strstr( status_line, "\r\n" ); + if( !reason_phrase_end ) + { + http->status = HTTP_STATUS_FAILED; + return http->status; + } + size_t reason_phrase_len = (size_t)( reason_phrase_end - status_line ); + if( reason_phrase_len >= sizeof( internal->reason_phrase ) ) + reason_phrase_len = sizeof( internal->reason_phrase ) - 1; + memcpy( internal->reason_phrase, status_line, reason_phrase_len ); + internal->reason_phrase[ reason_phrase_len ] = 0; + status_line = reason_phrase_end + 1; + + // extract content type + char const* content_type_start = strstr( status_line, "Content-Type: " ); + if( content_type_start ) + { + content_type_start += strlen( "Content-Type: " ); + char const* content_type_end = strstr( content_type_start, "\r\n" ); + if( content_type_end ) + { + size_t content_type_len = (size_t)( content_type_end - content_type_start ); + if( content_type_len >= sizeof( internal->content_type ) ) + content_type_len = sizeof( internal->content_type ) - 1; + memcpy( internal->content_type, content_type_start, content_type_len ); + internal->content_type[ content_type_len ] = 0; + } + } + + http->status = http->status_code < 300 ? HTTP_STATUS_COMPLETED : HTTP_STATUS_FAILED; + http->response_data = (void*)( ( (uintptr_t) internal->data ) + header_size ); + http->response_size = internal->data_size - header_size; + + // add an extra zero after the received data, but don't modify the size, so ascii results can be used as + // a zero terminated string. the size returned will be the string without this extra zero terminator. + ( (char*)http->response_data )[ http->response_size ] = 0; + return http->status; + } + } + + return http->status; + } + + +void http_release( http_t* http ) + { + http_internal_t* internal = (http_internal_t*) http; + #ifdef _WIN32 + closesocket( internal->socket ); + #else + close( internal->socket ); + #endif + + if( internal->request_header_large) HTTP_FREE( memctx, internal->request_header_large ); + HTTP_FREE( memctx, internal->data ); + HTTP_FREE( memctx, internal ); + #ifdef _WIN32 + WSACleanup(); + #endif + } + + +#endif /* HTTP_IMPLEMENTATION */ + +/* +revision history: + 1.0 first released version +*/ + +/* +------------------------------------------------------------------------------ + +This software is available under 2 licenses - you may choose the one you like. + +------------------------------------------------------------------------------ + +ALTERNATIVE A - MIT License + +Copyright (c) 2016 Mattias Gustavsson + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +------------------------------------------------------------------------------ + +ALTERNATIVE B - Public Domain (www.unlicense.org) + +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. + +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------------------ +*/ diff --git a/main.c b/main.c @@ -0,0 +1,386 @@ +/* + * The author(s) have dedicated this work to the public domain by waiving all + * of his or her rights to this work worldwide under copyright and patent law, + * including all related and neighboring rights, to the extent allowed by law. + * This work is provided 'as-is', without any express or implied warranty. + * See COPYING file for details. + */ + +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#define HTTP_IMPLEMENTATION +#include "http.h" +#include "pdjson/pdjson.h" + +#define TRUE 1 +#define FALSE 0 + +struct icestats { + char *admin; + char *host; + char *location; + char *server_id; + char *server_start; + char *server_start_iso8601; +}; + +struct source { + char *audio_bitrate; + char *audio_channels; + char *audio_info; + char *audio_samplerate; + char *bitrate; + char *channels; + char *genre; + char *ice_bitrate; + char *listener_peak; + char *listeners; + char *listenurl; + char *quality; + char *samplerate; + char *server_description; + char *server_name; + char *server_type; + char *server_url; + char *stream_start; + char *stream_start_iso8601; + char *subtype; + char *title; + char *dummy; +}; + +struct icestats icestats; +struct source *sources = NULL; +int sources_length = 0; + +char * +read_file(const char* fname) +{ + char *content; + long fsize; + FILE *file; + + file = fopen(fname, "rb"); + if (file == NULL) + goto err; + if (fseek(file, 0, SEEK_END) != 0) + goto err; + if ((fsize = ftell(file)) == -1) + goto err; + if (fseek(file, 0, SEEK_SET) != 0) + goto err; + if ((content = malloc((fsize + 1) * sizeof(char))) == NULL) + goto err; + if (fread(content, sizeof(char), fsize, file) != (size_t)fsize) + goto err; + fclose(file); + (content)[fsize] = '\0'; + return content; +err: + printf("ERROR: %s: %s\n", fname, strerror(errno)); + return NULL; +} + +char * +json_get_value(json_stream *json) +{ + enum json_type type = json_next(json); + if (type == JSON_NUMBER || type == JSON_STRING) + return strdup(json_get_string(json, NULL)); + else + return NULL; +} + +void +icestats_set(struct icestats *s, json_stream *json, const char *key) +{ + char **var = NULL; + + if (strcmp(key, "admin" ) == 0) var = &s->admin; + else if (strcmp(key, "host" ) == 0) var = &s->host; + else if (strcmp(key, "location" ) == 0) var = &s->location; + else if (strcmp(key, "server_id" ) == 0) var = &s->server_id; + else if (strcmp(key, "server_start" ) == 0) var = &s->server_start; + else if (strcmp(key, "server_start_iso8601" ) == 0) var = &s->server_start_iso8601; + + if (var == NULL) { + fprintf(stdout, "Failed to handle icestats key \"%s\"\n", key); + exit(1); + } else { + *var = json_get_value(json); + } +} + +void +icestats_print(struct icestats *s) +{ + puts("<table class=\"about\">"); + printf("<tr><td> Admin </td><td> %s </td></tr>\n", s->admin); + printf("<tr><td> Host </td><td> %s </td></tr>\n", s->host); + printf("<tr><td> Location </td><td> %s </td></tr>\n", s->location); + printf("<tr><td> Server ID </td><td> %s </td></tr>\n", s->server_id); + printf("<tr><td> Server Start </td><td> %s </td></tr>\n", s->server_start); + /* printf("<tr><td> server_start_iso8601 </td><td> %s </td></tr>\n", s->server_start_iso8601); */ + puts("</table>"); +} + +void +icestats_free(struct icestats *s) +{ + free(s->admin); + free(s->host); + free(s->location); + free(s->server_id); + free(s->server_start); + free(s->server_start_iso8601); +} + +void +source_set(struct source *s, json_stream *json, const char *key) +{ + char **var = NULL; + + if (strcmp(key, "audio_bitrate" ) == 0) var = &s->audio_bitrate; + else if (strcmp(key, "audio_channels" ) == 0) var = &s->audio_channels; + else if (strcmp(key, "audio_info" ) == 0) var = &s->audio_info; + else if (strcmp(key, "audio_samplerate" ) == 0) var = &s->audio_samplerate; + else if (strcmp(key, "bitrate" ) == 0) var = &s->bitrate; + else if (strcmp(key, "channels" ) == 0) var = &s->channels; + else if (strcmp(key, "genre" ) == 0) var = &s->genre; + else if (strcmp(key, "ice-bitrate" ) == 0) var = &s->ice_bitrate; + else if (strcmp(key, "listener_peak" ) == 0) var = &s->listener_peak; + else if (strcmp(key, "listeners" ) == 0) var = &s->listeners; + else if (strcmp(key, "listenurl" ) == 0) var = &s->listenurl; + else if (strcmp(key, "quality" ) == 0) var = &s->quality; + else if (strcmp(key, "samplerate" ) == 0) var = &s->samplerate; + else if (strcmp(key, "server_description" ) == 0) var = &s->server_description; + else if (strcmp(key, "server_name" ) == 0) var = &s->server_name; + else if (strcmp(key, "server_type" ) == 0) var = &s->server_type; + else if (strcmp(key, "server_url" ) == 0) var = &s->server_url; + else if (strcmp(key, "stream_start" ) == 0) var = &s->stream_start; + else if (strcmp(key, "stream_start_iso8601" ) == 0) var = &s->stream_start_iso8601; + else if (strcmp(key, "subtype" ) == 0) var = &s->subtype; + else if (strcmp(key, "title" ) == 0) var = &s->title; + else if (strcmp(key, "dummy" ) == 0) var = &s->dummy; + + if (var == NULL) { + fprintf(stdout, "Failed to handle icestats key \"%s\"\n", key); + exit(1); + } else { + *var = json_get_value(json); + } +} + +void +source_print(struct source *s) +{ + + puts("<div class=\"stream\">"); + + /* thumbnail and player */ + puts("<div>"); + puts("<img src=\"music.svg\" alt=\"music logo\" width=\"256\" height=\"256\"></a>"); + puts("<audio controls>"); + puts("<source src=\"https://theinterlude.live/autodj\" type=\"application/ogg\">"); + puts("Your browser does not support the audio element."); + puts("</audio>"); + puts("</div>"); + + /* info */ + puts("<table>"); + printf("<tr><th> Stream Name </th><th> %s </th></tr>\n", s->server_name); + printf("<tr><td> Stream Description </td><td> %s </td></tr>\n", s->server_description); + printf("<tr><td> Stream Type </td><td> %s </td></tr>\n", s->server_type); + /* printf("<tr><td> server_url </td><td> %s </td></tr>\n", s->server_url); */ + printf("<tr><td> Stream Start </td><td> %s </td></tr>\n", s->stream_start); + /* printf("<tr><td> stream_start_iso8601 </td><td> %s </td></tr>\n", s->stream_start_iso8601); */ + /* printf("<tr><td> audio_bitrate </td><td> %s </td></tr>\n", s->audio_bitrate); + printf("<tr><td> audio_channels </td><td> %s </td></tr>\n", s->audio_channels); + printf("<tr><td> audio_info </td><td> %s </td></tr>\n", s->audio_info); + printf("<tr><td> audio_samplerate </td><td> %s </td></tr>\n", s->audio_samplerate); */ + printf("<tr><td> Bitrate </td><td> %s </td></tr>\n", s->bitrate); + printf("<tr><td> Quality </td><td> %s </td></tr>\n", s->quality); + printf("<tr><td> Listeners (current) </td><td> %s </td></tr>\n", s->listeners); + printf("<tr><td> Listeners (peak) </td><td> %s </td></tr>\n", s->listener_peak); + printf("<tr><td> Genre </td><td> %s </td></tr>\n", s->genre); + /* printf("<tr><td> channels </td><td> %s </td></tr>\n", s->channels); + printf("<tr><td> ice_bitrate </td><td> %s </td></tr>\n", s->ice_bitrate); + printf("<tr><td> listenurl </td><td> %s </td></tr>\n", s->listenurl); + printf("<tr><td> samplerate </td><td> %s </td></tr>\n", s->samplerate); + printf("<tr><td> subtype </td><td> %s </td></tr>\n", s->subtype); + printf("<tr><td> title </td><td> %s </td></tr>\n", s->title); + printf("<tr><td> dummy </td><td> %s </td></tr>\n", s->dummy); */ + puts("</table>"); + + puts("</div>"); +} + +void +source_free(struct source *s) +{ + free(s->audio_bitrate); + free(s->audio_channels); + free(s->audio_info); + free(s->audio_samplerate); + free(s->bitrate); + free(s->channels); + free(s->genre); + free(s->ice_bitrate); + free(s->listener_peak); + free(s->listeners); + free(s->listenurl); + free(s->quality); + free(s->samplerate); + free(s->server_description); + free(s->server_name); + free(s->server_type); + free(s->server_url); + free(s->stream_start); + free(s->stream_start_iso8601); + free(s->subtype); + free(s->title); + free(s->dummy); +} + +void +print_navigation_bar(int index) +{ + char *names[] = { "Stream", "Music", "Contact", "About", "Login" }; + char *scripts[] = { "index.cgi", "music.cgi", "contact.cgi", "about.cgi", "login.cgi" }; + + puts("<ul>"); + puts("<li><a href=\"index.cgi\">IRCNow Radio</a></li>"); + + for (int i = 0; i < 5; i++) { + printf("<li><a "); + if (i == index) + printf("class=\"active\" "); + printf("href=\"%s\">%s</a></li>\n", scripts[i], names[i]); + } + + puts("</ul>"); +} + +void +json_handle(json_stream *json) +{ + if (json_next(json) != JSON_OBJECT) { + printf("ERROR: Invalid json key type.\n"); + exit(EXIT_FAILURE); + } + static int source_index = 0; + static int source_active = FALSE; + + while (json_peek(json) != JSON_OBJECT_END && !json_get_error(json)) { + json_next(json); + const char *key = json_get_string(json, NULL); + if (source_active == TRUE) { + source_set(&sources[source_index], json, key); + } else if (strcmp(key, "source") == 0) { + if (source_index == sources_length) { + sources_length++; + if ((sources = realloc(sources, sources_length * sizeof(struct source))) == NULL) { + printf("ERROR: relloc: %s\n", strerror(errno)); + exit(1); + } + } + source_active = TRUE; + json_handle(json); + source_index++; + source_active = FALSE; + } else if (strcmp(key, "icestats") == 0) { + json_handle(json); + } else { + icestats_set(&icestats, json, key); + } + } + json_next(json); +} + +int +main(int argc, char *argv[]) +{ + json_stream json; + char *buffer; + char *script_name; + + http_t* request; + http_status_t status = HTTP_STATUS_PENDING; + + puts("Status: 200 OK\r"); + puts("Content-Type: text/html\r"); + puts("\r"); + + request = http_get("http://theinterlude.live:8000/status-json.xsl", NULL); + if (!request) { + printf("Invalid request.\n"); + return 1; + } + + while (status == HTTP_STATUS_PENDING) + status = http_process(request); + if(status == HTTP_STATUS_FAILED) + { + printf("HTTP request failed (%d): %s.\n", request->status_code, request->reason_phrase); + http_release(request); + return 1; + } + buffer = (char*)request->response_data; + + json_open_string(&json, buffer); + json_set_streaming(&json, false); + json_handle(&json); + if (json_get_error(&json)) { + printf("ERROR: %lu: %s\n", json_get_lineno(&json), json_get_error(&json)); + exit(1); + } + + /* + * header + */ + if ((buffer = read_file("header.html")) == NULL) + exit(1); + printf("%s", buffer); + free(buffer); + + /* + * body + */ + script_name = getenv("SCRIPT_NAME"); + if (script_name == NULL) { + printf("ERROR: environment variable \"SCRIPT_NAME\" is not set.\n"); + return 1; + } + + if (strcmp(script_name, "/index.cgi") == 0) { + print_navigation_bar(0); + for (int i = 0; i < sources_length; i++) + source_print(&sources[i]); + } else if (strcmp(script_name, "/about.cgi") == 0) { + print_navigation_bar(3); + icestats_print(&icestats); + } + + /* + * footer + */ + if ((buffer = read_file("footer.html")) == NULL) + exit(1); + printf("%s", buffer); + free(buffer); + + /* cleanup */ + json_close(&json); + http_release(request); + if (sources != NULL) { + for (int i = 0; i < sources_length; i++) + source_free(&sources[i]); + free(sources); + } + icestats_free(&icestats); + + return 0; +} diff --git a/music.svg b/music.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"><g><path d="M50,5C25.1,5,5,25.1,5,50c0,24.9,20.1,45,45,45s45-20.1,45-45C95,25.1,74.9,5,50,5z M69.7,59c0,5-4.1,9.1-9.1,9.1 S51.5,64,51.5,59s4.1-9.1,9.1-9.1c1.7,0,3.2,0.4,4.6,1.2l-1.1-13.8l-17.2,3.1l1.7,22.5l0,0c0,0.2,0,0.3,0,0.5c0,5-4.1,9.1-9.1,9.1 s-9.1-4.1-9.1-9.1c0-5,4.1-9.1,9.1-9.1c1.7,0,3.2,0.4,4.6,1.2l-1.8-22.7l4.1-0.9l20.9-4.3l1,9.2l1.6,21.8l0,0 C69.7,58.6,69.7,58.8,69.7,59z"/></g></svg> +\ No newline at end of file diff --git a/pdjson b/pdjson @@ -0,0 +1 @@ +Subproject commit 67108d883061043e55d0fb13961ac1b6fc8a485c diff --git a/style.css b/style.css @@ -0,0 +1,97 @@ +body { + margin: 10px; +} + +ul { + list-style-type: none; + margin: 10px 0px; + padding: 0; + overflow: hidden; + border: 1px solid darkgrey; +} + +li { + float: left; +} + +li:last-child { + float: right; +} + +li a { + display: block; + color: dimgray; + text-align: center; + padding: 14px 16px; + text-decoration: none; +} + +li:first-child a { + font-weight: bold; + color: dodgerblue; +} + +li a:hover:not(.active) { + background-color: lightgrey; +} + +li a.active { + color: white; + background-color: dodgerblue; +} + +table.about { + margin-left: auto; + margin-right: auto; +} + +th, td { + padding: 5px 10px; + text-align: left; +} + +tr:nth-child(odd) { + background-color: gainsboro; +} + + +.stream { + display: table; + margin-right: auto; + margin-left: auto; +} + +.stream div { + float: left; + border: 1px solid darkgrey; +} + +.stream div img { + margin-left: auto; + margin-right: auto; + display: block; +} + +.stream div audio { + margin-left: auto; + margin-right: auto; + display: block; +} + +@media screen and (max-width: 500px) { + li, li:last-child { + float: none; + } + + .stream div { + float: none; + } +} + + +p.footer { + border: 1px solid darkgrey; + padding: 10px; + text-align: center; + color: dimgrey; +}