package com.webpieces.http2engine.impl.shared;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import org.webpieces.data.api.DataWrapper;
import org.webpieces.util.logging.Logger;
import org.webpieces.util.logging.LoggerFactory;
import com.webpieces.hpack.api.HpackParser;
import com.webpieces.hpack.api.UnmarshalState;
import com.webpieces.http2engine.api.ConnectionClosedException;
import com.webpieces.http2engine.api.client.Http2Config;
import com.webpieces.http2parser.api.ConnectionException;
import com.webpieces.http2parser.api.ParseFailReason;
import com.webpieces.http2parser.api.StreamException;
import com.webpieces.http2parser.api.dto.GoAwayFrame;
import com.webpieces.http2parser.api.dto.PingFrame;
import com.webpieces.http2parser.api.dto.PriorityFrame;
import com.webpieces.http2parser.api.dto.SettingsFrame;
import com.webpieces.http2parser.api.dto.WindowUpdateFrame;
import com.webpieces.http2parser.api.dto.lib.Http2Msg;
import com.webpieces.http2parser.api.dto.lib.Http2Setting;
import com.webpieces.http2parser.api.dto.lib.PartialStream;
import com.webpieces.http2parser.api.dto.lib.SettingsParameter;
public class Level3ParsingAndRemoteSettings {
private static final Logger log = LoggerFactory.getLogger(Level3ParsingAndRemoteSettings.class);
private HpackParser lowLevelParser;
private UnmarshalState parsingState;
private Level7MarshalAndPing marshalLayer;
private String id;
private Level6RemoteFlowControl remoteFlowControl;
private Level4AbstractStreamMgr level3StreamInit;
private HeaderSettings remoteSettings;
private HeaderSettings localSettings;
public Level3ParsingAndRemoteSettings(
Level4AbstractStreamMgr level3StreamInit,
Level6RemoteFlowControl level5FlowControl,
Level7MarshalAndPing level6NotifyListener,
HpackParser lowLevelParser,
Http2Config config,
HeaderSettings remoteSettings
) {
this.localSettings = config.getLocalSettings();
this.remoteSettings = remoteSettings;
this.id = config.getId();
this.level3StreamInit = level3StreamInit;
this.remoteFlowControl = level5FlowControl;
this.marshalLayer = level6NotifyListener;
this.lowLevelParser = lowLevelParser;
parsingState = lowLevelParser.prepareToUnmarshal(4096, localSettings.getHeaderTableSize(), localSettings.getMaxFrameSize());
}
/**
* NOT thread safe!!! BUT channelmanager keeps everything virtually thread-safe (ie. it's all going to come in order)
* @return
*/
public void parse(DataWrapper newData) {
CompletableFuture<Void> future;
try {
future = parseImpl(newData);
} catch(Throwable t) {
future = new CompletableFuture<Void>();
future.completeExceptionally(t);
}
future.handle((resp, t) -> handleError(resp, t));
}
private Void handleError(Object object, Throwable e) {
if(e == null)
return null;
else if(e instanceof ConnectionClosedException) {
log.trace(() -> "Normal exception since we are closing and they do not know yet", e);
} else if(e instanceof StreamException) {
log.error("shutting the stream down due to error", e);
level3StreamInit.sendRstToServerAndClient((StreamException) e).exceptionally( t -> logExc("stream", t));
} else if(e instanceof ConnectionException) {
log.error("shutting the connection down due to error", e);
level3StreamInit.sendClientResetsAndSvrGoAway((ConnectionException) e).exceptionally( t -> logExc("connection", t)); //send GoAway
} else {
log.error("shutting the connection down due to error", e);
level3StreamInit.sendClientResetsAndSvrGoAway(new ConnectionException(ParseFailReason.BUG, 0, e.getMessage(), e)).exceptionally( t -> logExc("connection", t)); //send GoAwa
}
return null;
}
private Void logExc(String thing, Throwable t) {
log.error("error trying to close "+thing, t);
return null;
}
public CompletableFuture<Void> parseImpl(DataWrapper newData) {
parsingState = lowLevelParser.unmarshal(parsingState, newData);
List<Http2Msg> parsedMessages = parsingState.getParsedFrames();
CompletableFuture<Void> future = CompletableFuture.completedFuture((Void)null);
for(Http2Msg lowLevelFrame : parsedMessages) {
CompletableFuture<Void> f = process(lowLevelFrame);
future = future.thenCompose(s -> f);
}
return future;
}
public CompletableFuture<Void> process(Http2Msg msg) {
log.info(id+"frame from socket="+msg);
if(msg instanceof PriorityFrame) {
return level3StreamInit.sendPriorityFrame((PriorityFrame)msg);
} else if(msg instanceof PartialStream) {
//we may be sending to client app or server app depending on if this is serverside or clientside
return level3StreamInit.sendPayloadToApp((PartialStream) msg);
} else if(msg instanceof GoAwayFrame) {
return marshalLayer.sendControlFrameToClient(msg);
} else if(msg instanceof PingFrame) {
return marshalLayer.processPing((PingFrame)msg);
} else if(msg instanceof SettingsFrame) {
return processHttp2SettingsFrame((SettingsFrame) msg);
} else if(msg instanceof WindowUpdateFrame){
return level3StreamInit.updateWindowSize((WindowUpdateFrame)msg);
} else
throw new IllegalArgumentException("Unknown HttpMsg type. msg="+msg+" type="+msg.getClass());
}
private CompletableFuture<Void> processHttp2SettingsFrame(SettingsFrame settings) {
if(settings.isAck()) {
log.info("server acked our settings frame");
return CompletableFuture.completedFuture(null);
} else {
log.info("applying remote settings frame");
applyRemoteSettings(settings);
//now that settings is applied, ack the settings
SettingsFrame settingsAck = new SettingsFrame(true);
log.info("sending remote settings ack frame");
return marshalLayer.sendControlDataToSocket(settingsAck);
}
}
public void applyRemoteSettings(SettingsFrame settings) {
for(Http2Setting setting : settings.getSettings()) {
SettingsParameter key = setting.getKnownName();
if(key == null)
//TODO: forward unknown settings to clients
continue; //unknown setting so skip it
apply(key, setting.getValue());
}
}
private void apply(SettingsParameter key, long value) {
switch(key) {
case SETTINGS_HEADER_TABLE_SIZE:
marshalLayer.setEncoderMaxTableSize( convertToInt(value));
break;
case SETTINGS_ENABLE_PUSH:
applyPushEnabled(value);
break;
case SETTINGS_MAX_CONCURRENT_STREAMS:
level3StreamInit.setMaxConcurrentStreams(value);
break;
case SETTINGS_INITIAL_WINDOW_SIZE:
remoteFlowControl.resetInitialWindowSize(value);
break;
case SETTINGS_MAX_FRAME_SIZE:
applyMaxFrameSize(value);
break;
case SETTINGS_MAX_HEADER_LIST_SIZE:
remoteSettings.setMaxHeaderListSize(value);
break;
default:
throw new RuntimeException("bug, someone forgot to add some new setting="+key+" which had value="+value);
}
}
private void applyPushEnabled(long value) {
if(value == 0)
remoteSettings.setPushEnabled(false);
else if(value == 1)
remoteSettings.setPushEnabled(true);
else
throw new RuntimeException("bug, this should not happen with other preconditions in place");
}
private void applyMaxFrameSize(long val) {
int value = convertToInt(val);
remoteSettings.setMaxFrameSize(value);
}
private int convertToInt(long value) {
if(value > Integer.MAX_VALUE)
throw new RuntimeException("Bug, hpack library only supports up to max integer. need to modify that library");
return (int)value;
}
public CompletableFuture<Void> sendSettings() {
SettingsFrame settings = HeaderSettings.createSettingsFrame(localSettings);
return marshalLayer.sendFrameToSocket(settings);
}
}