diff --git a/.gitignore b/.gitignore index 4a40b62..d622c7c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ .build build -gmni +/gmni gmnlm *.1 *.o diff --git a/include/gmni/gmni.h b/include/gmni/gmni.h new file mode 100644 index 0000000..7e27b48 --- /dev/null +++ b/include/gmni/gmni.h @@ -0,0 +1,164 @@ +#ifndef GEMINI_CLIENT_H +#define GEMINI_CLIENT_H +#include +#include +#include +#include + +enum gemini_result { + GEMINI_OK, + GEMINI_ERR_OOM, + GEMINI_ERR_INVALID_URL, + GEMINI_ERR_NOT_GEMINI, + GEMINI_ERR_RESOLVE, + GEMINI_ERR_CONNECT, + GEMINI_ERR_SSL, + GEMINI_ERR_SSL_VERIFY, + GEMINI_ERR_IO, + GEMINI_ERR_PROTOCOL, +}; + +enum gemini_status { + GEMINI_STATUS_INPUT = 10, + GEMINI_STATUS_SENSITIVE_INPUT = 11, + GEMINI_STATUS_SUCCESS = 20, + GEMINI_STATUS_REDIRECT_TEMPORARY = 30, + GEMINI_STATUS_REDIRECT_PERMANENT = 31, + GEMINI_STATUS_TEMPORARY_FAILURE = 40, + GEMINI_STATUS_SERVER_UNAVAILABLE = 41, + GEMINI_STATUS_CGI_ERROR = 42, + GEMINI_STATUS_PROXY_ERROR = 43, + GEMINI_STATUS_SLOW_DOWN = 44, + GEMINI_STATUS_PERMANENT_FAILURE = 50, + GEMINI_STATUS_NOT_FOUND = 51, + GEMINI_STATUS_GONE = 52, + GEMINI_STATUS_PROXY_REQUEST_REFUSED = 53, + GEMINI_STATUS_BAD_REQUEST = 59, + GEMINI_STATUS_CLIENT_CERTIFICATE_REQUIRED = 60, + GEMINI_STATUS_CERTIFICATE_NOT_AUTHORIZED = 61, + GEMINI_STATUS_CERTIFICATE_NOT_VALID = 62, +}; + +enum gemini_status_class { + GEMINI_STATUS_CLASS_INPUT = 10, + GEMINI_STATUS_CLASS_SUCCESS = 20, + GEMINI_STATUS_CLASS_REDIRECT = 30, + GEMINI_STATUS_CLASS_TEMPORARY_FAILURE = 40, + GEMINI_STATUS_CLASS_PERMANENT_FAILURE = 50, + GEMINI_STATUS_CLASS_CLIENT_CERTIFICATE_REQUIRED = 60, +}; + +struct gemini_response { + enum gemini_status status; + char *meta; + + // Response body may be read from here if appropriate: + BIO *bio; + + // Connection state + SSL_CTX *ssl_ctx; + SSL *ssl; + int fd; +}; + +struct gemini_options { + // If NULL, an SSL context will be created. If unset, the ssl field + // must also be NULL. + SSL_CTX *ssl_ctx; + + // If ai_family != AF_UNSPEC (the default value on most systems), the + // client will connect to this address and skip name resolution. + struct addrinfo *addr; + + // If non-NULL, these hints are provided to getaddrinfo. Useful, for + // example, to force IPv4/IPv6. + struct addrinfo *hints; +}; + +// Requests the specified URL via the gemini protocol. If options is non-NULL, +// it may specify some additional configuration to adjust client behavior. +// +// Returns a value indicating the success of the request. +// +// Caller must call gemini_response_finish afterwards to clean up resources +// before exiting or re-using it for another request. +enum gemini_result gemini_request(const char *url, + struct gemini_options *options, + struct gemini_response *resp); + +// Must be called after gemini_request in order to free up the resources +// allocated during the request. +void gemini_response_finish(struct gemini_response *resp); + +// Returns a user-friendly string describing an error. +const char *gemini_strerr(enum gemini_result r, struct gemini_response *resp); + +// Returns the given URL with the input response set to the specified value. +// The caller must free the string. +char *gemini_input_url(const char *url, const char *input); + +// Returns the general response class (i.e. with the second digit set to zero) +// of the given Gemini status code. +enum gemini_status_class gemini_response_class(enum gemini_status status); + +enum gemini_tok { + GEMINI_TEXT, + GEMINI_LINK, + GEMINI_PREFORMATTED_BEGIN, + GEMINI_PREFORMATTED_END, + GEMINI_PREFORMATTED_TEXT, + GEMINI_HEADING, + GEMINI_LIST_ITEM, + GEMINI_QUOTE, +}; + +struct gemini_token { + enum gemini_tok token; + + // The token field determines which of the union members is valid. + union { + char *text; + + struct { + char *text; + char *url; // May be NULL + } link; + + char *preformatted; + + struct { + char *title; + int level; // 1, 2, or 3 + } heading; + + char *list_item; + char *quote_text; + }; +}; + +struct gemini_parser { + BIO *f; + char *buf; + size_t bufsz; + size_t bufln; + bool preformatted; +}; + +// Initializes a text/gemini parser which reads from the specified BIO. +void gemini_parser_init(struct gemini_parser *p, BIO *f); + +// Finishes this text/gemini parser and frees up its resources. +void gemini_parser_finish(struct gemini_parser *p); + +// Reads the next token from a text/gemini file. +// +// Returns 0 on success, 1 on EOF, and -1 on failure. +// +// Caller must call gemini_token_finish before exiting or re-using the token +// parameter. +int gemini_parser_next(struct gemini_parser *p, struct gemini_token *token); + +// Must be called after gemini_next to free up resources for the next token. +void gemini_token_finish(struct gemini_token *token); + +#endif diff --git a/include/gmni/tofu.h b/include/gmni/tofu.h new file mode 100644 index 0000000..a88167b --- /dev/null +++ b/include/gmni/tofu.h @@ -0,0 +1,49 @@ +#ifndef GEMINI_TOFU_H +#define GEMINI_TOFU_H +#include +#include +#include +#include + +enum tofu_error { + TOFU_VALID, + // Expired, wrong CN, etc. + TOFU_INVALID_CERT, + // Cert is valid but we haven't seen it before + TOFU_UNTRUSTED_CERT, + // Cert is valid but we already trust another cert for this host + TOFU_FINGERPRINT_MISMATCH, +}; + +enum tofu_action { + TOFU_ASK, + TOFU_FAIL, + TOFU_TRUST_ONCE, + TOFU_TRUST_ALWAYS, +}; + +struct known_host { + char *host, *fingerprint; + time_t expires; + int lineno; + struct known_host *next; +}; + +// Called when the user needs to be prompted to agree to trust an unknown +// certificate. Return true to trust this certificate. +typedef enum tofu_action (tofu_callback_t)(enum tofu_error error, + const char *fingerprint, struct known_host *host, void *data); + +struct gemini_tofu { + char known_hosts_path[PATH_MAX+1]; + struct known_host *known_hosts; + int lineno; + tofu_callback_t *callback; + void *cb_data; +}; + +void gemini_tofu_init(struct gemini_tofu *tofu, + SSL_CTX *ssl_ctx, tofu_callback_t *cb, void *data); +void gemini_tofu_finish(struct gemini_tofu *tofu); + +#endif diff --git a/include/gmni/url.h b/include/gmni/url.h new file mode 100644 index 0000000..155fd55 --- /dev/null +++ b/include/gmni/url.h @@ -0,0 +1,103 @@ +#ifndef URLAPI_H +#define URLAPI_H +/*************************************************************************** + * _ _ ____ _ + * Project ___| | | | _ \| | + * / __| | | | |_) | | + * | (__| |_| | _ <| |___ + * \___|\___/|_| \_\_____| + * + * Copyright (C) 2018, Daniel Stenberg, , et al. + * + * This software is licensed as described in the file COPYING, which + * you should have received as part of this distribution. The terms + * are also available at https://curl.haxx.se/docs/copyright.html. + * + * You may opt to use, copy, modify, merge, publish, distribute and/or sell + * copies of the Software, and permit persons to whom the Software is + * furnished to do so, under the terms of the COPYING file. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ***************************************************************************/ + +/* the error codes for the URL API */ +typedef enum { + CURLUE_OK, + CURLUE_BAD_HANDLE, /* 1 */ + CURLUE_BAD_PARTPOINTER, /* 2 */ + CURLUE_MALFORMED_INPUT, /* 3 */ + CURLUE_BAD_PORT_NUMBER, /* 4 */ + CURLUE_UNSUPPORTED_SCHEME, /* 5 */ + CURLUE_URLDECODE, /* 6 */ + CURLUE_OUT_OF_MEMORY, /* 7 */ + CURLUE_USER_NOT_ALLOWED, /* 8 */ + CURLUE_UNKNOWN_PART, /* 9 */ + CURLUE_NO_SCHEME, /* 10 */ + CURLUE_NO_USER, /* 11 */ + CURLUE_NO_PASSWORD, /* 12 */ + CURLUE_NO_OPTIONS, /* 13 */ + CURLUE_NO_HOST, /* 14 */ + CURLUE_NO_PORT, /* 15 */ + CURLUE_NO_QUERY, /* 16 */ + CURLUE_NO_FRAGMENT /* 17 */ +} CURLUcode; + +typedef enum { + CURLUPART_URL, + CURLUPART_SCHEME, + CURLUPART_USER, + CURLUPART_PASSWORD, + CURLUPART_OPTIONS, + CURLUPART_HOST, + CURLUPART_PORT, + CURLUPART_PATH, + CURLUPART_QUERY, + CURLUPART_FRAGMENT +} CURLUPart; + +#define CURLU_PATH_AS_IS (1<<4) /* leave dot sequences */ +#define CURLU_DISALLOW_USER (1<<5) /* no user+password allowed */ +#define CURLU_URLDECODE (1<<6) /* URL decode on get */ +#define CURLU_URLENCODE (1<<7) /* URL encode on set */ +#define CURLU_APPENDQUERY (1<<8) /* append a form style part */ + +typedef struct Curl_URL CURLU; + +/* + * curl_url() creates a new CURLU handle and returns a pointer to it. + * Must be freed with curl_url_cleanup(). + */ +struct Curl_URL *curl_url(void); + +/* + * curl_url_cleanup() frees the CURLU handle and related resources used for + * the URL parsing. It will not free strings previously returned with the URL + * API. + */ +void curl_url_cleanup(struct Curl_URL *handle); + +/* + * curl_url_dup() duplicates a CURLU handle and returns a new copy. The new + * handle must also be freed with curl_url_cleanup(). + */ +struct Curl_URL *curl_url_dup(struct Curl_URL *in); + +/* + * curl_url_get() extracts a specific part of the URL from a CURLU + * handle. Returns error code. The returned pointer MUST be freed with + * free() afterwards. + */ +CURLUcode curl_url_get(struct Curl_URL *handle, CURLUPart what, + char **part, unsigned int flags); + +/* + * curl_url_set() sets a specific part of the URL in a CURLU handle. Returns + * error code. The passed in string will be copied. Passing a NULL instead of + * a part string, clears that part. + */ +CURLUcode curl_url_set(struct Curl_URL *handle, CURLUPart what, + const char *part, unsigned int flags); + +#endif