Microservices, whatever one may do, one of the most important concept that was invented in last years. It is possible to resist SOAP 2.0 as much as long, but sooner or later they will come for you and will turn you into their faith, or you will come to them and please to baptise yourself by fire and a sword. As well as any architectural concept microservices has cons. You need to include some authorization logic in requests from external systems or other microservices in each microservice. This logic can be directly "hardcoded" in microservice (and it isn't important that is a separate library), or can be delegated to other microservice, or can be declared. What "can be declared" means? For example, it is possible to agree that the special HTTP header, or some data structure with user information, comes in each request to each microservice. And data in this structure is needed to be absolutly trusted. All three options have cons, but within article we will talk about the last. For implementation usually API Gateway pattern is used:

Typically API Gateway restricts amount of requests to internal services, authorizes client's requests, does logging and audit, distributes requests across clients and transforms data if it is necessary. Even nginx can be used for API gateway. Consider an authorization function for user requests. If HTTP protocol is used, the standard practice considers adding of a certain token (not important as we received it) in Authorization header:

Authorization: Bearer <some token>

On gateway side this header is checked and then header is exchanged to another header that contains knowledge of the user to which the token was written out, for example its identifier. And this another token will be forwarded to internal microservices:

Customer: <id>

All it seems simple and clear, but the trouble is that Apache Thrift consists of several parts like a pie:

+-------------------------------------------+ | Server | | (single-threaded, event-driven etc) | +-------------------------------------------+ | Processor | | (compiler generated) | +-------------------------------------------+ | Protocol | | (JSON, compact, binary etc) | +-------------------------------------------+ | Transport | | (raw TCP, HTTP etc) | +-------------------------------------------+

In general we can't be got stuck on the protocol or transport. It is possible to select something one, of course. We could agree that we use only HTTP, but it restricts opportunities for changeover of transport and it forces to do certain external processors / filters in microservices (HTTP headers aren't native for Thrift).

At this moment the mad idea comes to mind: what if to use possibilities of the protocol for substitution an external authorization token by internal while passing request through our gateway?

convention over configuration

Ok, let's have service like this:

service InternalTestService { SomeReturnData getSomeData( 1: UserData userData, 2: RequestData requestData ) throws (1: SomeException e); }

UserData has something information about user. According on it microservice returns data for a certain user. And (I think you understand) this service can't be called from outworld. But what is possible? Maybe this:

service ExternalTestService { SomeReturnData getSomeData( 1: AuthToken authData, 2: RequestData requestData ) throws (1: SomeException e, 99: UnauthorizedException ue); }

As you see, difference beetween two services in the first argument and unauthorized exception as 99 field (I hope nobody needs more than 98 exceptions :) ). So we just need replace external token by internal and that's all.

entrails

Unfortunally Thrift has very scarse documentation. All guides including the best of them don't touch internal protocol implementation. Sad, but its clear. In 99% of cases developer doesn't need know this, but we need.

There are three most popular protocols:

Binary - just binary (strings, for example, transit as is at UTF-8)

Compact - binary but more compact

JSON - very specific JSON

Each of them has own implementation, that is incapsulated by API. Binary package of data from point of API's view looks like:

TMessage - meta information about message. It consists of name, method name, type sequence number of method in service. Type may be:

CALL = 1 - incoming message

REPLY = 2 - outcoming

EXCEPTION = 3 - no comments

ONEWAY = 4 - for void methods

Others are payload, that is packed at incoming message structure.

All presented protocols read data array byte by byte and store its current index to continue reading from the right place.

So we need algorithm as below:

Read TMessage Read message struct begining Read meta of first field in message Store current array index Read token Store current array index Exchange token on user information Serialize user information Create new array with three parts:

From begining till stored index from item 4 Serialized user information from item 8 From stored index from item 6 till message end

test it

No test, no code. So writes test first. For test we need follow services:

namespace java ru.aatarasoff.thrift exception SomeException { 1: string code } service ExternalTestService { SomeReturnData getSomeData( 1: AuthToken authData, 2: RequestData requestData ) throws (1: SomeException e); } service InternalTestService { SomeReturnData getSomeData( 1: UserData userData, 2: RequestData requestData ) throws (1: SomeException e); } struct SomeReturnData { 1: string someStringField, 2: i32 someIntField } struct RequestData { 1: string someStringField, 2: i32 someIntField } struct AuthToken { 1: string token, 2: i32 checksum } struct UserData { 1: string id }

Create and fill external service with data:

TMemoryBuffer externalServiceBuffer = new TMemoryBufferWithLength(1024); ExternalTestService.Client externalServiceClient = new ExternalTestService.Client(protocolFactory.getProtocol(externalServiceBuffer)); externalServiceClient.send_getSomeData( new AuthToken().setToken("sometoken").setChecksum(128), new RequestData().setSomeStringField("somevalue").setSomeIntField(8) );

TMemoryBufferWithLength - we need creating new class cause TMemoryBuffer has fatal defect. It cannot return actual array length. Instead of this it returns buffer length that can be more than message length, because some bytes are reserved.

Method send_getSomeData serialize message into the buffer.

We make the same with internal service:

internalServiceClient.send_getSomeData( new UserData().setId("user1"), new RequestData().setSomeStringField("somevalue").setSomeIntField(8) );

Next we create byte array with serialized data:

byte[] externalServiceMessage = Arrays.copyOf( externalServiceBuffer.getArray(), externalServiceBuffer.length() );

And finally we create our main class that would translate external message into internal: MessageTransalator.

public MessageTransalator(TProtocolFactory protocolFactory, AuthTokenExchanger authTokenExchanger) { this.protocolFactory = protocolFactory; this.authTokenExchanger = authTokenExchanger; } public byte[] process(byte[] thriftBody) throws TException { //some actions }

Token exchange implementation (AuthTokenExchanger) depends on a certain project needs, so we introduce interface:

public interface AuthTokenExchanger<T extends TBase, U extends TBase> { T createEmptyAuthToken(); U process(T authToken) throws TException; }

Method createEmptyAuthToken should return an object, that is presented empty token. MessageTransalator would fill it later. In method process we must implement token exchange. For our simple test implementation as below is enough:

@Override public AuthToken createEmptyAuthToken() { return new AuthToken(); } @Override public UserData process(AuthToken authToken) { if ("sometoken".equals(authToken.getToken())) { return new UserData().setId("user1"); } throw new RuntimeException("token is invalid"); }

Then add assertion:

assert.assertTrue( "Translated external message must be the same as internal message", Arrays.equals( new MessageTransalator( protocolFactory, new AuthTokenExchanger<AuthToken, UserData>() {} ).process(externalServiceMessage), internalServiceMessage ) )

Run tests, but they doesn't work. And it's so good!

greenlight

Implement process method according algorithm:

TProtocol protocol = createProtocol(thriftBody); int startPosition = findStartPosition(protocol); TBase userData = authTokenExchanger.process( extractAuthToken(protocol, authTokenExchanger.createEmptyAuthToken()) ); int endPosition = findEndPosition(protocol); return ArrayUtils.addAll( ArrayUtils.addAll( getSkippedPart(protocol, startPosition), serializeUserData(protocolFactory, userData) ), getAfterTokenPart(protocol, endPosition, thriftBody.length) );

We use TMemoryInputTransport transport that gives possibility to read direct from input buffer.

private TProtocol createProtocol(byte[] thriftBody) { return protocolFactory.getProtocol(new TMemoryInputTransport(thriftBody)); }

Implement methods that find boundaries of token data:

private int findStartPosition(TProtocol protocol) throws TException { skipMessageInfo(protocol); skipToFirstFieldData(protocol); return protocol.getTransport().getBufferPosition(); } private int findEndPosition(TProtocol protocol) throws TException { return protocol.getTransport().getBufferPosition(); } private void skipToFirstFieldData(TProtocol protocol) throws TException { protocol.readStructBegin(); protocol.readFieldBegin(); } private void skipMessageInfo(TProtocol protocol) throws TException { protocol.readMessageBegin(); }

Serialize user data:

TMemoryBufferWithLength memoryBuffer = new TMemoryBufferWithLength(1024); TProtocol protocol = protocolFactory.getProtocol(memoryBuffer); userData.write(protocol); return Arrays.copyOf(memoryBuffer.getArray(), memoryBuffer.length());

Run test, and...

sherlocking

So, tests for Binary and Compact protocol is passed, but not for JSON. What's going wrong? Debug and see difference beetween arrays that is compared:

//right JSON [1,"getSomeData",1,1,{"1":{"rec":{"1":{"str":"user1"}}},"2":{"rec":{"1":{"str":"somevalue"},"2":{"i32":8}}}}] //wrong JSON [1,"getSomeData",1,1,{"1":{"rec"{"1":{"str":"user1"}}},"2":{"rec":{"1":{"str":"somevalue"},"2":{"i32":8}}}}]

Don't see difference? But it is. After first "rec" symbol ":" is missed. We use one API but see different results. The solution came only after attentive reading a code of a class TJSONProtocol. It has field:

TJSONProtocol.JSONBaseContext context_ = new TJSONProtocol.JSONBaseContext();

This context stores different separators in stack while processing JSON-structure. And when it reads structure, it reads symbol ":", but it doesn't return separator back, cause our user data object doesn't have any context.

Add symbol manually in seriaizeUserData method:

if (protocol instanceof TJSONProtocol) { memoryBuffer.write(COLON, 0, 1); //":" }

Then run tests and see only green colors.

rise of exceptions

It's not the end. We've forgotten exception processing if authorization is failed. Ok, add unauthorize exception at the 99 position:

service ExternalTestService { SomeReturnData getSomeData( 1: AuthToken authData, 2: RequestData requestData ) throws (1: SomeException e, 99: UnauthorizedException ue); }

New method processError is needed to us.

public byte[] processError(TException exception) throws Exception

Thrift has two classes of exceptions that we can serialize into outcoming message. First is TApplicationException that is declared implicitly. Second is custom exceptions that is declared in throws part of service definition. So, if unexcpected exception is risen while authorizating, we should create message with TApplicationException. If user is not authorized and we know about it, we should create message with UnauthorizedException. Lets do it.

if (TApplicationException.class.equals(exception.getClass())) { protocol.writeMessageBegin(new TMessage(this.methodName, TMessageType.EXCEPTION, this.seqid)); ((TApplicationException) exception).write(protocol); protocol.writeMessageEnd(); } else { TStruct errorStruct = new TStruct(this.methodName + "_result"); TField errorField = new TField("exception", TType.STRUCT, (short) 99); protocol.writeMessageBegin(new TMessage(this.methodName, TMessageType.REPLY, this.seqid)); protocol.writeStructBegin(errorStruct); protocol.writeFieldBegin(errorField); exception.getClass().getMethod("write", TProtocol.class).invoke(exception, protocol); protocol.writeFieldEnd(); protocol.writeFieldStop(); protocol.writeStructEnd(); protocol.writeMessageEnd(); }

Some comments. According thrift protocol if TApplication is risen we should use TMessageType.EXCEPTION type of message. If custom exception is risen - TMessageType.REPLY.

Also we need introducing state inside our translator to store methodName and seqid that we should fill while parse TMessage.

example

Thats all. Now we can do something like this:

try { byte[] processed = messageTransalator.process(request.getContentData()); //do something } catch (TException e) { try { getResponse().getOutputStream().write(messageTransalator.processError(e)); } catch (Exception e1) { log.error("unexpected error", e1); } }

Full listing of MessageTranslator is here.

links

Github: https://github.com/aatarasoff/thrift-api-gateway-core

Bintray: https://bintray.com/aatarasoff/maven/thrift-api-gateway-core/view

snack peak

In next part we will build gateway on Spring stack.