DEV Community

Cover image for Developing and Using the jrest2 Library for HTTP/HTTPS Protocols
Misha Sterkhov
Misha Sterkhov

Posted on

Developing and Using the jrest2 Library for HTTP/HTTPS Protocols

Introduction

Hello everyone! Today, I want to introduce you to my pet project - the jrest2 library, which provides a complete implementation of HTTP/HTTPS protocols from scratch. This library is already available on GitHub and published on jitpack.io with released versions. In this post, I will talk about the features of jrest2, its usage, and how you can get started with it.

What is jrest2?

This library implements a complete HTTP/HTTPS protocol from scratch. It currently supports the following versions of the HTTP protocol:

VERSION SUPPORTED
HTTP/1.0 ✅ Supported
HTTP/1.1 ✅ Supported
HTTP/2 ⛔ Not Supported
HTTP/3 ⛔ Not Supported

On top of a completely self-contained protocol implementation is built a layered API structure with different configurations and ways of initializing and applying data to the connection flow.

Also, this library implements the ability to apply and read SSL certificates for both the client part of the connection (read/write) and the server part of the connection.

JitPack

To use the data library in your project, you need to prescribe a dependency.
Below is an example of how to use the dependency for different build systems:

Maven

Dependency block for Maven structure project:

<repositories>
    <repository>
        <id>jitpack.io</id>
        <url>https://jitpack.io</url>
    </repository>
</repositories>
Enter fullscreen mode Exit fullscreen mode
<dependency>
    <groupId>com.github.MikhailSterkhov</groupId>
    <artifactId>jrest2</artifactId>
    <version>${jrest.version}</version>
</dependency>
Enter fullscreen mode Exit fullscreen mode

Gradle

Dependency block for Gradle structure project:

repositories {
    mavenCentral()
    maven { url 'https://jitpack.io' }
}
Enter fullscreen mode Exit fullscreen mode
compileOnly 'com.github.MikhailSterkhov:jrest2:${jrest.version}'
Enter fullscreen mode Exit fullscreen mode

Features and Advantages of jrest2

  • Complete implementation of HTTP/HTTPS protocols
  • Binary executable client files
  • Support for SSL certificates for secure connections
  • Layered API structure for flexible configuration and usage
  • Easy integration and use

Getting Started

  • Installation: Install the library using jitpack.io. Installation instructions can be found in the GitHub repository.
  • Documentation: Refer to the documentation in the README file of the repository.
  • Usage Examples: Explore usage examples to understand how to get started with the library.

WIKI

Here are some examples of how to use the functionality of the top-level API to interact with the HTTP protocol

CLIENTS

Let's start with the client part of the connection.

First of all, it is necessary to determine what type of channel we will work with and what we need.
For this purpose, the client factory is implemented com.jrest.http.client.HttpClients:

// variants of Sockets implementation:
HttpClients.createSocketClient(ExecutorService);
HttpClients.createSocketClient(ExecutorService, boolean keepAlive);
HttpClients.createSocketClient(ExecutorService, int connectTimeout);
HttpClients.createSocketClient(ExecutorService, int connectTimeout, boolean keepAlive);
HttpClients.createSocketClient();
HttpClients.createSocketClient(boolean keepAlive);
HttpClients.createSocketClient(int connectTimeout);
HttpClients.createSocketClient(int connectTimeout, boolean keepAlive);

// variants of HttpURLConnection implementation:
HttpClients.createClient(ExecutorService);
HttpClients.createClient(ExecutorService, int connectTimeout);
HttpClients.createClient(ExecutorService, int connectTimeout, int readTimeout);
HttpClients.createClient();

// variants of binary http-client wrappers implementation:
HttpClients.binary(HttpClient httpClient, Reader reader);
HttpClients.binary(HttpClient httpClient, InputStream inputStream);
HttpClients.binary(HttpClient httpClient, File file) throws IOException;
HttpClients.binary(HttpClient httpClient, Path path) throws IOException;
Enter fullscreen mode Exit fullscreen mode

Suppose we decide to implement a Socket connection with the ability
to make requests asynchronously, and we set its connect-timeout = 3000ms,
and keep-alive = false to automatically close the socket after the request is executed.

Example:

HttpClient httpClient = HttpClients.createSocketClient(
        Executors.newCachedThreadPool(), connectTimeout, keepAlive);
Enter fullscreen mode Exit fullscreen mode

Next, we can already call any of more than a hundred functions
to fulfill the request and get an instant response.

For example, send a GET request to the public web page https://catfact.ninja/fact,
from where we will get the result as JSON with a random fact about cats :D

Example:

httpClient.executeGet("https://catfact.ninja/fact")
        .ifPresent(response -> {

            HttpProtocol protocol = response.getProtocol(); // HTTP/1.1
            String statusLine = response.getHeaders().getFirst(null); // HTTP/1.1 200 OK

            ResponseCode responseCode = response.getCode();

            if (!responseCode.isSuccessful()) {
                throw new RuntimeException("Content not found - " + responseCode);
            }

            System.out.println(httpResponse.getContent().getText());
            // {"fact":"A cat usually has about 12 whiskers on each side of its face.","length":61}
        });
Enter fullscreen mode Exit fullscreen mode

The client API also implements one cool thing, thanks to which
you can simplify the implementation of HTTP requests as much
as possible by writing just a few words in the code to do it!

BINARY FILES

Basic information you need to know when writing a binary:

The first lines are general Properties that can be applied in the queries themselves.
The most important among them is the host = ... line.
It is mandatory in application, and indicates the main address part of the URL that will be accessed.

Next after Properties are the functions.
Their structure is described by the following signature:

<name>: <METHOD> /<URI> {
  ...
}
Enter fullscreen mode Exit fullscreen mode

The content of the function is divided into several keywords
that can be used within the body of the function:

  • head: One of the headings of the query
  • attr: URI attributes that will be appended to the URL with a '?' (e.g. /employee?id=1, where 'id' is an attribute)
  • body: Request body
    • length: The size of the body to be sent under the guise of the 'Content-Length' header
    • type: The body type that will be sent under the 'Content-Type' header appearance
    • text: Header content as Hyper text

The values that come after the keyword are mostly
in the Properties format.

Example binary (/catfacts.restbin):

host = https://catfact.ninja/
randomCatFact = A cat usually has about 12 whiskers on each side of its face.
userAgent = JRest-Binary
contentType = application/json

getFact: GET /fact {
    head User-Agent = ${userAgent}
    head Accept = text/plain
    attr length = 50
}

createFact: POST /fact {
    head User-Agent = ${userAgent}
    body {
        type = ${contentType}
        text = {"fact": "${randomCatFact}", "length": 61}
    }
}
Enter fullscreen mode Exit fullscreen mode

After successfully writing our binary, we can start executing it by first creating a BinaryHttpClient
via the factory: HttpClients.createBinaryClient(HttpClient, <path-to-binary>)

BinaryHttpClient has 2 additional methods that distinguish
it from other HTTP clients: executeBinary(name) and executeBinaryAsync(name).

Example (Java Client):

BinaryHttpClient httpClient = HttpClients.binary(
        HttpClients.createClient(),
        getClass().getResourceAsStream("/catfacts.restbin"));

httpClient.executeBinary("getFact")
        .ifPresent(httpResponse -> {

                HttpProtocol protocol = response.getProtocol(); // HTTP/1.1
                String statusLine = response.getHeaders().getFirst(null); // HTTP/1.1 200 OK

                ResponseCode responseCode = response.getCode();

                if (!responseCode.isSuccessful()) {
                    throw new RuntimeException("Content not found - " + responseCode);
                }

                System.out.println(httpResponse.getContent().getText());
                // {"fact":"A cat usually has about 12 whiskers on each side of its face.","length":61}
        });
Enter fullscreen mode Exit fullscreen mode

And also for executing binary functions you can use input properties to
customize the request from the outside.

Here is an example.

Example (binary with inputs):

host = http://localhost:8080/

get_employee: GET /employee {
    attr id = ${input.employee_id}
}

post_employee: POST /employee {
    body {
        text = ${input.employee}
    }
}
Enter fullscreen mode Exit fullscreen mode

Here we can notice the ${input.employee_id} property, we expect
to get it from the client.
Below I will give an example of applying it to an executable file.

Example (Java Client):

BinaryHttpClient httpClient = HttpClients.binary(
        HttpClients.createClient(),
        HttpClientBinaryUrlTest.class.getResourceAsStream("/employee.restbin"));

httpClient.executeBinary("get_employee",
                Attributes.newAttributes().with("employee_id", 567))
        .ifPresent(httpResponse -> {

            System.out.println(httpResponse.getContent().getText());
            // {"id":567,"firstName":"Piter","lastName":"Harrison","jobInfo":{"company":"Microsoft Corporation","website":"https://www.microsoft.com/","profession":"Developer C#","salary":3500}}
        });
Enter fullscreen mode Exit fullscreen mode

SERVERS

To create a server and initialize it, things are a bit more complicated,
but only because it is a server, and it needs full business logic.

Let's start with the simplest creation of the server as an object,
form it from the parameters we need:

Example:

HttpServer httpServer = HttpServer.builder()
        .build();
Enter fullscreen mode Exit fullscreen mode

Several components are required to properly initialize the server,
each of which affects a specific part of the software part:

PARAMETER TYPE USAGE EXAMPLE DESCRIPTION
InetSocketAddress .socketAddress(new InetSocketAddress(80)) Server bindings address and port.
ExecutorService .executorService(Executors.newCachedThreadPool()) Service to execute threads, if not specified, a cached thread pool is used. (CachedThreadPool is used by default if null is specified)
HttpProtocol .protocol(HttpProtocol.HTTP_1_0) HTTP protocol, by default HTTP/1.1.
SslContent .ssl(SslContent.builder()...) SSL settings for HTTPS, if null, HTTP is used.
HttpListener .notFoundListener(httpRequest -> ...) Listener to handle requests that have not found an appropriate handler. (404 Not Found)

Now based on this information let's try to implement a server
that supports HTTP/1.1 protocol without SSL certificates

Example;

HttpServer httpServer = HttpServer.builder()
        .socketAddress(new InetSocketAddress(8080))
        .build();

httpServer.bind();
Enter fullscreen mode Exit fullscreen mode

We can now intercept requests that come to us by skipping or sending
back some kind of response. Request listeners can be either asynchronous
or synchronous.

Examples:

httpServer.registerListener(httpRequest -> {

    System.out.println(httpRequest);
    return HttpResponse.ok();
});
Enter fullscreen mode Exit fullscreen mode
httpServer.registerAsyncListener("/employee", httpRequest -> 
        HttpResponse.ok(Content.fromEntity(
                Employee.builder()
                        .id(567))
                        .jobInfo(EmployeeJob.builder()
                                .company("Microsoft Corporation")
                                .website("https://www.microsoft.com/")
                                .profession("Developer C#")
                                .salary(3500)
                                .build())
                        .firstName("Piter")
                        .lastName("Harrison")
                        .build())));
Enter fullscreen mode Exit fullscreen mode

Realizing perfectly well that handling each such request in the form of
registering them through listeners would not be entirely convenient,
especially in the case where there may be quite a few endpoints.

Therefore, the MVC module was implemented, providing a more flexible
and readable implementation of HTTP requests interception.

For the example, let's create an instance that will be a repository
of HTTP requests for our server and register it:

@HttpServer
public class EmployeesHttpRepository {
}
Enter fullscreen mode Exit fullscreen mode
HttpServer httpServer = HttpServer.builder()
        .socketAddress(new InetSocketAddress(8080))
        .build();

httpServer.registerRepository(new EmployeesHttpRepository()); // <----

httpServer.bind();
Enter fullscreen mode Exit fullscreen mode

Now we can proceed to the nuances of its further construction,
because this is where the most interesting things begin!

To implement some endpoint, we have several annotations that
allow us to do so:

  • @HttpRequestMapping
  • @HttpGet
  • @HttpPost
  • @HttpDelete
  • @HttpPut
  • @HttpPatch
  • @HttpConnect
  • @HttpHead
  • @HttpOptions
  • @HttpTrace

Let's start with the simplest one and implement the processing
of GET request to the path /employee with the possibility of
specifying the identifier of the Employee we need
through attributes (for example, /employee?id=567)

Example:

@HttpServer
public class EmployeesHttpRepository {

    @HttpGet("/employee")
    public HttpResponse getEmployee(HttpRequest request) {
        Attributes attributes = request.getAttributes();
        Optional<Integer> attributeIdOptional = attributes.getInteger("id");

        if (!attributeIdOptional.isPresent()) {
            return ...;
        }
        return HttpResponse.ok(Content.fromEntity(
                Employee.builder()
                        .id(attributeIdOptional.get())
                        .jobInfo(EmployeeJob.builder()
                                .company("Microsoft Corporation")
                                .website("https://www.microsoft.com/")
                                .profession("Developer C#")
                                .salary(3500)
                                .build())
                        .firstName("Piter")
                        .lastName("Harrison")
                        .build()));
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, suppose in the line where we check for the passed
attribute !attributeIdOptional.isPresent() we need to pass
the processing of this request to the NotFoundListener that
was specified when HttpServer was initialized.

To do this, we need to return the HttpListener.SKIP_ACTION constant:

if (!attributeIdOptional.isPresent()) {
    return HttpListener.SKIP_ACTION;
}
Enter fullscreen mode Exit fullscreen mode

But in this case it would be more correct to return
a 400 Bad Request error, and for this we can call
the function from HttpResponse in one of two ways:

if (!attributeIdOptional.isPresent()) {
    return HttpResponse.builder()
                .code(ResponseCode.BAD_REQUEST)
                .build();
}
Enter fullscreen mode Exit fullscreen mode

or just:

if (!attributeIdOptional.isPresent()) {
    return HttpResponse.badRequest();
}
Enter fullscreen mode Exit fullscreen mode

Now when we query the http://localhost:8080/employee?id=567
page, we get the following result:

browse


ADDITIONAL HTTP-SERVER FEATURES

But that's not all!

The HTTP server repository has several other features that add
some flexibility and convenience in exceptional cases of library use.

Let's go through some of them!


Annotation @HttpBeforeExecution:

Annotation allows you to pre-validate an incoming request,
change some parameters, or perform additional processes before
processing:

Example:

@HttpBeforeExecution
public void before(HttpRequest httpRequest) {
    httpRequest.setHeaders(
            httpRequest.getHeaders()
                    .set(Headers.Def.USER_AGENT, "Mikhail Sterkhov")
    );
}
Enter fullscreen mode Exit fullscreen mode

Annotation @HttpAsync:

You can hang this annotation on literally any method that handles queries.

It implements some kind of wrapper of the handler in separate threads,
if it is really necessary for the implementation.

Example:

@HttpAsync
@HttpPatch("/employee")
public HttpResponse patchEmployee(HttpRequest request) {
    Employee employee = request.getContent().toEntity(Employee.class);
    try {
        employeesService.patch(employee);
        return HttpResponse.ok();
    } 
    catch (EmployeeException exception) {
        return HttpResponse.internalError();
    }
}
Enter fullscreen mode Exit fullscreen mode

Annotation @HttpAuthenticator:

To verify requests via authorization, you can use a fully dedicated
functionality for this purpose, which provides you with an entire
model API to implement HTTP request authentication.

For examples:

Basic

private static final Token.UsernameAndPassword APPROVAL_TOKEN =
            Token.UsernameAndPassword.of("jrest_admin", "password");

@HttpAuthenticator
public ApprovalResult approveAuth(UnapprovedRequest request) {
    return request.basicAuthenticate(APPROVAL_TOKEN);
}
Enter fullscreen mode Exit fullscreen mode

Bearer

private static final String GENERATED_API_TOKEN = TokenGenerator.defaults(30).generate();

@HttpAuthenticator
public ApprovalResult approveAuth(UnapprovedRequest request) {
    return request.bearerAuthenticate(GENERATED_API_TOKEN);
}
Enter fullscreen mode Exit fullscreen mode

Bearer (custom)

private static final String GENERATED_API_TOKEN = TokenGenerator.defaults(30).generate();

@HttpAuthenticator
public ApprovalResult approveAuth(UnapprovedRequest request) {
    if (request.getAuthentication() != Authentication.BEARER) {
        return ApprovalResult.forbidden();
    }

    HttpCredentials credentials = request.getRequestCredentials();
    Token token = credentials.getToken();

    if (Objects.equals(token.getValue(), GENERATED_API_TOKEN)) {
        return ApprovalResult.approve();
    }

    return ApprovalResult.forbidden();
}
Enter fullscreen mode Exit fullscreen mode

In case you want to apply authorization
without using classes with the @HttpServer annotation,
there are ways to do it too, let's look at a few of them:

HttpServer httpServer = ...;
Token.UsernameAndPassword credentials = Token.UsernameAndPassword.of("jrest_admin", "password")
httpServer.addAuthenticator(HttpBasicAuthenticator.of(credentials));
Enter fullscreen mode Exit fullscreen mode
HttpServer httpServer = ...;
httpServer.addAuthenticator(HttpBearerAuthenticator.of(
        Arrays.asList(
                "c9636ffe984e41d7b03c1b42d72402210aa9e64f2bedd6064a70416ba5e",
                "f9af04c492c35e468100f9eead215903a67cdc3168fd95d78ca9bd4f9173",
                "fe332dc685090ddbbf1a7569f22ac2bbe0f13644dbcd3f77cbeaf8f86c47"));
Enter fullscreen mode Exit fullscreen mode
HttpServer httpServer = ...;
httpServer.addAuthenticator(HttpBearerAuthenticator.single(
        "c9636ffe984e41d7b03c1b42d72402210aa9e64f2bedd6064a70416ba5e"));
Enter fullscreen mode Exit fullscreen mode
HttpServer httpServer = ...;
httpServer.addAuthenticator(Authentication.DIGEST, (unapprovedRequest) -> { return ApprovalResult.forbidden(); });
Enter fullscreen mode Exit fullscreen mode

Annotation @HttpNotAuthorized:

With this annotation, you can mark methods with HTTP requests
to be excluded from the request authentication process.

Example:

@HttpNotAuthorized
@HttpGet("/employee")
public HttpResponse doGet(HttpRequest request) {
    // request handle logic...
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

I hope the jrest2 library will be useful for your project. If you have any questions or suggestions, feel free to open an issue in the repository or contact me directly. Your feedback and contributions are always welcome!

Link to the jrest2 repository on GitHub

Top comments (0)